Compare commits

...

1057 Commits

Author SHA1 Message Date
RiotRobot 1a0bdb0f98 v15.3.0 2021-12-20 13:58:07 +00:00
RiotRobot e870bebd3d Prepare changelog for v15.3.0 2021-12-20 13:58:07 +00:00
RiotRobot 22a98b3e4f v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
RiotRobot 934a0e8943 Prepare changelog for v15.3.0-rc.1 2021-12-14 14:37:58 +00:00
Michael Telatynski aeec4aa4a8 Update typescript-eslint plugin & parser (#2064) 2021-12-13 23:28:54 +00:00
Michael Telatynski 169b6b5572 Filter out falsey opts in /relations API hits (#2059) 2021-12-13 15:38:03 +00:00
RiotRobot 6244d77d44 Merge branch 'master' into develop 2021-12-13 15:22:07 +00:00
RiotRobot f4839a3b4f v15.2.1 2021-12-13 15:17:37 +00:00
RiotRobot 4b21b67a45 Prepare changelog for v15.2.1 2021-12-13 15:17:37 +00:00
David Baker 799a83a115 Update olm to 3.2.8 (#2062) 2021-12-13 14:59:03 +00:00
Michael Telatynski f3ebb2a314 Update Typescript to 4.5 (#2060) 2021-12-13 11:22:50 +00:00
Michael Telatynski c0fbed914e Update olm in yarn.lock (#2058) 2021-12-10 10:49:35 +00:00
Hubert Chathi dd8c157bb9 Improve fallback key behaviour (#2037) 2021-12-09 19:00:23 -05:00
Travis Ralston 8bd2d640f2 Update eslint 2021-12-09 16:01:07 -07:00
Aaron R 80aaa6c32b Fix imports to work with new lint rules (#2003)
* Add eslint-plugin-import

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Autofix

Signed-off-by: Aaron Raimist <aaron@raim.ist>

* Manual fix

Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-12-09 15:57:43 -07:00
Michael Telatynski 4e9fe8f53f Fix types (#2056) 2021-12-09 15:44:07 +00:00
Ajay Bura f926a2f777 Fix paginateEventTimeline resolve to boolean (#2054)
* Fix paginateEventTimeline resolve to boolean

Earlier paginateEventTimeline used to resolve to  chunk instead of returning boolean. Now it return boolean as specified in doc.

* added type
2021-12-09 20:00:55 +05:30
Michael Telatynski f8097221e6 Improve typing (#2055) 2021-12-09 14:22:58 +00:00
Callum Brown 95e7a76ba9 Finish adding registration token UIAA type (#2048)
Signed-off-by: Callum Brown <callum@calcuode.com>
2021-12-08 19:38:22 -07:00
Germain 72bc13eb39 Allow to omit optional params for /relations calls (#2053) 2021-12-08 08:23:53 +00:00
Germain 2206b80e65 Threads notifications after app startup (#2043) 2021-12-07 10:58:34 +00:00
Germain 39a8399977 Add new room event filter fields (#2051) 2021-12-07 09:42:52 +00:00
RiotRobot b7247fcedc Resetting package fields for development 2021-12-06 15:14:16 +00:00
RiotRobot e4494707fa Merge branch 'master' into develop 2021-12-06 15:14:16 +00:00
RiotRobot 45b7cab203 v15.2.0 2021-12-06 15:10:51 +00:00
RiotRobot 4dfc3b9dbb Prepare changelog for v15.2.0 2021-12-06 15:10:50 +00:00
Hugh Nimmo-Smith 15d05cd92f Fix incorrect MSC3089 typings and add null checks (#2049)
* Fix MSC3089TreeSpace typings to match actual code

* Implement strict null checks to resolve TS strictNullChecks=true issues
2021-12-06 10:54:43 +00:00
skyka13711 ba1b1cc6fa fix exports (#2047) 2021-12-02 19:40:10 -07:00
Michael Telatynski b716cde9b5 Add method to fetch /account/whoami (#2046) 2021-12-02 13:43:15 +00:00
RiotRobot 46a0645865 v15.2.0-rc.1 2021-11-30 18:16:24 +00:00
RiotRobot 2b30f45f60 Prepare changelog for v15.2.0-rc.1 2021-11-30 18:16:23 +00:00
J. Ryan Stinnett 87b920698f Upgrade allchange to 1.0.6 (#2042) 2021-11-30 17:55:38 +00:00
Germain 4f4811e1d9 Emit thread update after event decryption (#2040) 2021-11-30 14:40:59 +00:00
Hugh Nimmo-Smith db9936e07c Standardise content type handling in MSC3089 createFile() and createNewVersion() (#2014)
* Provide cross platform compatible versions of createFile() and createNewVersion()

The exist implementations are deprecated as they only work in a browser and support a different type of contents from MatrixClient.uploadContent()

* Fix MSC3089 content upload meta data in NodeJS runtime

* Break unstable createFile() and createNewVersion() instead of deprecating

Test using NodeJS types instead of mocked browser Blob

* chore: remove incorrect comment
2021-11-30 09:02:41 +00:00
Hugh Nimmo-Smith 7dcee2eb40 Implementation of deriveKey() for NodeJS (#2021)
Based on approach used by aes.ts
2021-11-30 09:02:26 +00:00
skyka13711 6b34ea6fe5 Exporting missing types (#2023)
* fix types export

* fix lint

* index.ts fix exports

* revert yarn.lock

Co-authored-by: Alexey Avramenko <2822698@gmail.com>
Co-authored-by: nikita <nikita@42px.ru>
2021-11-29 19:54:08 -07:00
Germain b33b01df0f Handle remote echo when creating a thread (#2038) 2021-11-25 08:27:13 +00:00
Germain 1c895432ed Fix backwards compatibility for redactEvent (#2039) 2021-11-24 15:42:10 +00:00
Germain ddd6a05198 Make local echo work for threads (#2026) 2021-11-24 08:19:17 +00:00
Germain 0ccde7807f Comprehensive events emitter typing (#2034) 2021-11-23 15:16:35 +00:00
Will Hunt 9f97992196 getStateEvent should return any (#2032)
* getStateEvent returns any

getStateEvent returns the content of an event, not the whole state event as per https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey

* Make the return type Record<string, any>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-11-23 13:54:54 +00:00
Germain 67b8da719f Add a flag to track user participation in a thread (#2030) 2021-11-23 08:20:27 +00:00
Šimon Brandner 04fad564f7 Fix call upgrades (#2024)
* Init call feeds with non-muted state

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve CallFeed docs

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-11-22 18:17:35 -07:00
Germain aee698c285 Add KeyVerificationRequest msgType (#2033) 2021-11-22 17:08:24 +00:00
RiotRobot 55b489b17a Resetting package fields for development 2021-11-22 13:32:08 +00:00
RiotRobot c5d188c30a Merge branch 'master' into develop 2021-11-22 13:32:08 +00:00
RiotRobot 6fff3d6db5 v15.1.1 2021-11-22 13:28:39 +00:00
RiotRobot 8043d83921 Prepare changelog for v15.1.1 2021-11-22 13:28:39 +00:00
Michael Telatynski b2d83c1f80 Iterate type definitions (#2029) 2021-11-19 17:35:09 +00:00
Travis Ralston 1b91cd7037 Add optional force parameter when ensuring Olm sessions (#2027)
This is useful for debugging crypto where Element's `/discardsession` doesn't reset the Olm session.
2021-11-19 07:33:45 -07:00
skyka13711 0baf926c84 TimelineWindow fix load function signature (#2028) 2021-11-19 09:00:08 +00:00
Robert Long 763e25e031 Update sync state to error when aborting (#2025) 2021-11-18 12:16:57 -08:00
RiotRobot 861023b3f6 v15.1.1-rc.1 2021-11-17 13:52:47 +00:00
RiotRobot 1f5a83994b Prepare changelog for v15.1.1-rc.1 2021-11-17 13:52:47 +00:00
Dariusz Niemczyk af523522de Add LocalStorageErrorEventListener (#2019)
Add a way to listen to LocalStorage error events from element-web
and matrix-react-sdk in the same manner.

Related: https://github.com/vector-im/element-web/issues/18423
2021-11-15 18:08:20 +00:00
Dariusz Niemczyk c3a266b3e7 Implement TypedEventEmitter for better TS support (#2018)
We're using stringly typed events everywhere, this is the first step for
better typescript support with our event emitters before we replace it
with something much better for React support.
2021-11-15 14:47:21 +00:00
Callum Brown f41d815aa6 Add registration token UIA type (#2020) 2021-11-15 09:16:23 +00:00
Germain ad8a93dde8 Fix check supportExperimentalThreads (#2017) 2021-11-10 10:54:13 +00:00
Germain b07d44a6c0 Getter for last thread reply (#2015) 2021-11-09 14:46:48 +00:00
RiotRobot 1dc899ba6e Resetting package fields for development 2021-11-08 17:37:26 +00:00
RiotRobot 0b1e3edaff Merge branch 'master' into develop 2021-11-08 17:37:25 +00:00
RiotRobot 9fb4fed044 v15.1.0 2021-11-08 17:34:08 +00:00
RiotRobot 730b0b121d Prepare changelog for v15.1.0 2021-11-08 17:34:07 +00:00
Aaron R 9d9d9e2cfa Fix edit history being broken after editing an unencrypted event with an encrypted event (#2013) 2021-11-08 08:55:41 +00:00
Germain 43bc09f392 Copy relations to thread root in the thread timeline (#2012) 2021-11-05 14:16:45 +00:00
Germain 195498e9db Make events pagination responses parse threads (#2011) 2021-11-04 11:29:14 +00:00
Faye Duxovni 50332c4999 End authentication attempt immediately if we couldn't find an appropriate flow (#2008)
If `doRequest()` in `interactive-auth.ts` fails to obtain an appropriate authentication flow, it should return immediately after rejecting the promise.  If it continues, it'll attempt to check `chosenFlow.stages`, which will cause an error because `chosenFlow` is `null`.  This was breaking the interactive auth spec tests with Node 16.
2021-11-02 10:40:46 -04:00
RiotRobot 9e46646b16 v15.1.0-rc.1 2021-11-02 14:09:01 +00:00
RiotRobot a74a03e414 Prepare changelog for v15.1.0-rc.1 2021-11-02 14:09:00 +00:00
David Baker fe8b9eca8e Update allchange to 1.0.5 (#2010) 2021-11-02 13:30:49 +00:00
Germain a7cc1ae630 Fetch thread root event if it's missing from the sync (#2009) 2021-11-02 11:56:43 +00:00
Šimon Brandner 14e008bbad Don't emit CallError when we fail to get screen-sharing stream (#2005)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-28 16:23:06 +01:00
Travis Ralston 6a12c265cf PSFD-455: Try to set a sender on search result events if possible (#2004)
This is to ensure that search results have aesthetic information such as display name and avatar. Though the membership event won't be in context for when the event was sent, it'll at least be something better than a bare user ID.
2021-10-28 07:48:27 -06:00
Šimon Brandner c35cb57a79 Port some changes from group calls branch to develop (#2001)
* Add some useful getters

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Add removeLocalFeed()

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Don't updateStream() if they're the same

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Add isSpeaking()

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Improve speaking detection using history

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Make code for placing and answering calls more flexible

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>

* Correctly log stream id

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Remove mistaken parameter

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add a unit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Robert Long <robert@robertlong.me>
2021-10-26 11:59:15 -07:00
Christian Paul 51c776f51e Correct TypeScript signature of client.sendCompleteEvent (#1997)
* Correct TypeScript signature of client.sendCompleteEvent

* Revert renaming txnId to txnIdOrCallback
2021-10-25 14:25:38 -06:00
Hubert Chathi 7ce0eb26d4 Fetch room membership from server rather than relying on stored data (#1998)
To work around the possibility that the persisted sync data may be out of date.
2021-10-25 10:29:46 -04:00
RiotRobot f0091fa811 Resetting package fields for development 2021-10-25 11:25:30 +01:00
RiotRobot 25c88bc986 Merge branch 'master' into develop 2021-10-25 11:25:29 +01:00
RiotRobot ab06f57965 v15.0.0 2021-10-25 11:22:22 +01:00
RiotRobot c46209a159 Prepare changelog for v15.0.0 2021-10-25 11:22:21 +01:00
Germain 0c47e27f9f Add explicit options to disable pending events (#1993) 2021-10-22 09:23:12 +01:00
Hubert Chathi 0385f265e8 Mark old verification methods as deprecated (#1994)
The old verification methods start a verification by sending an
`m.key.verification.start` message, rather than an `m.key.verification.request`
message.  This behaviour is deprecated in the
spec (https://github.com/matrix-org/matrix-doc/pull/3122), so these functions
should be deprecated too.
2021-10-21 16:10:13 -04:00
David Baker 59b3960a42 Strip direction override characters from display names (#1992)
Strip RLO and LRO characters from name and rawDisplayName so they
can safely be embedded into other text without messing up the text
ordering.

Fixes https://github.com/vector-im/element-web/issues/1712
2021-10-20 17:08:40 +01:00
Germain d5ab4ba2ef Update thread replies count to only include m.thread (#1991) 2021-10-20 16:49:54 +01:00
Travis Ralston f59b81f46b Update allchange (#1989) 2021-10-19 18:21:52 +01:00
Michael Telatynski 4c39ce8c96 Add method to fetch the MSC3266 Room Summary of a Room (#1988) 2021-10-19 16:13:29 +01:00
Germain Souquet 94ff0ea4cd Upgrade yarn dependencies 2021-10-19 11:24:53 +01:00
RiotRobot 526758a07f v15.0.0-rc.1 2021-10-19 10:50:42 +01:00
RiotRobot 9686fba81e Prepare changelog for v15.0.0-rc.1 2021-10-19 10:50:41 +01:00
Germain bbc547fd7f Merge pull request #1982 from matrix-org/gsouquet/thread-event-new 2021-10-15 14:32:19 +01:00
Germain 7d5dbeaf2d Merge pull request #1981 from matrix-org/gsouquet/threads-root-guard 2021-10-15 14:32:13 +01:00
Germain Souquet 8c19cf45e2 Dispatch a Thread.New event 2021-10-15 11:09:14 +01:00
Germain Souquet c9277de8ee Remove console statement 2021-10-15 10:31:43 +01:00
Germain Souquet b5baca51a6 Null-guard for thread root event not loaded yet 2021-10-15 10:22:32 +01:00
Germain fc800821f9 Merge pull request #1980 from matrix-org/gsouquet/threads-relations 2021-10-15 08:53:42 +01:00
Germain Souquet efbc46937f Null-guard for event that do not have a thread 2021-10-14 17:37:33 +01:00
Germain Souquet 9ceeb2d220 Delete UnstableValue and use hardcoded value for m.thread 2021-10-14 17:09:00 +01:00
Germain Souquet 7b89862ed0 Make threads use 'm.thread' relation 2021-10-14 16:56:48 +01:00
David Baker 6804e4290a Merge pull request #1972 from SimonBrandner/feature/answer-no-cam
Try to answer a call without video if we can't access the camera
2021-10-14 12:15:35 +01:00
David Baker 0a3d820fda Merge pull request #1943 from freaktechnik/develop
Fix `requestVerificationDM` with chronological `pendingEventOrdering`
2021-10-14 12:14:20 +01:00
Michael Telatynski e2a9c95ea8 Merge pull request #1974 from SimonBrandner/task/security-ts 2021-10-14 10:02:14 +01:00
Šimon Brandner 3aefc9f02e Add tests for falling back to answering with no video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-14 08:46:36 +02:00
Šimon Brandner 0042cb5292 Fix importRoomKeys() definition
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-13 16:50:27 +02:00
Denis Kasak b288c97372 Merge pull request #1975 from matrix-org/dkasak/more-detailed-room-key-sharing-logging
More detailed logging when sharing room keys
2021-10-12 13:26:43 +00:00
Denis Kasak a0de1740a4 Add comma per review
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-10-12 13:23:22 +00:00
Denis Kasak 8e2e3018d8 Log device objects for existing/new devices when sharing room keys. 2021-10-12 13:48:41 +02:00
Denis Kasak 60e93024e7 Fix comment regarding marking blocked devices as notified. 2021-10-12 13:43:28 +02:00
Denis Kasak d89aa365f9 More detailed logging of blocked devices.
Separately log how many blocked devices there are in total and list them
out.

Then, log how many devices were notified because they were newly
blocked and list those out too.
2021-10-12 13:43:28 +02:00
Denis Kasak 4241263f2c Make log message regarding notifying unnotified failed devices clearer. 2021-10-12 13:43:26 +02:00
Denis Kasak b009f9c21b Log more clearly when we're sharing room keys with *new* Olm sessions. 2021-10-12 13:41:04 +02:00
Denis Kasak 04ccec6de3 Log which devices room keys were shared with. 2021-10-12 13:41:04 +02:00
RiotRobot e7b41fadd0 Merge branch 'master' into develop 2021-10-12 08:50:43 +01:00
RiotRobot 2dc1acef2e Resetting package fields for development 2021-10-12 08:36:13 +01:00
RiotRobot b977b629f4 Merge branch 'release-v14.0.1' 2021-10-12 08:35:42 +01:00
RiotRobot 6bbd493e20 v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 38ad435af5 Prepare changelog for v14.0.1 2021-10-12 08:29:30 +01:00
RiotRobot 8f245af317 Merge branch 'master' into develop 2021-10-11 11:48:22 +01:00
RiotRobot 9b2bfbe7d0 v14.0.0 2021-10-11 11:44:54 +01:00
RiotRobot f69ea85e55 Prepare changelog for v14.0.0 2021-10-11 11:44:53 +01:00
Šimon Brandner 7559a10526 Make opts in importRoomKeys optional
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:36:11 +02:00
Šimon Brandner 68dbe959bb Add a logline
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 74a875aa23 Add MockMediaDeviceInfo
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:29 +02:00
Šimon Brandner 4b679c5056 Try to answer without video if we can't access it
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-10 08:19:22 +02:00
Šimon Brandner cb8a260b9c Improve handling when we don't have a camera
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:36:19 +02:00
Robert Long 7d7975b8bb Fix connecting to a call without a webcam
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-09 08:05:59 +02:00
Travis Ralston f12513da96 Merge pull request #1971 from matrix-org/travis/spam-log
Remove excess logging from getRoomUpgradeHistory()
2021-10-08 10:33:07 -06:00
Travis Ralston 785a3e3381 Remove excess logging from getRoomUpgradeHistory()
On large accounts this can now cause a massive amount of spam. It's not really providing any value in day-to-day debugging either.
2021-10-08 10:28:20 -06:00
RiotRobot f5fcc20840 v14.0.0-rc.2 2021-10-07 17:42:40 +01:00
RiotRobot 17e20c17b3 Prepare changelog for v14.0.0-rc.2 2021-10-07 17:42:40 +01:00
Michael Telatynski 39fd350ffe Merge pull request #1969 from matrix-org/t3chguy/cp/space-hierarchy 2021-10-06 16:51:32 +01:00
Michael Telatynski b4d100baff Tweak Room Hierarchy root visibility and add loading getter 2021-10-06 13:29:13 +01:00
Michael Telatynski 29bc83f4a7 Merge pull request #1968 from matrix-org/t3chguy/fix/19276 2021-10-06 13:27:12 +01:00
Michael Telatynski 13c8774d45 Tweak Room Hierarchy root visibility and add loading getter 2021-10-06 10:25:27 +01:00
Travis Ralston c9212e770d Merge pull request #1966 from Alexendoo/declaration-map
Enable TypeScript declaration maps
2021-10-05 13:39:47 -06:00
Germain 08aa7803e3 Merge pull request #1964 from SimonBrandner/feature/call-feed-opts 2021-10-04 15:02:49 +01:00
Alex Macleod 770858c255 Enable TypeScript declaration maps
This allows go-to-definition to reach the original .ts files for
consumers of the package. It emits .d.ts.map files alongside the regular
.d.ts files

e.g. in sdk.createClient go-to-definition on createClient will place the
cursor at the function in src/matrix.ts rather than inside a definition
file lib/matrix.d.ts

Signed-off-by: Alex Macleod <alex@macleod.io>
2021-10-04 14:36:50 +01:00
Germain Souquet c73bb7d408 Update dependencies 2021-10-04 12:30:14 +01:00
RiotRobot 15cd675354 v14.0.0-rc.1 2021-10-04 11:55:19 +01:00
RiotRobot e8137d3f88 Prepare changelog for v14.0.0-rc.1 2021-10-04 11:55:19 +01:00
Šimon Brandner c63abe9988 Use ICallFeedOpts in the CallFeed constructor
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-02 08:12:43 +02:00
Germain a7a08fd760 Merge pull request #1962 from matrix-org/gsouquet/fix-19229 2021-10-01 15:46:30 +01:00
Germain df51f95fbd Merge pull request #1963 from matrix-org/gsouquet/fix-19225 2021-10-01 12:11:35 +01:00
Germain Souquet a14cf1a339 Remove unused DuplicateStrategy 2021-10-01 12:07:16 +01:00
Germain Souquet 30758600f0 Prepend events to thread when fetching the reply chain 2021-10-01 10:49:46 +01:00
Germain 823242a93c Merge pull request #1959 from psrpinto/fix/joined-rooms-response 2021-09-30 17:54:53 +01:00
Martin Giger 482e3c4cb7 Skip waiting if already sent and abort on cancel 2021-09-30 16:58:08 +02:00
Germain Souquet 1dbd7158ad Prevent redactions to be sent as pending events
Co-authored-by: Dariusz Niemczyk <Palid@users.noreply.github.com>
2021-09-30 13:56:52 +01:00
Michael Telatynski 46b3f0babd Merge pull request #1958 from SimonBrandner/fix/call-length 2021-09-29 15:04:02 +01:00
Šimon Brandner b4bc554d7a Make sure to callLengthInterval only once
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-29 15:46:22 +02:00
Paulo Pinto 1485969493 Return IJoinedRoomsResponse from getJoinedRooms(), instead of string[]
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-09-29 11:45:01 +01:00
Travis Ralston 92f7ddcf3e Merge pull request #1954 from SimonBrandner/task/src-ts
Changes for TS migration
2021-09-27 13:45:06 -06:00
Travis Ralston 90d480b4cf Merge pull request #1952 from matrix-org/travis/fsdk/versions
Implement file versioning for tree spaces
2021-09-27 10:04:22 -06:00
RiotRobot 2515d07c8f Resetting package fields for development 2021-09-27 14:23:47 +01:00
RiotRobot 7acb9416c2 Merge branch 'master' into develop 2021-09-27 14:23:47 +01:00
RiotRobot 095e3a8a88 v13.0.0 2021-09-27 14:20:00 +01:00
RiotRobot 92623a246a Prepare changelog for v13.0.0 2021-09-27 14:20:00 +01:00
Michael Telatynski 0c4212ca90 Merge pull request #1935 from matrix-org/t3chguy/fix/18969 2021-09-27 11:08:07 +01:00
Germain 9a06a05a75 Merge pull request #1953 from SimonBrandner/fix/audio-upgrade 2021-09-27 10:18:13 +01:00
Šimon Brandner f05a0615f7 Export IRequestMsisdnTokenResponse
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 09:45:07 +02:00
Šimon Brandner 6b93687ff0 Pass correct params to upgradeCall() to upgrade audio
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-26 13:10:00 +02:00
Travis Ralston 8d910d5e26 Merge pull request #1950 from SimonBrandner/feature/flexible-answer
Allow answering calls without audio/video
2021-09-25 23:39:20 -06:00
Šimon Brandner 9264050f41 Update src/webrtc/call.ts
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-09-26 07:26:08 +02:00
Travis Ralston eab0c54663 Appease the linter 2021-09-25 20:47:59 -06:00
Travis Ralston 82a254b7bd Implement file versioning for tree spaces
The diff isn't super clear on how this works, but the general idea is that the MSC3089Branches (files) now know which directory they came from, and the directories (MSC3089TreeSpace) can tell when files are triggering uploads referencing themselves so it can add all the replacement metadata onto the event.

There's a few challenges with how relations work in the js-sdk which has shaped the API surface exposed by this change. Specifically, there is a `getVersionHistory()` function instead of a function to get a certain prior version: the js-sdk doesn't track parent events in the relation structures so cannot determine what the event could have replaced. 

In order to trigger the built-in relations structures, we must trigger decryption with `emit: true`. This is so an internal listener in the relations system can pick up `Event.decrypted`.
2021-09-25 20:44:17 -06:00
Šimon Brandner 0f6a59ed98 Allow answering calls without audio/video
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-25 10:00:51 +02:00
Travis Ralston afb4682987 Merge pull request #1949 from SimonBrandner/fix/mic-state
Fix not returning new mic mute state
2021-09-25 01:14:11 -06:00
Šimon Brandner a18326519b Add missing return statement
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-25 08:26:13 +02:00
Germain b033540f95 Merge pull request #1948 from matrix-org/gsouquet/fix-sync-partitioning 2021-09-24 15:43:27 +01:00
Germain Souquet ff529d733d Fix event partitioning from non threading ready clients 2021-09-24 14:54:40 +01:00
Germain 5951958c05 Merge pull request #1945 from matrix-org/gsouquet/thread-serialise-18720 2021-09-24 11:17:39 +01:00
David Baker 886f2fb95d Merge pull request #1947 from matrix-org/dbkr/we_like_tests
Add tests requirements to CONTRIBUTING
2021-09-23 18:15:32 +01:00
David Baker f6ce0c581e Add tests requirements to CONTRIBUTING
Fixes https://github.com/vector-im/element-web/issues/19136
2021-09-23 14:09:01 +01:00
Germain 5ce68b6c12 Merge pull request #1944 from matrix-org/gsouquet/thread-state-events-18719 2021-09-23 09:17:02 +01:00
Germain Souquet c748aebfc5 Add context to explain why we fetch roomstate in threads 2021-09-22 17:36:12 +01:00
Germain Souquet f8ec445ff0 Fix addLiveEvent with 'replace' strategy 2021-09-22 17:14:18 +01:00
Germain Souquet 5d4f347eaa Add thread serialisation helper 2021-09-22 11:35:19 +01:00
Germain Souquet 1359c0574e Share room state events with thread's timelines 2021-09-22 11:19:08 +01:00
Travis Ralston b1d239b292 Merge pull request #1934 from aaronraimist/kick-api
Switch to /kick API
2021-09-21 23:30:23 -06:00
Travis Ralston 70031415e8 Merge pull request #1938 from matrix-org/travis/fix-widgets-relations
Ensure unencrypted fields get exposed by getEffectiveEvent()
2021-09-21 23:16:18 -06:00
Martin Giger 3c97d17770 Fix requestVerificationDM with chronological pendingEventOrdering
Signed-off-by: Martin Giger <martin@humanoids.be>
2021-09-21 20:04:07 +02:00
Travis Ralston 5a73a9758a Merge pull request #1941 from SimonBrandner/feature/bound
Add `bound` to `IThreepid`
2021-09-21 11:36:36 -06:00
Travis Ralston 6848e96244 Merge pull request #1942 from SimonBrandner/feature/trusted_locally
Add `trusted_locally` to `TrustInfo`
2021-09-21 11:26:35 -06:00
Travis Ralston b53566e074 Merge pull request #1827 from SimonBrandner/feature/call-upgrades
Support for call upgrades
2021-09-21 09:10:02 -06:00
Šimon Brandner 012b914a97 Add trusted_locally to TrustInfo
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-21 16:20:39 +02:00
Šimon Brandner 3c6eed0135 Add bound to IThreepid
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-21 14:56:33 +02:00
RiotRobot f2a2e7f5dd v13.0.0-rc.1 2021-09-21 09:24:22 +01:00
RiotRobot 15b6a9a257 Prepare changelog for v13.0.0-rc.1 2021-09-21 09:24:21 +01:00
Šimon Brandner b83977e72f Add @param docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-20 16:40:46 +02:00
Travis Ralston 8f5082cd2f Merge pull request #1940 from SimonBrandner/feature/getHistoryVisibility
Add `getHistoryVisibility()` and `getGuestAccess()`
2021-09-20 08:34:14 -06:00
David Baker 50c877cb60 Merge pull request #1939 from SimonBrandner/fix/stream-stopping
Fix stream stopping and improve logging
2021-09-20 11:56:36 +01:00
Šimon Brandner e3e7fd5900 Add getGuestAccess()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 10:47:18 +02:00
Šimon Brandner 69823f9cae Add getHistoryVisibility()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 10:36:45 +02:00
Šimon Brandner e5dcdf109a Fix stream stopping and improve logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 09:29:16 +02:00
Šimon Brandner 5c6a643436 Merge branch 'feature/call-upgrades' of https://github.com/SimonBrandner/matrix-js-sdk into feature/call-upgrades 2021-09-19 07:44:40 +02:00
Šimon Brandner 1ca1a69eb8 Fix docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-19 07:44:28 +02:00
Šimon Brandner 76ea1ba192 Improve log
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-09-19 07:43:09 +02:00
Šimon Brandner 8cd8ec134e Merge remote-tracking branch 'upstream/develop' into feature/call-upgrades 2021-09-19 07:42:04 +02:00
Travis Ralston 6c649e2bfe Merge pull request #1937 from SimonBrandner/fix/getUserMediaStream
Fix `getUserMediaStream()` not being able to handle streams of different track combinations
2021-09-18 16:29:15 -06:00
Travis Ralston 9f42647a75 Ensure unencrypted fields get exposed by getEffectiveEvent()
Fixes https://github.com/vector-im/element-web/issues/19062
2021-09-18 16:26:08 -06:00
Šimon Brandner 9747e5e0d5 Fix getUserMediaStream() not being able to handle streams of different track combinations
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 13:18:28 +02:00
Šimon Brandner a4cacbc73a Add call upgrade support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:32:13 +02:00
Šimon Brandner f24840d09c Make CallFeed handle addTrack events
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:31:54 +02:00
Šimon Brandner acb7991cc5 Fix getUserMediaStream() not being able to handle streams of different track combinations
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:27:24 +02:00
Šimon Brandner 6addc7dd3d Add MockMediaStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-18 12:27:08 +02:00
Eric Eastwood 2bd60f4160 Merge pull request #1933 from matrix-org/madlittlemods/more-better-pr-descriptions
Update PR guidelines for easier review
2021-09-17 11:54:13 -05:00
David Baker e71362a6df Merge pull request #1936 from matrix-org/dbkr/await_save_backup_key
Fix race on automatic backup restore
2021-09-17 17:06:43 +01:00
David Baker 673c09cd77 Fix race on automatic backup restore
We forgot to await the saving of the backup key, so there was a race
where it would fail to get the backup key because it hadn't been saved
yet, so the backup wouldn't be restored automatically.

Fixes https://github.com/vector-im/element-web/issues/17781
2021-09-17 15:53:14 +01:00
Michael Telatynski 4606422e57 Make RoomState::hasSufficientPowerLevelFor public 2021-09-17 15:35:03 +01:00
Dariusz Niemczyk 4bc8f968e0 Merge pull request #1932 from matrix-org/palid/threads-typescriptify
Fix PR comments
2021-09-17 08:11:36 +02:00
Aaron Raimist 35ee58dfe1 Switch to /kick API
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-09-17 01:05:21 -05:00
Eric Eastwood 3705426b6d Make PR's easier to review 2021-09-16 16:05:49 -05:00
Dariusz Niemczyk 5813aea27f Properly rename ThreadEvents to ThreadEvent 2021-09-16 17:49:31 +02:00
Dariusz Niemczyk 5ba836a31b Fix PR comments 2021-09-16 17:12:57 +02:00
Dariusz Niemczyk 4f387d15b9 Merge pull request #1930 from matrix-org/palid/threads-typescriptify
Typescriptify thread events a bit
2021-09-16 17:12:02 +02:00
Dariusz Niemczyk 8ab0246ca2 Fix PR comments 2021-09-16 16:03:47 +02:00
Dariusz Niemczyk ed68bd4108 Typescriptify thread events a bit 2021-09-16 15:59:41 +02:00
Dariusz Niemczyk 3d39e8ec26 Merge pull request #1880 from matrix-org/palid/fix/dont-persist-log-level
Palid/fix/dont persist log level
2021-09-16 14:52:52 +02:00
Robert Long 53bdab7cc8 Merge pull request #1929 from matrix-org/robertlong/datachannels
Add WebRTC DataChannels to MatrixCall
2021-09-15 14:25:13 -07:00
Robert Long fafa55ba1d Merge pull request #1928 from matrix-org/robertlong/clone-streams
Clone and stop local media streams from mediaHandler
2021-09-15 14:25:00 -07:00
Travis Ralston 3a818c1419 Merge pull request #1927 from SimonBrandner/feature/getBuffer
Add `getBuffer()` to `QRCodeData`
2021-09-15 14:42:11 -06:00
Robert Long 422853298c Reset media stream arrays 2021-09-15 13:20:52 -07:00
Robert Long f3a2bd6b40 Clean up stopAllMedia 2021-09-15 12:38:12 -07:00
Robert Long 6834137f50 Add datachannels to MatrixCall 2021-09-15 12:28:09 -07:00
Robert Long c4d7fef0cd Clone and stop local media streams from mediaHandler 2021-09-15 12:08:28 -07:00
Šimon Brandner 556b5ff628 Add getBuffer() to QRCodeData
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 20:27:13 +02:00
David Baker 2daa1ec4ce Merge pull request #1916 from matrix-org/dbkr/verify_key_share_tests
Tests for key sharing security fix
2021-09-15 18:38:03 +01:00
Travis Ralston 348ea72b70 Merge pull request #1925 from SimonBrandner/task/setCodecPreferences
Add `setCodecPreferences()` to `global.d.ts`
2021-09-15 09:48:27 -06:00
Šimon Brandner 4e14d810c5 Add setCodecPreferences()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 17:44:16 +02:00
Travis Ralston f84905b003 Merge pull request #1924 from SimonBrandner/revert-1923-task/die-rtx-hack
Revert "Remove RTX codec hack "
2021-09-15 09:23:27 -06:00
Šimon Brandner 9bb87145f6 Revert "Remove RTX codec hack " 2021-09-15 17:17:56 +02:00
Travis Ralston c7675570aa Merge pull request #1923 from SimonBrandner/task/die-rtx-hack
Remove RTX codec hack
2021-09-15 09:16:55 -06:00
Šimon Brandner be942fd66a Remove RTX codec hack as the issue is no longer present on Chromium 90
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-15 16:38:59 +02:00
James Salter 8544aca362 Merge pull request #1922 from matrix-org/revert-errors-ts
Revert "Migrate errors to TypeScript"
2021-09-15 12:08:32 +01:00
James Salter c3308e1de9 Revert "Migrate errors to TypeScript"
This reverts commit ab906faa17.
2021-09-15 12:01:34 +01:00
David Baker 047d7125a1 Merge pull request #1920 from matrix-org/dbkr/fix_autodiscovery_types
Fix types
2021-09-15 11:29:10 +01:00
David Baker cf4d3117c1 Fix types
This is 'any' (defined as any by IWellKnownConfig.raw) rather than
string (which the jsdoc claimed). We were calling this method ourselves
but with an object that was also 'any', so it was fine, but react-sdk
calls it with an object literal, which typescript knows it not a string.
2021-09-15 10:29:14 +01:00
David Baker 88c2844e48 Merge pull request #1919 from matrix-org/dbkr/yarnupgrade_20210914
yarn upgrade
2021-09-15 10:01:18 +01:00
Germain 6ca4694fe6 Merge pull request #1907 from matrix-org/gsouquet/ts-last-push 2021-09-15 08:30:52 +01:00
Germain 062287e270 Merge pull request #1913 from matrix-org/gsouquet/ts-security 2021-09-15 08:27:29 +01:00
Germain Souquet 50e0d76f10 remove erroneous comment related to typings 2021-09-15 09:26:25 +02:00
Travis Ralston df039d3d8c Merge pull request #1911 from SimonBrandner/feature/media-handling
Improve `MatrixCall` media handling and internally remove `CallType`s
2021-09-14 14:40:45 -06:00
Travis Ralston 799606b73c Merge pull request #1909 from matrix-org/travis/fsdk/locking
Add file locking to MSC3089 branches
2021-09-14 12:39:41 -06:00
Travis Ralston c048f5d357 Merge pull request #1910 from matrix-org/travis/fsdk/tests
Add missing tests for MSC3089 implementation
2021-09-14 12:37:54 -06:00
Robert Long f712a8cecb Merge pull request #1903 from matrix-org/robertlong/fix-register-types
Add types for the MatrixClient register method
2021-09-14 10:56:12 -07:00
David Baker e1d8d6bf4e Not typescript again
because this time they just removed setCodecPreferences from webrtc?
2021-09-14 18:52:35 +01:00
Robert Long 14ed8a110c Update src/client.ts
Co-authored-by: Travis Ralston <travisr@matrix.org>
2021-09-14 10:50:50 -07:00
David Baker cb6700f21b yarn upgrade 2021-09-14 18:41:14 +01:00
Travis Ralston d4f35bf07a fix main package? 2021-09-14 10:03:23 -06:00
RiotRobot b4eb29138b Resetting package fields for development 2021-09-14 15:52:38 +01:00
RiotRobot 8ca3a071f9 Merge branch 'master' into develop 2021-09-14 15:51:45 +01:00
RiotRobot 5abd211436 Merge branch 'release-v12.5.0' 2021-09-14 15:51:13 +01:00
RiotRobot 99f428e091 v12.5.0 2021-09-14 15:46:09 +01:00
RiotRobot a734fdf9b0 Prepare changelog for v12.5.0 2021-09-14 15:46:08 +01:00
David Baker 6f91b2bc80 Merge pull request #1917 from matrix-org/dbkr/verify_keyshare_tests_125rel
Tests for key sharing security fix
2021-09-14 15:33:05 +01:00
David Baker 32d7272939 Tests for key sharing security fix
Tests for 894c24880d
2021-09-14 15:24:15 +01:00
David Baker c8d85452bf Merge remote-tracking branch 'origin/develop' into dbkr/verify_key_share_tests 2021-09-14 15:16:49 +01:00
RiotRobot 557f800919 Verify target device key on reshare 2021-09-14 15:14:46 +01:00
David Baker 2119e88c78 Tests for key sharing security fix
Tests for 894c24880d
2021-09-14 13:11:18 +01:00
Travis Ralston 24e7a7e6bf Merge pull request #1915 from SimonBrandner/feature/has-been-cancelled
Add `hasBeenCancelled` to `VerificationBase`
2021-09-13 20:54:03 -06:00
Šimon Brandner 4e5847f356 Add hasBeenCancelled to VerificationBase
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-13 18:01:24 +02:00
RiotRobot 72d580edfd Merge branch 'master' into develop 2021-09-13 12:50:02 +01:00
RiotRobot e878b7683b v12.4.1 2021-09-13 12:47:04 +01:00
RiotRobot fac700bf4d Changelog for 12.4.1 2021-09-13 12:43:13 +01:00
RiotRobot 894c24880d Verify target device key on reshare 2021-09-13 12:34:48 +01:00
Germain d742249fd1 Merge pull request #1914 from matrix-org/gsouquet/ts-misc
Types adjustment for TS migration
2021-09-13 10:00:26 +01:00
Germain Souquet 9c100fea48 Types update for UserView TS migration 2021-09-12 10:12:59 +02:00
Germain Souquet 7daab62850 Types fixes for CreateKeyBackupDialog TS migration 2021-09-12 09:21:04 +02:00
Germain Souquet fed0ced89d Type fixes for CreateSecretStorageDialog TS migration 2021-09-12 08:56:31 +02:00
Germain Souquet df9cfaed1d Merge branch 'develop' into gsouquet/ts-last-push 2021-09-11 16:18:25 +01:00
Germain Souquet edd614dd0c fix cyclic dependencies for AutoDiscoveryActionEnum 2021-09-11 16:17:40 +01:00
Germain Souquet 375b228bb2 fix missing types and linting errors 2021-09-11 16:06:29 +01:00
Šimon Brandner 7143ef8a32 Internally remove call types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:58 +02:00
Šimon Brandner 1bd7de5a18 Use MediaHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:57 +02:00
Šimon Brandner a1f73eee86 Add MediaHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-11 08:21:51 +02:00
Travis Ralston d9f8710758 Add missing tests for MSC3089 implementation
Fixes https://github.com/vector-im/element-web/issues/18461
2021-09-10 14:44:50 -06:00
Travis Ralston 3a9d5439a2 Add tests 2021-09-10 14:18:11 -06:00
Travis Ralston 5d1be6e8be Add file locking to MSC3089 branches
This isn't hooked up to versioning, yet, but will be eventually.
2021-09-10 14:15:32 -06:00
Germain 775e85a2e8 Merge pull request #1906 from matrix-org/gsouquet/threads-relations-18721 2021-09-10 19:22:41 +01:00
Travis Ralston dc8f403c44 Merge pull request #1908 from SimonBrandner/task/dialogs-ts
Add `ISasEvent`
2021-09-10 12:17:41 -06:00
Šimon Brandner cf9fa991d1 Add ISasEvent
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-10 19:18:19 +02:00
Robert Long d26dc49755 Merge pull request #1904 from matrix-org/robertlong/callfeed-dispose
Properly dispose of CallFeeds
2021-09-10 10:07:10 -07:00
Germain be04559a66 Merge pull request #1878 from SimonBrandner/task/i-hate-my-code 2021-09-10 16:16:51 +01:00
Germain Souquet 0722dafe58 Migrate AutoDiscovery to TypeScript 2021-09-10 16:04:55 +01:00
Germain Souquet ab906faa17 Migrate errors to TypeScript 2021-09-10 15:39:36 +01:00
Germain Souquet 9d63a12469 Migrate realtime-callbacks to TypeScript 2021-09-10 15:31:21 +01:00
Germain Souquet f40d0d24c7 Move relations and redactions to thread timeline 2021-09-10 09:21:25 +01:00
Germain Souquet 2cdea36abf Merge branch 'develop' into gsouquet/threads-relations-18721 2021-09-10 08:18:03 +01:00
Travis Ralston 8d4f4c8832 Merge pull request #1901 from matrix-org/travis/rl/perf-metrics
[Release] Exclude opt-in Element performance metrics from encryption
2021-09-09 18:27:25 -06:00
Robert Long 3b4dcbb01d Properly dispose of CallFeeds 2021-09-09 17:15:41 -07:00
Robert Long b52f3b1c57 Add types for the MatrixClient register method 2021-09-09 17:00:45 -07:00
Germain 2cdf5629e5 Merge pull request #1900 from matrix-org/gsouquet/thread-disable-echo 2021-09-09 18:56:02 +01:00
Germain Souquet 97b4f0e369 restore correct local echo 2021-09-09 18:51:12 +01:00
Germain Souquet a508c8d236 Make supportsExperimentalThreads calls more resilient 2021-09-09 18:40:15 +01:00
Travis Ralston 7911e6371a Exclude opt-in Element performance metrics from encryption
This is needed for the system which is meant to be monitoring this metadata. See https://github.com/matrix-org/matrix-react-sdk/pull/6766
2021-09-09 10:41:09 -06:00
Travis Ralston bbace01e9a Merge pull request #1897 from matrix-org/travis/measure-encryption-perf
Exclude opt-in Element performance metrics from encryption
2021-09-09 10:38:18 -06:00
Germain Souquet 87469fea63 Disable pending events for threads
There's a fair few things to update to make them work with Threads
Will get back to it when the plan is to build a more polished UI ready for production
2021-09-09 17:24:55 +01:00
Germain Souquet 48a9db0315 Add helper to check threads support 2021-09-09 17:24:34 +01:00
Germain Souquet 33c9471112 Make timelineSet public readonly 2021-09-09 17:23:45 +01:00
Travis Ralston 04fdcbb672 Exclude opt-in Element performance metrics from encryption
This is needed for the system which is meant to be monitoring this metadata. See https://github.com/matrix-org/matrix-react-sdk/pull/6766
2021-09-08 11:34:01 -06:00
Michael Telatynski 4d2286c5be Merge pull request #1888 from matrix-org/t3chguy/ts1234 2021-09-08 16:59:17 +01:00
Michael Telatynski 620e45e4dc fix typing mishap 2021-09-08 16:56:19 +01:00
Michael Telatynski fb97e74344 iterate PR based on feedback 2021-09-08 16:49:17 +01:00
Michael Telatynski 3bbb7df72d fix tests private field access 2021-09-08 12:57:12 +01:00
Michael Telatynski 58b08821bf Fix bugs identified by TS 2021-09-08 12:49:30 +01:00
Michael Telatynski 19bdfd5b84 Add missing interface 2021-09-08 12:49:22 +01:00
Michael Telatynski b6c857e2a2 Convert crypto/verification/* to Typescript 2021-09-08 12:34:44 +01:00
Michael Telatynski 0991626e85 Merge pull request #1884 from matrix-org/t3chguy/ts/123 2021-09-08 10:03:25 +01:00
Germain 7b10d4fac0 Merge pull request #1886 from janpawellek/janpawellek-fix-removeEventListener
Fix calling removeEventListener if it does not exist
2021-09-08 09:13:27 +01:00
Jan Pawellek 980dacaa87 Add explaination why the check for global.window.removeEventListener is necessary 2021-09-08 09:59:09 +02:00
Travis Ralston 3790e8abb4 Merge pull request #1887 from matrix-org/revert-1872-fix/call-notifs
Revert "Count notifications in encrypted rooms client-side"
2021-09-07 14:15:15 -06:00
Travis Ralston 14ba851daf Revert "Count notifications in encrypted rooms client-side" 2021-09-07 14:15:01 -06:00
Travis Ralston 95d80772d7 Merge pull request #1872 from SimonBrandner/fix/call-notifs
Count notifications in encrypted rooms client-side
2021-09-07 12:31:49 -06:00
RiotRobot 341e936082 v12.5.0-rc.1 2021-09-07 18:20:07 +01:00
RiotRobot e87399c4a1 Prepare changelog for v12.5.0-rc.1 2021-09-07 18:20:06 +01:00
Jan Pawellek a7678ea1e0 Fix calling removeEventListener if it does not exist 2021-09-07 17:12:21 +02:00
Germain 0388bd3b48 Merge pull request #1885 from matrix-org/gsouquet/improve-threads-dedupe 2021-09-07 15:31:56 +01:00
Germain Souquet f27327164b Merge branch 'develop' into gsouquet/improve-threads-dedupe 2021-09-07 15:28:05 +01:00
Germain a0f85075c1 Merge pull request #1882 from matrix-org/gsouquet/fix-replyinthread 2021-09-07 15:26:35 +01:00
Germain Souquet e97d18a03b Improve thread deduplication thread process
Some threads were still holding a reference to the original thread models they were assigned to, leading to some unexpected timeline rendering
2021-09-07 15:05:45 +01:00
Michael Telatynski f07cdb43b1 Fix dehydration Crypto type 2021-09-07 14:39:49 +01:00
Michael Telatynski da8c8f166d Improve jsdoc 2021-09-07 14:36:20 +01:00
Michael Telatynski 0830a30498 Fix tests 2021-09-07 14:36:10 +01:00
Michael Telatynski 68a2762b75 fix forwarding_curve25519_key_chain in OlmDevice::exportInboundGroupSession 2021-09-07 13:29:42 +01:00
Michael Telatynski 666e471369 Convert OlmDevice to Typescript 2021-09-07 13:22:27 +01:00
Germain Souquet d89f4a74f9 Fix replyInThread getter to match event m.in_reply_to structure 2021-09-07 10:20:01 +01:00
Germain Souquet 191535d01a Fix replyInThread getter to match event m.in_reply_to structure 2021-09-07 10:17:24 +01:00
Šimon Brandner 241203f804 Merge remote-tracking branch 'upstream/develop' into fix/call-notifs 2021-09-03 18:28:34 +02:00
Šimon Brandner 26cc7a0f64 Return a TODO
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-03 18:24:37 +02:00
Travis Ralston 324f9e58ea Merge pull request #1873 from SimonBrandner/feature/call-timer/18566
Give `MatrixCall` the capability to emit `LengthChanged` events
2021-09-02 22:12:31 -06:00
Dariusz Niemczyk e71519c638 Remove persisted log levels 2021-09-02 20:17:32 +02:00
Šimon Brandner 9b6cee0cab Merge remote-tracking branch 'upstream/develop' into fix/call-notifs 2021-09-02 16:32:26 +02:00
Germain Souquet 2a5f6a7b3d Move all relations event in thread timeline
If an event is related to another event who's in a thread, we want to make sure to move that event to the thread's timeline
2021-09-02 14:14:30 +01:00
Šimon Brandner cae03817cb Use source id directly
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-02 14:10:43 +02:00
Germain 37736184a0 Merge pull request #1876 from matrix-org/gsouquet/replies-fix-18717
Add reply events rendering hint
2021-09-02 08:37:11 +01:00
Šimon Brandner 9431a52abe Move DesktopCapturerSource out of global.d.ts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:22:51 +02:00
Šimon Brandner 13c664ad34 Remove Element-specifc screen-sharing code out of the js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:22:45 +02:00
Šimon Brandner d7640d9e15 Don't use a callback in setScreensharingEnabled()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-01 18:08:59 +02:00
Michael Telatynski 3c36be9839 Merge pull request #1877 from matrix-org/t3chguy/fix/18798 2021-09-01 16:08:37 +01:00
Michael Telatynski 9dce7bbb97 Improve typing around join rules 2021-09-01 15:54:04 +01:00
Germain Souquet fa44300abc Add reply events rendering hint
Adds an unstable hint for clients to know whether to render an event in the room timeline or in a thread

There are currently no MSC associated with that unstable "io.element.in_thread" property as I am only experimenting before settling on a specific approach
2021-09-01 12:12:01 +01:00
RiotRobot 2783d162b7 Resetting package fields for development 2021-08-31 13:32:29 +01:00
RiotRobot 39566dbd4f Merge branch 'master' into develop 2021-08-31 13:32:28 +01:00
RiotRobot f8186add92 v12.4.0 2021-08-31 13:29:19 +01:00
RiotRobot c15f22e81d Prepare changelog for v12.4.0 2021-08-31 13:29:18 +01:00
Michael Telatynski 052ac8f95b Merge pull request #1874 from SimonBrandner/task/call-types
Improve TypeScript in `MatrixCall`
2021-08-31 13:06:23 +01:00
Šimon Brandner c37f8ba4c7 Make placeCalls public
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-31 14:00:37 +02:00
Germain 7e2458d5b5 Merge pull request #1828 from matrix-org/gsouquet/threaded-messaging-2349 2021-08-31 09:19:48 +01:00
Germain fe989177bb Merge pull request #1875 from psrpinto/fix/browser-example 2021-08-31 09:15:39 +01:00
Paulo Pinto 412645f025 Use HTTPS
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto e527c46f34 Add empty favicon
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto da471b57ad Define document encoding
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Paulo Pinto a4ff97c8df Add required lang attribute
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-08-30 13:32:30 +01:00
Šimon Brandner 411fc47f28 Improve TypeScript in MatrixCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-28 09:27:47 +02:00
Šimon Brandner b97e3113d1 Add/improve call event content types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-28 09:27:34 +02:00
Šimon Brandner f107d63fab Give MatrixCall the capability to emit LengthChanged events
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-27 15:06:55 +02:00
Šimon Brandner a65ac5a82b Count notifications in encrypted rooms client-side
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-27 08:38:59 +02:00
Šimon Brandner c7c1db00e8 Use getWirePushActions()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:40 +02:00
Šimon Brandner 824f93daa3 Add wirePushRules
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:39 +02:00
Šimon Brandner 9fec2f8c58 Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:39 +02:00
Šimon Brandner 0b46389176 currentCount -> currentHighlightCount
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:38 +02:00
Šimon Brandner db03f16b8e Add a type
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-26 20:01:37 +02:00
David Baker f83fcb1a18 Merge pull request #1870 from matrix-org/dbkr/allchange102
Update changelog generator
2021-08-26 09:42:37 +01:00
David Baker ba0f1f86b9 Merge pull request #1871 from matrix-org/dbkr/verification_request_cancel
Fix verification request cancellation
2021-08-26 09:40:53 +01:00
David Baker 81d349d993 Clarify operator precedence 2021-08-25 20:55:03 +01:00
David Baker d65d2b94b9 Fix verification request cancellation
If you started trying to verify against your other devices and
then cancelled the dialog, the device destination in the cancellation
to-device message would be 'null', so the cancellation wouldn't go
anywhere.

If we haven't yet picked a specific device to talk to, send the
cancellation to all devices (like we did the request).
2021-08-25 18:37:45 +01:00
David Baker acd52bc070 Update changelog generator 2021-08-25 17:19:25 +01:00
David Baker a64339763f Merge pull request #1869 from matrix-org/dbkr/yarn_upgrade_20210824
yarn upgrade
2021-08-25 16:39:45 +01:00
David Baker 4f21b95bbf Merge pull request #1865 from SimonBrandner/feature/voice-activity
Give `CallFeed` the capability to emit on volume changes
2021-08-25 14:40:24 +01:00
David Baker 03e4dcf1ed yarn upgrade 2021-08-25 10:57:37 +01:00
Germain Souquet 3c56d1372d Appease the linter 2021-08-25 09:15:07 +01:00
Germain Souquet 27ecdef09b Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-25 08:53:29 +01:00
RiotRobot 121e8a51c1 v12.4.0-rc.1 2021-08-24 17:04:11 +01:00
RiotRobot 88e3ada701 Prepare changelog for v12.4.0-rc.1 2021-08-24 17:04:10 +01:00
Dariusz Niemczyk 92d822d494 Merge pull request #1847 from matrix-org/jaller94-patch-2
SSSSCryptoCallbacks.getSecretStorageKey: Test if delegateCryptoCallbacks has getSecretStorageKey
2021-08-24 15:24:54 +02:00
Travis Ralston 62c93f54cc Merge pull request #1818 from matrix-org/t3chguy/fix/18089
Add support for /hierarchy API and fall back to /spaces API if needed
2021-08-23 15:51:28 -06:00
Germain Souquet 500c2ed4f2 Remove thread spec 2021-08-23 15:03:10 +01:00
Šimon Brandner 425362edfe Emit Speaking events from CallFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 14:48:26 +02:00
Germain Souquet 9c554f9b3d Add experimentalThreadSupport flag 2021-08-19 11:18:46 +01:00
Germain Souquet b7a5f81f95 Add experimental indicators for all threading related features 2021-08-19 11:10:20 +01:00
Šimon Brandner 0746a64c83 Add some docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 12:03:57 +02:00
Šimon Brandner 22cd475d22 Add missing types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:26 +02:00
Šimon Brandner 6904f6b36f Emit volume changes
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:19 +02:00
Šimon Brandner e25a649e1a Add webkitAudioContext to window
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-19 11:49:18 +02:00
Germain Souquet 0eb8cbee4d Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-19 10:45:23 +01:00
Dariusz Niemczyk 96e8f65af7 Merge pull request #1864 from matrix-org/palid/dx/typescriptify-password-reset
Add necessary changes for PasswordReset migration
2021-08-19 10:42:59 +02:00
Dariusz Niemczyk 8fb036ba2d Make eslint happy 2021-08-19 09:46:54 +02:00
Dariusz Niemczyk 0a10fa12ef Add necessary changes for PasswordReset migration 2021-08-19 08:59:55 +02:00
David Baker e30dad7913 Merge pull request #1859 from SimonBrandner/task/revert-quick-voip-fix
Revert quick VoIP fix
2021-08-18 18:50:37 +01:00
Travis Ralston c2a0c02898 Merge pull request #1850 from matrix-org/travis/fsdk/filemeta
Add method for including extra fields when uploading to a tree space
2021-08-18 07:29:49 -06:00
Dariusz Niemczyk 7c1e89f86a Merge pull request #1849 from matrix-org/jaller94-patch-3
Make delegateCryptoCallbacks an optional parameter of EncryptionSetupBuilder and SSSSCryptoCallbacks
2021-08-18 14:01:44 +02:00
Germain 29f8a4cba2 Merge pull request #1853 from matrix-org/gsouquet/ts-components-migration
Expose IMyDevice interface
2021-08-18 12:35:57 +01:00
Germain Souquet b884accc99 Dispatch thread events 2021-08-17 17:43:00 +01:00
Germain Souquet d56540320e Merge branch 'develop' into gsouquet/threaded-messaging-2349 2021-08-17 11:14:10 +01:00
Germain Souquet 526fe7e9a4 use timeline set instead of an array of events 2021-08-17 11:10:20 +01:00
Germain Souquet aa696c4c15 Move threading logic to the Sync API 2021-08-17 10:37:34 +01:00
RiotRobot 3216d7e5a7 Merge branch 'master' into develop 2021-08-17 09:27:42 +01:00
RiotRobot b68f649414 v12.3.1 2021-08-17 09:24:43 +01:00
RiotRobot 543f35d9a8 Prepare changelog for v12.3.1 2021-08-17 09:24:42 +01:00
David Baker 03d42a98cd Merge pull request #1862 from matrix-org/dbkr/release_script_handle_no_signing_id_rel_take_2
Fix release script to handle no signing ID
2021-08-17 09:19:30 +01:00
David Baker 4f48554cd2 Merge pull request #1861 from matrix-org/dbkr/release_script_handle_no_signing_id_rel
Fix release script to handle no signing ID
2021-08-17 09:13:59 +01:00
David Baker 85d9c0f403 Merge pull request #1860 from matrix-org/dbkr/1858_rel
Fix multiple VoIP regressions
2021-08-17 09:11:36 +01:00
David Baker 66b4279dd8 Fix release script to handle no signing ID
We now use the file for stuff other than just the signing ID, so
the file may be present but with no signing ID: handle this case.
2021-08-17 09:10:11 +01:00
Šimon Brandner 1ee84fbeca Fix regressions
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-17 09:06:53 +01:00
Šimon Brandner c0e15b206a Revert "Fix regressions"
This reverts commit bd8690de57.
2021-08-17 10:03:46 +02:00
David Baker 290575ba65 Merge pull request #1858 from SimonBrandner/fix/voip
Fix broken voice calls, no ringing and broken call notifications
2021-08-17 08:54:11 +01:00
Šimon Brandner bd8690de57 Fix regressions
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-17 08:09:14 +02:00
Šimon Brandner 0d09f87777 Revert "Revert "Fix glare related regressions""
This reverts commit 4c552cc350.
2021-08-17 07:33:40 +02:00
David Baker 0f45b7e516 Merge pull request #1857 from matrix-org/revert-1851-fix/glare/18538
Revert "Fix glare related regressions"
2021-08-16 23:18:13 +01:00
David Baker 4c552cc350 Revert "Fix glare related regressions" 2021-08-16 23:06:57 +01:00
David Baker 6a87f54292 Merge pull request #1856 from matrix-org/dbkr/release_script_handle_no_signing_id
Fix release script to handle no signing ID
2021-08-16 15:32:09 +01:00
David Baker bfb2c5aad0 Fix release script to handle no signing ID
We now use the file for stuff other than just the signing ID, so
the file may be present but with no signing ID: handle this case.
2021-08-16 14:38:42 +01:00
RiotRobot db898e7bcf Resetting package fields for development 2021-08-16 13:55:37 +01:00
RiotRobot dbebbe3a19 Merge branch 'master' into develop 2021-08-16 13:55:37 +01:00
RiotRobot 10e8aff46a v12.3.0 2021-08-16 13:52:36 +01:00
RiotRobot 81e2ea6a61 Prepare changelog for v12.3.0 2021-08-16 13:52:35 +01:00
David Baker 2594c59250 Merge pull request #1855 from matrix-org/dbkr/update_allchange
Update changelog generator
2021-08-16 13:31:28 +01:00
David Baker b56a4ee6ec Fix dates 2021-08-16 13:24:11 +01:00
David Baker ab524ad3b4 Update changelog generator 2021-08-16 13:18:06 +01:00
David Baker 83bf80feec Merge pull request #1854 from SimonBrandner/release/fix/glare/18538
[Release] Fix glare related regressions
2021-08-16 12:40:08 +01:00
Šimon Brandner b9e5417218 Await handleCallEvent()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:22 +02:00
Šimon Brandner 082169a756 Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:21 +02:00
Šimon Brandner cdc87d371c Reformat some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:21 +02:00
Šimon Brandner f8ec3bc591 Remove weird method call
If we can get localUsermediaStream gotUserMediaForAnswer() has alredy been called before

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:21 +02:00
Šimon Brandner 85f4651e9b Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:20 +02:00
Šimon Brandner da53241c48 Remove unnecessary logs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:20 +02:00
Šimon Brandner fa265296e8 Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:30:19 +02:00
David Baker ccf296ea92 Merge pull request #1851 from SimonBrandner/fix/glare/18538
Fix glare related regressions
2021-08-16 12:27:51 +01:00
Šimon Brandner d65b48204b Await handleCallEvent()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-16 13:14:04 +02:00
J. Ryan Stinnett 977720cee4 Merge pull request #1852 from matrix-org/jryans/comment-to-json
Clarify usages of event's toJSON
2021-08-16 10:30:50 +01:00
Christian Paul 3c7cdb1da8 Update src/crypto/EncryptionSetup.ts 2021-08-16 10:36:21 +02:00
Germain Souquet dd80e744ff Expose IMyDevice interface 2021-08-16 09:06:52 +01:00
J. Ryan Stinnett 0b12e37459 Clarify usages of event's toJSON 2021-08-16 09:04:14 +01:00
Šimon Brandner 51e815f0cb Fix glare
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:03:18 +02:00
Šimon Brandner f3856d569d Reformat some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:03:08 +02:00
Šimon Brandner e6d1909f0b Remove weird method call
If we can get localUsermediaStream gotUserMediaForAnswer() has alredy been called before

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 13:02:39 +02:00
Šimon Brandner 408976a199 Simplifie some code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:58:38 +02:00
Šimon Brandner 4da49d926b Remove unnecessary logs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:57:27 +02:00
Šimon Brandner 75750ed760 Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-15 12:56:48 +02:00
Travis Ralston 2f74779bf1 Merge pull request #1772 from matrix-org/t3chguy/ts/12
Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to TS
2021-08-13 10:08:54 -06:00
Michael Telatynski 7b038393b1 Merge pull request #1792 from matrix-org/t3chguy/kill-groups
Deprecate groups APIs
2021-08-13 16:39:47 +01:00
Travis Ralston 768c0e7f77 Add method for including extra fields when uploading to a tree space
We ultimately need this to include things like `info` and other metadata in a specific environment.
2021-08-13 09:25:19 -06:00
Christian Paul c6009b1056 Make delegateCryptoCallbacks parameter optional on EncryptionSetupBuilder 2021-08-13 16:04:47 +02:00
Christian Paul 2a280afa88 Make SSSSCryptoCallbacks.delegateCryptoCallbacks optional 2021-08-13 15:58:44 +02:00
Christian Paul be980f4bc9 Update src/crypto/EncryptionSetup.ts 2021-08-13 15:38:22 +02:00
Dariusz Niemczyk 78118e9442 Merge pull request #1846 from matrix-org/jaller94-patch-1
SSSSCryptoCallbacks.getSecretStorageKey return Promise<null> if no key was found
2021-08-13 15:36:53 +02:00
Christian Paul a2c10a7913 SSSSCryptoCallbacks.getSecretStorageKey: Test if delegateCryptoCallbacks has getSecretStorageKey 2021-08-13 15:28:01 +02:00
Christian Paul 7c4f7c9dad SSSSCryptoCallbacks.getSecretStorageKey: Return null, if no key found 2021-08-13 15:23:47 +02:00
Christian Paul 62058e6d48 SSSSCryptoCallbacks.getSecretStorageKey may return Promise<void>
If no result is found, the function may reach the last line without `return` ever being called.
2021-08-13 15:15:20 +02:00
Dariusz Niemczyk a2f514b544 Merge pull request #1834 from matrix-org/palid/fix/signaling-state-errors
Fix temporary call messages being handled without call
2021-08-13 14:17:30 +02:00
RiotRobot 67434bc5d4 v12.3.0-rc.2 2021-08-12 15:11:35 +01:00
RiotRobot 167183775e Prepare changelog for v12.3.0-rc.2 2021-08-12 15:11:35 +01:00
David Baker 4ce6313126 Merge pull request #1840 from matrix-org/dbkr/allchange_npm
Use changelog generator from npm
2021-08-12 15:02:12 +01:00
David Baker 1298ed61b6 Merge pull request #1842 from matrix-org/dbkr/fix_types_build
Fix the types in shipped package
2021-08-12 14:33:01 +01:00
Michael Telatynski 14b424ee94 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/18089 2021-08-12 11:55:04 +01:00
David Baker 2bc1f2fe21 Apparently target matters when not emitting 2021-08-12 11:54:19 +01:00
David Baker b392dbf6f8 Fix the types building
1829 added the spec files to 'src', presumably because they just
were't type checked otherwise, but this chaqnged the implicit rootDir
so the lib directory we shipped has types in src/ and spec/ which
meant lib/index.d.ts was at lib/src/index.d.ts.

Specify the rootDir on the build tsconfig to put it back in the
right place, and also try to make the base tsconfig never emit
to avoid it ever generating .d.ts files in silly places. Move all
the optoins related to emitting to -build since they aren't relevant
in the main one.

Is this a sensible way to do this? It doesn't feel terribly elegant.

Fixes https://github.com/vector-im/element-web/issues/18503
Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1829
2021-08-12 11:46:49 +01:00
Travis Ralston 696b3ef1ce Merge pull request #1841 from matrix-org/travis/fsdk/fix-delete
Fix conditional on returning file tree spaces
2021-08-11 15:21:20 -06:00
Travis Ralston 4e2ee3b3a8 It helps to fix the other tests too 2021-08-11 15:18:24 -06:00
Travis Ralston 1f9fab9a0c Fix conditional on returning file tree spaces
If a tree space was deleted then it'll still hold a Room object for us, which is not much help if we're effectively trying to get the joined trees.
2021-08-11 15:11:13 -06:00
David Baker 6170796ed8 Use changelog generator from npm 2021-08-11 21:50:13 +01:00
Michael Telatynski d91d1cea34 Apply suggestions from code review
Co-authored-by: Travis Ralston <travisr@matrix.org>
2021-08-11 21:47:51 +01:00
Michael Telatynski 69ba32683c Iterate PR based on feedback 2021-08-11 21:45:51 +01:00
Michael Telatynski fc78e63ad1 Fix RoomHierarchy comments 2021-08-11 20:57:40 +01:00
Michael Telatynski db212a555d Update to latest version of MSC2946 API s/next_token/next_batch/ 2021-08-11 20:49:23 +01:00
RiotRobot 429f32dacb v12.3.0-rc.1 2021-08-11 15:41:40 +01:00
David Baker ad036104e6 Changelog 2021-08-11 15:41:00 +01:00
Dariusz Niemczyk 4607f0177c Fix invalid string quotes 2021-08-11 14:39:00 +02:00
Dariusz Niemczyk d7dbaeba46 Update src/webrtc/callEventHandler.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:20 +02:00
Dariusz Niemczyk 3e94db1837 Update src/webrtc/call.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:13 +02:00
Dariusz Niemczyk 4fd77c2f05 Update src/webrtc/callEventHandler.ts
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-11 14:38:08 +02:00
Dariusz Niemczyk 9fe05e7d40 Fix temporary call messages being handled without call
In cases where a rogue client, or just a simple bug, sends a temporary
call message, like CallNegotiate the messages should just be discarded
if there's no actual p2p connection created.
2021-08-11 14:28:10 +02:00
Matthew Hodgson 946dcd0301 Merge pull request #1838 from matrix-org/matthew/fix-turn
fix TURN by fixing regression preventing multiple ICE candidates from sending.
2021-08-11 11:35:36 +01:00
Matthew Hodgson 97d718e850 tab->space 2021-08-11 11:31:40 +01:00
Matthew Hodgson 913b2d148c clarify blank candidate comment 2021-08-11 11:30:42 +01:00
Matthew Hodgson 916f6a6126 fix TURN by fixing regression preventing multiple candidates from sending
In https://github.com/matrix-org/matrix-js-sdk/commit/eafecd36bc860e79d9aa280a7e60455dbd7c6f67#diff-df33fb6c18a9b5896cd500875824d6c10980d42c92b21cedac6d8f570a8d52b7L1639
the candidateSendTries reset was accidentally lost, meaning that we never send more than one m.call.candidates, breaking trickle ICE.

Also, we leak blank candidates out in m.call.candidates on Safari and FF, so fix that too.
2021-08-11 04:42:32 +01:00
David Baker 7dbee19456 Merge pull request #1837 from matrix-org/dbkr/fix_blank_membership_events
Fix blank profile in join events
2021-08-10 22:13:16 +01:00
David Baker 2375b00b52 more null checking 2021-08-10 22:09:01 +01:00
David Baker 13cdca028f Fix blank profile in join events
This definitely needs explanation, but hopefully the comment is sufficient.

Type: defect
Fixes https://github.com/vector-im/element-web/issues/18321
Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/1765
2021-08-10 21:59:40 +01:00
Germain Souquet e5a67500e4 Stop checking for event type as it is sometimes encrypted 2021-08-10 16:21:59 +02:00
David Baker 264636c879 Merge pull request #1835 from matrix-org/travis/fsdk/get-perm
Add support for reading permissions from tree spaces
2021-08-10 11:08:51 +01:00
Michael Telatynski 9b50455049 Iterate PR, merge types with @types/PushRules 2021-08-10 11:03:05 +01:00
Michael Telatynski a531446396 tidy a bit more 2021-08-10 10:46:13 +01:00
Michael Telatynski f876c35283 Fix some bad conversion artifacts 2021-08-10 10:34:05 +01:00
Michael Telatynski 1bcec53c6b delint 2021-08-10 10:22:40 +01:00
Michael Telatynski 0ad5ef4e9b Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/12 2021-08-10 10:21:13 +01:00
David Baker 94726aa1d5 Merge pull request #1832 from matrix-org/dbkr/preview_changelog
Add changelog preview action
2021-08-09 20:21:22 +01:00
Travis Ralston 8da2f91e22 Add support for reading permissions from tree spaces
This was originally thought to be able to be performed in another way, but unfortunately isn't as easy as originally predicted.
2021-08-09 08:43:46 -06:00
Dariusz Niemczyk b03555a50b Merge pull request #1833 from SimonBrandner/fix/screensharingc-crash
Fix error on turning off screensharing
2021-08-08 21:08:52 +02:00
Šimon Brandner b61312feca Fix screensharing crash
We have to first stop the track and only then delete the feed as stopping the tracks depends on the feed being defined

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-08 11:10:08 +02:00
David Baker ea9459d330 Add changelog preview action 2021-08-06 23:05:00 +01:00
Travis Ralston 3edfe70a7b Merge pull request #1831 from matrix-org/travis/fsdk/expose-get-file
Expose a getFileEvent() function off a MSC3089Branch
2021-08-06 09:15:24 -06:00
Travis Ralston 5482a19979 Expose a getFileEvent() function off a MSC3089Branch 2021-08-06 09:09:03 -06:00
Michael Telatynski e1678aaf11 Merge pull request #1830 from matrix-org/t3chguy/js-docfix
Fix type documentation on MatrixClient::setRoomMutePushRule
2021-08-06 14:48:05 +01:00
Michael Telatynski 2bdb570f3c Fix type documentation on MatrixClient::setRoomMutePushRule 2021-08-06 09:38:50 +01:00
Germain 3723b2da39 Merge pull request #1829 from matrix-org/gsouquet/jest-typescript 2021-08-05 11:00:40 +01:00
Germain Souquet b63af00579 Exclude tests when building browserify package 2021-08-05 11:56:26 +02:00
David Baker 4ed6fbe5fe Merge pull request #1820 from SimonBrandner/fix/hangup-reason/18219
Send `user_hangup` reason if the opponent supports it
2021-08-05 10:41:28 +01:00
Germain Souquet 026260502b Fix linting issues in TypeScript test files 2021-08-05 11:37:27 +02:00
Germain Souquet c45bc91389 Include all TypeScript @types listed in project dependency
Fixes vector-im/element-web#17743

The package @types/jest was not discovered because if types is specified, only packages listed will be included in the global scope.
2021-08-05 11:16:28 +02:00
Germain Souquet 18113900fe Implementation of event threading 2021-08-04 17:28:08 +02:00
David Baker 69358f8372 Merge pull request #1825 from matrix-org/dbkr/codeowners
Add CODEOWNERS
2021-08-04 13:21:37 +01:00
David Baker 8800c6d2e0 Add CODEOWNERS
This should make github auto review request from the element-web team
2021-08-04 13:16:15 +01:00
David Baker b63de6a902 Merge pull request #1823 from matrix-org/dbkr/argh_the_linting
Update eslint plugin & fix silly indenting
2021-08-04 10:03:20 +01:00
David Baker b1cec7d6a5 Merge pull request #1824 from matrix-org/dbkr/allchange
Switch to new changelog generator
2021-08-04 10:03:08 +01:00
David Baker 08821e499c Switch to new changelog generator 2021-08-03 17:51:42 +01:00
David Baker 3ca84cfc49 Update eslint plugin & fix silly indenting
As per matrix-org/eslint-plugin-matrix-org#15
this caused a bunch of silly indenting to creep in, so this fixes it
back to the previous style.
2021-08-03 17:15:25 +01:00
David Baker be8ecce995 Merge pull request #1821 from SimonBrandner/task/cleanup
Clean-up some VoIP code
2021-08-03 13:18:47 +01:00
Šimon Brandner 775c3465bb Rename so that VS Code spell checker shutsup
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 10:05:05 +02:00
Šimon Brandner 7484e3c032 Improve wording
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:48:23 +02:00
Šimon Brandner b93e79274f Move for better organization
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:45:27 +02:00
Šimon Brandner fee6a96350 Rename for consistency
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:44:44 +02:00
Šimon Brandner ac2c09108f Improve styling a bit
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:42:20 +02:00
Šimon Brandner 5b13785115 Make a few things into one liners
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:40:04 +02:00
Šimon Brandner dd3c53edac Extract weSentTheEvent into a variable
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:37:41 +02:00
Šimon Brandner 904071ebe6 Extract type into a variable
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:33:40 +02:00
Šimon Brandner 7e0aeb425d Move things we can into a switch statement
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-03 09:31:06 +02:00
Šimon Brandner f4e985b5cc Fix typos
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:38:05 +02:00
Šimon Brandner 03134832e9 Standardise screensharing casing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:30:33 +02:00
Šimon Brandner dbb0f66094 Remove unused mute state stuff
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:29:09 +02:00
Šimon Brandner 4631cd1fe3 Make localUsermediaStream into a getter
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:28:49 +02:00
Šimon Brandner bc07ad5909 Make localScreenSharingStream into a getter
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:25:55 +02:00
Šimon Brandner 8b0c4a0efb Merge remote-tracking branch 'upstream/develop' into task/cleanup
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:17:14 +02:00
Šimon Brandner eac77a6695 Remove config as it wasn't used
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:15:36 +02:00
Šimon Brandner aae8f1c706 screenSharingStream -> localScreenSharingStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:14:59 +02:00
Šimon Brandner 1e281f6838 localAVStream -> localUsermediaStream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-08-02 21:02:09 +02:00
David Baker aef89e4a26 Merge pull request #1812 from SimonBrandner/feature/muting
Support for MSC3291: Muting in VoIP calls
2021-08-02 17:20:42 +01:00
RiotRobot 8a1bc98d4e Resetting package fields for development 2021-08-02 13:03:59 +01:00
RiotRobot 8b2d5959ec Merge branch 'master' into develop 2021-08-02 13:03:59 +01:00
RiotRobot 4891446dc3 v12.2.0 2021-08-02 13:00:06 +01:00
David Baker cace8f232f Changelog for v12.2.0 2021-08-02 12:45:25 +01:00
Michael Telatynski 88b8b24629 Merge room hierarchy pages 2021-07-30 11:01:08 +01:00
Šimon Brandner b15ba8c5e3 Send hangup reason if the opponent supports it
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-30 08:54:39 +02:00
Michael Telatynski 58c60d85e7 Refactor Space Hierarchy stuff in preparation for pagination 2021-07-29 17:35:17 +01:00
Hubert Chathi 529fe93ab1 Merge pull request #1819 from uhoreg/fix_aes_iv
Only clear bit 63 when we create the IV
2021-07-29 12:07:54 -04:00
Hubert Chathi 600438d02e only clear bit 63 when we create the IV
If we got the IV from somewhere else, we should just use it as-is, rather than
trying to re-clear the bit.
2021-07-29 11:58:08 -04:00
Michael Telatynski 21a357c433 fix typo 2021-07-29 15:00:39 +01:00
Michael Telatynski 9c05077c26 Merge pull request #1816 from matrix-org/t3chguy/hidden-names 2021-07-29 12:55:40 +01:00
Michael Telatynski d49d6936a5 Add support for /children API and fall back to /spaces API if M_UNRECOGNIZED 2021-07-29 12:16:34 +01:00
Šimon Brandner e6696f78f4 Allow recieving unprefixed version of m.call.sdp_stream_metadata_changed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-29 13:00:48 +02:00
Michael Telatynski f2da3ba6ae Apply hidden char check to rawDisplayName too 2021-07-29 11:00:30 +01:00
Šimon Brandner db5de4c6e4 Merge remote-tracking branch 'upstream/develop' into feature/muting 2021-07-29 09:10:38 +02:00
Šimon Brandner ca042b3647 Fix handling when remoteSDPStreamMetadata is null
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 20:12:21 +02:00
Šimon Brandner 87fd3521ea Fix types
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 20:11:51 +02:00
Šimon Brandner b3c66848e2 Handle missing params
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 19:06:24 +02:00
Šimon Brandner 606aa29381 Use recursivelyAssign
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 18:37:48 +02:00
Šimon Brandner 2d25150a21 Write tests for recursivelyAssign()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 17:57:16 +02:00
Šimon Brandner ca7a457094 Add recursivelyAssign()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 17:56:51 +02:00
Šimon Brandner 23ab3e3ec0 Test mute metadata gets mapped onto feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:29:28 +02:00
Šimon Brandner b11a8459d8 Add a method for setting remoteSDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:16:24 +02:00
Šimon Brandner 057eb0f2a5 Make mute state props mandetory
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 15:00:21 +02:00
Šimon Brandner 8de6c5aad1 Fix typo which caused all feeds to be muted by default
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 14:59:57 +02:00
Šimon Brandner 6fda2a0c57 Handle purpose changes
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 14:15:00 +02:00
David Baker efeeba265f Merge pull request #1813 from SimonBrandner/fix/call-view/18221
Handle DTMF support
2021-07-28 11:27:09 +01:00
Šimon Brandner 35ca7fdc48 Fix call transfer support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-28 08:15:50 +02:00
Šimon Brandner 959e3f68a4 Handle DTMF support
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 17:51:13 +02:00
RiotRobot f20b3211e6 v12.2.0-rc.1 2021-07-27 15:43:44 +01:00
RiotRobot 9b5302ebf2 Prepare changelog for v12.2.0-rc.1 2021-07-27 15:43:43 +01:00
Šimon Brandner 19f90f56be Merge remote-tracking branch 'upstream/develop' into feature/muting 2021-07-27 16:40:07 +02:00
David Baker a69d9a9f20 Merge pull request #1685 from SimonBrandner/fix/12652/screen-share
Support for screen-sharing using multi-stream VoIP (MSC3077)
2021-07-27 15:36:40 +01:00
Michael Telatynski c5e113d0ca Merge pull request #1808 from matrix-org/t3chguy/ts/13 2021-07-27 15:36:21 +01:00
Šimon Brandner 1df90f8fc7 Basic implementation of MSC3291
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:31:13 +02:00
Šimon Brandner 8e6040ad6f Give CallFeed a mute state
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:23:58 +02:00
Šimon Brandner 21f107d734 Add types for MSC3291
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-27 14:19:54 +02:00
Dariusz Niemczyk e6d40f0c17 Merge pull request #1810 from matrix-org/fix/update-eslint-config
Update ESLint config
2021-07-27 09:51:58 +02:00
Dariusz Niemczyk 6b8bb46790 Update ESLint config 2021-07-26 16:37:47 +02:00
Michael Telatynski 05c3236e50 Merge pull request #1807 from bradtgmurray/improve-event-clear-event-state 2021-07-26 12:55:49 +01:00
Brad Murray 4f8c20ddae Fix reattempting decryption check 2021-07-26 07:49:42 -04:00
Brad Murray 8aa283d994 Fix lint in tests 2021-07-25 11:45:09 -04:00
Brad Murray e85b3b6a8d Fix the tests and add a test 2021-07-25 11:41:08 -04:00
Brad Murray 04cd9b55f7 Merge remote-tracking branch 'upstream/develop' into improve-event-clear-event-state 2021-07-25 11:23:11 -04:00
Brad Murray faf237e588 Respect the comment in getClearContent 2021-07-25 11:19:36 -04:00
Šimon Brandner 019abc7ad8 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-25 08:13:23 +02:00
Michael Telatynski db4b6fe2af Update to beta better-docs to fix the gendocs issue 2021-07-24 12:10:45 +01:00
Michael Telatynski c340e6f1ba delint and fix tests 2021-07-24 12:01:23 +01:00
Michael Telatynski f227a307c1 Fix some confused signatures/types 2021-07-24 11:40:30 +01:00
Michael Telatynski 011a2acfd2 Convert crypto/store/* to Typescript 2021-07-24 11:35:30 +01:00
Michael Telatynski c2b5b14d26 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/12
 Conflicts:
	src/client.ts
	src/interactive-auth.ts
	src/models/search-result.ts
2021-07-23 23:46:15 +01:00
Brad Murray ad79a64f1c Address review feedback 2021-07-23 16:25:39 -04:00
Brad Murray 91f5df1e48 Fix incorrect indentation 2021-07-23 16:16:39 -04:00
Brad Murray 980d6fc2ae Differentiate between an undefined clearEvent and a falsey property of the clearEvent 2021-07-23 16:13:46 -04:00
Brad Murray 5aa60b8056 Clean up Event.clearEvent handling 2021-07-23 15:56:24 -04:00
Michael Telatynski dc154a00a8 Merge pull request #1806 from matrix-org/t3chguy/eslint1 2021-07-23 16:24:58 +01:00
Michael Telatynski a087acf65d Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/eslint1 2021-07-23 16:08:31 +01:00
Michael Telatynski 05d0755b6a Update matrix-org-eslint-plugin and tighten max warning limit
Whilst it is down, make the most of it!
2021-07-23 16:07:49 +01:00
Michael Telatynski cd86aa7d9e Merge pull request #1745 from matrix-org/t3chguy/fix/17686 2021-07-23 09:09:15 +01:00
Šimon Brandner fcbbcc0398 Always return true for voice calls
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-22 09:24:59 +02:00
Travis Ralston 2bd99b5cb0 Merge pull request #1771 from matrix-org/j94/functional-members
Functional members
2021-07-21 18:25:14 -06:00
David Baker 82d248cb8e Merge pull request #1802 from matrix-org/dbkr/yarn_upgrade_210721
yarn upgrade
2021-07-21 16:58:49 +01:00
David Baker ce2803ebaa yarn upgrade 2021-07-21 15:37:19 +01:00
Germain e85038e888 Merge pull request #1801 from matrix-org/gsouquet/localecompare-perf
Improve calculateRoomName performances by using Intl.Collator
2021-07-21 15:28:22 +01:00
Šimon Brandner ceafb1c5d0 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-21 15:58:46 +02:00
Michael Telatynski a87858840b Merge pull request #1798 from matrix-org/t3chguy/eslint 2021-07-21 14:53:12 +01:00
Germain Souquet f708c47385 Improve calculateRoomName performances by using Intl.Collator 2021-07-21 14:18:22 +02:00
Michael Telatynski 9eb918f701 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-21 10:56:21 +01:00
Christian Paul b4bffd2700 Merge remote-tracking branch 'origin/develop' into j94/functional-members 2021-07-21 11:23:22 +02:00
Šimon Brandner 80d7c88b40 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-20 15:47:36 +02:00
Travis Ralston fb315ac75b Merge pull request #1799 from matrix-org/travis/error1
Actually print IndexedDB command error if there is one
2021-07-20 07:38:43 -06:00
Šimon Brandner bee840ce16 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-20 13:30:01 +02:00
Christian Paul afa67688f8 Add test for service member who is not a room member 2021-07-20 12:51:08 +02:00
Michael Telatynski 0c06e47782 Update eslint-plugin-matrix-org 2021-07-20 09:15:26 +01:00
Travis Ralston ea646c673b Actually print idb command error if there is one 2021-07-19 23:31:39 -06:00
Michael Telatynski 5cf6684129 Conform to new typescript eslint rules 2021-07-19 22:47:32 +01:00
Christian Paul cb2b9619ab Replace .indexOf() !== -1 with .includes() for readability 2021-07-19 18:22:29 +02:00
Christian Paul b8c2a57829 Add test with multiple service_members 2021-07-19 18:17:39 +02:00
Christian Paul 718a22f574 Merge branch 'j94/functional-members' of github.com:matrix-org/matrix-js-sdk into j94/functional-members 2021-07-19 18:01:05 +02:00
Christian Paul 324cd886a2 Merge remote-tracking branch 'origin/develop' into j94/functional-members 2021-07-19 18:00:59 +02:00
Christian Paul 4cf655785e Add tests 2021-07-19 17:59:11 +02:00
Michael Telatynski f686d70157 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-19 16:47:39 +01:00
RiotRobot b936b4a2ce Resetting package fields for development 2021-07-19 15:52:47 +01:00
RiotRobot 026839d7e7 Merge branch 'master' into develop 2021-07-19 15:52:46 +01:00
RiotRobot 830a4a741e v12.1.0 2021-07-19 15:49:55 +01:00
RiotRobot a3d9ed19a4 Prepare changelog for v12.1.0 2021-07-19 15:49:54 +01:00
David Baker 8f9f5fd5b0 Merge pull request #1796 from matrix-org/dbkr/upgrade_eslint_plugin_matrix_org
Update eslint-plugin-matrix-org
2021-07-19 15:33:45 +01:00
David Baker 805267f97f Update eslint-plugin-matrix-org 2021-07-19 15:23:20 +01:00
Christian Paul 4a1cd159a4 Update event.ts 2021-07-19 11:53:43 +02:00
Christian Paul 7129c7c7af Update src/models/room.ts
Co-authored-by: Travis Ralston <travisr@matrix.org>
2021-07-19 10:56:15 +02:00
Travis Ralston ecc9fda28b Merge pull request #1778 from matrix-org/travis/notifications-2
Add minimal types for "notification settings" UI
2021-07-16 23:53:13 -06:00
Travis Ralston 7f7c3cbea5 Fix exclusion types 2021-07-16 23:48:41 -06:00
Travis Ralston fc8cd39d1a Optionalize opts 2021-07-16 16:32:49 -06:00
Travis Ralston 589c66bb12 Re-appease linter 2021-07-16 16:30:06 -06:00
Travis Ralston 16fe5a91d5 Consolidate types 2021-07-16 16:27:50 -06:00
Travis Ralston b57b7a65ee Merge branch 'develop' into travis/notifications-2 2021-07-16 16:26:05 -06:00
Šimon Brandner 019e93e76c Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-16 21:38:10 +02:00
Michael Telatynski 69415ba2c3 Merge pull request #1791 from matrix-org/t3chguy/querystring 2021-07-16 19:57:22 +01:00
Šimon Brandner 1607ad2111 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-16 20:11:39 +02:00
Michael Telatynski f1a0c46a29 Deprecate groups APIs 2021-07-16 17:45:07 +01:00
Michael Telatynski 9b81b805be create utility to decode qs into objects 2021-07-16 13:07:01 +01:00
Michael Telatynski 8379bb818e Switch from url to URL constructor 2021-07-16 13:01:27 +01:00
Michael Telatynski c62f2a0965 Merge pull request #1789 from matrix-org/t3chguy/fix/18008 2021-07-16 11:06:56 +01:00
Michael Telatynski d383e71aca Switch callEventHandler from listening on event to Room.timeline
so that it doesn't end up processing ephemerals, account data, remote echoes, etc etc
2021-07-15 17:44:17 +01:00
David Baker dbe93f598a Merge pull request #1787 from matrix-org/dbkr/new_changelogs
Contributing guidelines for new changelog generation
2021-07-15 14:12:20 +01:00
David Baker 2bc5f7132d Merge remote-tracking branch 'origin/develop' into dbkr/new_changelogs 2021-07-15 14:07:51 +01:00
David Baker b17f339011 Merge pull request #1786 from matrix-org/dbkr/fix_contributing_link
Fix link to CONTRIBUTING.md
2021-07-15 14:06:20 +01:00
Michael Telatynski e2b04d543a Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686
 Conflicts:
	src/@types/partials.ts
	src/client.ts
2021-07-15 10:03:05 +01:00
Michael Telatynski 0601f98265 Merge pull request #1788 from SimonBrandner/feature/hidden-rrs
Use an unstable prefix for MSC2885: Hidden read receipts
2021-07-15 08:46:54 +01:00
Šimon Brandner bae5650e7a Fix MSC number
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-15 09:41:40 +02:00
Šimon Brandner ff40b4dc4a Fix MSC number
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-15 09:41:35 +02:00
Germain 6098f74d8f Merge pull request #1782 from psrpinto/fix/naming 2021-07-15 08:07:05 +01:00
David Baker 45c153321c Use X-Breaking-Change
Apparently we already have this label in react SDK (for some reason)
so let's use it. Plus, it's clearer that it's for changes rather than
issues that cause things to break.
2021-07-14 22:46:22 +01:00
David Baker 19aedebff1 Update other label mentions 2021-07-14 22:16:00 +01:00
David Baker 5392f616b4 Use existing issues labels / scheme
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
2021-07-14 22:15:18 +01:00
David Baker c4ccb9d493 Use a real-life example which should hopefully be clearer 2021-07-14 22:00:42 +01:00
David Baker b0726b5008 On brand ourself
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
2021-07-14 21:57:48 +01:00
David Baker 9c221419ef On-brand
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
2021-07-14 21:57:29 +01:00
David Baker fddf0c47fb Fix formatting
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-14 21:57:04 +01:00
David Baker b17862e212 s/tag/label/
Also make breaking change notation clearer.
2021-07-14 19:55:58 +01:00
David Baker e133898554 Fix link to CONTRIBUTING.md 2021-07-14 19:48:47 +01:00
David Baker ee5a0e7edf Internal PRs shouldn't have changelog entries by default 2021-07-14 19:47:46 +01:00
David Baker b538c06fcb Update contributing guidelines to for new changelog stuff 2021-07-14 19:45:42 +01:00
David Baker 49cbaa579f Merge pull request #1785 from matrix-org/dbkr/contributing-to-md
Convert CONTRIBUTING to markdown
2021-07-14 19:05:26 +01:00
David Baker 20bd9b525b Convert CONTRIBUTING to markdown
Because everything else is
2021-07-14 19:02:03 +01:00
Michael Telatynski 3038fc523c Merge pull request #1780 from matrix-org/t3chguy/ts/c3
Use webpack worker-loader instead of homegrown hack
2021-07-14 18:25:21 +01:00
Michael Telatynski 52a85e3f82 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/c3 2021-07-14 17:52:42 +01:00
Michael Telatynski 3df1c4b507 Merge pull request #1779 from matrix-org/t3chguy/ts/c2 2021-07-14 17:44:35 +01:00
Travis Ralston 6ada02e245 Merge pull request #1784 from matrix-org/travis/widgets/fix-enc
Expose MatrixEvent's internal clearEvent as a function
2021-07-14 10:33:45 -06:00
Šimon Brandner 9184a0d902 Use MSC2885 unstable prefix
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-14 18:27:38 +02:00
Travis Ralston 4bc4e1b76f remove useless code 2021-07-14 10:22:36 -06:00
Michael Telatynski cb192d3c74 Merge pull request #1777 from matrix-org/t3chguy/ts/c1 2021-07-14 17:19:03 +01:00
Travis Ralston 11dabf9a68 rename 2021-07-14 10:18:12 -06:00
Travis Ralston ea4dfd003f It's not a partial anymore 2021-07-14 10:09:23 -06:00
Travis Ralston 326b14402f Return a properly shaped event 2021-07-14 10:07:43 -06:00
RiotRobot d71ca8a9e0 v12.1.0-rc.1 2021-07-14 16:16:04 +01:00
RiotRobot 45259f0773 Prepare changelog for v12.1.0-rc.1 2021-07-14 16:16:03 +01:00
Travis Ralston 83337eee57 Expose MatrixEvent's internal clearEvent as a function 2021-07-13 23:09:37 -06:00
Michael Telatynski aed861036b Merge pull request #1783 from SimonBrandner/ignore-vscode 2021-07-13 19:19:58 +01:00
Šimon Brandner 3ff05c0947 Ignore vscode
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-13 20:11:58 +02:00
Paulo Pinto 5b2a53a2b4 Undo changes to the CHANGELOG
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 18:30:57 +01:00
Paulo Pinto 3f6aee1443 Use correct term for identity server
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 16:27:40 +01:00
Paulo Pinto dba70d4ce2 Standardise casing of integration manager
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 16:27:40 +01:00
Paulo Pinto ab3dfd48ae Standardise spelling of identity server
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 16:27:40 +01:00
Paulo Pinto ab59e9134b Standardise casing of identity server
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 16:27:40 +01:00
Paulo Pinto 56921f0961 Standardise spelling and casing of homeserver
Signed-off-by: Paulo Pinto <paulo.pinto@automattic.com>
2021-07-13 16:27:40 +01:00
Michael Telatynski f9005c33e9 Fix casing of IIndexedDBBackend 2021-07-13 11:29:32 +01:00
Michael Telatynski b7c5b36556 Merge pull request #1781 from matrix-org/t3chguy/dt1
Make `Crypto::inRoomVerificationRequests` public
2021-07-13 11:12:23 +01:00
Michael Telatynski 33deef9f2c Rework devtools and get rid of flippy tgl button 2021-07-13 10:33:33 +01:00
Travis Ralston d5c3cb1b7f More minimal types for pushers & push rules 2021-07-12 21:50:27 -06:00
Michael Telatynski 19d8f2bbac Use webpack worker-loader to load the IndexedDB worker instead of homegrown hack 2021-07-12 18:43:21 +01:00
Michael Telatynski 1aa103d8d2 Fix spurious argument appearing on the interface and TODO 2021-07-12 18:41:26 +01:00
Michael Telatynski 75f630e45d Add missing interface 2021-07-12 18:39:29 +01:00
Michael Telatynski 1defde0f5e Convert IndexedDB store & worker to Typescript 2021-07-12 18:36:47 +01:00
Michael Telatynski e38595e735 remove unused import 2021-07-12 09:14:44 +01:00
Michael Telatynski c0e16ac98c Update some more 2021-07-12 09:10:27 +01:00
Michael Telatynski b33429317c Fix setTimeout/setInterval typing 2021-07-12 09:02:47 +01:00
Michael Telatynski e3f7c848d7 iterate types based on PR review 2021-07-12 08:44:34 +01:00
Travis Ralston 5f2ed4450e -- 2021-07-11 21:04:10 -06:00
Travis Ralston 11e25ea9a2 Appease linter 2021-07-11 21:02:33 -06:00
Travis Ralston fb7184e6fb Reorganize types a bit 2021-07-11 20:56:52 -06:00
Travis Ralston 3c77a8772a Add minimal types for "notification settings" UI
For https://github.com/vector-im/element-web/issues/17782
2021-07-11 20:54:51 -06:00
Travis Ralston 5ce787c6ea Merge pull request #1765 from SimonBrandner/fix/add-event-meta-for-filtered/5692
Call `setEventMetadata()` for filtered `timelineSet`s
2021-07-11 02:51:51 -06:00
Šimon Brandner 67189ad323 Use an enum
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-11 10:47:42 +02:00
Michael Telatynski 3bff5430f6 delint 2021-07-10 15:55:19 +01:00
Michael Telatynski 17efcad6fe Improve and consolidate typing 2021-07-10 15:50:40 +01:00
Christian Paul d7e6cee19c Update room.ts 2021-07-10 03:44:03 +02:00
Christian Paul 116085e927 Fix linter error 2021-07-10 03:08:51 +02:00
Christian Paul 96e6e7b1e8 Update event.ts 2021-07-09 16:44:39 +02:00
Hubert Chathi 061199ec3c Merge pull request #1775 from uhoreg/symmetric_backup
Symmetric backup
2021-07-09 10:31:43 -04:00
David Baker 2d67a35af5 Merge pull request #1776 from matrix-org/dbkr/rescue_lost_megolm_key
Attempt to fix megolm key not being in SSSS
2021-07-09 14:30:55 +01:00
Michael Telatynski 4cb6ad9e70 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-09 08:53:24 +01:00
Hubert Chathi d639a29501 more type improvements 2021-07-08 18:47:22 -04:00
Hubert Chathi ac02f30dc8 improve types 2021-07-08 18:10:37 -04:00
David Baker 05a20ab56f Fix up keys even if key backup isn't enabled
Mostly because that's what the test tests, so let's keep that
behaviour the same.
2021-07-08 21:30:50 +01:00
David Baker 964aa6d94a Update comment 2021-07-08 21:20:34 +01:00
David Baker 1a17b138ce Attempt to fix megolm key not being in SSSS
If the account has both a key backup and SSSS enabled but the key
backup key isn't stored in SSSS, try to save it there by getting
it from either the cache or the user (if perhaps it's the same as
their security passphrase but lost a passthrough entry).

Fixes https://github.com/vector-im/element-web/issues/17886
2021-07-08 21:12:45 +01:00
Hubert Chathi 2e27e247f3 Merge branch 'develop' into symmetric_backup 2021-07-08 15:16:07 -04:00
Hubert Chathi ef8e4d6d35 fix the tests that got broken by the last commit 2021-07-08 11:19:46 -04:00
Hubert Chathi 8bee38367a use the MSC number 2021-07-08 10:14:53 -04:00
Šimon Brandner 0318c65847 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-08 13:22:34 +02:00
David Baker ab5b69bbdb Merge pull request #1774 from matrix-org/dbkr/tsify_secretstorage
Convert SecretStorage to TypeScript
2021-07-08 09:18:23 +01:00
Hubert Chathi 9f4c5af665 fix test 2021-07-07 20:19:23 -04:00
Hubert Chathi e275301e8b lint 2021-07-07 19:50:16 -04:00
Hubert Chathi c8a7820cb3 fix types 2021-07-07 19:46:01 -04:00
Hubert Chathi 17dc1bda19 Merge branch 'develop' into symmetric_backup 2021-07-07 19:19:56 -04:00
David Baker 8d9de37099 type for reason 2021-07-07 21:21:22 +01:00
David Baker 29b403da45 visibility 2021-07-07 21:20:48 +01:00
David Baker 1fd1ba2ada de-underscore 2021-07-07 21:19:51 +01:00
David Baker 5fbce3b928 Consistent values between various account data interfaces 2021-07-07 21:18:14 +01:00
Hubert Chathi 9019bce843 mark session as untrusted when the session data has the untrusted flag 2021-07-07 16:16:07 -04:00
David Baker fd2d106f2a extract SecretStorageKeyObject 2021-07-07 21:14:34 +01:00
David Baker 64a1c83acc param names 2021-07-07 21:11:11 +01:00
David Baker 8cc250e19d param names 2021-07-07 21:10:51 +01:00
David Baker dbd737c87d param names & comment any 2021-07-07 21:10:13 +01:00
David Baker 33398e78cd Fix duplicate inlined / explicit members 2021-07-07 21:05:45 +01:00
David Baker 79c6d0180c remove TODO 2021-07-07 21:04:07 +01:00
David Baker e7a198dc0c Line breaks
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 21:00:02 +01:00
David Baker ef08c35c6f Line break
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:59:32 +01:00
David Baker 41b816538e Line break
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:58:47 +01:00
David Baker 87d9fe24f5 Shorthand
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:58:08 +01:00
David Baker d11c95a23f Add type
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:57:40 +01:00
David Baker 3c9b846116 Shorthand
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:56:55 +01:00
David Baker c701e2e6d3 Inline members in ctor
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:55:23 +01:00
David Baker 11b490c77f Line breaks
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:54:48 +01:00
David Baker 0544ab2617 Add return type
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-07 20:52:07 +01:00
David Baker 5c6d3724b0 Move some interfaces around 2021-07-07 20:21:44 +01:00
David Baker c34d4e777f Convert SecretStorage to TypeScript 2021-07-07 19:37:22 +01:00
Michael Telatynski 588d783a55 Merge pull request #1721 from matrix-org/t3chguy/fix/17494 2021-07-07 18:10:35 +01:00
Michael Telatynski 79f74fdff4 Improve return type for getUrlPreview 2021-07-07 17:54:46 +01:00
Germain 6f5dbb782e Merge pull request #1773 from matrix-org/gsouquet/ci-pure-lockfile
Do not generate a lockfile when running in CI
2021-07-07 15:23:25 +01:00
Germain Souquet f0ae9b0100 Do not generate a lockfile when running in CI 2021-07-07 16:11:14 +02:00
Michael Telatynski fd260a023c Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17494
 Conflicts:
	src/client.js
2021-07-07 14:03:12 +01:00
Michael Telatynski e775bcac3c Convert SearchResult, InteractiveAuth, PushProcessor and Scheduler to Typescript 2021-07-07 11:08:54 +01:00
Šimon Brandner 641c852ee2 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-07-07 10:43:23 +02:00
Christian Paul ab679c2216 Update src/@types/event.ts 2021-07-07 00:39:26 +02:00
Christian Paul 96420a75ab room.ts: Use UNSTABLE_ELEMENT_FUNCTIONAL_USERS 2021-07-06 19:35:56 +02:00
Christian Paul c4263692f0 Update event.ts 2021-07-06 19:34:44 +02:00
Christian Paul 0074b71cbf Fix object-curly-spacing linting errors 2021-07-06 19:10:25 +02:00
Christian Paul d219c2f462 Fix mistakes 2021-07-06 19:07:20 +02:00
Christian Paul 4700dc0375 Remove trailing spaces 2021-07-06 18:59:02 +02:00
Christian Paul a248bbc46b Update src/models/room.ts
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-07-06 18:58:07 +02:00
Christian Paul d60affc1b8 room.calculateRoomName: Exclude service members 2021-07-06 18:35:34 +02:00
Christian Paul 8c0f5b4e31 Update event.ts: Add io.element.functional_members 2021-07-06 16:48:57 +02:00
Michael Telatynski a617f6cc55 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-06 10:12:18 +01:00
Michael Telatynski 9e66026bdc Export MSC3244 type 2021-07-06 10:08:40 +01:00
RiotRobot dc90115a1b Resetting package fields for development 2021-07-05 14:57:15 +01:00
RiotRobot 13b250d291 Merge branch 'master' into develop 2021-07-05 14:57:15 +01:00
RiotRobot 834ab22923 v12.0.1 2021-07-05 14:54:21 +01:00
RiotRobot 17be68a39d Prepare changelog for v12.0.1 2021-07-05 14:54:21 +01:00
Michael Telatynski 48ee03e278 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-05 13:45:56 +01:00
David Baker 5b86d8b837 Merge pull request #1766 from matrix-org/dbkr/secretrequest_nopromise
Tidy up secret requesting code
2021-07-02 14:50:50 +01:00
David Baker 521fce59ea Tidy up secret requesting code
Use a plain async function rather than a promise, so we don't
have to squelch the lint warning.
2021-07-02 14:46:30 +01:00
Michael Telatynski bbe1ba7328 Improve typing 2021-07-02 14:38:17 +01:00
Michael Telatynski ce7f20219f Merge pull request #1763 from matrix-org/t3chguy/ts/11
Convert Sync and SyncAccumulator to Typescript
2021-07-02 11:03:27 +01:00
Šimon Brandner aad33bd1b7 Formatting
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-02 11:04:54 +02:00
Šimon Brandner 9923c7e577 Merge remote-tracking branch 'upstream/develop' into fix/add-event-meta-for-filtered/5692
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-02 11:03:54 +02:00
Michael Telatynski ae870b1cc1 Merge pull request #1762 from matrix-org/t3chguy/ts/10 2021-07-02 09:51:56 +01:00
Michael Telatynski e3cb199540 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-02 09:06:15 +01:00
Šimon Brandner d9d19fdc63 delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-02 09:51:26 +02:00
Šimon Brandner f506882bf8 Call setEventMetadata() for filtered timelineSets
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-07-02 09:47:47 +02:00
David Baker e782183a49 Merge pull request #1764 from matrix-org/dbkr/ts_semicolons
Comply with new member-delimiter-style rule
2021-07-01 23:54:07 +01:00
David Baker 6699c4d8af Revert e0012d9b81
Accidentally comitted to develop
2021-07-01 23:49:13 +01:00
David Baker f027cbf170 Bump eslint plugin version
Also yarn has decided that other line is going away too
2021-07-01 23:48:56 +01:00
David Baker e0012d9b81 Bump eslint plugin version
Also yarn has decided that other line is going away too
2021-07-01 23:46:13 +01:00
David Baker b2ad957d29 Comply with new member-delimiter-style rule
Just `eslint --fix` with rule from
https://github.com/matrix-org/eslint-plugin-matrix-org/pull/9 in place
2021-07-01 23:28:18 +01:00
Michael Telatynski cab334ed73 Convert Sync and SyncAccumulator to Typescript 2021-07-01 22:47:50 +01:00
Michael Telatynski a99c1e96d6 fix field accesses in tests and default params 2021-07-01 21:05:58 +01:00
Michael Telatynski 261ac8b2d6 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-07-01 20:49:20 +01:00
Michael Telatynski 399237e781 use better types 2021-07-01 10:01:49 +01:00
Michael Telatynski 4b29f02f1c Convert EventTimeline, EventTimelineSet and TimelineWindow to TS 2021-07-01 09:53:55 +01:00
Hubert Chathi 558da5528b fix validity checks for backup info 2021-06-30 21:53:11 -04:00
J. Ryan Stinnett e3a00c2cb4 Merge pull request #1754 from aaronraimist/string-pl
Do not honor string power levels
2021-06-30 16:39:44 +01:00
Michael Telatynski 12deaa80a6 Merge pull request #1508 from matrix-org/t3chguy/ts/4 2021-06-29 22:24:18 +01:00
Germain a6507768bc Merge pull request #1760 from matrix-org/gsouquet/filepanel-typescript
Make filterId read/write and optional
2021-06-29 15:15:10 +01:00
RiotRobot 8f19ab066c v12.0.1-rc.1 2021-06-29 14:30:59 +01:00
RiotRobot 17decea576 Prepare changelog for v12.0.1-rc.1 2021-06-29 14:30:58 +01:00
Germain Souquet a2442add5b Make filterId read/write and optional 2021-06-29 13:46:42 +01:00
Michael Telatynski 7dbe95fa92 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686
 Conflicts:
	src/@types/partials.ts
	src/client.ts
2021-06-29 12:28:38 +01:00
Michael Telatynski 3339a2874d Merge pull request #1759 from matrix-org/t3chguy/fix/17753
Fix broken /messages filtering due to internal field changes in FilterComponent
2021-06-29 11:47:23 +01:00
Michael Telatynski 393047dec5 Fix broken /messages filtering due to internal field changes in FilterComponent 2021-06-29 11:34:59 +01:00
Aaron Raimist 55fb3d4e8e Also prevent sending unless safe integer
Synapse enforces this but I guess it doesn't hurt to also check here

Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-06-28 22:32:27 -05:00
Aaron Raimist 047180edc9 Merge branch 'develop' into string-pl 2021-06-28 22:17:55 -05:00
Michael Telatynski 1b0a388eb3 delint import 2021-06-25 10:27:55 +01:00
Michael Telatynski d50e559f97 MegolmDecryption::deviceId is not a valid field, sub it out for undefined as it isn't used meaningfully anyhow 2021-06-25 10:21:28 +01:00
Michael Telatynski 835aafcb17 Type the rest of algorithms because tests are unhappy otherwise 2021-06-25 10:18:46 +01:00
Michael Telatynski 48ad9ba3d7 Some more types 2021-06-24 21:48:55 +01:00
Michael Telatynski 3675e95970 fix the upset CI 2021-06-24 21:22:56 +01:00
Michael Telatynski 1ca13f4ce9 delint 2021-06-24 19:28:04 +01:00
Michael Telatynski 40aa6ba96a Even moar typescriptification 2021-06-24 19:19:41 +01:00
Michael Telatynski b4dc1e1555 Moar typescriptification 2021-06-24 17:41:52 +01:00
Michael Telatynski 3effeb7dc4 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/4
 Conflicts:
	package.json
	src/@types/global.d.ts
	src/crypto/key_passphrase.ts
	src/crypto/olmlib.ts
	src/utils.ts
2021-06-24 17:04:45 +01:00
Michael Telatynski 8c11839e4d Merge pull request #1749 from matrix-org/t3chguy/ts/5.1 2021-06-24 15:23:55 +01:00
Michael Telatynski 41817995bd Merge pull request #1739 from matrix-org/t3chguy/fix/15051 2021-06-24 11:20:10 +01:00
Hubert Chathi 8c9799d64c add tests and some fixes 2021-06-23 18:20:00 -04:00
Michael Telatynski 3a5e4ffa91 Fix yet more underscored accesses 2021-06-23 15:54:48 +01:00
Michael Telatynski 02afcc7d4b delint 2021-06-23 15:48:21 +01:00
Michael Telatynski e9007429dd fix more underscored accesses 2021-06-23 15:02:01 +01:00
Michael Telatynski 664d920dd1 Fix issues identified by Typescriptification 2021-06-23 14:51:40 +01:00
Michael Telatynski 5a8299f1a5 Convert more of js-sdk crypto and fix underscored field accesses 2021-06-23 14:47:25 +01:00
Michael Telatynski 6017fead19 Fix imports 2021-06-23 13:33:56 +01:00
Michael Telatynski 69050ed338 Fix import cycle 2021-06-23 13:12:26 +01:00
Michael Telatynski 2664848545 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/5.1 2021-06-23 13:05:34 +01:00
Aaron Raimist b18f0ff738 Do not honor string power levels
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2021-06-22 22:34:34 -05:00
Michael Telatynski 1171c33f7a Merge pull request #1753 from matrix-org/t3chguy/ts/8 2021-06-22 22:04:22 +01:00
Michael Telatynski 6deb2bc7a9 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/15051
 Conflicts:
	src/models/MSC3089TreeSpace.ts
2021-06-22 22:03:44 +01:00
Michael Telatynski 7bda13aba6 Fix types of MatrixEvent sender & target 2021-06-22 20:37:50 +01:00
Travis Ralston 75719c3e0f Merge pull request #1744 from matrix-org/travis/keyshare-file-trees
Add keysharing on invites to File Tree Spaces
2021-06-22 10:48:57 -06:00
Michael Telatynski 2fb033b5ba Merge pull request #1746 from matrix-org/t3chguy/ts/5 2021-06-22 17:41:26 +01:00
Michael Telatynski 9f1db7a707 Merge pull request #1752 from matrix-org/t3chguy/ts/7
Improve type of IContent msgtype
2021-06-22 17:36:53 +01:00
Michael Telatynski a93de99d42 Improve type of IContent msgtype 2021-06-22 17:18:54 +01:00
Michael Telatynski 3d5774ac9e Update early MSC3083 support 2021-06-22 16:07:05 +01:00
Travis Ralston 1d8e0398e9 Merge pull request #1747 from SimonBrandner/feature/pr-template
Add PR template
2021-06-21 14:06:50 -06:00
Michael Telatynski cf76375ce0 Move Promise::allSettled typing from react-sdk to js-sdk 2021-06-21 21:04:29 +01:00
RiotRobot 284e2bb911 Resetting package fields for development 2021-06-21 16:28:40 +01:00
RiotRobot ecb790c179 Merge branch 'master' into develop 2021-06-21 16:28:40 +01:00
RiotRobot 467b75f2dc v12.0.0 2021-06-21 16:20:26 +01:00
RiotRobot 1f728cab92 Prepare changelog for v12.0.0 2021-06-21 16:20:25 +01:00
Michael Telatynski 66b17aa019 Add type for another-json dep 2021-06-19 19:45:29 +01:00
Michael Telatynski 27a6d1f878 Fix missing await identified by TS conversion 2021-06-19 19:44:47 +01:00
Michael Telatynski fc67dc6497 Convert crypto index to TS 2021-06-19 19:41:45 +01:00
Travis Ralston fedd9a981a Merge pull request #1738 from matrix-org/travis/event-fixes
Add functions to assist in immutability of Event objects
2021-06-18 11:23:08 -06:00
Travis Ralston 47e972a66c Incorporate merge conflict resolution 2021-06-18 11:18:29 -06:00
Travis Ralston 36bd4b5408 Merge remote-tracking branch 'origin/develop' into travis/event-fixes 2021-06-18 11:15:00 -06:00
Travis Ralston f502233ddc try to add more docs 2021-06-18 11:14:43 -06:00
Michael Telatynski 59c1edb623 fix tests 2021-06-18 17:56:51 +01:00
Michael Telatynski 9dd00c7731 Fix tests and tweak some optional types 2021-06-18 17:40:04 +01:00
Šimon Brandner 311edb4e4c Add PR template
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-18 17:30:50 +02:00
Michael Telatynski 924b8629d8 Updates around the use of private fields out of class 2021-06-18 16:29:10 +01:00
Michael Telatynski 9e11da1fa5 Fix typing 2021-06-18 16:11:11 +01:00
Michael Telatynski b760fa0ff5 Switch typescript target to es2020 as we use new things like Promise.allSettled and it needs it 2021-06-18 15:30:53 +01:00
Michael Telatynski cbce2f46c3 Fix position of invite_room_state on the event 2021-06-18 15:30:30 +01:00
Michael Telatynski 7aa5a79c86 Convert Room and RoomState to Typescript 2021-06-18 15:29:45 +01:00
Michael Telatynski fc029a9b9e Add MSC3244 types 2021-06-18 11:39:43 +01:00
Michael Telatynski 86ef80ae9a Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17686 2021-06-18 10:06:05 +01:00
Michael Telatynski e5b475ad89 Create Typescript types for JoinRule, GuestAccess, HistoryVisibility, including join_rule=restricted 2021-06-18 10:05:29 +01:00
Michael Telatynski 2c6858f149 Merge pull request #1742 from matrix-org/t3chguy/ts/5 2021-06-18 09:49:00 +01:00
Travis Ralston c1bff0b2ea delint 2021-06-17 17:38:57 -06:00
Travis Ralston 39892c98f9 Add keysharing on invites to File Tree Spaces 2021-06-17 17:36:49 -06:00
Travis Ralston b15487ec03 Misc lint 2021-06-17 14:24:39 -06:00
Travis Ralston 303119e113 Merge pull request #1743 from matrix-org/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-06-17 11:08:33 -06:00
dependabot[bot] 09b729beb5 Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

---
updated-dependencies:
- dependency-name: lodash
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-17 17:07:56 +00:00
Travis Ralston 6b73a77b2d Merge pull request #1740 from matrix-org/travis/invite-retry
Add invite retries to file trees
2021-06-17 11:07:06 -06:00
Michael Telatynski a2449ff6a7 Fix typos 2021-06-17 15:23:40 +01:00
Michael Telatynski b1b7522b80 Fix tests by updating private field names and spies 2021-06-17 15:18:52 +01:00
Michael Telatynski 608b0e7b93 Fix up some more type defs 2021-06-17 14:49:27 +01:00
Michael Telatynski 7c61b9cf7e Fix more type definitions 2021-06-17 14:24:53 +01:00
Michael Telatynski 50a973409a Typescript fixes due to MatrixEvent being TSified 2021-06-17 14:06:03 +01:00
Michael Telatynski bfea882416 Convert MatrixEvent to TS 2021-06-17 14:04:04 +01:00
Michael Telatynski f5e8fe836e Convert Room Member and User to TS 2021-06-17 14:03:52 +01:00
Michael Telatynski c4664a185f Convert Event Context to TS 2021-06-17 14:03:34 +01:00
Michael Telatynski c5f093c42f Merge pull request #1741 from matrix-org/t3chguy/ts/5
Convert IndexedDBStore to TS
2021-06-17 12:15:10 +01:00
Michael Telatynski 64f369b5de Fix IndexedDBStore ts-ification 2021-06-17 12:10:48 +01:00
Michael Telatynski 4b5653c09b Convert IndexedDBStore to TS 2021-06-17 11:37:41 +01:00
Michael Telatynski 837e739ec6 Merge pull request #1736 from matrix-org/t3chguy/ts4 2021-06-17 10:51:11 +01:00
Michael Telatynski b780ee8373 Update src/models/relations.ts
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
2021-06-17 10:23:09 +01:00
Travis Ralston fe5bfbf76f The linter needed appeasing 2021-06-16 20:27:03 -06:00
Travis Ralston d924617672 Add invite retries to file trees 2021-06-16 20:24:48 -06:00
Michael Telatynski c545c7ca70 fix MSC3089TreeSpace incorrect type assumption 2021-06-16 20:32:46 +01:00
Michael Telatynski 9eb9f3a117 also do same update on the 3pid invite paths 2021-06-16 20:26:50 +01:00
Michael Telatynski 7862fd9679 just update jsdoc/ts return types due to contention. 2021-06-16 20:24:57 +01:00
Travis Ralston 17402e8475 Work around docgen 2021-06-15 14:32:53 -06:00
Travis Ralston aac77440db work around docgen? 2021-06-15 14:29:26 -06:00
Travis Ralston 13c9c4bea5 Add functions to assist in immutability of Event objects 2021-06-15 14:23:27 -06:00
RiotRobot 68c1171294 v12.0.0-rc.1 2021-06-15 16:10:48 +01:00
RiotRobot f4f01913fe Prepare changelog for v12.0.0-rc.1 2021-06-15 16:10:48 +01:00
Michael Telatynski eb5908d5d2 fix tests 2021-06-15 10:15:43 +01:00
Michael Telatynski e0c36498e6 delint 2021-06-15 10:06:30 +01:00
Michael Telatynski 913710dd99 Convert filter classes to typescript 2021-06-15 10:02:05 +01:00
Michael Telatynski 2f0d96d030 Convert some utils to typescript 2021-06-15 10:01:51 +01:00
Michael Telatynski 265802acb1 Convert some models to typescript 2021-06-15 10:01:38 +01:00
Michael Telatynski 045f31a0dc Convert some stores to typescript 2021-06-15 10:01:22 +01:00
Travis Ralston 9f6ed4fb33 Merge pull request #1730 from SimonBrandner/show-username
Rework how disambiguation is handled
2021-06-15 00:24:06 -06:00
Michael Telatynski 434117c771 Merge pull request #1735 from matrix-org/t3chguy/fix/17282 2021-06-14 22:15:12 +01:00
Michael Telatynski cd95b8724a Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix/17282 2021-06-14 21:46:31 +01:00
Michael Telatynski ec2a4d473e Iterate algorithm, base it on new js-sdk string lib 2021-06-14 21:28:33 +01:00
Travis Ralston 47eaf3cd58 Merge pull request #1734 from matrix-org/travis/fix-types
Move various types from the react-sdk to the js-sdk
2021-06-14 13:48:07 -06:00
Travis Ralston d7b23a8634 liiiinttteeerrrr 2021-06-14 13:37:19 -06:00
Travis Ralston 6db7972f04 preset 2021-06-14 13:35:20 -06:00
Travis Ralston 6840ee077c Appease the linter forever 2021-06-14 13:34:37 -06:00
Travis Ralston 3c85bcc3c9 Move various types from the react-sdk to the js-sdk 2021-06-14 13:32:28 -06:00
Travis Ralston 759eca6479 Merge pull request #1732 from matrix-org/travis/file-trees
Unstable implementation of MSC3089: File Trees
2021-06-14 12:04:09 -06:00
Travis Ralston 4488f174aa const 2021-06-14 12:00:59 -06:00
Travis Ralston 991a255041 Fix average on .5 2021-06-14 11:55:41 -06:00
Travis Ralston cfef635e1b Appease the linter 2021-06-14 10:41:32 -06:00
Travis Ralston d3027e1fe8 Offset the alphabet by 1 2021-06-14 10:28:26 -06:00
Travis Ralston d99ea1c6b4 Update src/utils.ts
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2021-06-14 08:21:09 -06:00
Travis Ralston 9af214007e APPEASE. THE. LINTER. 2021-06-11 11:37:44 -06:00
Travis Ralston 0541b7f3c5 Remove a layer of indirection 2021-06-11 11:36:52 -06:00
Travis Ralston 63fa774af7 Another round of appeasement 2021-06-11 11:34:37 -06:00
Travis Ralston 4b19b36de1 Crude JS->TS conversion on utils test because of linter and BigInt 2021-06-11 11:27:46 -06:00
Travis Ralston 5715df6b18 BigInt, rollover, and developer lint 2021-06-11 11:18:18 -06:00
Michael Telatynski 22bcacc715 Merge pull request #1729 from matrix-org/t3chguy/fix/17282
Add MSC3230 event type to enum
2021-06-11 12:40:23 +01:00
Travis Ralston f1e270ca9d appease the linter 2021-06-10 15:54:56 -06:00
Travis Ralston f535e7535c Update string averaging utils 2021-06-10 15:38:49 -06:00
David Baker 912c5e13e1 Merge pull request #1731 from matrix-org/dbkr/transferred_reason_code
Add separate reason code for transferred calls
2021-06-10 13:21:41 +01:00
Travis Ralston 4eb44ee2ea de-lint 2 2021-06-09 22:14:11 -06:00
Travis Ralston e41a2beb65 de-lint 2021-06-09 22:09:26 -06:00
Travis Ralston 1f6ba31a3f Use a sane padStart instead 2021-06-09 21:56:46 -06:00
Travis Ralston bcccc909c5 Pre-lint format 2021-06-09 21:55:06 -06:00
Travis Ralston b3a11030f2 Early file management APIs 2021-06-09 21:54:17 -06:00
Travis Ralston baaf76668f Early directory management 2021-06-09 21:54:17 -06:00
Šimon Brandner ca37ae6794 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-09 19:20:21 +02:00
Hubert Chathi cf21d64aa8 allow backup algorithm to determine whether restored keys should be trusted 2021-06-09 13:02:47 -04:00
David Baker 36e2533164 Add separate reason code for transferred calls
'Replaced' is special cased so the media isn't torn down, but we
were passing Replaced for calls we transferred off elsewhere which
meant we never closed the media capture. Calls transferred elsewhere
aren't really replaced: they may as well have their own code.
2021-06-09 16:52:58 +01:00
Hubert Chathi c04d79d9a0 initial work on symmetric algorithm for key backups 2021-06-08 21:39:37 -04:00
Travis Ralston 9084b4e7aa Early implementation of MSC3089 (file trees)
MSC: https://github.com/matrix-org/matrix-doc/pull/3089
Includes part of MSC3088 (room subtyping): https://github.com/matrix-org/matrix-doc/pull/3088

The NamespacedValue stuff is borrowed from the Extensible Events implementation PR in the react-sdk as a useful thing to put here. When/if the MSCs become stable, we'd convert the values to enums and drop the constants (or keep them for migration purposes, but switch to stable). 

This flags the whole thing as unstable because it's highly subject to change.
2021-06-08 14:43:20 -06:00
Šimon Brandner f724da7e84 Merge remote-tracking branch 'upstream/develop' into disambiguate-prop 2021-06-08 11:30:50 +02:00
David Baker 4b8f47e2b4 Merge pull request #1728 from matrix-org/dbkr/hold_with_sendonly
Use sendonly for call hold
2021-06-08 09:26:57 +01:00
RiotRobot 35ecbed29d Resetting package fields for development 2021-06-07 17:00:30 +01:00
RiotRobot 30ed153ad1 Merge branch 'master' into develop 2021-06-07 17:00:30 +01:00
RiotRobot a1098989ff v11.2.0 2021-06-07 16:57:42 +01:00
RiotRobot 42bb63e07d Prepare changelog for v11.2.0 2021-06-07 16:57:42 +01:00
Michael Telatynski 683aae5ed5 Add MSC3230 event type to enum 2021-06-07 09:00:08 +01:00
David Baker 49ef274a83 Use sendonly for call hold
Instead of inactive. Remove the logic for trying to remember who
had who on hold, which is now unnecesasary. Appears to work fine
with element android & ios too.
2021-06-04 17:22:14 +01:00
J. Ryan Stinnett af16bba1ec Merge pull request #1727 from matrix-org/t3chguy/arg
Stop breeding sync listeners
2021-06-04 14:09:45 +01:00
Michael Telatynski 781086608e Stop breeding sync listeners 2021-06-04 13:58:19 +01:00
Šimon Brandner c1625e5c27 More styling :(
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:44:31 +02:00
Šimon Brandner 0e05f9fd73 Styling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:41:35 +02:00
Šimon Brandner ef7595bb06 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-04 11:38:13 +02:00
J. Ryan Stinnett e9c98b03b0 Merge pull request #1724 from matrix-org/jryans/semi-linting
Fix semicolons in TS files
2021-06-04 10:31:56 +01:00
J. Ryan Stinnett 75370f7855 Reduce allowable warning count 2021-06-04 10:24:55 +01:00
J. Ryan Stinnett 4a79e13410 Auto-fix more errors 2021-06-04 10:24:09 +01:00
J. Ryan Stinnett 220061f022 Merge remote-tracking branch 'origin/develop' into jryans/semi-linting 2021-06-04 10:22:06 +01:00
Šimon Brandner 090c2c0891 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-06-04 08:27:46 +02:00
Travis Ralston 6c317b7f28 Fix types on checkOwnCrossSigningTrust 2021-06-04 00:10:33 -06:00
Travis Ralston 3788fbf607 Merge pull request #1718 from matrix-org/travis/ts-mtxcli
[BREAKING] Convert MatrixClient to TypeScript
2021-06-03 19:09:23 -06:00
Travis Ralston 382854c04c Appease linter 2021-06-03 19:04:49 -06:00
Travis Ralston c2fae3bad8 Fix missed conversion fallout 2021-06-03 19:02:46 -06:00
Travis Ralston 92ebd39391 Reincorporate crypto changes
https://github.com/matrix-org/matrix-js-sdk/pull/1697
2021-06-03 18:59:01 -06:00
Travis Ralston f53a32a6b4 Merge branch 'develop' into travis/ts-mtxcli 2021-06-03 18:49:08 -06:00
Hubert Chathi 6c882e6605 Merge pull request #1697 from uhoreg/backup_refactor
Factor out backup management to a separate module
2021-06-03 18:56:19 -04:00
Hubert Chathi ca85dfc6ff re-lint 2021-06-03 18:52:06 -04:00
Hubert Chathi e22ecc6b6d Merge branch 'develop' into backup_refactor 2021-06-03 18:43:46 -04:00
Hubert Chathi 2608dd2d64 mark members as public 2021-06-03 18:15:05 -04:00
J. Ryan Stinnett 7d20d24249 Convert small blocks to inline expressions 2021-06-03 15:51:33 +01:00
Travis Ralston a3ac3692af private->protected 2021-06-02 22:05:53 -06:00
Travis Ralston 0070c8f843 Merge pull request #1723 from schmop/power-levels-unknown-state-key
Ignore power_levels events with unknown state_key on room-state initialization
2021-06-02 19:04:13 -06:00
Travis Ralston b360fc8308 Fix test failure 2021-06-02 18:42:52 -06:00
Travis Ralston bf9ba65ac4 Fix olmVersion types 2021-06-02 13:42:20 -06:00
Travis Ralston 43abfbc537 Upstream build pass 2 2021-06-02 13:42:20 -06:00
Travis Ralston 2700f0acf6 Upstream build pass 1 2021-06-02 13:42:20 -06:00
Travis Ralston 9156bed961 Tests pass 2 2021-06-02 13:42:20 -06:00
Travis Ralston 71dc0bac56 Tests pass 1 2021-06-02 13:42:20 -06:00
Travis Ralston 40f55b2964 Lint pass 4 2021-06-02 13:42:20 -06:00
Travis Ralston 9307f9f345 Build pass 2 2021-06-02 13:42:20 -06:00
Travis Ralston dfb918adc3 Lint pass 3 2021-06-02 13:42:20 -06:00
Travis Ralston 2f87a4859e Lint pass 2 2021-06-02 13:42:20 -06:00
Travis Ralston 191f73e0d0 Appease typescript 2021-06-02 13:42:20 -06:00
Travis Ralston 263e55f25d Build pass 1 2021-06-02 13:42:20 -06:00
Travis Ralston 5c55dce13e Lint pass 1 2021-06-02 13:42:20 -06:00
Travis Ralston a1a6ec6dfa Fix remaining hot paths 2021-06-02 13:42:20 -06:00
Travis Ralston e1edd84700 Early pass to fix runtime/build errors 2021-06-02 13:42:20 -06:00
Travis Ralston 07ee256756 Incorporate https://github.com/matrix-org/matrix-js-sdk/pull/1720 2021-06-02 13:42:20 -06:00
Travis Ralston 48888e530e Defer types 2021-06-02 13:42:20 -06:00
Travis Ralston 4ef50bef55 define this.identityServer 2021-06-02 13:42:20 -06:00
Travis Ralston 486369e97c Clean up "base-apis" find&replace 2021-06-02 13:42:20 -06:00
Travis Ralston 67994f7a53 Move new MatrixClient into place 2021-06-02 13:42:20 -06:00
Travis Ralston f027ddaf35 Autoformat 2021-06-02 13:42:20 -06:00
Travis Ralston f3b27d1e06 Cleanup 2021-06-02 13:42:20 -06:00
Travis Ralston 92e18b32dc Import MatrixError 2021-06-02 13:42:20 -06:00
Travis Ralston 4030ec9c8b Fix easy typing errors 2021-06-02 13:42:20 -06:00
Travis Ralston 497c2dc8df Bring in BaseApis to MatrixClient 2021-06-02 13:42:20 -06:00
Travis Ralston 8a1d34c419 [Combined] First pass of JS->TS for MatrixClient 2021-06-02 13:42:20 -06:00
Travis Ralston caab5befaa Rename client.js -> 1client.ts for future commits 2021-06-02 13:42:20 -06:00
Travis Ralston 0d316e3d3e Move useful docs to ICreateClientOpts 2021-06-02 13:42:20 -06:00
Šimon Brandner d8fd12103a Merge branch 'develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-06-02 16:09:11 +02:00
David Baker 8f9a682d34 Merge pull request #1725 from matrix-org/dbkr/revert_1579
Revert 1579 (Fix extra negotiate message in Firefox)
2021-06-02 15:04:17 +01:00
David Baker 102d5acf09 Revert 1579
This is causing all sorts of problems, like
https://github.com/vector-im/element-web/issues/17450
and also issues where we fail to respond to negotiates because
we can't adjust a direction of a stopped transceiver. Until
s/inactive/sendonly/ let's live with the minor firefox bug rather
than all this brokenness.
2021-06-02 14:41:55 +01:00
J. Ryan Stinnett e2ec8952e3 Fix semicolons in TS files
This updates the linting config to include the semi fix in
https://github.com/matrix-org/eslint-plugin-matrix-org/pull/8. The various semi
errors have been auto-fixed.
2021-06-02 11:49:39 +01:00
Lars Richard b4eff9b996 Ignore m.room.power_levels events with unknown state_key on room-state initialization
Signed-off-by: Lars Richard <lars.richard@iserv.eu>
2021-06-02 11:42:50 +02:00
Michael Telatynski bcb4071993 Strip hash from urls being previewed to de-duplicate 2021-06-01 11:17:30 +01:00
Hubert Chathi c543358826 add unit test and minor fixes 2021-05-31 21:52:20 -04:00
Šimon Brandner 5724462c2c Delint
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:45:52 +02:00
Šimon Brandner 0a0489750c Add missing space
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:37:55 +02:00
Šimon Brandner f46190509a Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-28 17:37:17 +02:00
Šimon Brandner 75321220fd Simplifie code - don't be an idiot
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-27 18:49:02 +02:00
Šimon Brandner 43198b0425 Disable RTX only for screen-sharing transceivers
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-27 18:41:48 +02:00
Šimon Brandner 4aa2c03ff0 Merge branch 'develop' into fix/12652/screen-share 2021-05-27 18:23:22 +02:00
Hubert Chathi 07bfa5532e fix more unit tests 2021-05-26 18:18:30 -04:00
Hubert Chathi e73b969066 lint 2021-05-25 22:10:15 -04:00
Hubert Chathi 98e2154f0f fix unit tests 2021-05-25 21:59:08 -04:00
Šimon Brandner db4c6af472 Fix an odd mistake
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:54:08 +02:00
Šimon Brandner 87689ca733 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-20 15:50:15 +02:00
Šimon Brandner a1a5d85979 Stop tracks only if disabling screen-sharing
The other thing lead to usermedia tracks being stopped when on hold

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:49:56 +02:00
Šimon Brandner b1459a43ef Also put getRidOfRTXCodecs() before createAnswer()
Just to be sure

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-20 15:11:23 +02:00
Šimon Brandner 8797381f44 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 21:07:04 +02:00
Šimon Brandner 92e89ffbcf Add getRidOfRTXCodecs() method
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-19 21:06:35 +02:00
Šimon Brandner c2da4376e0 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 15:21:00 +02:00
Šimon Brandner 78ebf8f117 Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share 2021-05-19 08:46:54 +02:00
Hubert Chathi 3ec89a89df fix some types 2021-05-18 18:40:36 -04:00
Hubert Chathi 9e6b72bf38 some linting 2021-05-18 18:31:19 -04:00
Hubert Chathi 747723c8fb factor out backup management to a separate module 2021-05-18 18:15:22 -04:00
Šimon Brandner 5de189bfa3 Improve logging in pushRemoteFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-15 11:06:04 +02:00
Šimon Brandner 44fc820f99 Make feed pushing methods more verbose
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-13 18:12:48 +02:00
Šimon Brandner 5a8e5a9785 Log local SDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-13 13:43:44 +02:00
Šimon Brandner 960c5da3b2 Merge branch 'develop' into fix/12652/screen-share 2021-05-13 13:39:51 +02:00
Šimon Brandner 00a28e743d Move track.stop() to deleteFeedByStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:18:39 +02:00
Šimon Brandner 2596c25ccc Add missing semicolon
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:17:07 +02:00
Šimon Brandner 582aafa552 Simpler naming
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 19:12:33 +02:00
Šimon Brandner 61e7d4f807 Remove some leftovers from placeScreensharingCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 18:22:06 +02:00
Šimon Brandner f2c215311f If we can't get constraints don't error
We do this because it could mean the user just hasn't selected a window/screen

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 13:01:06 +02:00
Šimon Brandner a13cf0e1e0 Remove placeScreenSharingCall()
This method is quite problematic and doesn't have any benefits

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 12:27:21 +02:00
Šimon Brandner ff60bbac9d Remove import that was a mistake
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-10 07:35:25 +02:00
Šimon Brandner 16f569136b Simplifie and avoid repetation
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-09 18:48:08 +02:00
Šimon Brandner 27c172361f Add a log line to pushLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 20:03:47 +02:00
Šimon Brandner 1e0d6b9d4a Jest: should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 15:50:20 +02:00
Šimon Brandner b67cd94ee2 Jest: should map SDPStreamMetadata to feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 15:39:11 +02:00
Šimon Brandner c6764490c6 Use for loop
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:17:10 +02:00
Šimon Brandner 18580624e6 Use opponentSupportsSDPStreamMetadata()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:16:43 +02:00
Šimon Brandner 7b333a34b5 Warn level
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 14:16:30 +02:00
Šimon Brandner d0707e183d Make shift-click work again
This is VERY ugly but it works

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 10:02:27 +02:00
Šimon Brandner fa3b246de5 Add addToPeerConnection param
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 10:00:59 +02:00
Šimon Brandner df28d87d25 Remove log line
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:29:41 +02:00
Šimon Brandner fc68bb3ae0 Add ()!!!
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:29:31 +02:00
Šimon Brandner 82c530da95 Add setScreensharingEnabledWithoutMetadataSupport as a fallback()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:08:06 +02:00
Šimon Brandner d250e7387c Merge screenshare track into usermedia stream
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 09:02:01 +02:00
Šimon Brandner cbc74815d8 Use getScreensharingStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:46:29 +02:00
Šimon Brandner e9b802deb3 Use getScreensharingStream() in setScreensharingEnabled()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:03:45 +02:00
Šimon Brandner 377ca0c678 Add getScreensharingStream()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 08:01:56 +02:00
Šimon Brandner 972aef7a9d Merge feed delete methods and add sender arrays
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-08 07:40:46 +02:00
Šimon Brandner a35559be65 Add a method to start screensharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:33:05 +02:00
Šimon Brandner 0e6b43a769 Hook up methods to delete feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:02:56 +02:00
Šimon Brandner 8c8a68d3ae Add methods to delete feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 21:01:56 +02:00
Šimon Brandner 4d74b5cdad Send SDPStreamMetadata in negotiation response
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 20:57:45 +02:00
Šimon Brandner b1ace49f9a Add methods useful for screensharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 20:53:19 +02:00
Šimon Brandner 640d13af99 Set remoteSDPStreamMetadata in onNegotiateReceived()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 13:20:36 +02:00
Šimon Brandner 50e0f6353a Move adding tracks into pushLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 13:15:09 +02:00
Šimon Brandner 30f2263443 Rework pushing of remote feeds for MSC3077
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:18:25 +02:00
Šimon Brandner 25eb6de220 Send SDPStreamMetadata in answer
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:17:08 +02:00
Šimon Brandner cebdc44689 Set remoteSDPStreamMetadata from answer
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 11:11:31 +02:00
Šimon Brandner 23f5c2e03f Add a separate method to push local feed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 10:43:36 +02:00
Šimon Brandner 4d4a6ede21 Use somicolons instead
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 10:41:59 +02:00
Šimon Brandner 6a920fe623 Get sdpStreamMetadata from invite
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-07 07:56:27 +02:00
Šimon Brandner 631faa2046 Send SDPStreamMetadata
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-06 17:18:30 +02:00
Šimon Brandner a4e853e1d4 Add types for MSC3077
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-05-06 17:15:04 +02:00
Šimon Brandner e4fbbd56a9 Improve wording
Co-authored-by: Travis Ralston <travpc@gmail.com>
2021-04-22 07:36:53 +02:00
Šimon Brandner 4290e8e56b Merge branch 'develop' into disambiguate-prop 2021-04-21 18:07:50 +02:00
Šimon Brandner a416fd562b Oops - remove log
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-20 11:11:33 +02:00
Šimon Brandner 323a096dd7 Merge branch 'disambiguate-prop' of https://github.com/SimonBrandner/matrix-js-sdk into disambiguate-prop 2021-04-20 11:01:20 +02:00
Šimon Brandner 628dd7bf41 Return false by default
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-20 11:01:13 +02:00
Šimon Brandner 71c6d71cae Fix docs
Co-authored-by: Jonathan de Jong <jonathandejong02@gmail.com>
2021-04-20 10:53:39 +02:00
Šimon Brandner e049edd449 Add docs
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-20 10:44:17 +02:00
Šimon Brandner 0fdfc3ff53 Rework how disambiguation is handled
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-04-18 21:01:53 +02:00
Michael Telatynski 54a03b234a Merge branch 'develop' into t3chguy/ts/4 2020-10-22 16:46:06 +01:00
Michael Telatynski ad2f537887 Merge remote-tracking branch 'origin/t3chguy/ts/4' into t3chguy/ts/4 2020-10-13 22:34:22 +01:00
Michael Telatynski df36f0dab4 fix couple more errors
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-13 22:34:06 +01:00
Hubert Chathi 608d7faa29 fix some typescript errors 2020-10-13 17:25:58 -04:00
Michael Telatynski 7845957d0d fix Olm types import
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-13 19:43:31 +01:00
Michael Telatynski 13c5c4e4f5 Use utf8 instead of utf-8
as per https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-13 19:36:03 +01:00
Michael Telatynski 62114028d5 Convert aes, key_passphrase, olmlib, and recoverykey to TS
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-13 19:28:16 +01:00
178 changed files with 42398 additions and 31453 deletions
+1
View File
@@ -0,0 +1 @@
* @matrix-org/element-web
+7
View File
@@ -0,0 +1,7 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
Notes:
-->
+12
View File
@@ -0,0 +1,12 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
+3
View File
@@ -16,3 +16,6 @@ out
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
/matrix-js-sdk-*.tgz
.vscode
.vscode/
+399
View File
@@ -1,3 +1,402 @@
Changes in [15.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0) (2021-12-20)
==================================================================================================
## ✨ Features
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
## 🐛 Bug Fixes
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
Changes in [15.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.3.0-rc.1) (2021-12-14)
============================================================================================================
## ✨ Features
* Improve fallback key behaviour ([\#2037](https://github.com/matrix-org/matrix-js-sdk/pull/2037)).
* Add new room event filter fields ([\#2051](https://github.com/matrix-org/matrix-js-sdk/pull/2051)).
* Add method to fetch /account/whoami ([\#2046](https://github.com/matrix-org/matrix-js-sdk/pull/2046)).
## 🐛 Bug Fixes
* Filter out falsey opts in /relations API hits ([\#2059](https://github.com/matrix-org/matrix-js-sdk/pull/2059)). Fixes vector-im/element-web#20137.
* Fix paginateEventTimeline resolve to boolean ([\#2054](https://github.com/matrix-org/matrix-js-sdk/pull/2054)).
* Fix incorrect MSC3089 typings and add null checks ([\#2049](https://github.com/matrix-org/matrix-js-sdk/pull/2049)).
Changes in [15.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.1) (2021-12-13)
==================================================================================================
* Security release with updated version of Olm to fix https://matrix.org/blog/2021/12/03/pre-disclosure-upcoming-security-release-of-libolm-and-matrix-js-sdk
Changes in [15.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0) (2021-12-06)
==================================================================================================
## ✨ Features
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
## 🐛 Bug Fixes
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
Changes in [15.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.2.0-rc.1) (2021-11-30)
============================================================================================================
## ✨ Features
* Remove support for `ArrayBuffer` in unstable MSC3089 `createFile()` and `createNewVersion()` and instead use same content types as handled by `MatrixClient.uploadContent()`. This enables support for Node.js. ([\#2014](https://github.com/matrix-org/matrix-js-sdk/pull/2014)).
* Support for password-based backup on Node.js ([\#2021](https://github.com/matrix-org/matrix-js-sdk/pull/2021)).
* Add optional force parameter when ensuring Olm sessions ([\#2027](https://github.com/matrix-org/matrix-js-sdk/pull/2027)).
## 🐛 Bug Fixes
* Fix call upgrades ([\#2024](https://github.com/matrix-org/matrix-js-sdk/pull/2024)). Contributed by @SimonBrandner.
Changes in [15.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1) (2021-11-22)
==================================================================================================
## 🐛 Bug Fixes
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
Changes in [15.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.1-rc.1) (2021-11-17)
============================================================================================================
## 🐛 Bug Fixes
* Fix edit history being broken after editing an unencrypted event with an encrypted event ([\#2013](https://github.com/matrix-org/matrix-js-sdk/pull/2013)). Fixes vector-im/element-web#19651 and vector-im/element-web#19651. Contributed by @aaronraimist.
* Make events pagination responses parse threads ([\#2011](https://github.com/matrix-org/matrix-js-sdk/pull/2011)). Fixes vector-im/element-web#19587 and vector-im/element-web#19587.
Changes in [15.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0) (2021-11-08)
==================================================================================================
## 🦖 Deprecations
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
## ✨ Features
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
## 🐛 Bug Fixes
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
Changes in [15.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v15.1.0-rc.1) (2021-11-02)
============================================================================================================
## 🦖 Deprecations
* Mark old verification methods as deprecated ([\#1994](https://github.com/matrix-org/matrix-js-sdk/pull/1994)).
## ✨ Features
* Try to set a sender on search result events if possible ([\#2004](https://github.com/matrix-org/matrix-js-sdk/pull/2004)).
* Port some changes from group calls branch to develop ([\#2001](https://github.com/matrix-org/matrix-js-sdk/pull/2001)). Contributed by @SimonBrandner.
* Fetch room membership from server rather than relying on stored data ([\#1998](https://github.com/matrix-org/matrix-js-sdk/pull/1998)).
* Add method to fetch the MSC3266 Room Summary of a Room ([\#1988](https://github.com/matrix-org/matrix-js-sdk/pull/1988)).
## 🐛 Bug Fixes
* Don't show `Unable to access microphone` when cancelling screensharing dialog ([\#2005](https://github.com/matrix-org/matrix-js-sdk/pull/2005)). Fixes vector-im/element-web#19533 and vector-im/element-web#19533. Contributed by @SimonBrandner.
* Strip direction override characters from display names ([\#1992](https://github.com/matrix-org/matrix-js-sdk/pull/1992)). Fixes vector-im/element-web#1712 and vector-im/element-web#1712.
Changes in [15.0.0](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0) (2021-10-25)
===================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [15.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v15.0.0-rc.1) (2021-10-19)
=============================================================================================================
## 🚨 BREAKING CHANGES
* Use `ICallFeedOpts` in the `CallFeed` constructor. To construct a new `CallFeed` object you have to pass `ICallFeedOpts` e.g. `const callFeed = new CallFeed({client ([\#1964](https://github.com/matrix-org/matrix-js-sdk/pull/1964)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Make threads use 'm.thread' relation ([\#1980](https://github.com/matrix-org/matrix-js-sdk/pull/1980)).
* Try to answer a call without video if we can't access the camera ([\#1972](https://github.com/matrix-org/matrix-js-sdk/pull/1972)). Fixes vector-im/element-web#17975 and vector-im/element-web#17975. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Make `opts` in `importRoomKeys()` optional ([\#1974](https://github.com/matrix-org/matrix-js-sdk/pull/1974)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Enable TypeScript declaration maps ([\#1966](https://github.com/matrix-org/matrix-js-sdk/pull/1966)). Contributed by [Alexendoo](https://github.com/Alexendoo).
## 🐛 Bug Fixes
* Fix `requestVerificationDM` with chronological `pendingEventOrdering` ([\#1943](https://github.com/matrix-org/matrix-js-sdk/pull/1943)). Contributed by [freaktechnik](https://github.com/freaktechnik).
Changes in [14.0.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.1) (2021-10-12)
===================================================================================================
## 🚨 BREAKING CHANGES
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
Changes in [14.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v14.0.0-rc.1) (2021-10-04)
=============================================================================================================
## 🚨 BREAKING CHANGES
* Support for call upgrades. `setLocalVideoMuted()` and `setMicrophoneMuted()` are now `async` and return the new mute state ([\#1827](https://github.com/matrix-org/matrix-js-sdk/pull/1827)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## ✨ Features
* Implement file versioning for tree spaces ([\#1952](https://github.com/matrix-org/matrix-js-sdk/pull/1952)).
* Allow answering calls without audio/video ([\#1950](https://github.com/matrix-org/matrix-js-sdk/pull/1950)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `bound` to `IThreepid` ([\#1941](https://github.com/matrix-org/matrix-js-sdk/pull/1941)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `trusted_locally` to `TrustInfo` ([\#1942](https://github.com/matrix-org/matrix-js-sdk/pull/1942)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix incorrect return value type in getJoinedRooms() ([\#1959](https://github.com/matrix-org/matrix-js-sdk/pull/1959)). Contributed by [psrpinto](https://github.com/psrpinto).
* Make sure to set `callLengthInterval` only once ([\#1958](https://github.com/matrix-org/matrix-js-sdk/pull/1958)). Fixes vector-im/element-web#19221 and vector-im/element-web#19221. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix event partitioning from non threading ready clients ([\#1948](https://github.com/matrix-org/matrix-js-sdk/pull/1948)).
* Ensure unencrypted fields get exposed by getEffectiveEvent() ([\#1938](https://github.com/matrix-org/matrix-js-sdk/pull/1938)). Fixes vector-im/element-web#19062 and vector-im/element-web#19062.
Changes in [13.0.0](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0) (2021-09-27)
===================================================================================================
## ✨ Features
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
## 🐛 Bug Fixes
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
Changes in [13.0.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v13.0.0-rc.1) (2021-09-21)
=============================================================================================================
## ✨ Features
* Add `getHistoryVisibility()` and `getGuestAccess()` ([\#1940](https://github.com/matrix-org/matrix-js-sdk/pull/1940)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `getBuffer()` to `QRCodeData` ([\#1927](https://github.com/matrix-org/matrix-js-sdk/pull/1927)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Added `createDataChannel()` and `CallEvent.DataChannel` to `MatrixCall` for creating and listening for WebRTC datachannels. ([\#1929](https://github.com/matrix-org/matrix-js-sdk/pull/1929)). Contributed by [robertlong](https://github.com/robertlong).
* Add file locking to MSC3089 branches ([\#1909](https://github.com/matrix-org/matrix-js-sdk/pull/1909)).
* Add `hasBeenCancelled` to `VerificationBase` ([\#1915](https://github.com/matrix-org/matrix-js-sdk/pull/1915)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Add `ISasEvent` ([\#1908](https://github.com/matrix-org/matrix-js-sdk/pull/1908)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Count notifications in encrypted rooms client-side ([\#1872](https://github.com/matrix-org/matrix-js-sdk/pull/1872)). Fixes vector-im/element-web#15393 and vector-im/element-web#15393. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Exclude opt-in Element performance metrics from encryption ([\#1897](https://github.com/matrix-org/matrix-js-sdk/pull/1897)).
## 🐛 Bug Fixes
* Fix race on automatic backup restore ([\#1936](https://github.com/matrix-org/matrix-js-sdk/pull/1936)). Fixes vector-im/element-web#17781 and vector-im/element-web#17781.
Changes in [12.5.0](https://github.com/vector-im/element-desktop/releases/tag/v12.5.0) (2021-09-14)
===================================================================================================
## ✨ Features
* [Release] Exclude opt-in Element performance metrics from encryption ([\#1901](https://github.com/matrix-org/matrix-js-sdk/pull/1901)).
* Give `MatrixCall` the capability to emit `LengthChanged` events ([\#1873](https://github.com/matrix-org/matrix-js-sdk/pull/1873)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Improve browser example ([\#1875](https://github.com/matrix-org/matrix-js-sdk/pull/1875)). Contributed by [psrpinto](https://github.com/psrpinto).
* Give `CallFeed` the capability to emit on volume changes ([\#1865](https://github.com/matrix-org/matrix-js-sdk/pull/1865)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* Fix verification request cancellation ([\#1871](https://github.com/matrix-org/matrix-js-sdk/pull/1871)).
Changes in [12.4.1](https://github.com/vector-im/element-desktop/releases/tag/v12.4.1) (2021-09-13)
===================================================================================================
## 🔒 SECURITY FIXES
* Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
for details.
Changes in [12.4.0](https://github.com/vector-im/element-desktop/releases/tag/v12.4.0) (2021-08-31)
===================================================================================================
## 🦖 Deprecations
* Deprecate groups APIs. Groups are no longer supported, only Synapse has support. They are being replaced by Spaces which build off of Rooms and are far more flexible. ([\#1792](https://github.com/matrix-org/matrix-js-sdk/pull/1792)).
## ✨ Features
* Add method for including extra fields when uploading to a tree space ([\#1850](https://github.com/matrix-org/matrix-js-sdk/pull/1850)).
## 🐛 Bug Fixes
* Fix broken voice calls, no ringing and broken call notifications ([\#1858](https://github.com/matrix-org/matrix-js-sdk/pull/1858)). Fixes vector-im/element-web#18578 vector-im/element-web#18538 and vector-im/element-web#18578. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Revert "Fix glare related regressions" ([\#1857](https://github.com/matrix-org/matrix-js-sdk/pull/1857)).
* Fix glare related regressions ([\#1851](https://github.com/matrix-org/matrix-js-sdk/pull/1851)). Fixes vector-im/element-web#18538 and vector-im/element-web#18538. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix temporary call messages being handled without call ([\#1834](https://github.com/matrix-org/matrix-js-sdk/pull/1834)). Contributed by [Palid](https://github.com/Palid).
* Fix conditional on returning file tree spaces ([\#1841](https://github.com/matrix-org/matrix-js-sdk/pull/1841)).
Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
===================================================================================================
## 🐛 Bug Fixes
* Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)).
Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16)
===================================================================================================
## ✨ Features
* Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Support for screen-sharing using multi-stream VoIP (MSC3077) ([\#1685](https://github.com/matrix-org/matrix-js-sdk/pull/1685)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* [Release] Fix glare related regressions ([\#1854](https://github.com/matrix-org/matrix-js-sdk/pull/1854)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503.
* Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321.
* fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)).
* Send `user_hangup` reason if the opponent supports it ([\#1820](https://github.com/matrix-org/matrix-js-sdk/pull/1820)). Fixes vector-im/element-web#18219. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)).
* Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)).
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-08-02)
===================================================================================================
## ✨ Features
* Improve calculateRoomName performances by using Intl.Collator
[\#1801](https://github.com/matrix-org/matrix-js-sdk/pull/1801)
* Switch callEventHandler from listening on `event` to `Room.timeline`
[\#1789](https://github.com/matrix-org/matrix-js-sdk/pull/1789)
* Expose MatrixEvent's internal clearEvent as a function
[\#1784](https://github.com/matrix-org/matrix-js-sdk/pull/1784)
## 🐛 Bug Fixes
* Clean up Event.clearEvent handling to fix a bug where malformed events with falsey content wouldn't be considered decrypted
[\#1807](https://github.com/matrix-org/matrix-js-sdk/pull/1807)
* Standardise spelling and casing of homeserver, identity server, and integration manager
[\#1782](https://github.com/matrix-org/matrix-js-sdk/pull/1782)
Changes in [12.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0) (2021-07-19)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.1.0-rc.1...v12.1.0)
* No changes from rc.1
Changes in [12.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0-rc.1) (2021-07-14)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.1...v12.1.0-rc.1)
* Add VS Code to gitignore
[\#1783](https://github.com/matrix-org/matrix-js-sdk/pull/1783)
* Make `Crypto::inRoomVerificationRequests` public
[\#1781](https://github.com/matrix-org/matrix-js-sdk/pull/1781)
* Call `setEventMetadata()` for filtered `timelineSet`s
[\#1765](https://github.com/matrix-org/matrix-js-sdk/pull/1765)
* Symmetric backup
[\#1775](https://github.com/matrix-org/matrix-js-sdk/pull/1775)
* Attempt to fix megolm key not being in SSSS
[\#1776](https://github.com/matrix-org/matrix-js-sdk/pull/1776)
* Convert SecretStorage to TypeScript
[\#1774](https://github.com/matrix-org/matrix-js-sdk/pull/1774)
* Strip hash from urls being previewed to de-duplicate
[\#1721](https://github.com/matrix-org/matrix-js-sdk/pull/1721)
* Do not generate a lockfile when running in CI
[\#1773](https://github.com/matrix-org/matrix-js-sdk/pull/1773)
* Tidy up secret requesting code
[\#1766](https://github.com/matrix-org/matrix-js-sdk/pull/1766)
* Convert Sync and SyncAccumulator to Typescript
[\#1763](https://github.com/matrix-org/matrix-js-sdk/pull/1763)
* Convert EventTimeline, EventTimelineSet and TimelineWindow to TS
[\#1762](https://github.com/matrix-org/matrix-js-sdk/pull/1762)
* Comply with new member-delimiter-style rule
[\#1764](https://github.com/matrix-org/matrix-js-sdk/pull/1764)
* Do not honor string power levels
[\#1754](https://github.com/matrix-org/matrix-js-sdk/pull/1754)
* Typescriptify some crypto stuffs
[\#1508](https://github.com/matrix-org/matrix-js-sdk/pull/1508)
* Make filterId read/write and optional
[\#1760](https://github.com/matrix-org/matrix-js-sdk/pull/1760)
Changes in [12.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.1) (2021-07-05)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.1-rc.1...v12.0.1)
* No changes from rc.1
Changes in [12.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.1-rc.1) (2021-06-29)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.0...v12.0.1-rc.1)
* Fix broken /messages filtering due to internal field changes in
FilterComponent
[\#1759](https://github.com/matrix-org/matrix-js-sdk/pull/1759)
* Convert crypto index to TS
[\#1749](https://github.com/matrix-org/matrix-js-sdk/pull/1749)
* Fix typescript return types for membership update events
[\#1739](https://github.com/matrix-org/matrix-js-sdk/pull/1739)
* Fix types of MatrixEvent sender & target
[\#1753](https://github.com/matrix-org/matrix-js-sdk/pull/1753)
* Add keysharing on invites to File Tree Spaces
[\#1744](https://github.com/matrix-org/matrix-js-sdk/pull/1744)
* Convert Room and RoomState to Typescript
[\#1746](https://github.com/matrix-org/matrix-js-sdk/pull/1746)
* Improve type of IContent msgtype
[\#1752](https://github.com/matrix-org/matrix-js-sdk/pull/1752)
* Add PR template
[\#1747](https://github.com/matrix-org/matrix-js-sdk/pull/1747)
* Add functions to assist in immutability of Event objects
[\#1738](https://github.com/matrix-org/matrix-js-sdk/pull/1738)
* Convert Event Context to TS
[\#1742](https://github.com/matrix-org/matrix-js-sdk/pull/1742)
* Bump lodash from 4.17.20 to 4.17.21
[\#1743](https://github.com/matrix-org/matrix-js-sdk/pull/1743)
* Add invite retries to file trees
[\#1740](https://github.com/matrix-org/matrix-js-sdk/pull/1740)
* Convert IndexedDBStore to TS
[\#1741](https://github.com/matrix-org/matrix-js-sdk/pull/1741)
* Convert additional files to typescript
[\#1736](https://github.com/matrix-org/matrix-js-sdk/pull/1736)
Changes in [12.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0) (2021-06-21)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.0-rc.1...v12.0.0)
* No changes since rc.1
Changes in [12.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0-rc.1) (2021-06-15)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0...v12.0.0-rc.1)
* Rework how disambiguation is handled
[\#1730](https://github.com/matrix-org/matrix-js-sdk/pull/1730)
* Fix baseToString for n=0 edge case to match inverse stringToBase
[\#1735](https://github.com/matrix-org/matrix-js-sdk/pull/1735)
* Move various types from the react-sdk to the js-sdk
[\#1734](https://github.com/matrix-org/matrix-js-sdk/pull/1734)
* Unstable implementation of MSC3089: File Trees
[\#1732](https://github.com/matrix-org/matrix-js-sdk/pull/1732)
* Add MSC3230 event type to enum
[\#1729](https://github.com/matrix-org/matrix-js-sdk/pull/1729)
* Add separate reason code for transferred calls
[\#1731](https://github.com/matrix-org/matrix-js-sdk/pull/1731)
* Use sendonly for call hold
[\#1728](https://github.com/matrix-org/matrix-js-sdk/pull/1728)
* Stop breeding sync listeners
[\#1727](https://github.com/matrix-org/matrix-js-sdk/pull/1727)
* Fix semicolons in TS files
[\#1724](https://github.com/matrix-org/matrix-js-sdk/pull/1724)
* [BREAKING] Convert MatrixClient to TypeScript
[\#1718](https://github.com/matrix-org/matrix-js-sdk/pull/1718)
* Factor out backup management to a separate module
[\#1697](https://github.com/matrix-org/matrix-js-sdk/pull/1697)
* Ignore power_levels events with unknown state_key on room-state
initialization
[\#1723](https://github.com/matrix-org/matrix-js-sdk/pull/1723)
* Revert 1579 (Fix extra negotiate message in Firefox)
[\#1725](https://github.com/matrix-org/matrix-js-sdk/pull/1725)
Changes in [11.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0) (2021-06-07)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0-rc.1...v11.2.0)
* No changes since rc.1
Changes in [11.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0-rc.1) (2021-06-01)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0...v11.2.0-rc.1)
+215
View File
@@ -0,0 +1,215 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch.
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
Things that should *not* go into your PR description:
* Any information on how the code works or why you chose to do it the way
you did. If this isn't obvious from your code, you haven't written enough
comments.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
If your PR is a feature (ie. if it's being labelled with the 'T-Enhancement'
label) then we require that the PR also includes tests. These need to test that
your feature works as expected and ideally test edge cases too. For the js-sdk
itself, your tests should generally be unit tests. matrix-react-sdk also uses
these guidelines, so for that your tests can be unit tests using
react-test-utils, snapshot tests or screenshot tests.
We don't require tests for bug fixes (T-Defect) but strongly encourage regression
tests for the bug itself wherever possible.
In the future we may formalise this more with a minimum test coverage
percentage for the diff.
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
-131
View File
@@ -1,131 +0,0 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
How to contribute
~~~~~~~~~~~~~~~~~
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
**The single biggest thing you need to know is: please base your changes on
the develop branch - /not/ master.**
We use the master branch to track the most recent release, so that folks who
blindly clone the repo and automatically check out master get something that
works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use GitHub's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Code style
~~~~~~~~~~
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **never** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
~~~~~~~~~~~
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix::
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the ``-s`` flag to
``git commit``, which uses the name and email set in your ``user.name`` and
``user.email`` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase::
git rebase --signoff origin/develop
+1 -1
View File
@@ -1,6 +1,6 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));
+3 -1
View File
@@ -1,6 +1,8 @@
<html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8"/>
<link rel="icon" href="data:,">
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
+15 -10
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "11.2.0-rc.1",
"version": "15.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -9,13 +9,14 @@
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 72 src spec",
"lint:js": "eslint --max-warnings 4 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
@@ -54,7 +55,9 @@
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.4",
"eslint-plugin-import": "^2.25.2",
"loglevel": "^1.7.1",
"p-retry": "^4.5.0",
"qs": "^6.9.6",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
@@ -72,20 +75,22 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@types/bs58": "^4.0.1",
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.3.2",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#48ec1e6af2cfb8310b9a6e23edf2dc7a26ddd580",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
@@ -95,7 +100,7 @@
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typescript": "^4.1.3"
"typescript": "^4.5.3"
},
"jest": {
"testEnvironment": "node",
+7 -17
View File
@@ -100,12 +100,7 @@ fi
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
yarn install --ignore-scripts --pure-lockfile
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
@@ -133,14 +128,6 @@ if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
@@ -156,7 +143,7 @@ fi
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
update_changelog -f "$changelog_file" "$release"
yarn run allchange "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
@@ -204,7 +191,10 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
fi
@@ -225,7 +215,7 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install
yarn install --pure-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
+2 -1
View File
@@ -20,6 +20,7 @@ limitations under the License.
import './olm-loader';
import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { WebStorageSessionStore } from "../src/store/session/webstorage";
@@ -69,7 +70,7 @@ export function TestClient(
this.deviceKeys = null;
this.oneTimeKeys = {};
this._callEventHandler = {
this.callEventHandler = {
calls: new Map(),
};
}
+2 -1
View File
@@ -17,9 +17,10 @@ limitations under the License.
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import MockHttpBackend from "matrix-mock-request";
import { MockStorageApi } from "../MockStorageApi";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import MockHttpBackend from "matrix-mock-request";
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
import * as utils from "../test-utils";
+8 -8
View File
@@ -165,7 +165,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
@@ -202,7 +202,7 @@ describe("DeviceList management:", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -235,7 +235,7 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -256,7 +256,7 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -286,7 +286,7 @@ describe("DeviceList management:", function() {
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -322,7 +322,7 @@ describe("DeviceList management:", function() {
);
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -358,7 +358,7 @@ describe("DeviceList management:", function() {
);
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
@@ -379,7 +379,7 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
+1 -1
View File
@@ -159,7 +159,7 @@ function aliDownloadsKeys() {
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return Promise.all([p1, p2]).then(() => {
return aliTestClient.client._crypto._deviceList.saveIfDirty();
return aliTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId];
+1 -1
View File
@@ -336,7 +336,7 @@ describe("MatrixClient", function() {
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
};
logger.log("Ed25519: " + ed25519key);
+2 -1
View File
@@ -1,5 +1,6 @@
import * as utils from "../test-utils";
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils";
import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
@@ -4,13 +4,13 @@ import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client = null;
let httpBackend = null;
let client: TestClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room;
let room: Room;
beforeEach(function() {
scheduler = new MatrixScheduler();
@@ -23,7 +23,7 @@ describe("MatrixClient retrying", function() {
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId);
room = new Room(roomId, client, userId);
client.store.storeRoom(room);
});
@@ -50,17 +50,23 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
const p1 = client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => {
}, () => {
// this is expected
});
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, "m2");
client.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
@@ -72,7 +78,7 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
@@ -88,7 +94,7 @@ describe("MatrixClient retrying", function() {
}).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => {
const p3 = new Promise<void>((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => {
if (ev0 === ev1) {
resolve();
+69 -4
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import anotherjson from "another-json";
import * as testUtils from "../test-utils";
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
@@ -998,7 +999,7 @@ describe("megolm", function() {
...rawEvent,
room: ROOM_ID,
});
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeTruthy();
});
}).then(() => {
@@ -1012,17 +1013,81 @@ describe("megolm", function() {
},
event: true,
});
event._senderCurve25519Key = testSenderKey;
return testClient.client._crypto._onRoomKeyEvent(event);
event.senderCurve25519Key = testSenderKey;
return testClient.client.crypto.onRoomKeyEvent(event);
}).then(() => {
const event = testUtils.mkEvent({
event: true,
...rawEvent,
room: ROOM_ID,
});
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeFalsy();
});
});
});
it("Alice can decrypt a message with falsey content", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
const plaintext = {
type: "m.room.message",
content: undefined,
room_id: ROOM_ID,
};
const messageEncrypted = {
event_id: 'test_megolm_event',
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: groupSession.encrypt(JSON.stringify(plaintext)),
device_id: "testDevice",
sender_key: testSenderKey,
session_id: groupSession.session_id(),
},
type: "m.room.encrypted",
};
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getRoomId()).toEqual(ROOM_ID);
expect(event.getContent()).toEqual({});
expect(event.getClearContent()).toBeUndefined();
});
});
});
+7 -7
View File
@@ -51,7 +51,7 @@ export function mock(constr, name) {
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
@@ -91,7 +91,7 @@ export function mkEvent(opts) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].indexOf(opts.type) !== -1) {
"com.example.state"].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
@@ -357,12 +357,12 @@ export function setHttpResponses(
);
const httpReq = httpResponseObj.request.bind(httpResponseObj);
client._http = [
client.http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
client._http.authedRequest.mockImplementation(httpReq);
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
client._http.requestWithPrefix.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq);
client.http.authedRequest.mockImplementation(httpReq);
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
client.http.requestWithPrefix.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
}
+78
View File
@@ -0,0 +1,78 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { NamespacedValue, UnstableValue } from "../../src/NamespacedValue";
describe("NamespacedValue", () => {
it("should prefer stable over unstable", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBe(ns.unstable);
});
it("should return unstable if there is no stable", () => {
const ns = new NamespacedValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
});
it("should match against either stable or unstable", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.matches("no")).toBe(false);
expect(ns.matches(ns.stable)).toBe(true);
expect(ns.matches(ns.unstable)).toBe(true);
});
it("should not permit falsey values for both parts", () => {
try {
new UnstableValue(null, null);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("One of stable or unstable values must be supplied");
}
});
});
describe("UnstableValue", () => {
it("should prefer unstable over stable", () => {
const ns = new UnstableValue("stable", "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBe(ns.stable);
});
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
});
it("should not permit falsey unstable values", () => {
try {
new UnstableValue("stable", null);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("Unstable value must be supplied");
}
});
});
+1
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { EventEmitter } from "events";
import { ReEmitter } from "../../src/ReEmitter";
const EVENTNAME = "UnknownEntry";
+1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
+14 -13
View File
@@ -1,4 +1,6 @@
import '../olm-loader';
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
@@ -8,7 +10,6 @@ import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { EventEmitter } from "events";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
@@ -65,7 +66,7 @@ describe("Crypto", function() {
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
@@ -213,7 +214,7 @@ describe("Crypto", function() {
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient._crypto._olmDevice
const key = await aliceClient.crypto.olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
@@ -234,7 +235,7 @@ describe("Crypto", function() {
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent._senderCurve25519Key = "akey";
ksEvent.senderCurve25519Key = "akey";
return ksEvent;
}
@@ -273,19 +274,19 @@ describe("Crypto", function() {
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient._crypto.encryptEvent(event, aliceRoom);
event._clearEvent = {};
event._senderCurve25519Key = null;
event._claimedEd25519Key = null;
await aliceClient.crypto.encryptEvent(event, aliceRoom);
event.clearEvent = undefined;
event.senderCurve25519Key = null;
event.claimedEd25519Key = null;
try {
await bobClient._crypto.decryptEvent(event);
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
@@ -302,7 +303,7 @@ describe("Crypto", function() {
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient._cryptoStore;
const cryptoStore = bobClient.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
@@ -344,7 +345,7 @@ describe("Crypto", function() {
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient._cryptoStore;
const cryptoStore = aliceClient.cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
@@ -377,7 +378,7 @@ describe("Crypto", function() {
// key requests get queued until the sync has finished, but we don't
// let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now.
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
+145 -94
View File
@@ -256,94 +256,145 @@ describe("MegolmDecryption", function() {
});
});
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
describe("session reuse and key reshares", () => {
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
beforeEach(async () => {
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
aliceDeviceInfo = {
deviceId: 'aliceDevice',
isBlocked: jest.fn().mockReturnValue(false),
isUnverified: jest.fn().mockReturnValue(false),
getIdentityKey: jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: jest.fn().mockReturnValue(''),
};
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
aliceDevice: aliceDeviceInfo,
},
},
}));
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
it("re-uses sessions for sequential messages", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
mockBaseApis.claimOneTimeKeys.mockReset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
it("re-shares keys to devices it's already sent to", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
});
it("does not re-share keys to devices whose keys have changed", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
aliceDeviceInfo.getIdentityKey = jest.fn().mockReturnValue(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
'@alice:home.server',
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
});
});
});
@@ -362,9 +413,9 @@ describe("MegolmDecryption", function() {
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice1 = bobClient1._crypto._olmDevice;
const bobDevice2 = bobClient2._crypto._olmDevice;
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice1 = bobClient1.crypto.olmDevice;
const bobDevice2 = bobClient2.crypto.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -401,11 +452,11 @@ describe("MegolmDecryption", function() {
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
aliceClient.crypto.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
return this.getDevicesFromStore(userIds);
};
let run = false;
@@ -445,7 +496,7 @@ describe("MegolmDecryption", function() {
body: "secret",
},
});
await aliceClient._crypto.encryptEvent(event, room);
await aliceClient.crypto.encryptEvent(event, room);
expect(run).toBe(true);
@@ -465,8 +516,8 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice = bobClient._crypto._olmDevice;
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice = bobClient.crypto.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -505,11 +556,11 @@ describe("MegolmDecryption", function() {
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
aliceClient.crypto.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
return this.getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
@@ -543,7 +594,7 @@ describe("MegolmDecryption", function() {
event_id: "$event",
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await sendPromise;
});
@@ -558,11 +609,11 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const bobDevice = bobClient.crypto.olmDevice;
const roomId = "!someroom";
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -575,7 +626,7 @@ describe("MegolmDecryption", function() {
},
}));
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -601,14 +652,14 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice;
aliceClient.crypto.downloadKeys = async () => {};
const bobDevice = bobClient.crypto.olmDevice;
const roomId = "!someroom";
const now = Date.now();
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -625,7 +676,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -652,15 +703,15 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient.crypto.olmDevice;
aliceClient.crypto.downloadKeys = async () => {};
const roomId = "!someroom";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
@@ -675,7 +726,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
+2 -2
View File
@@ -246,7 +246,7 @@ describe("OlmDevice", function() {
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
@@ -254,7 +254,7 @@ describe("OlmDevice", function() {
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
+215 -66
View File
@@ -28,6 +28,7 @@ import * as testUtils from "../../test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
const Olm = global.Olm;
@@ -51,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
origin_server_ts: 1507753886000,
});
const KEY_BACKUP_DATA = {
const CURVE25519_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
@@ -72,14 +73,41 @@ const KEY_BACKUP_DATA = {
},
};
const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1",
const AES256_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
+ 'RgaDHkfzoA3g3aeQ',
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
},
};
const CURVE25519_BACKUP_INFO = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const AES256_BACKUP_INFO = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
// FIXME: add iv and mac
},
};
const keys = {};
function getCrossSigningKey(type) {
@@ -138,11 +166,12 @@ describe("MegolmBackup", function() {
let megolmDecryption;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
@@ -215,16 +244,18 @@ describe("MegolmBackup", function() {
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = jest.fn();
mockCrypto.backupManager = {
backupGroupSession: jest.fn(),
};
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server', function() {
it('sends backups to the server (Curve25519 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -257,14 +288,14 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -272,7 +303,7 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
@@ -292,12 +323,91 @@ describe("MegolmBackup", function() {
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession(
"roomId",
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});
it('sends backups to the server (AES-256 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(1);
@@ -335,39 +445,48 @@ describe("MegolmBackup", function() {
});
await resetCrossSigningKeys(client);
let numCalls = 0;
await new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({});
}
resolve();
return Promise.resolve({});
};
await Promise.all([
new Promise((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) {
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({});
}
backupInfo = data;
return Promise.resolve({});
} else if (numCalls === 2) {
expect(method).toBe("GET");
expect(path).toBe("/room_keys/version");
resolve();
return Promise.resolve(backupInfo);
} else {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many times"));
return Promise.resolve({});
}
};
}),
client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
});
expect(numCalls).toBe(1);
}),
]);
expect(numCalls).toBe(2);
});
it('retries when a backup fails', function() {
@@ -427,14 +546,14 @@ describe("MegolmBackup", function() {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
@@ -442,7 +561,7 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
@@ -468,12 +587,9 @@ describe("MegolmBackup", function() {
);
}
};
client._crypto.backupGroupSession(
"roomId",
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(2);
@@ -505,30 +621,47 @@ describe("MegolmBackup", function() {
client.stopClient();
});
it('can restore from backup', function() {
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
});
it('can restore backup by room', function() {
client._http.authedRequest = function() {
it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
});
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
},
},
},
@@ -536,7 +669,7 @@ describe("MegolmBackup", function() {
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
null, null, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
@@ -546,28 +679,44 @@ describe("MegolmBackup", function() {
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey();
await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
CURVE25519_BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
it("fails if an known algorithm is used", async function() {
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
)).rejects.toThrow();
});
});
});
+45 -40
View File
@@ -17,6 +17,7 @@ limitations under the License.
import '../../olm-loader';
import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { HttpResponse, setHttpResponses } from '../../test-utils';
@@ -64,8 +65,8 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
await olmlib.verifySignature(
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
);
});
alice.uploadKeySignatures = async () => {};
@@ -138,7 +139,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -201,24 +202,28 @@ describe("Cross Signing", function() {
const uploadSigsPromise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = jest.fn(async (content) => {
await olmlib.verifySignature(
alice._crypto._olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
"@alice:example.com",
);
resolve();
try {
await olmlib.verifySignature(
alice.crypto.olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
"@alice:example.com",
);
resolve();
} catch (e) {
reject(e);
}
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -226,7 +231,7 @@ describe("Cross Signing", function() {
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
await alice.crypto.signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
// feed sync result that includes master key, ssk, device key
@@ -354,7 +359,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -383,7 +388,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobPubkey]: sig,
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be TOFU
@@ -417,8 +422,8 @@ describe("Cross Signing", function() {
null,
aliceKeys,
);
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
@@ -433,14 +438,14 @@ describe("Cross Signing", function() {
]);
const keyChangePromise = new Promise((resolve, reject) => {
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -448,7 +453,7 @@ describe("Cross Signing", function() {
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
await alice.crypto.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
@@ -602,7 +607,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -625,7 +630,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be untrusted
@@ -669,7 +674,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -697,7 +702,7 @@ describe("Cross Signing", function() {
bobDevice.signatures = {};
bobDevice.signatures["@bob:example.com"] = {};
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
@@ -729,7 +734,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -766,7 +771,7 @@ describe("Cross Signing", function() {
// Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
@@ -801,20 +806,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key
await resetCrossSigningKeys(bob);
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
},
verified: 1,
known: true,
},
});
alice._crypto._deviceList.storeCrossSigningForUser(
alice.crypto.deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob._crypto._crossSigningInfo.toStorage(),
bob.crypto.crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => {};
@@ -834,7 +839,7 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
@@ -844,9 +849,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
await new Promise((resolve) => {
alice._crypto.on("userTrustStatusChanged", resolve);
alice.crypto.on("userTrustStatusChanged", resolve);
});
await upgradePromise;
+10 -10
View File
@@ -6,28 +6,28 @@ export async function resetCrossSigningKeys(client, {
level,
authUploadDeviceSigningKeys = async func => await func(),
} = {}) {
const crypto = client._crypto;
const crypto = client.crypto;
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
try {
await crypto._crossSigningInfo.resetKeys(level);
await crypto._signObject(crypto._crossSigningInfo.keys.master);
await crypto.crossSigningInfo.resetKeys(level);
await crypto.signObject(crypto.crossSigningInfo.keys.master);
// write a copy locally so we know these are trusted keys
await crypto._cryptoStore.doTxn(
await crypto.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto._cryptoStore.storeCrossSigningKeys(
txn, crypto._crossSigningInfo.keys);
crypto.cryptoStore.storeCrossSigningKeys(
txn, crypto.crossSigningInfo.keys);
},
);
} catch (e) {
// If anything failed here, revert the keys so we know to try again from the start
// next time.
crypto._crossSigningInfo.keys = oldKeys;
crypto.crossSigningInfo.keys = oldKeys;
throw e;
}
crypto._baseApis.emit("crossSigning.keysChanged", {});
await crypto._afterCrossSigningLocalKeyChange();
crypto.baseApis.emit("crossSigning.keysChanged", {});
await crypto.afterCrossSigningLocalKeyChange();
}
export async function createSecretStorageKey() {
@@ -18,28 +18,26 @@ import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import {
ROOM_KEY_REQUEST_STATES,
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
state: ROOM_KEY_REQUEST_STATES.SENT,
state: RoomKeyRequestState.Sent,
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
state: ROOM_KEY_REQUEST_STATES.SENT,
state: RoomKeyRequestState.Sent,
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
state: ROOM_KEY_REQUEST_STATES.UNSENT,
state: RoomKeyRequestState.Unsent,
},
];
@@ -68,9 +66,9 @@ describe.each([
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
expect(r).toHaveLength(2);
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
expect(r).toContainEqual(e);
});
});
@@ -78,10 +76,10 @@ describe.each([
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
expect(r.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r);
});
});
+18 -19
View File
@@ -23,7 +23,6 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
try {
@@ -47,7 +46,7 @@ async function makeTestClient(userInfo, options) {
await client.initCrypto();
// No need to download keys for these tests
client._crypto.downloadKeys = async function() {};
client.crypto.downloadKeys = async function() {};
return client;
}
@@ -99,11 +98,11 @@ describe("Secrets", function() {
},
},
);
alice._crypto._crossSigningInfo.setKeys({
alice.crypto.crossSigningInfo.setKeys({
master: signingkeyInfo,
});
const secretStorage = alice._crypto._secretStorage;
const secretStorage = alice.crypto.secretStorage;
alice.setAccountData = async function(eventType, contents, callback) {
alice.store.storeAccountDataEvents([
@@ -120,7 +119,7 @@ describe("Secrets", function() {
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([
new MatrixEvent({
@@ -234,11 +233,11 @@ describe("Secrets", function() {
},
);
const vaxDevice = vax.client._crypto._olmDevice;
const osborne2Device = osborne2.client._crypto._olmDevice;
const secretStorage = osborne2.client._crypto._secretStorage;
const vaxDevice = vax.client.crypto.olmDevice;
const osborne2Device = osborne2.client.crypto.olmDevice;
const secretStorage = osborne2.client.crypto.secretStorage;
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
user_id: "@alice:example.com",
device_id: "VAX",
@@ -249,7 +248,7 @@ describe("Secrets", function() {
},
},
});
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
@@ -265,7 +264,7 @@ describe("Secrets", function() {
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished();
await vax.client._crypto._olmDevice.createOutboundSession(
await vax.client.crypto.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key,
Object.values(otks)[0],
);
@@ -334,8 +333,8 @@ describe("Secrets", function() {
createSecretStorageKey,
});
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@@ -376,10 +375,10 @@ describe("Secrets", function() {
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
bob.crypto.backupManager.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({
@@ -394,7 +393,7 @@ describe("Secrets", function() {
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
bob.crypto.deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
@@ -479,7 +478,7 @@ describe("Secrets", function() {
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -619,7 +618,7 @@ describe("Secrets", function() {
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
@@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS],
},
);
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
keys: {
@@ -69,7 +69,7 @@ describe("verification request integration tests with crypto layer", function()
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
@@ -77,6 +77,6 @@ describe("verification request integration tests with crypto layer", function()
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
aliceVerifier.endTimer();
});
});
+6 -6
View File
@@ -87,8 +87,8 @@ describe("SAS verification", function() {
},
);
const aliceDevice = alice.client._crypto._olmDevice;
const bobDevice = bob.client._crypto._olmDevice;
const aliceDevice = alice.client.crypto.olmDevice;
const bobDevice = bob.client.crypto.olmDevice;
ALICE_DEVICES = {
Osborne2: {
@@ -114,14 +114,14 @@ describe("SAS verification", function() {
},
};
alice.client._crypto._deviceList.storeDevicesForUser(
alice.client.crypto.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client._crypto._deviceList.storeDevicesForUser(
bob.client.crypto.deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES,
);
bob.client.downloadKeys = () => {
@@ -296,9 +296,9 @@ describe("SAS verification", function() {
await resetCrossSigningKeys(bob.client);
bob.client._crypto._deviceList.storeCrossSigningForUser(
bob.client.crypto.deviceList.storeCrossSigningForUser(
"@alice:example.com", {
keys: alice.client._crypto._crossSigningInfo.keys,
keys: alice.client.crypto.crossSigningInfo.keys,
},
);
@@ -48,18 +48,18 @@ describe("self-verifications", () => {
storeCrossSigningKeyCache: jest.fn(),
};
const _crossSigningInfo = new CrossSigningInfo(
const crossSigningInfo = new CrossSigningInfo(
userId,
{},
cacheCallbacks,
);
_crossSigningInfo.keys = {
crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } },
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
};
const _secretStorage = {
const secretStorage = {
request: jest.fn().mockReturnValue({
promise: Promise.resolve(encodeBase64(testKey)),
}),
@@ -69,13 +69,13 @@ describe("self-verifications", () => {
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
const client = {
_crypto: {
_crossSigningInfo,
_secretStorage,
crypto: {
crossSigningInfo,
secretStorage,
storeSessionBackupPrivateKey,
getSessionBackupPrivateKey: () => null,
},
requestSecret: _secretStorage.request.bind(_secretStorage),
requestSecret: secretStorage.request.bind(secretStorage),
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
@@ -93,13 +93,13 @@ describe("self-verifications", () => {
undefined, // startEvent
request,
);
verification._resolve = () => undefined;
verification.resolve = () => undefined;
const result = await verification.done();
/* We should request, and store, 3 cross signing keys and the key backup key */
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
expect(_secretStorage.request.mock.calls.length).toBe(4);
expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
+3 -2
View File
@@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import nodeCrypto from "crypto";
import { logger } from '../../../../src/logger';
export async function makeTestClients(userInfos, options) {
@@ -36,7 +37,7 @@ export async function makeTestClients(userInfos, options) {
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) :
event.attemptDecryption(client.crypto) :
Promise.resolve();
decryptionPromise.then(
+4 -4
View File
@@ -3,8 +3,8 @@ import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
function mockRoomStates(timeline) {
timeline._startState = utils.mock(RoomState, "startState");
timeline._endState = utils.mock(RoomState, "endState");
timeline.startState = utils.mock(RoomState, "startState");
timeline.endState = utils.mock(RoomState, "endState");
}
describe("EventTimeline", function() {
@@ -48,10 +48,10 @@ describe("EventTimeline", function() {
}),
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
expect(timeline.startState.setStateEvents).toHaveBeenCalledWith(
events,
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
expect(timeline.endState.setStateEvents).toHaveBeenCalledWith(
events,
);
});
+197 -5
View File
@@ -1,6 +1,18 @@
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src/client";
import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
EventType,
RoomCreateTypeField,
RoomType,
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
jest.useFakeTimers();
@@ -144,12 +156,12 @@ describe("MatrixClient", function() {
scheduler: scheduler,
userId: userId,
});
// FIXME: We shouldn't be yanking _http like this.
client._http = [
// FIXME: We shouldn't be yanking http like this.
client.http = [
"authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
client._http.authedRequest.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq);
client.http.authedRequest.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
// set reasonable working defaults
acceptKeepalives = true;
@@ -166,11 +178,191 @@ describe("MatrixClient", function() {
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.mockImplementation(function() {
client.http.authedRequest.mockImplementation(function() {
return new Promise(() => {});
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
const roomName = "Test Tree";
const mockRoom = {};
const fn = jest.fn().mockImplementation((opts) => {
expect(opts).toMatchObject({
name: roomName,
preset: Preset.PrivateChat,
power_level_content_override: {
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
users: {
[userId]: 100,
},
},
creation_content: {
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: [
{
// We use `unstable` to ensure that the code is actually using the right identifier
type: UNSTABLE_MSC3088_PURPOSE.unstable,
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
},
},
{
type: EventType.RoomEncryption,
state_key: "",
content: {
algorithm: MEGOLM_ALGORITHM,
},
},
],
});
return { room_id: roomId };
});
client.getUserId = () => userId;
client.createRoom = fn;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = await client.unstableCreateFileTree(roomName);
expect(tree).toBeDefined();
expect(tree.roomId).toEqual(roomId);
expect(tree.room).toBe(mockRoom);
expect(fn.mock.calls.length).toBe(1);
});
it("should get (unstable) file trees with valid state", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
content: {
[RoomCreateTypeField]: RoomType.Space,
},
});
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
// We use `unstable` to ensure that the code is actually using the right identifier
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
},
});
} else {
throw new Error("Unexpected event type or state key");
}
},
},
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeDefined();
expect(tree.roomId).toEqual(roomId);
expect(tree.room).toBe(mockRoom);
});
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees for unknown rooms", async () => {
const roomId = "!room:example.org";
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return null; // imply unknown
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees with invalid create contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
content: {
[RoomCreateTypeField]: "org.example.not_space",
},
});
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
// We use `unstable` to ensure that the code is actually using the right identifier
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
},
});
} else {
throw new Error("Unexpected event type or state key");
}
},
},
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
content: {
[RoomCreateTypeField]: RoomType.Space,
},
});
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
return new MatrixEvent({
content: {
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
},
});
} else {
throw new Error("Unexpected event type or state key");
}
},
},
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
+331
View File
@@ -0,0 +1,331 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
import { Room } from "../../../src/models/room";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
describe("MSC3089Branch", () => {
let client: MatrixClient;
// @ts-ignore - TS doesn't know that this is a type
let indexEvent: any;
let directory: MSC3089TreeSpace;
let branch: MSC3089Branch;
let branch2: MSC3089Branch;
const branchRoomId = "!room:example.org";
const fileEventId = "$file";
const fileEventId2 = "$second_file";
const staticTimelineSets = {} as EventTimelineSet;
const staticRoom = {
getUnfilteredTimelineSet: () => staticTimelineSets,
} as any as Room; // partial
beforeEach(() => {
// TODO: Use utility functions to create test rooms and clients
client = <MatrixClient>{
getRoom: (roomId: string) => {
if (roomId === branchRoomId) {
return staticRoom;
} else {
throw new Error("Unexpected fetch for unknown room");
}
},
};
indexEvent = ({
getRoomId: () => branchRoomId,
getStateKey: () => fileEventId,
});
directory = new MSC3089TreeSpace(client, branchRoomId);
branch = new MSC3089Branch(client, indexEvent, directory);
branch2 = new MSC3089Branch(client, {
getRoomId: () => branchRoomId,
getStateKey: () => fileEventId2,
} as MatrixEvent, directory);
});
it('should know the file event ID', () => {
expect(branch.id).toEqual(fileEventId);
});
it('should know if the file is active or not', () => {
indexEvent.getContent = () => ({});
expect(branch.isActive).toBe(false);
indexEvent.getContent = () => ({ active: false });
expect(branch.isActive).toBe(false);
indexEvent.getContent = () => ({ active: true });
expect(branch.isActive).toBe(true);
indexEvent.getContent = () => ({ active: "true" }); // invalid boolean, inactive
expect(branch.isActive).toBe(false);
});
it('should be able to delete the file', async () => {
const eventIdOrder = [fileEventId, fileEventId2];
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({});
expect(content['active']).toBeUndefined();
expect(stateKey).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
return Promise.resolve(); // return value not used
});
client.redactEvent = redactFn;
branch.getVersionHistory = () => Promise.resolve([branch, branch2]);
branch2.getVersionHistory = () => Promise.resolve([branch2]);
await branch.delete();
expect(stateFn).toHaveBeenCalledTimes(2);
expect(redactFn).toHaveBeenCalledTimes(2);
});
it('should know its name', async () => {
const name = "My File.txt";
indexEvent.getContent = () => ({ active: true, name: name });
const res = branch.getName();
expect(res).toEqual(name);
});
it('should be able to change its name', async () => {
const name = "My File.txt";
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({
retained: true, // canary for copying state
active: true,
name: name,
});
expect(stateKey).toEqual(fileEventId);
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
await branch.setName(name);
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be v1 by default', () => {
indexEvent.getContent = () => ({ active: true });
const res = branch.version;
expect(res).toEqual(1);
});
it('should be vN when set', () => {
indexEvent.getContent = () => ({ active: true, version: 3 });
const res = branch.version;
expect(res).toEqual(3);
});
it('should be unlocked by default', async () => {
indexEvent.getContent = () => ({ active: true });
const res = branch.isLocked();
expect(res).toEqual(false);
});
it('should use lock status from index event', async () => {
indexEvent.getContent = () => ({ active: true, locked: true });
const res = branch.isLocked();
expect(res).toEqual(true);
});
it('should be able to change its locked status', async () => {
const locked = true;
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(content).toMatchObject({
retained: true, // canary for copying state
active: true,
locked: locked,
});
expect(stateKey).toEqual(fileEventId);
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
await branch.setLocked(locked);
expect(stateFn).toHaveBeenCalledTimes(1);
});
it('should be able to return event information', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({
findEventById: (eventId) => {
expect(eventId).toEqual(fileEventId);
return fileEvent;
},
}) as EventTimelineSet;
client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
};
client.decryptEventIfNeeded = () => Promise.resolve();
const res = await branch.getFileInfo();
expect(res).toBeDefined();
expect(res).toMatchObject({
info: fileContent,
// Escape regex from MDN guides: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
});
});
it('should be able to return the event object', async () => {
const mxcLatter = "example.org/file";
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
const fileEvent = { getId: () => fileEventId, getOriginalContent: () => ({ file: fileContent }) };
staticRoom.getUnfilteredTimelineSet = () => ({
findEventById: (eventId) => {
expect(eventId).toEqual(fileEventId);
return fileEvent;
},
}) as EventTimelineSet;
client.mxcUrlToHttp = (mxc: string) => {
expect(mxc).toEqual("mxc://" + mxcLatter);
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
};
client.decryptEventIfNeeded = () => Promise.resolve();
const res = await branch.getFileEvent();
expect(res).toBeDefined();
expect(res).toBe(fileEvent);
});
it('should create new versions of itself', async () => {
const canaryName = "canary";
const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile;
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
const stateFn = jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
if (stateKey === fileEventId) {
expect(content).toMatchObject({
retained: true, // canary for copying state
active: false,
});
} else if (stateKey === fileEventId2) {
expect(content).toMatchObject({
active: true,
version: 2,
name: canaryName,
});
} else {
throw new Error("Unexpected state key: " + stateKey);
}
return Promise.resolve(); // return value not used
});
client.sendStateEvent = stateFn;
const createFn = jest.fn().mockImplementation((
name: string,
contents: ArrayBuffer,
info: Partial<IEncryptedFile>,
addl: IContent,
) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
"rel_type": RelationType.Replace,
"event_id": fileEventId,
},
});
return Promise.resolve({ event_id: fileEventId2 });
});
directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
expect(stateFn).toHaveBeenCalledTimes(2);
expect(createFn).toHaveBeenCalledTimes(1);
});
it('should fetch file history', async () => {
branch2.getFileEvent = () => Promise.resolve({
replacingEventId: () => undefined,
getId: () => fileEventId2,
} as MatrixEvent);
branch.getFileEvent = () => Promise.resolve({
replacingEventId: () => fileEventId2,
getId: () => fileEventId,
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: () => null,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
directory.getFile = (evId: string) => {
expect(evId).toEqual(fileEventId);
return branch;
};
const results = await branch2.getVersionHistory();
expect(results).toMatchObject([
branch2,
branch,
]);
});
});
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../src/models/event";
describe('MatrixEvent', () => {
it('should create copies of itself', () => {
const a = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
num: 42,
},
});
const clone = a.toSnapshot();
expect(clone).toBeDefined();
expect(clone).not.toBe(a);
expect(clone.event).not.toBe(a.event);
expect(clone.event).toMatchObject(a.event);
// The other properties we're not super interested in, honestly.
});
it('should compare itself to other events using json', () => {
const a = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
num: 42,
},
});
const b = new MatrixEvent({
type: "com.example.test______B",
content: {
isTest: true,
num: 42,
},
});
expect(a.isEquivalentTo(b)).toBe(false);
expect(a.isEquivalentTo(a)).toBe(true);
expect(b.isEquivalentTo(a)).toBe(false);
expect(b.isEquivalentTo(b)).toBe(true);
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
});
});
+7 -7
View File
@@ -16,11 +16,13 @@ limitations under the License.
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations";
describe("Relations", function() {
it("should deduplicate annotations", function() {
const relations = new Relations("m.annotation", "m.reaction");
const room = new Room("room123", null, null);
const relations = new Relations("m.annotation", "m.reaction", room);
// Create an instance of an annotation
const eventData = {
@@ -95,16 +97,14 @@ describe("Relations", function() {
});
// Stub the room
const room = {
getPendingEvent() { return null; },
getUnfilteredTimelineSet() { return null; },
};
const room = new Room("room123", null, null);
// Add the target event first, then the relation event
{
const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve);
})
});
const timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
@@ -119,7 +119,7 @@ describe("Relations", function() {
{
const relationsCreated = new Promise(resolve => {
targetEvent.once("Event.relationsCreated", resolve);
})
});
const timelineSet = new EventTimelineSet(room, {
unstableClientRelationAggregation: true,
+28
View File
@@ -124,6 +124,34 @@ describe("RoomMember", function() {
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
it("should not honor string power levels.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
},
event: true,
});
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
+15 -28
View File
@@ -1,6 +1,5 @@
import * as utils from "../test-utils";
import { RoomState } from "../../src/models/room-state";
import { RoomMember } from "../../src/models/room-member";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -193,12 +192,7 @@ describe("RoomState", function() {
expect(emitCount).toEqual(2);
});
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels",
function() {
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels", function() {
const powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
@@ -208,18 +202,16 @@ describe("RoomState", function() {
},
});
// spy on the room members
jest.spyOn(state.members[userA], "setPowerLevelEvent");
jest.spyOn(state.members[userB], "setPowerLevelEvent");
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function() {
const memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true,
});
@@ -243,13 +235,12 @@ describe("RoomState", function() {
});
it("should call setMembershipEvent on the right RoomMember", function() {
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
const memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true,
});
// spy on the room members
jest.spyOn(state.members[userA], "setMembershipEvent");
jest.spyOn(state.members[userB], "setMembershipEvent");
state.setStateEvents([memberEvent]);
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
@@ -374,17 +365,13 @@ describe("RoomState", function() {
user_ids: [userA],
},
});
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
// spy on the room members
jest.spyOn(state.members[userA], "setTypingEvent");
jest.spyOn(state.members[userB], "setTypingEvent");
state.setTypingEvent(typingEvent);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
typingEvent,
);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
typingEvent,
);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(typingEvent);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(typingEvent);
});
});
+306 -14
View File
@@ -1,8 +1,9 @@
import * as utils from "../test-utils";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { DuplicateStrategy, EventStatus, MatrixEvent } from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
import { Room } from "../../src/models/room";
import { RoomState } from "../../src";
import { Room } from "../../src";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
describe("Room", function() {
@@ -16,9 +17,9 @@ describe("Room", function() {
beforeEach(function() {
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =
room.oldState = room.getLiveTimeline().startState =
utils.mock(RoomState, "oldState");
room.currentState = room.getLiveTimeline()._endState =
room.currentState = room.getLiveTimeline().endState =
utils.mock(RoomState, "currentState");
});
@@ -86,9 +87,11 @@ describe("Room", function() {
];
it("should call RoomState.setTypingEvent on m.typing events", function() {
room.currentState = utils.mock(RoomState);
const typing = utils.mkEvent({
room: roomId, type: "m.typing", event: true, content: {
room: roomId,
type: "m.typing",
event: true,
content: {
user_ids: [userA],
},
});
@@ -110,7 +113,7 @@ describe("Room", function() {
dupe.event.event_id = events[0].getId();
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addLiveEvents([dupe], "replace");
room.addLiveEvents([dupe], DuplicateStrategy.Replace);
expect(room.timeline[0]).toEqual(dupe);
});
@@ -140,8 +143,8 @@ describe("Room", function() {
expect(callCount).toEqual(2);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
@@ -652,7 +655,8 @@ describe("Room", function() {
const roomName = "flibble";
const event = addMember(userA, "invite");
event.event.invite_room_state = [
event.event.unsigned = {};
event.event.unsigned.invite_room_state = [
{
type: "m.room.name",
state_key: "",
@@ -671,7 +675,8 @@ describe("Room", function() {
const roomName = "flibble";
setRoomName(roomName);
const roomNameToIgnore = "ignoreme";
event.event.invite_room_state = [
event.event.unsigned = {};
event.event.unsigned.invite_room_state = [
{
type: "m.room.name",
state_key: "",
@@ -1314,7 +1319,7 @@ describe("Room", function() {
isRoomEncrypted: function() {
return false;
},
_http: {
http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
@@ -1384,7 +1389,7 @@ describe("Room", function() {
}
expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent];
client.http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
@@ -1452,4 +1457,291 @@ describe("Room", function() {
expect(room.maySendMessage()).toEqual(true);
});
});
describe("getDefaultRoomName", function() {
it("should return 'Empty room' if a user is the only member",
function() {
const room = new Room(roomId, null, userA);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return a display name if one other member is in the room",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name if one other member is banned",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "ban",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
});
it("should return a display name if one other member is invited",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "invite",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room (was User B)' if User B left the room",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "leave",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
});
it("should return 'User B and User C' if in a room with two other users",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
});
it("should return 'User B and 2 others' if in a room with three other users",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkMembership({
user: userD, mship: "join",
room: roomId, event: true, name: "User D",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
});
describe("io.element.functional_users", function() {
it("should return a display name (default behaviour) if no one is marked as a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: [],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name (default behaviour) if service members is a number (invalid)",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: 1,
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name (default behaviour) if service members is a string (invalid)",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: userB,
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room' if the only other member is a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: [userB],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return 'User B' if User B is the only other member who isn't a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room' if all other members are functional members",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userB, userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should not break if an unjoined user is marked as a service user",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
});
});
});
-262
View File
@@ -1,262 +0,0 @@
import * as utils from "../../src/utils";
describe("utils", function() {
describe("encodeParams", function() {
it("should url encode and concat with &s", function() {
const params = {
foo: "bar",
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40",
);
});
});
describe("encodeUri", function() {
it("should replace based on object keys and url encode", function() {
const path = "foo/bar/%something/%here";
const vals = {
"%something": "baz",
"%here": "beer@",
};
expect(utils.encodeUri(path, vals)).toEqual(
"foo/bar/baz/beer%40",
);
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual([66, 77]);
});
it("should be able to remove in reverse order", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn, true);
expect(arr).toEqual([55, 66]);
});
it("should remove nothing if the function never returns true", function() {
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual(arr);
});
});
describe("isFunction", function() {
it("should return true for functions", function() {
expect(utils.isFunction([])).toBe(false);
expect(utils.isFunction([5, 3, 7])).toBe(false);
expect(utils.isFunction()).toBe(false);
expect(utils.isFunction(null)).toBe(false);
expect(utils.isFunction({})).toBe(false);
expect(utils.isFunction("foo")).toBe(false);
expect(utils.isFunction(555)).toBe(false);
expect(utils.isFunction(function() {})).toBe(true);
const s = { foo: function() {} };
expect(utils.isFunction(s.foo)).toBe(true);
});
});
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() {
utils.checkObjectHasKeys({}, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasKeys({
foo: "bar",
}, ["foo"]);
}).not.toThrow();
});
});
describe("checkObjectHasNoAdditionalKeys", function() {
it("should throw for extra keys", function() {
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4,
}, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
}, ["foo"]);
}).not.toThrow();
});
});
describe("deepCompare", function() {
const assert = {
isTrue: function(x) {
expect(x).toBe(true);
},
isFalse: function(x) {
expect(x).toBe(false);
},
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
const r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
assert.isTrue(utils.deepCompare({ 1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 } },
{ 1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 } }));
assert.isFalse(utils.deepCompare({ 1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 } },
{ 1: { name: "mhc", age: 28 },
2: { name: "arb", age: 27 } }));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
const func = function(x) {
return true;
};
const func2 = function(x) {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
describe("extend", function() {
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should ignore null", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
},
},
});
const target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
function fn1() {
return new Promise(async function(resolve, reject) {
await utils.sleep(1);
expect(promiseCount).toEqual(0);
++promiseCount;
resolve();
});
}
function fn2() {
return new Promise(function(resolve, reject) {
expect(promiseCount).toEqual(1);
++promiseCount;
resolve();
});
}
await utils.chunkPromises([fn1, fn2], 1);
expect(promiseCount).toEqual(2);
});
});
});
+509
View File
@@ -0,0 +1,509 @@
import * as utils from "../../src/utils";
import {
alphabetPad,
averageBetweenStrings,
baseToString,
deepSortedObjectEntries,
DEFAULT_ALPHABET,
lexicographicCompare,
nextString,
prevString,
simpleRetryOperation,
stringToBase,
} from "../../src/utils";
import { logger } from "../../src/logger";
// TODO: Fix types throughout
describe("utils", function() {
describe("encodeParams", function() {
it("should url encode and concat with &s", function() {
const params = {
foo: "bar",
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40",
);
});
it("should handle boolean and numeric values", function() {
const params = {
string: "foobar",
number: 12345,
boolean: false,
};
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
});
});
describe("encodeUri", function() {
it("should replace based on object keys and url encode", function() {
const path = "foo/bar/%something/%here";
const vals = {
"%something": "baz",
"%here": "beer@",
};
expect(utils.encodeUri(path, vals)).toEqual(
"foo/bar/baz/beer%40",
);
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual([66, 77]);
});
it("should be able to remove in reverse order", function() {
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn, true);
expect(arr).toEqual([55, 66]);
});
it("should remove nothing if the function never returns true", function() {
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual(arr);
});
});
describe("isFunction", function() {
it("should return true for functions", function() {
expect(utils.isFunction([])).toBe(false);
expect(utils.isFunction([5, 3, 7])).toBe(false);
expect(utils.isFunction(undefined)).toBe(false);
expect(utils.isFunction(null)).toBe(false);
expect(utils.isFunction({})).toBe(false);
expect(utils.isFunction("foo")).toBe(false);
expect(utils.isFunction(555)).toBe(false);
expect(utils.isFunction(function() {})).toBe(true);
const s = { foo: function() {} };
expect(utils.isFunction(s.foo)).toBe(true);
});
});
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() {
utils.checkObjectHasKeys({}, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasKeys({
foo: "bar",
}, ["foo"]);
}).not.toThrow();
});
});
describe("checkObjectHasNoAdditionalKeys", function() {
it("should throw for extra keys", function() {
expect(function() {
utils.checkObjectHasNoAdditionalKeys({ foo: "bar", baz: 4 }, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({ foo: "bar" }, ["foo"]);
}).not.toThrow();
});
});
describe("deepCompare", function() {
const assert = {
isTrue: function(x: any) {
expect(x).toBe(true);
},
isFalse: function(x: any) {
expect(x).toBe(false);
},
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
const r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"), new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"), new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
assert.isTrue(utils.deepCompare({
1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 },
}, {
1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 },
}));
assert.isFalse(utils.deepCompare({
1: { name: "mhc", age: 28 },
2: { name: "arb", age: 26 },
}, {
1: { name: "mhc", age: 28 },
2: { name: "arb", age: 27 },
}));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
const func = function() {
return true;
};
const func2 = function() {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
async function fn1() {
await utils.sleep(1);
expect(promiseCount).toEqual(0);
++promiseCount;
}
async function fn2() {
expect(promiseCount).toEqual(1);
++promiseCount;
}
await utils.chunkPromises([fn1, fn2], 1);
expect(promiseCount).toEqual(2);
});
});
describe('simpleRetryOperation', () => {
it('should retry', async () => {
let count = 0;
const val = {};
const fn = (attempt: any) => {
count++;
// If this expectation fails then it can appear as a Jest Timeout due to
// the retry running beyond the test limit.
expect(attempt).toEqual(count);
if (count > 1) {
return Promise.resolve(val);
} else {
return Promise.reject(new Error("Iterative failure"));
}
};
const ret = await simpleRetryOperation(fn);
expect(ret).toBe(val);
expect(count).toEqual(2);
});
// We don't test much else of the function because then we're just testing that the
// underlying library behaves, which should be tested on its own. Our API surface is
// all that concerns us.
});
describe('DEFAULT_ALPHABET', () => {
it('should be usefully printable ASCII in order', () => {
expect(DEFAULT_ALPHABET).toEqual(
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
);
});
});
describe('alphabetPad', () => {
it('should pad to the alphabet length', () => {
const len = 12;
expect(alphabetPad("a", len)).toEqual("a" + ("".padEnd(len - 1, DEFAULT_ALPHABET[0])));
expect(alphabetPad("a", len, "123")).toEqual("a" + ("".padEnd(len - 1, '1')));
});
});
describe('baseToString', () => {
it('should calculate the appropriate string from numbers', () => {
// Verify the whole alphabet
for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) {
logger.log({ i }); // for debugging
expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]);
}
// Just quickly double check that repeated characters aren't treated as padding, particularly
// at the beginning of the alphabet where they are most vulnerable to this behaviour.
expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1));
expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2));
expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3));
expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4));
expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5));
expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6));
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]);
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('j');
expect(baseToString(BigInt(6337))).toEqual("ab");
expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb');
});
});
describe('stringToBase', () => {
it('should calculate the appropriate number for a string', () => {
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1));
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96));
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121));
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496));
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121));
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496));
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1));
expect(stringToBase("a")).toEqual(BigInt(66));
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3));
expect(stringToBase("ab")).toEqual(BigInt(6337));
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80));
});
});
describe('averageBetweenStrings', () => {
it('should average appropriately', () => {
expect(averageBetweenStrings(" ", "!!")).toEqual(" P");
expect(averageBetweenStrings(" ", "!")).toEqual(" ");
expect(averageBetweenStrings('A', 'B')).toEqual('A ');
expect(averageBetweenStrings('AA', 'BB')).toEqual('Aq');
expect(averageBetweenStrings('A', 'z')).toEqual(']');
expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m');
expect(averageBetweenStrings('AA', 'zz')).toEqual('^.');
expect(averageBetweenStrings('aa', 'zz', "abcdefghijklmnopqrstuvwxyz")).toEqual('mz');
expect(averageBetweenStrings('cat', 'doggo')).toEqual("d9>Cw");
expect(averageBetweenStrings('cat', 'doggo', "abcdefghijklmnopqrstuvwxyz")).toEqual("cumqh");
});
});
describe('nextString', () => {
it('should find the next string appropriately', () => {
expect(nextString('A')).toEqual('B');
expect(nextString('b', 'abcdefghijklmnopqrstuvwxyz')).toEqual('c');
expect(nextString('cat')).toEqual('cau');
expect(nextString('cat', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cau');
});
});
describe('prevString', () => {
it('should find the next string appropriately', () => {
expect(prevString('B')).toEqual('A');
expect(prevString('c', 'abcdefghijklmnopqrstuvwxyz')).toEqual('b');
expect(prevString('cau')).toEqual('cat');
expect(prevString('cau', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cat');
});
});
// Let's just ensure the ordering is sensible for lexicographic ordering
describe('string averaging unified', () => {
it('should be truly previous and next', () => {
let midpoint = "cat";
// We run this test 100 times to ensure we end up with a sane sequence.
for (let i = 0; i < 100; i++) {
const next = nextString(midpoint);
const prev = prevString(midpoint);
logger.log({ i, midpoint, next, prev }); // for test debugging
expect(lexicographicCompare(midpoint, next) < 0).toBe(true);
expect(lexicographicCompare(midpoint, prev) > 0).toBe(true);
expect(averageBetweenStrings(prev, next)).toBe(midpoint);
midpoint = next;
}
});
it('should roll over', () => {
const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1];
const firstAlpha = DEFAULT_ALPHABET[0];
const highRoll = firstAlpha + firstAlpha;
const lowRoll = lastAlpha;
expect(nextString(lowRoll)).toEqual(highRoll);
expect(prevString(highRoll)).toEqual(lowRoll);
});
it('should be reversible on small strings', () => {
// Large scale reversibility is tested for max space order value
const input = "cats";
expect(prevString(nextString(input))).toEqual(input);
});
// We want to explicitly make sure that Space order values are supported and roll appropriately
it('should properly handle rolling over at 50 characters', () => {
// Note: we also test reversibility of large strings here.
const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50);
const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51);
expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar);
expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue);
// We're testing that the rollover happened, which means that the next string come before
// the maximum space order value lexicographically.
expect(lexicographicCompare(maxSpaceValue, fiftyFirstChar) > 0).toBe(true);
});
});
describe('lexicographicCompare', () => {
it('should work', () => {
// Simple tests
expect(lexicographicCompare('a', 'b') < 0).toBe(true);
expect(lexicographicCompare('ab', 'b') < 0).toBe(true);
expect(lexicographicCompare('cat', 'dog') < 0).toBe(true);
// Simple tests (reversed)
expect(lexicographicCompare('b', 'a') > 0).toBe(true);
expect(lexicographicCompare('b', 'ab') > 0).toBe(true);
expect(lexicographicCompare('dog', 'cat') > 0).toBe(true);
// Simple equality tests
expect(lexicographicCompare('a', 'a') === 0).toBe(true);
expect(lexicographicCompare('A', 'A') === 0).toBe(true);
// ASCII rule testing
expect(lexicographicCompare('A', 'a') < 0).toBe(true);
expect(lexicographicCompare('a', 'A') > 0).toBe(true);
});
});
describe('deepSortedObjectEntries', () => {
it('should auto-return non-objects', () => {
expect(deepSortedObjectEntries(42)).toEqual(42);
expect(deepSortedObjectEntries("not object")).toEqual("not object");
expect(deepSortedObjectEntries(true)).toEqual(true);
expect(deepSortedObjectEntries([42])).toEqual([42]);
expect(deepSortedObjectEntries(null)).toEqual(null);
expect(deepSortedObjectEntries(undefined)).toEqual(undefined);
});
it('should sort objects appropriately', () => {
const input = {
a: 42,
b: {
d: {},
a: "test",
b: "alpha",
},
[72]: "test",
};
const output: any = [
["72", "test"],
["a", 42],
["b", [
["a", "test"],
["b", "alpha"],
["d", []],
]],
];
expect(deepSortedObjectEntries(input)).toMatchObject(output);
});
});
describe("recursivelyAssign", () => {
it("doesn't override with null/undefined", () => {
const result = utils.recursivelyAssign(
{
string: "Hello world",
object: {},
float: 0.1,
}, {
string: null,
object: undefined,
},
true,
);
expect(result).toStrictEqual({
string: "Hello world",
object: {},
float: 0.1,
});
});
it("assigns recursively", () => {
const result = utils.recursivelyAssign(
{
number: 42,
object: {
message: "Hello world",
day: "Monday",
langs: {
compiled: ["c++"],
},
},
thing: "string",
}, {
number: 2,
object: {
message: "How are you",
day: "Friday",
langs: {
compiled: ["c++", "c"],
},
},
thing: {
aSubThing: "something",
},
},
);
expect(result).toStrictEqual({
number: 2,
object: {
message: "How are you",
day: "Friday",
langs: {
compiled: ["c++", "c"],
},
},
thing: {
aSubThing: "something",
},
});
});
});
});
+106 -8
View File
@@ -16,6 +16,8 @@ limitations under the License.
import { TestClient } from '../../TestClient';
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
import { RoomMember } from "../../../src";
const DUMMY_SDP = (
"v=0\r\n" +
@@ -82,6 +84,28 @@ class MockRTCPeerConnection {
getStats() { return []; }
}
class MockMediaStream {
constructor(
public id: string,
) {}
getTracks() { return []; }
getAudioTracks() { return [{ enabled: true }]; }
getVideoTracks() { return [{ enabled: true }]; }
addEventListener() {}
}
class MockMediaDeviceInfo {
constructor(
public kind: "audio" | "video",
) {}
}
class MockMediaHandler {
getUserMediaStream() { return new MockMediaStream("mock_stream_from_media_handler"); }
stopUserMediaStream() {}
}
describe('Call', function() {
let client;
let call;
@@ -97,13 +121,9 @@ describe('Call', function() {
global.navigator = {
mediaDevices: {
// @ts-ignore Mock
getUserMedia: () => {
return {
getTracks: () => [],
getAudioTracks: () => [],
getVideoTracks: () => [],
};
},
getUserMedia: () => new MockMediaStream("local_stream"),
// @ts-ignore Mock
enumerateDevices: async () => [new MockMediaDeviceInfo("audio"), new MockMediaDeviceInfo("video")],
},
};
@@ -114,7 +134,7 @@ describe('Call', function() {
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
@@ -123,6 +143,8 @@ describe('Call', function() {
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.client.mediaHandler = new MockMediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
@@ -298,4 +320,80 @@ describe('Call', function() {
// Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true);
});
it("should map SDPStreamMetadata to feeds", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" };
};
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {
"remote_stream": {
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: true,
video_muted: false,
},
},
};
},
});
call.pushRemoteFeed(new MockMediaStream("remote_stream"));
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
expect(feed?.isAudioMuted()).toBeTruthy();
expect(feed?.isVideoMuted()).not.toBeTruthy();
});
it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.getOpponentMember = () => {
return { userId: "@bob:bar.uk" } as RoomMember;
};
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
call.setScreensharingEnabledWithoutMetadataSupport = jest.fn();
call.setScreensharingEnabled(true);
expect(call.setScreensharingEnabledWithoutMetadataSupport).toHaveBeenCalled();
});
it("should fallback to answering with no video", async () => {
await client.httpBackend.flush();
call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue;
client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject");
await call.answer(true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true);
expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false);
});
});
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface IIdentityServerProvider {
/**
* Gets an access token for use against the identity server,
* for the associated client.
* @returns {Promise<string>} Resolves to the access token.
*/
getAccessToken(): Promise<string>;
}
+165
View File
@@ -0,0 +1,165 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
export enum PushRuleActionName {
DontNotify = "dont_notify",
Notify = "notify",
Coalesce = "coalesce",
}
export enum TweakName {
Highlight = "highlight",
Sound = "sound",
}
export type Tweak<N extends TweakName, V> = {
set_tweak: N;
value: V;
};
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
export type TweakSound = Tweak<TweakName.Sound, string>;
export type Tweaks = TweakHighlight | TweakSound;
export enum ConditionOperator {
ExactEquals = "==",
LessThan = "<",
GreaterThan = ">",
GreaterThanOrEqual = ">=",
LessThanOrEqual = "<=",
}
export type PushRuleAction = Tweaks | PushRuleActionName;
export type MemberCountCondition
<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals>
= `${Op}${N}` | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
return condition === "==2" || condition === "2";
}
export enum ConditionKind {
EventMatch = "event_match",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
}
export interface IPushRuleCondition<N extends ConditionKind | string> {
[k: string]: any; // for custom conditions, there can be other fields here
kind: N;
}
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern: string;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
is: AnyMemberCountCondition;
}
export interface ISenderNotificationPermissionCondition
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
key: string;
}
// XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition = IEventMatchCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition;
export enum PushRuleKind {
Override = "override",
ContentSpecific = "content",
RoomSpecific = "room",
SenderSpecific = "sender",
Underride = "underride",
}
export enum RuleId {
Master = ".m.rule.master",
ContainsDisplayName = ".m.rule.contains_display_name",
ContainsUserName = ".m.rule.contains_user_name",
AtRoomNotification = ".m.rule.roomnotif",
DM = ".m.rule.room_one_to_one",
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
Message = ".m.rule.message",
EncryptedMessage = ".m.rule.encrypted",
InviteToSelf = ".m.rule.invite_for_me",
MemberEvent = ".m.rule.member_event",
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
}
export type PushRuleSet = {
[k in PushRuleKind]?: IPushRule[];
};
export interface IPushRule {
actions: PushRuleAction[];
conditions?: PushRuleCondition[];
default: boolean;
enabled: boolean;
pattern?: string;
rule_id: RuleId | string;
}
export interface IAnnotatedPushRule extends IPushRule {
kind: PushRuleKind;
}
export interface IPushRules {
global: PushRuleSet;
device?: PushRuleSet;
}
export interface IPusher {
app_display_name: string;
app_id: string;
data: {
format?: string;
url?: string; // TODO: Required if kind==http
brand?: string; // TODO: For email notifications only? Unspecced field
};
device_display_name: string;
kind: "http" | string;
lang: string;
profile_tag?: string;
pushkey: string;
}
export interface IPusherRequest extends IPusher {
append?: boolean;
}
/* eslint-enable camelcase */
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const SERVICE_TYPES = Object.freeze({
IS: 'SERVICE_TYPE_IS', // An Identity Service
IM: 'SERVICE_TYPE_IM', // An Integration Manager
});
declare module "another-json" {
export function stringify(o: object): string;
}
+91
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
export enum EventType {
// Room state events
RoomCanonicalAlias = "m.room.canonical_alias",
@@ -51,6 +53,8 @@ export enum EventType {
CallReject = "m.call.reject",
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed",
CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed",
CallReplaces = "m.call.replaces",
CallAssertedIdentity = "m.call.asserted_identity",
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
@@ -71,6 +75,7 @@ export enum EventType {
// Room account_data events
FullyRead = "m.fully_read",
Tag = "m.tag",
SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
// User account_data events
PushRules = "m.push_rules",
@@ -84,6 +89,19 @@ export enum EventType {
Dummy = "m.dummy",
}
export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
/**
* Note, "io.element.thread" is hardcoded
* Should be replaced with "m.thread" once MSC3440 lands
* Can not use `UnstableValue` as TypeScript does not
* allow computed values in enums
* https://github.com/microsoft/TypeScript/issues/27976
*/
Thread = "io.element.thread",
}
export enum MsgType {
Text = "m.text",
Emote = "m.emote",
@@ -93,6 +111,7 @@ export enum MsgType {
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
KeyVerificationRequest = "m.key.verification.request",
}
export const RoomCreateTypeField = "type";
@@ -100,3 +119,75 @@ export const RoomCreateTypeField = "type";
export enum RoomType {
Space = "m.space",
}
/**
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
* including its eventual removal.
*/
export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
/**
* Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
* including its eventual removal.
*/
export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
/**
* Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*/
export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
/**
* Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*/
export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
/**
* Branch (Leaf Reference) type for the index approach in a
* [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is
* UNSTABLE and subject to breaking changes, including its eventual removal.
*/
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
/**
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*
* Schema (TypeScript):
* {
* service_members?: string[]
* }
*
* Example:
* {
* "service_members": [
* "@helperbot:localhost",
* "@reminderbot:alice.tdl"
* ]
* }
*/
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
export interface IEncryptedFile {
url: string;
mimetype?: string;
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
iv: string;
hashes: {[alg: string]: string};
v: string;
}
+30 -19
View File
@@ -15,11 +15,17 @@ limitations under the License.
*/
// this is needed to tell TS about global.Olm
import * as Olm from "@matrix-org/olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
import "@matrix-org/olm";
export {};
declare global {
// use `number` as the return type in all cases for global.set{Interval,Timeout},
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
// The overload for clear{Interval,Timeout} is resolved as expected.
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
namespace NodeJS {
interface Global {
localStorage: Storage;
@@ -27,11 +33,11 @@ declare global {
}
interface Window {
electron?: Electron;
webkitAudioContext: typeof AudioContext;
}
interface Electron {
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"];
}
interface MediaDevices {
@@ -56,21 +62,6 @@ declare global {
};
}
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
interface GetSourcesOptions {
types: Array<string>;
thumbnailSize?: {
height: number;
width: number;
};
fetchWindowIcons?: boolean;
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
@@ -84,4 +75,24 @@ declare global {
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
export interface ISettledFulfilled<T> {
status: "fulfilled";
value: T;
}
export interface ISettledRejected {
status: "rejected";
reason: any;
}
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
}
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface IImageInfo {
size?: number;
mimetype?: string;
thumbnail_info?: { // eslint-disable-line camelcase
w?: number;
h?: number;
size?: number;
mimetype?: string;
};
w?: number;
h?: number;
}
export enum Visibility {
Public = "public",
Private = "private",
}
export enum Preset {
PrivateChat = "private_chat",
TrustedPrivateChat = "trusted_private_chat",
PublicChat = "public_chat",
}
export type ResizeMethod = "crop" | "scale";
// TODO move to http-api after TSification
export interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
export type IdServerUnbindResult = "no-support" | "success";
// Knock and private are reserved keywords which are not yet implemented.
export enum JoinRule {
Public = "public",
Invite = "invite",
/**
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/
Private = "private",
Knock = "knock",
Restricted = "restricted",
}
export enum RestrictedAllowType {
RoomMembership = "m.room_membership",
}
export interface IJoinRuleEventContent {
join_rule: JoinRule; // eslint-disable-line camelcase
allow?: {
type: RestrictedAllowType;
room_id: string; // eslint-disable-line camelcase
}[];
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}
+156
View File
@@ -0,0 +1,156 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Callback } from "../client";
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
export interface IJoinRoomOpts {
/**
* True to do a room initial sync on the resulting
* room. If false, the <strong>returned Room object will have no current state.
* </strong> Default: true.
*/
syncRoom?: boolean;
/**
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
*/
inviteSignUrl?: string;
/**
* The server names to try and join through in addition to those that are automatically chosen.
*/
viaServers?: string[];
}
export interface IRedactOpts {
reason?: string;
}
export interface ISendEventResponse {
event_id: string;
}
export interface IPresenceOpts {
presence: "online" | "offline" | "unavailable";
status_msg?: string;
}
export interface IPaginateOpts {
backwards?: boolean;
limit?: number;
}
export interface IGuestAccessOpts {
allowJoin: boolean;
allowRead: boolean;
}
export interface ISearchOpts {
keys?: SearchKey[];
query: string;
}
export interface IEventSearchOpts {
filter?: IRoomEventFilter;
term: string;
}
export interface IInvite3PID {
id_server: string;
id_access_token?: string; // this gets injected by the js-sdk
medium: string;
address: string;
}
export interface ICreateRoomStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: IContent;
}
export interface ICreateRoomOpts {
room_alias_name?: string;
visibility?: Visibility;
name?: string;
topic?: string;
preset?: Preset;
power_level_content_override?: object;
creation_content?: object;
initial_state?: ICreateRoomStateEvent[];
invite?: string[];
invite_3pid?: IInvite3PID[];
is_direct?: boolean;
room_version?: string;
}
export interface IRoomDirectoryOptions {
server?: string;
limit?: number;
since?: string;
filter?: {
generic_search_term: string;
};
include_all_networks?: boolean;
third_party_instance_id?: string;
}
export interface IUploadOpts {
name?: string;
includeFilename?: boolean;
type?: string;
rawResponse?: boolean;
onlyContentUri?: boolean;
callback?: Callback;
progressHandler?: (state: {loaded: number, total: number}) => void;
}
export interface IAddThreePidOnlyBody {
auth?: {
type: string;
session?: string;
};
client_secret: string;
sid: string;
}
export interface IBindThreePidBody {
client_secret: string;
id_server: string;
id_access_token: string;
sid: string;
}
export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
}
export interface IRelationsResponse {
original_event: IEvent;
chunk: IEvent[];
next_batch?: string;
prev_batch?: string;
}
/* eslint-enable camelcase */
+118
View File
@@ -0,0 +1,118 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Types relating to the /search API
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
import { IRoomEventFilter } from "../filter";
import { SearchResult } from "../models/search-result";
/* eslint-disable camelcase */
export interface IEventWithRoomId extends IRoomEvent {
room_id: string;
}
export interface IStateEventWithRoomId extends IStateEvent {
room_id: string;
}
export interface IMatrixProfile {
avatar_url?: string;
displayname?: string;
}
export interface IResultContext {
events_before: IEventWithRoomId[];
events_after: IEventWithRoomId[];
profile_info: Record<string, IMatrixProfile>;
start?: string;
end?: string;
}
export interface ISearchResult {
rank: number;
result: IEventWithRoomId;
context: IResultContext;
}
enum GroupKey {
RoomId = "room_id",
Sender = "sender",
}
export interface IResultRoomEvents {
count: number;
highlights: string[];
results: ISearchResult[];
state?: { [roomId: string]: IStateEventWithRoomId[] };
groups?: {
[groupKey in GroupKey]: {
[value: string]: {
next_batch?: string;
order: number;
results: string[];
};
};
};
next_batch?: string;
}
interface IResultCategories {
room_events: IResultRoomEvents;
}
export type SearchKey = "content.body" | "content.name" | "content.topic";
export enum SearchOrderBy {
Recent = "recent",
Rank = "rank",
}
export interface ISearchRequestBody {
search_categories: {
room_events: {
search_term: string;
keys?: SearchKey[];
filter?: IRoomEventFilter;
order_by?: SearchOrderBy;
event_context?: {
before_limit?: number;
after_limit?: number;
include_profile?: boolean;
};
include_state?: boolean;
groupings?: {
group_by: {
key: GroupKey;
}[];
};
};
};
}
export interface ISearchResponse {
search_categories: IResultCategories;
}
export interface ISearchResults {
_query?: ISearchRequestBody;
results: SearchResult[];
highlights: string[];
count?: number;
next_batch?: string;
pendingRequest?: Promise<ISearchResults>;
}
/* eslint-enable camelcase */
+25
View File
@@ -0,0 +1,25 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface ISignatures {
[entity: string]: {
[keyId: string]: string;
};
}
export interface ISigned {
signatures?: ISignatures;
}
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPublicRoomsChunkRoom } from "../client";
import { RoomType } from "./event";
import { IStrippedState } from "../sync-accumulator";
// Types relating to Rooms of type `m.space` and related APIs
/* eslint-disable camelcase */
/** @deprecated Use hierarchy instead where possible. */
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
num_refs: number;
room_type: string;
}
/** @deprecated Use hierarchy instead where possible. */
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
origin_server_ts: number;
type: string;
state_key: string;
sender: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string[];
};
}
export interface IHierarchyRelation extends IStrippedState {
room_id: string;
origin_server_ts: number;
content: {
order?: string;
suggested?: boolean;
via?: string[];
};
}
export interface IHierarchyRoom extends IPublicRoomsChunkRoom {
room_type?: RoomType | string;
children_state: IHierarchyRelation[];
}
/* eslint-enable camelcase */
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IdServerUnbindResult } from "./partials";
// Types relating to Synapse Admin APIs
/* eslint-disable camelcase */
export interface ISynapseAdminWhoisResponse {
user_id: string;
devices: {
[deviceId: string]: {
sessions: {
connections: {
ip: string;
last_seen: number; // millis since epoch
user_agent: string;
}[];
}[];
};
};
}
export interface ISynapseAdminDeactivateResponse {
id_server_unbind_result: IdServerUnbindResult;
}
/* eslint-enable camelcase */
+29
View File
@@ -0,0 +1,29 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum ThreepidMedium {
Email = "email",
Phone = "msisdn",
}
// TODO: Are these types universal, or specific to just /account/3pid?
export interface IThreepid {
medium: ThreepidMedium;
address: string;
validated_at: number; // eslint-disable-line camelcase
added_at: number; // eslint-disable-line camelcase
bound?: boolean;
}
+93
View File
@@ -0,0 +1,93 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
* is provided that the stable prefix should be used when representing the identifier.
*/
export class NamespacedValue<S extends string, U extends string> {
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
// Goal is to to have developers explicitly say there is no stable value (if applicable).
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) {
if (!this.unstable && !this.stable) {
throw new Error("One of stable or unstable values must be supplied");
}
}
public get name(): U | S {
if (this.stable) {
return this.stable;
}
return this.unstable;
}
public get altName(): U | S | null {
if (!this.stable) {
return null;
}
return this.unstable;
}
public matches(val: string): boolean {
return this.name === val || this.altName === val;
}
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): T {
let val: T;
if (this.name) {
val = obj?.[this.name];
}
if (!val && this.altName) {
val = obj?.[this.altName];
}
return val;
}
public includedIn(arr: any[]): boolean {
let included = false;
if (this.name) {
included = arr.includes(this.name);
}
if (!included && this.altName) {
included = arr.includes(this.altName);
}
return included;
}
}
/**
* Represents a namespaced value which prioritizes the unstable value over the stable
* value.
*/
export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
// Note: Constructor difference is that `unstable` is *required*.
public constructor(stable: S, unstable: U) {
super(stable, unstable);
if (!this.unstable) {
throw new Error("Unstable value must be supplied");
}
}
public get name(): U {
return this.unstable;
}
public get altName(): S {
return this.stable;
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ export class ReEmitter {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (...args) => {
const forSource = (...args: any[]) => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
+56 -133
View File
@@ -17,79 +17,20 @@ limitations under the License.
/** @module auto-discovery */
import { logger } from './logger';
import { URL as NodeURL } from "url";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
export enum AutoDiscoveryAction {
SUCCESS = "SUCCESS",
IGNORE = "IGNORE",
PROMPT = "PROMPT",
FAIL_PROMPT = "FAIL_PROMPT",
FAIL_ERROR = "FAIL_ERROR",
}
/**
@@ -102,55 +43,36 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
static get ERROR_INVALID() {
return "Invalid homeserver discovery response";
}
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
static get ERROR_GENERIC_FAILURE() {
return "Failed to get autodiscovery configuration from server";
}
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
static get ERROR_INVALID_HS_BASE_URL() {
return "Invalid base_url for m.homeserver";
}
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
static get ERROR_INVALID_HOMESERVER() {
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
static get ERROR_INVALID_IS_BASE_URL() {
return "Invalid base_url for m.identity_server";
}
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
static get ERROR_INVALID_IDENTITY_SERVER() {
return "Identity server URL does not appear to be a valid identity server";
}
// eslint-disable-next-line
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
static get ERROR_INVALID_IS() {
return "Invalid identity server discovery response";
}
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
static get ERROR_MISSING_WELLKNOWN() {
return "No .well-known JSON file found";
}
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
static get ERROR_INVALID_JSON() {
return "Invalid JSON";
}
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
static get ALL_ERRORS() {
return [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
}
public static readonly ALL_ERRORS = [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
/**
* The auto discovery failed. The client is expected to communicate
@@ -158,7 +80,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR;
/**
* The auto discovery failed, however the client may still recover
@@ -169,7 +91,7 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT;
/**
* The auto discovery didn't fail but did not find anything of
@@ -178,14 +100,14 @@ export class AutoDiscovery {
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
public static readonly PROMPT = AutoDiscoveryAction.PROMPT;
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
static get SUCCESS() { return "SUCCESS"; }
public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS;
/**
* Validates and verifies client configuration information for purposes
@@ -193,13 +115,13 @@ export class AutoDiscovery {
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {string} wellknown The configuration object itself, as returned
* @param {object} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
@@ -240,7 +162,7 @@ export class AutoDiscovery {
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
const hsUrl = this.sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"],
);
if (!hsUrl) {
@@ -250,7 +172,7 @@ export class AutoDiscovery {
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
const hsVersions = await this.fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
@@ -272,7 +194,7 @@ export class AutoDiscovery {
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
let isUrl: string | boolean = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
@@ -287,7 +209,7 @@ export class AutoDiscovery {
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
isUrl = this.sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) {
@@ -299,10 +221,10 @@ export class AutoDiscovery {
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
const isResponse = await this.fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
@@ -317,7 +239,7 @@ export class AutoDiscovery {
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
if (isUrl && isUrl.toString().length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
@@ -359,7 +281,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
@@ -395,13 +317,13 @@ export class AutoDiscovery {
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
const wellknown = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS") {
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
if (wellknown.action === AutoDiscoveryAction.IGNORE) {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
@@ -427,12 +349,12 @@ export class AutoDiscovery {
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
public static async getRawClientConfig(domain: string): Promise<IClientWellKnown> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this._fetchWellKnownObject(
const response = await this.fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!response) return {};
@@ -447,7 +369,7 @@ export class AutoDiscovery {
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
private static sanitizeWellKnownUrl(url: string): string | boolean {
if (!url) return false;
try {
@@ -495,8 +417,9 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve, reject) {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
@@ -505,10 +428,10 @@ export class AutoDiscovery {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = "FAIL_PROMPT";
let action = AutoDiscoveryAction.FAIL_PROMPT;
let reason = (err ? err.message : null) || "General failure";
if (response && response.statusCode === 404) {
action = "IGNORE";
action = AutoDiscoveryAction.IGNORE;
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({ raw: {}, action: action, reason: reason, error: err });
@@ -516,7 +439,7 @@ export class AutoDiscovery {
}
try {
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS });
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
@@ -524,7 +447,7 @@ export class AutoDiscovery {
}
resolve({
raw: {},
action: "FAIL_PROMPT",
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason,
error: e,
});
-2427
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as matrixcs from "./matrix";
import request from "browser-request";
import queryString from "qs";
import * as matrixcs from "./matrix";
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
-6057
View File
File diff suppressed because it is too large Load Diff
+9251
View File
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,15 +17,17 @@ limitations under the License.
/** @module ContentHelpers */
import { MsgType } from "./@types/event";
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlMessage(body, htmlBody) {
export function makeHtmlMessage(body: string, htmlBody: string) {
return {
msgtype: "m.text",
msgtype: MsgType.Text,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -38,9 +40,9 @@ export function makeHtmlMessage(body, htmlBody) {
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlNotice(body, htmlBody) {
export function makeHtmlNotice(body: string, htmlBody: string) {
return {
msgtype: "m.notice",
msgtype: MsgType.Notice,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -53,9 +55,9 @@ export function makeHtmlNotice(body, htmlBody) {
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlEmote(body, htmlBody) {
export function makeHtmlEmote(body: string, htmlBody: string) {
return {
msgtype: "m.emote",
msgtype: MsgType.Emote,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -67,9 +69,9 @@ export function makeHtmlEmote(body, htmlBody) {
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeTextMessage(body) {
export function makeTextMessage(body: string) {
return {
msgtype: "m.text",
msgtype: MsgType.Text,
body: body,
};
}
@@ -79,9 +81,9 @@ export function makeTextMessage(body) {
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
export function makeNotice(body) {
export function makeNotice(body: string) {
return {
msgtype: "m.notice",
msgtype: MsgType.Notice,
body: body,
};
}
@@ -91,9 +93,9 @@ export function makeNotice(body) {
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeEmoteMessage(body) {
export function makeEmoteMessage(body: string) {
return {
msgtype: "m.emote",
msgtype: MsgType.Emote,
body: body,
};
}
+16 -11
View File
@@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -34,8 +33,14 @@ import * as utils from "./utils";
* for such URLs.
* @return {string} The complete URL to the content.
*/
export function getHttpUriForMxc(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
export function getHttpUriForMxc(
baseUrl: string,
mxc: string,
width: number,
height: number,
resizeMethod: string,
allowDirectLinks = false,
): string {
if (typeof mxc !== "string" || !mxc) {
return '';
}
@@ -48,16 +53,16 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
const params: Record<string, string> = {};
if (width) {
params.width = Math.round(width);
params["width"] = Math.round(width).toString();
}
if (height) {
params.height = Math.round(height);
params["height"] = Math.round(height).toString();
}
if (resizeMethod) {
params.method = resizeMethod;
params["method"] = resizeMethod;
}
if (Object.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
@@ -71,7 +76,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(Object.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
}
@@ -1,6 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,22 +19,51 @@ limitations under the License.
* @module crypto/CrossSigning
*/
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { EventEmitter } from 'events';
import { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { logger } from '../logger';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
function publicKeyFromKeyInfo(keyInfo) {
function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
// We assume only a single key, and we want the bare form without type
// prefix, so we select the values.
return Object.values(keyInfo.keys)[0];
}
export interface ICacheCallbacks {
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
}
export interface ICrossSigningInfo {
keys: Record<string, ICrossSigningKey>;
firstUse: boolean;
crossSigningVerifiedBefore: boolean;
}
export class CrossSigningInfo extends EventEmitter {
public keys: Record<string, ICrossSigningKey> = {};
public firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverified later for any reason.
private crossSigningVerifiedBefore = false;
/**
* Information about a user's cross-signing keys
*
@@ -46,27 +74,15 @@ export class CrossSigningInfo extends EventEmitter {
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
*/
constructor(userId, callbacks, cacheCallbacks) {
constructor(
public readonly userId: string,
private callbacks: ICryptoCallbacks = {},
private cacheCallbacks: ICacheCallbacks = {},
) {
super();
// you can't change the userId
Object.defineProperty(this, 'userId', {
enumerable: true,
value: userId,
});
this._callbacks = callbacks || {};
this._cacheCallbacks = cacheCallbacks || {};
this.keys = {};
this.firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverifed later for any reason.
this.crossSigningVerifiedBefore = false;
}
static fromStorage(obj, userId) {
public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
@@ -76,7 +92,7 @@ export class CrossSigningInfo extends EventEmitter {
return res;
}
toStorage() {
public toStorage(): ICrossSigningInfo {
return {
keys: this.keys,
firstUse: this.firstUse,
@@ -92,10 +108,10 @@ export class CrossSigningInfo extends EventEmitter {
* the stored public key for the given key type.
* @returns {Array} An array with [ public key, Olm.PkSigning ]
*/
async getCrossSigningKey(type, expectedPubkey) {
public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
if (!this._callbacks.getCrossSigningKey) {
if (!this.callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied");
}
@@ -103,7 +119,7 @@ export class CrossSigningInfo extends EventEmitter {
expectedPubkey = this.getId(type);
}
function validateKey(key) {
function validateKey(key: Uint8Array): [string, PkSigning] {
if (!key) return;
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key);
@@ -114,9 +130,8 @@ export class CrossSigningInfo extends EventEmitter {
}
let privkey;
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this._cacheCallbacks
.getCrossSigningKeyCache(type, expectedPubkey);
if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
}
const cacheresult = validateKey(privkey);
@@ -124,11 +139,11 @@ export class CrossSigningInfo extends EventEmitter {
return cacheresult;
}
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey);
const result = validateKey(privkey);
if (result) {
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
}
return result;
}
@@ -156,10 +171,9 @@ export class CrossSigningInfo extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStoredInSecretStorage(secretStorage) {
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
// check what SSSS keys have encrypted the master key (if any)
const stored =
await secretStorage.isStored("m.cross_signing.master", false) || {};
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
for (const k of Object.keys(stored)) {
@@ -169,9 +183,7 @@ export class CrossSigningInfo extends EventEmitter {
}
}
for (const type of ["self_signing", "user_signing"]) {
intersect(
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
);
intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {});
}
return Object.keys(stored).length ? stored : null;
}
@@ -184,7 +196,10 @@ export class CrossSigningInfo extends EventEmitter {
* @param {Map} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data
*/
static async storeInSecretStorage(keys, secretStorage) {
public static async storeInSecretStorage(
keys: Map<string, Uint8Array>,
secretStorage: SecretStorage,
): Promise<void> {
for (const [type, privateKey] of keys) {
const encodedKey = encodeBase64(privateKey);
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
@@ -200,7 +215,7 @@ export class CrossSigningInfo extends EventEmitter {
* @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key
*/
static async getFromSecretStorage(type, secretStorage) {
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) {
return null;
@@ -215,8 +230,8 @@ export class CrossSigningInfo extends EventEmitter {
* "self_signing", or "user_signing". Optional, will check all by default.
* @returns {boolean} True if all keys are stored in the local cache.
*/
async isStoredInKeyCache(type) {
const cacheCallbacks = this._cacheCallbacks;
public async isStoredInKeyCache(type?: string): Promise<boolean> {
const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return false;
const types = type ? [type] : ["master", "self_signing", "user_signing"];
for (const t of types) {
@@ -232,9 +247,9 @@ export class CrossSigningInfo extends EventEmitter {
*
* @returns {Map} A map from key type (string) to private key (Uint8Array)
*/
async getCrossSigningKeysFromCache() {
public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
const keys = new Map();
const cacheCallbacks = this._cacheCallbacks;
const cacheCallbacks = this.cacheCallbacks;
if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) {
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
@@ -255,8 +270,7 @@ export class CrossSigningInfo extends EventEmitter {
*
* @return {string} the ID
*/
getId(type) {
type = type || "master";
public getId(type = "master"): string {
if (!this.keys[type]) return null;
const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo);
@@ -269,8 +283,8 @@ export class CrossSigningInfo extends EventEmitter {
*
* @param {CrossSigningLevel} level The key types to reset
*/
async resetKeys(level) {
if (!this._callbacks.saveCrossSigningKeys) {
public async resetKeys(level?: CrossSigningLevel): Promise<void> {
if (!this.callbacks.saveCrossSigningKeys) {
throw new Error("No saveCrossSigningKeys callback supplied");
}
@@ -285,12 +299,12 @@ export class CrossSigningInfo extends EventEmitter {
CrossSigningLevel.USER_SIGNING |
CrossSigningLevel.SELF_SIGNING
);
} else if (level === 0) {
} else if (level === 0 as CrossSigningLevel) {
return;
}
const privateKeys = {};
const keys = {};
const privateKeys: Record<string, Uint8Array> = {};
const keys: Record<string, ICrossSigningKey> = {};
let masterSigning;
let masterPub;
@@ -347,7 +361,7 @@ export class CrossSigningInfo extends EventEmitter {
}
Object.assign(this.keys, keys);
this._callbacks.saveCrossSigningKeys(privateKeys);
this.callbacks.saveCrossSigningKeys(privateKeys);
} finally {
if (masterSigning) {
masterSigning.free();
@@ -358,12 +372,12 @@ export class CrossSigningInfo extends EventEmitter {
/**
* unsets the keys, used when another session has reset the keys, to disable cross-signing
*/
clearKeys() {
public clearKeys(): void {
this.keys = {};
}
setKeys(keys) {
const signingKeys = {};
public setKeys(keys: Record<string, ICrossSigningKey>): void {
const signingKeys: Record<string, ICrossSigningKey> = {};
if (keys.master) {
if (keys.master.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
@@ -434,7 +448,7 @@ export class CrossSigningInfo extends EventEmitter {
}
}
updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void {
// It is critical that this value latches forward from false to true but
// never back to false to avoid a downgrade attack.
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
@@ -442,7 +456,7 @@ export class CrossSigningInfo extends EventEmitter {
}
}
async signObject(data, type) {
public async signObject<T extends object>(data: T, type: string): Promise<T & { signatures: ISignatures }> {
if (!this.keys[type]) {
throw new Error(
"Attempted to sign with " + type + " key but no such key present",
@@ -451,13 +465,13 @@ export class CrossSigningInfo extends EventEmitter {
const [pubkey, signing] = await this.getCrossSigningKey(type);
try {
pkSign(data, signing, this.userId, pubkey);
return data;
return data as T & { signatures: ISignatures };
} finally {
signing.free();
}
}
async signUser(key) {
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey> {
if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user");
return;
@@ -465,7 +479,7 @@ export class CrossSigningInfo extends EventEmitter {
return this.signObject(key.keys.master, "user_signing");
}
async signDevice(userId, device) {
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey> {
if (userId !== this.userId) {
throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`,
@@ -475,7 +489,7 @@ export class CrossSigningInfo extends EventEmitter {
logger.info("No self signing key: not signing device");
return;
}
return this.signObject(
return this.signObject<Omit<ISignedKey, "signatures">>(
{
algorithms: device.algorithms,
keys: device.keys,
@@ -492,7 +506,7 @@ export class CrossSigningInfo extends EventEmitter {
*
* @returns {UserTrustLevel}
*/
checkUserTrust(userCrossSigning) {
public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
// if we're checking our own key, then it's trusted if the master key
// and self-signing key match
if (this.userId === userCrossSigning.userId
@@ -530,12 +544,17 @@ export class CrossSigningInfo extends EventEmitter {
*
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param {module:crypto/deviceinfo} device The device to check
* @param {bool} localTrust Whether the device is trusted locally
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices
* @param {boolean} localTrust Whether the device is trusted locally
* @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices
*
* @returns {DeviceTrustLevel}
*/
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
public checkDeviceTrust(
userCrossSigning: CrossSigningInfo,
device: DeviceInfo,
localTrust: boolean,
trustCrossSignedDevices: boolean,
): DeviceTrustLevel {
const userTrust = this.checkUserTrust(userCrossSigning);
const userSSK = userCrossSigning.keys.self_signing;
@@ -552,29 +571,23 @@ export class CrossSigningInfo extends EventEmitter {
// if we can verify the user's SSK from their master key...
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
// ...and this device's key from their SSK...
pkVerify(
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
);
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(
userTrust, localTrust, trustCrossSignedDevices,
);
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
} catch (e) {
return new DeviceTrustLevel(
false, false, localTrust, trustCrossSignedDevices,
);
return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
}
}
/**
* @returns {object} Cache callbacks
*/
getCacheCallbacks() {
return this._cacheCallbacks;
public getCacheCallbacks(): ICacheCallbacks {
return this.cacheCallbacks;
}
}
function deviceToObject(device, userId) {
function deviceToObject(device: DeviceInfo, userId: string) {
return {
algorithms: device.algorithms,
keys: device.keys,
@@ -584,49 +597,49 @@ function deviceToObject(device, userId) {
};
}
export const CrossSigningLevel = {
MASTER: 4,
USER_SIGNING: 2,
SELF_SIGNING: 1,
};
export enum CrossSigningLevel {
MASTER = 4,
USER_SIGNING = 2,
SELF_SIGNING = 1,
}
/**
* Represents the ways in which we trust a user
*/
export class UserTrustLevel {
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
this._crossSigningVerified = crossSigningVerified;
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore;
this._tofu = tofu;
}
constructor(
private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean,
) {}
/**
* @returns {bool} true if this user is verified via any means
* @returns {boolean} true if this user is verified via any means
*/
isVerified() {
public isVerified(): boolean {
return this.isCrossSigningVerified();
}
/**
* @returns {bool} true if this user is verified via cross signing
* @returns {boolean} true if this user is verified via cross signing
*/
isCrossSigningVerified() {
return this._crossSigningVerified;
public isCrossSigningVerified(): boolean {
return this.crossSigningVerified;
}
/**
* @returns {bool} true if we ever verified this user before (at least for
* @returns {boolean} true if we ever verified this user before (at least for
* the history of verifications observed by this device).
*/
wasCrossSigningVerified() {
return this._crossSigningVerifiedBefore;
public wasCrossSigningVerified(): boolean {
return this.crossSigningVerifiedBefore;
}
/**
* @returns {bool} true if this user's key is trusted on first use
* @returns {boolean} true if this user's key is trusted on first use
*/
isTofu() {
return this._tofu;
public isTofu(): boolean {
return this.tofu;
}
}
@@ -634,58 +647,62 @@ export class UserTrustLevel {
* Represents the ways in which we trust a device
*/
export class DeviceTrustLevel {
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) {
this._crossSigningVerified = crossSigningVerified;
this._tofu = tofu;
this._localVerified = localVerified;
this._trustCrossSignedDevices = trustCrossSignedDevices;
}
constructor(
public readonly crossSigningVerified: boolean,
public readonly tofu: boolean,
private readonly localVerified: boolean,
private readonly trustCrossSignedDevices: boolean,
) {}
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
public static fromUserTrustLevel(
userTrustLevel: UserTrustLevel,
localVerified: boolean,
trustCrossSignedDevices: boolean,
): DeviceTrustLevel {
return new DeviceTrustLevel(
userTrustLevel._crossSigningVerified,
userTrustLevel._tofu,
userTrustLevel.isCrossSigningVerified(),
userTrustLevel.isTofu(),
localVerified,
trustCrossSignedDevices,
);
}
/**
* @returns {bool} true if this device is verified via any means
* @returns {boolean} true if this device is verified via any means
*/
isVerified() {
public isVerified(): boolean {
return Boolean(this.isLocallyVerified() || (
this._trustCrossSignedDevices && this.isCrossSigningVerified()
this.trustCrossSignedDevices && this.isCrossSigningVerified()
));
}
/**
* @returns {bool} true if this device is verified via cross signing
* @returns {boolean} true if this device is verified via cross signing
*/
isCrossSigningVerified() {
return this._crossSigningVerified;
public isCrossSigningVerified(): boolean {
return this.crossSigningVerified;
}
/**
* @returns {bool} true if this device is verified locally
* @returns {boolean} true if this device is verified locally
*/
isLocallyVerified() {
return this._localVerified;
public isLocallyVerified(): boolean {
return this.localVerified;
}
/**
* @returns {bool} true if this device is trusted from a user's key
* @returns {boolean} true if this device is trusted from a user's key
* that is trusted on first use
*/
isTofu() {
return this._tofu;
public isTofu(): boolean {
return this.tofu;
}
}
export function createCryptoStoreCacheCallbacks(store, olmdevice) {
export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
return {
getCrossSigningKeyCache: async function(type, _expectedPublicKey) {
const key = await new Promise((resolve) => {
getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
const key = await new Promise<any>((resolve) => {
return store.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
@@ -696,50 +713,56 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
});
if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmdevice._pickleKey);
const pickleKey = Buffer.from(olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, type);
return decodeBase64(decrypted);
} else {
return key;
}
},
storeCrossSigningKeyCache: async function(type, key) {
storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
);
}
const pickleKey = Buffer.from(olmdevice._pickleKey);
key = await encryptAES(encodeBase64(key), pickleKey, type);
const pickleKey = Buffer.from(olmDevice.pickleKey);
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
return store.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.storeSecretStorePrivateKey(txn, type, key);
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
},
);
},
};
}
export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void];
/**
* Request cross-signing keys from another device during verification.
*
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
* @param {MatrixClient} baseApis base Matrix API interface
* @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified
*/
export async function requestKeysDuringVerification(baseApis, userId, deviceId) {
export function requestKeysDuringVerification(
baseApis: MatrixClient,
userId: string,
deviceId: string,
): Promise<KeysDuringVerification | void> {
// If this is a self-verification, ask the other party for keys
if (baseApis.getUserId() !== userId) {
return;
}
logger.log("Cross-signing: Self-verification done; requesting keys");
// This happens asynchronously, and we're not concerned about waiting for
// it. We return here in order to test.
return new Promise((resolve, reject) => {
// it. We return here in order to test.
return new Promise<KeysDuringVerification | void>((resolve, reject) => {
const client = baseApis;
const original = client._crypto._crossSigningInfo;
const original = client.crypto.crossSigningInfo;
// We already have all of the infrastructure we need to validate and
// cache cross-signing keys, so instead of replicating that, here we set
@@ -748,8 +771,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
const crossSigning = new CrossSigningInfo(
original.userId,
{ getCrossSigningKey: async (type) => {
logger.debug("Cross-signing: requesting secret",
type, deviceId);
logger.debug("Cross-signing: requesting secret", type, deviceId);
const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [deviceId],
);
@@ -757,7 +779,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
const decoded = decodeBase64(result);
return Uint8Array.from(decoded);
} },
original._cacheCallbacks,
original.getCacheCallbacks(),
);
crossSigning.keys = original.keys;
@@ -765,7 +787,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
// https://github.com/vector-im/element-web/issues/12604
// then change here to reject on the timeout
// Requests can be ignored, so don't wait around forever
const timeout = new Promise((resolve, reject) => {
const timeout = new Promise<void>((resolve) => {
setTimeout(
resolve,
KEY_REQUEST_TIMEOUT_MS,
@@ -774,8 +796,8 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
});
// also request and cache the key backup key
const backupKeyPromise = new Promise(async resolve => {
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
const backupKeyPromise = (async () => {
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret(
@@ -785,29 +807,26 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
client._crypto.storeSessionBackupPrivateKey(
await client.crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache(
undefined, undefined, backupInfo,
).then(() => {
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
logger.info("Backup restored.");
});
}
resolve();
});
})();
// We call getCrossSigningKey() for its side-effects
return Promise.race([
return Promise.race<KeysDuringVerification | void>([
Promise.all([
crossSigning.getCrossSigningKey("master"),
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise,
]),
]) as Promise<KeysDuringVerification>,
timeout,
]).then(resolve, reject);
}).catch((e) => {
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,41 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { EventEmitter } from "events";
import { createCryptoStoreCacheCallbacks } from "./CrossSigning";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { PREFIX_UNSTABLE } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import {
PREFIX_UNSTABLE,
} from "../http-api";
CrossSigningKeys,
ICrossSigningKey,
ICryptoCallbacks,
ISignedKey,
KeySignatures,
} from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
interface ICrossSigningKeys {
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
keys: Record<string, ICrossSigningKey>;
}
/**
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
@@ -17,18 +47,23 @@ import {
* more than once.
*/
export class EncryptionSetupBuilder {
public readonly accountDataClientAdapter: AccountDataClientAdapter;
public readonly crossSigningCallbacks: CrossSigningCallbacks;
public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
private crossSigningKeys: ICrossSigningKeys = null;
private keySignatures: KeySignatures = null;
private keyBackupInfo: IKeyBackupInfo = null;
private sessionBackupPrivateKey: Uint8Array;
/**
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
*/
constructor(accountData, delegateCryptoCallbacks) {
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
this.crossSigningCallbacks = new CrossSigningCallbacks();
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
this._crossSigningKeys = null;
this._keySignatures = null;
this._keyBackupInfo = null;
}
/**
@@ -42,8 +77,8 @@ export class EncryptionSetupBuilder {
* an empty authDict, to obtain the flows.
* @param {Object} keys the new keys
*/
addCrossSigningKeys(authUpload, keys) {
this._crossSigningKeys = { authUpload, keys };
public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void {
this.crossSigningKeys = { authUpload, keys };
}
/**
@@ -54,8 +89,8 @@ export class EncryptionSetupBuilder {
*
* @param {Object} keyBackupInfo as received from/sent to the server
*/
addSessionBackup(keyBackupInfo) {
this._keyBackupInfo = keyBackupInfo;
public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void {
this.keyBackupInfo = keyBackupInfo;
}
/**
@@ -65,8 +100,8 @@ export class EncryptionSetupBuilder {
*
* @param {Uint8Array} privateKey
*/
addSessionBackupPrivateKeyToCache(privateKey) {
this._sessionBackupPrivateKey = privateKey;
public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void {
this.sessionBackupPrivateKey = privateKey;
}
/**
@@ -75,14 +110,14 @@ export class EncryptionSetupBuilder {
*
* @param {String} userId
* @param {String} deviceId
* @param {String} signature
* @param {Object} signature
*/
addKeySignature(userId, deviceId, signature) {
if (!this._keySignatures) {
this._keySignatures = {};
public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void {
if (!this.keySignatures) {
this.keySignatures = {};
}
const userSignatures = this._keySignatures[userId] || {};
this._keySignatures[userId] = userSignatures;
const userSignatures = this.keySignatures[userId] || {};
this.keySignatures[userId] = userSignatures;
userSignatures[deviceId] = signature;
}
@@ -91,21 +126,21 @@ export class EncryptionSetupBuilder {
* @param {Object} content
* @return {Promise}
*/
setAccountData(type, content) {
return this.accountDataClientAdapter.setAccountData(type, content);
public async setAccountData(type: string, content: object): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
/**
* builds the operation containing all the parts that have been added to the builder
* @return {EncryptionSetupOperation}
*/
buildOperation() {
const accountData = this.accountDataClientAdapter._values;
public buildOperation(): EncryptionSetupOperation {
const accountData = this.accountDataClientAdapter.values;
return new EncryptionSetupOperation(
accountData,
this._crossSigningKeys,
this._keyBackupInfo,
this._keySignatures,
this.crossSigningKeys,
this.keyBackupInfo,
this.keySignatures,
);
}
@@ -118,28 +153,27 @@ export class EncryptionSetupBuilder {
* @param {Crypto} crypto
* @return {Promise}
*/
async persist(crypto) {
public async persist(crypto: Crypto): Promise<void> {
// store private keys in cache
if (this._crossSigningKeys) {
const cacheCallbacks = createCryptoStoreCacheCallbacks(
crypto._cryptoStore, crypto._olmDevice);
if (this.crossSigningKeys) {
const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice);
for (const type of ["master", "self_signing", "user_signing"]) {
logger.log(`Cache ${type} cross-signing private key locally`);
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
}
// store own cross-sign pubkeys as trusted
await crypto._cryptoStore.doTxn(
await crypto.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto._cryptoStore.storeCrossSigningKeys(
txn, this._crossSigningKeys.keys);
crypto.cryptoStore.storeCrossSigningKeys(
txn, this.crossSigningKeys.keys);
},
);
}
// store session backup key in cache
if (this._sessionBackupPrivateKey) {
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
if (this.sessionBackupPrivateKey) {
await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey);
}
}
}
@@ -157,66 +191,66 @@ export class EncryptionSetupOperation {
* @param {Object} keyBackupInfo
* @param {Object} keySignatures
*/
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
this._accountData = accountData;
this._crossSigningKeys = crossSigningKeys;
this._keyBackupInfo = keyBackupInfo;
this._keySignatures = keySignatures;
}
constructor(
private readonly accountData: Map<string, object>,
private readonly crossSigningKeys: ICrossSigningKeys,
private readonly keyBackupInfo: IKeyBackupInfo,
private readonly keySignatures: KeySignatures,
) {}
/**
* Runs the (remaining part of, in the future) operation by sending requests to the server.
* @param {Crypto} crypto
* @param {Crypto} crypto
*/
async apply(crypto) {
const baseApis = crypto._baseApis;
public async apply(crypto: Crypto): Promise<void> {
const baseApis = crypto.baseApis;
// upload cross-signing keys
if (this._crossSigningKeys) {
const keys = {};
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
if (this.crossSigningKeys) {
const keys: Partial<CrossSigningKeys> = {};
for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
keys[name + "_key"] = key;
}
// We must only call `uploadDeviceSigningKeys` from inside this auth
// helper to ensure we properly handle auth errors.
await this._crossSigningKeys.authUpload(authDict => {
return baseApis.uploadDeviceSigningKeys(authDict, keys);
await this.crossSigningKeys.authUpload(authDict => {
return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
});
// pass the new keys to the main instance of our own CrossSigningInfo.
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys);
}
// set account data
if (this._accountData) {
for (const [type, content] of this._accountData) {
if (this.accountData) {
for (const [type, content] of this.accountData) {
await baseApis.setAccountData(type, content);
}
}
// upload first cross-signing signatures with the new key
// (e.g. signing our own device)
if (this._keySignatures) {
await baseApis.uploadKeySignatures(this._keySignatures);
if (this.keySignatures) {
await baseApis.uploadKeySignatures(this.keySignatures);
}
// need to create/update key backup info
if (this._keyBackupInfo) {
if (this._keyBackupInfo.version) {
if (this.keyBackupInfo) {
if (this.keyBackupInfo.version) {
// session backup signature
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis._http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
await baseApis.http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
undefined, {
algorithm: this._keyBackupInfo.algorithm,
auth_data: this._keyBackupInfo.auth_data,
algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data,
},
{ prefix: PREFIX_UNSTABLE },
);
} else {
// add new key backup
await baseApis._http.authedRequest(
await baseApis.http.authedRequest(
undefined, "POST", "/room_keys/version",
undefined, this._keyBackupInfo,
undefined, this.keyBackupInfo,
{ prefix: PREFIX_UNSTABLE },
);
}
@@ -229,20 +263,20 @@ export class EncryptionSetupOperation {
* implementing the methods related to account data in MatrixClient
*/
class AccountDataClientAdapter extends EventEmitter {
public readonly values = new Map<string, MatrixEvent>();
/**
* @param {Object.<String, MatrixEvent>} accountData existing account data
* @param {Object.<String, MatrixEvent>} existingValues existing account data
*/
constructor(accountData) {
constructor(private readonly existingValues: Record<string, MatrixEvent>) {
super();
this._existingValues = accountData;
this._values = new Map();
}
/**
* @param {String} type
* @return {Promise<Object>} the content of the account data
*/
getAccountDataFromServer(type) {
public getAccountDataFromServer(type: string): Promise<any> {
return Promise.resolve(this.getAccountData(type));
}
@@ -250,12 +284,12 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {String} type
* @return {Object} the content of the account data
*/
getAccountData(type) {
const modifiedValue = this._values.get(type);
public getAccountData(type: string): MatrixEvent {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue;
}
const existingValue = this._existingValues[type];
const existingValue = this.existingValues[type];
if (existingValue) {
return existingValue.getContent();
}
@@ -267,15 +301,16 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {Object} content
* @return {Promise}
*/
setAccountData(type, content) {
const lastEvent = this._values.get(type);
this._values.set(type, content);
public setAccountData(type: string, content: any): Promise<{}> {
const lastEvent = this.values.get(type);
this.values.set(type, content);
// ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit("accountData", event, lastEvent);
return {};
});
}
}
@@ -285,27 +320,25 @@ class AccountDataClientAdapter extends EventEmitter {
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
* See CrossSigningInfo constructor
*/
class CrossSigningCallbacks {
constructor() {
this.privateKeys = new Map();
}
class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
public readonly privateKeys = new Map<string, Uint8Array>();
// cache callbacks
getCrossSigningKeyCache(type, expectedPublicKey) {
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array> {
return this.getCrossSigningKey(type, expectedPublicKey);
}
storeCrossSigningKeyCache(type, key) {
public storeCrossSigningKeyCache(type: string, key: Uint8Array): Promise<void> {
this.privateKeys.set(type, key);
return Promise.resolve();
}
// non-cache callbacks
getCrossSigningKey(type, _expectedPubkey) {
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array> {
return Promise.resolve(this.privateKeys.get(type));
}
saveCrossSigningKeys(privateKeys) {
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
for (const [type, privateKey] of Object.entries(privateKeys)) {
this.privateKeys.set(type, privateKey);
}
@@ -317,39 +350,37 @@ class CrossSigningCallbacks {
* the SecretStorage crypto callbacks
*/
class SSSSCryptoCallbacks {
constructor(delegateCryptoCallbacks) {
this._privateKeys = new Map();
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
}
private readonly privateKeys = new Map<string, Uint8Array>();
async getSecretStorageKey({ keys }, name) {
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
public async getSecretStorageKey(
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
name: string,
): Promise<[string, Uint8Array]|null> {
for (const keyId of Object.keys(keys)) {
const privateKey = this._privateKeys.get(keyId);
const privateKey = this.privateKeys.get(keyId);
if (privateKey) {
return [keyId, privateKey];
}
}
// if we don't have the key cached yet, ask
// for it to the general crypto callbacks and cache it
if (this._delegateCryptoCallbacks) {
const result = await this._delegateCryptoCallbacks.
if (this?.delegateCryptoCallbacks?.getSecretStorageKey) {
const result = await this.delegateCryptoCallbacks.
getSecretStorageKey({ keys }, name);
if (result) {
const [keyId, privateKey] = result;
this._privateKeys.set(keyId, privateKey);
this.privateKeys.set(keyId, privateKey);
}
return result;
}
return null;
}
addPrivateKey(keyId, keyInfo, privKey) {
this._privateKeys.set(keyId, privKey);
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
this.privateKeys.set(keyId, privKey);
// Also pass along to application to cache if it wishes
if (
this._delegateCryptoCallbacks &&
this._delegateCryptoCallbacks.cacheSecretStorageKey
) {
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
}
this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@ limitations under the License.
*/
import { logger } from '../logger';
import { MatrixClient } from "../client";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
import { CryptoStore, OutgoingRoomKeyRequest } from './store/base';
import { EventType } from "../@types/event";
/**
* Internal module. Management of outgoing room key requests.
@@ -57,61 +61,58 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
*
* @enum {number}
*/
export const ROOM_KEY_REQUEST_STATES = {
export enum RoomKeyRequestState {
/** request not yet sent */
UNSENT: 0,
Unsent,
/** request sent, awaiting reply */
SENT: 1,
Sent,
/** reply received, cancellation not yet sent */
CANCELLATION_PENDING: 2,
CancellationPending,
/**
* Cancellation not yet sent and will transition to UNSENT instead of
* being deleted once the cancellation has been sent.
*/
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
};
CancellationPendingAndWillResend,
}
export class OutgoingRoomKeyRequestManager {
constructor(baseApis, deviceId, cryptoStore) {
this._baseApis = baseApis;
this._deviceId = deviceId;
this._cryptoStore = cryptoStore;
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
private sendOutgoingRoomKeyRequestsTimer: number = null;
// handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
this._sendOutgoingRoomKeyRequestsTimer = null;
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequests
private sendOutgoingRoomKeyRequestsRunning = false;
// sanity check to ensure that we don't end up with two concurrent runs
// of _sendOutgoingRoomKeyRequests
this._sendOutgoingRoomKeyRequestsRunning = false;
private clientRunning = false;
this._clientRunning = false;
}
constructor(
private readonly baseApis: MatrixClient,
private readonly deviceId: string,
private readonly cryptoStore: CryptoStore,
) {}
/**
* Called when the client is started. Sets background processes running.
*/
start() {
this._clientRunning = true;
public start(): void {
this.clientRunning = true;
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
public stop(): void {
logger.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
this.clientRunning = false;
}
/**
* Send any requests that have been queued
*/
sendQueuedRequests() {
this._startTimer();
public sendQueuedRequests(): void {
this.startTimer();
}
/**
@@ -131,95 +132,97 @@ export class OutgoingRoomKeyRequestManager {
* pending list (or we have established that a similar request already
* exists)
*/
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
public async queueRoomKeyRequest(
requestBody: IRoomKeyRequestBody,
recipients: IRoomKeyRequestRecipient[],
resend = false,
): Promise<void> {
const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
if (!req) {
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
requestId: this.baseApis.makeTxnId(),
state: RoomKeyRequestState.Unsent,
});
} else {
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
case ROOM_KEY_REQUEST_STATES.UNSENT:
// nothing to do here, since we're going to send a request anyways
return;
case RoomKeyRequestState.CancellationPendingAndWillResend:
case RoomKeyRequestState.Unsent:
// nothing to do here, since we're going to send a request anyways
return;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.SENT;
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
);
break;
}
case ROOM_KEY_REQUEST_STATES.SENT: {
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state =
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
const updatedReq =
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this._baseApis.makeTxnId(),
},
);
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.queueRoomKeyRequest(
requestBody, recipients, resend,
);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
true,
);
} catch (e) {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
case RoomKeyRequestState.CancellationPending: {
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ?
RoomKeyRequestState.CancellationPendingAndWillResend :
RoomKeyRequestState.Sent;
await this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.CancellationPending, {
state,
cancellationTxnId: this.baseApis.makeTxnId(),
},
);
break;
}
break;
}
default:
throw new Error('unhandled state: ' + req.state);
case RoomKeyRequestState.Sent: {
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state =
RoomKeyRequestState.CancellationPendingAndWillResend;
const updatedReq =
await this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.Sent, {
state,
cancellationTxnId: this.baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this.baseApis.makeTxnId(),
},
);
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.queueRoomKeyRequest(
requestBody, recipients, resend,
);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this.sendOutgoingRoomKeyRequestCancellation(
updatedReq,
true,
);
} catch (e) {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
}
break;
}
default:
throw new Error('unhandled state: ' + req.state);
}
}
}
@@ -232,21 +235,21 @@ export class OutgoingRoomKeyRequestManager {
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody) {
return this._cryptoStore.getOutgoingRoomKeyRequest(
public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<unknown> {
return this.cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
).then((req): unknown => {
if (!req) {
// no request was made for this key
return;
}
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
case RoomKeyRequestState.CancellationPending:
case RoomKeyRequestState.CancellationPendingAndWillResend:
// nothing to do here
return;
case ROOM_KEY_REQUEST_STATES.UNSENT:
case RoomKeyRequestState.Unsent:
// just delete it
// FIXME: ghahah we may have attempted to send it, and
@@ -258,16 +261,14 @@ export class OutgoingRoomKeyRequestManager {
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
);
return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent);
case ROOM_KEY_REQUEST_STATES.SENT: {
case RoomKeyRequestState.Sent: {
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
cancellationTxnId: this._baseApis.makeTxnId(),
return this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.Sent, {
state: RoomKeyRequestState.CancellationPending,
cancellationTxnId: this.baseApis.makeTxnId(),
},
).then((updatedReq) => {
if (!updatedReq) {
@@ -294,14 +295,14 @@ export class OutgoingRoomKeyRequestManager {
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
this._sendOutgoingRoomKeyRequestCancellation(
this.sendOutgoingRoomKeyRequestCancellation(
updatedReq,
).catch((e) => {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
this.startTimer();
});
});
}
@@ -320,10 +321,8 @@ export class OutgoingRoomKeyRequestManager {
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingSentRoomKeyRequest(userId, deviceId) {
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
);
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
}
/**
@@ -333,29 +332,27 @@ export class OutgoingRoomKeyRequestManager {
* For example, after initialization or self-verification.
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
*/
async cancelAndResendAllOutgoingRequests() {
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
ROOM_KEY_REQUEST_STATES.SENT,
);
public async cancelAndResendAllOutgoingRequests(): Promise<void[]> {
const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
this.queueRoomKeyRequest(requestBody, recipients, true)));
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
if (this._sendOutgoingRoomKeyRequestsTimer) {
private startTimer(): void {
if (this.sendOutgoingRoomKeyRequestsTimer) {
return;
}
const startSendingOutgoingRoomKeyRequests = () => {
if (this._sendOutgoingRoomKeyRequestsRunning) {
if (this.sendOutgoingRoomKeyRequestsRunning) {
throw new Error("RoomKeyRequestSend already in progress!");
}
this._sendOutgoingRoomKeyRequestsRunning = true;
this.sendOutgoingRoomKeyRequestsRunning = true;
this._sendOutgoingRoomKeyRequests().finally(() => {
this._sendOutgoingRoomKeyRequestsRunning = false;
this.sendOutgoingRoomKeyRequests().finally(() => {
this.sendOutgoingRoomKeyRequestsRunning = false;
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
@@ -365,7 +362,7 @@ export class OutgoingRoomKeyRequestManager {
});
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
startSendingOutgoingRoomKeyRequests,
SEND_KEY_REQUESTS_DELAY_MS,
);
@@ -374,47 +371,47 @@ export class OutgoingRoomKeyRequestManager {
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
_sendOutgoingRoomKeyRequests() {
if (!this._clientRunning) {
this._sendOutgoingRoomKeyRequestsTimer = null;
private sendOutgoingRoomKeyRequests(): Promise<void> {
if (!this.clientRunning) {
this.sendOutgoingRoomKeyRequestsTimer = null;
return Promise.resolve();
}
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
return this.cryptoStore.getOutgoingRoomKeyRequestByState([
RoomKeyRequestState.CancellationPending,
RoomKeyRequestState.CancellationPendingAndWillResend,
RoomKeyRequestState.Unsent,
]).then((req: OutgoingRoomKeyRequest) => {
if (!req) {
this._sendOutgoingRoomKeyRequestsTimer = null;
this.sendOutgoingRoomKeyRequestsTimer = null;
return;
}
let prom;
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.UNSENT:
prom = this._sendOutgoingRoomKeyRequest(req);
case RoomKeyRequestState.Unsent:
prom = this.sendOutgoingRoomKeyRequest(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
case RoomKeyRequestState.CancellationPending:
prom = this.sendOutgoingRoomKeyRequestCancellation(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
prom = this._sendOutgoingRoomKeyRequestCancellation(req, true);
case RoomKeyRequestState.CancellationPendingAndWillResend:
prom = this.sendOutgoingRoomKeyRequestCancellation(req, true);
break;
}
return prom.then(() => {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
return this.sendOutgoingRoomKeyRequests();
}).catch((e) => {
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this.sendOutgoingRoomKeyRequestsTimer = null;
});
});
}
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<unknown> {
logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
@@ -423,24 +420,24 @@ export class OutgoingRoomKeyRequestManager {
const requestMessage = {
action: "request",
requesting_device_id: this._deviceId,
requesting_device_id: this.deviceId,
request_id: req.requestId,
body: req.requestBody,
};
return this._sendMessageToDevices(
return this.sendMessageToDevices(
requestMessage, req.recipients, req.requestTxnId || req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
{ state: ROOM_KEY_REQUEST_STATES.SENT },
return this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent },
);
});
}
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<unknown> {
logger.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
@@ -450,30 +447,30 @@ export class OutgoingRoomKeyRequestManager {
const requestMessage = {
action: "request_cancellation",
requesting_device_id: this._deviceId,
requesting_device_id: this.deviceId,
request_id: req.requestId,
};
return this._sendMessageToDevices(
return this.sendMessageToDevices(
requestMessage, req.recipients, req.cancellationTxnId,
).then(() => {
if (andResend) {
// We want to resend, so transition to UNSENT
return this._cryptoStore.updateOutgoingRoomKeyRequest(
return this.cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
{ state: ROOM_KEY_REQUEST_STATES.UNSENT },
RoomKeyRequestState.CancellationPendingAndWillResend,
{ state: RoomKeyRequestState.Unsent },
);
}
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
return this.cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.CancellationPending,
);
});
}
// send a RoomKeyRequest to a list of recipients
_sendMessageToDevices(message, recipients, txnId) {
const contentMap = {};
private sendMessageToDevices(message, recipients, txnId: string): Promise<{}> {
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
@@ -481,9 +478,7 @@ export class OutgoingRoomKeyRequestManager {
contentMap[recip.userId][recip.deviceId] = message;
}
return this._baseApis.sendToDevice(
'm.room_key_request', contentMap, txnId,
);
return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,45 +20,52 @@ limitations under the License.
* Manages the list of encrypted rooms
*/
import { CryptoStore } from './store/base';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
/* eslint-disable camelcase */
export interface IRoomEncryption {
algorithm: string;
rotation_period_ms?: number;
rotation_period_msgs?: number;
}
/* eslint-enable camelcase */
/**
* @alias module:crypto/RoomList
*/
export class RoomList {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
private roomEncryption: Record<string, IRoomEncryption> = {};
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
constructor(private readonly cryptoStore: CryptoStore) {}
async init() {
await this._cryptoStore.doTxn(
public async init(): Promise<void> {
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
this._roomEncryption = result;
this.cryptoStore.getEndToEndRooms(txn, (result) => {
this.roomEncryption = result;
});
},
);
}
getRoomEncryption(roomId) {
return this._roomEncryption[roomId] || null;
public getRoomEncryption(roomId: string): IRoomEncryption {
return this.roomEncryption[roomId] || null;
}
isRoomEncrypted(roomId) {
public isRoomEncrypted(roomId: string): boolean {
return Boolean(this.getRoomEncryption(roomId));
}
async setRoomEncryption(roomId, roomInfo) {
public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise<void> {
// important that this happens before calling into the store
// as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events
this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn(
this.roomEncryption[roomId] = roomInfo;
await this.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
},
);
}
@@ -1,5 +1,5 @@
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,61 +14,103 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from 'events';
import { EventEmitter } from 'stream';
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { randomString } from '../randomstring';
import { encryptAES, decryptAES } from './aes';
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
import { encodeBase64 } from "./olmlib";
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
export const SECRET_STORAGE_ALGORITHM_V1_AES
= "m.secret_storage.v1.aes-hmac-sha2";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
// Some of the key functions use a tuple and some use an object...
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
export type SecretStorageKeyObject = {keyId: string, keyInfo: ISecretStorageKeyInfo};
export interface ISecretRequest {
requestId: string;
promise: Promise<string>;
cancel: (reason: string) => void;
}
export interface IAccountDataClient extends EventEmitter {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: <T extends {[k: string]: any}>(eventType: string) => Promise<T>;
getAccountData: (eventType: string) => MatrixEvent;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
interface ISecretRequestInternal {
name: string;
devices: string[];
resolve: (reason: string) => void;
reject: (error: Error) => void;
}
interface IDecryptors {
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
}
interface ISecretInfo {
encrypted: {
// eslint-disable-next-line camelcase
key_id: IEncryptedPayload;
};
}
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks) {
super();
this._baseApis = baseApis;
this._cryptoCallbacks = cryptoCallbacks;
this._requests = {};
this._incomingRequests = {};
}
export class SecretStorage {
private requests = new Map<string, ISecretRequestInternal>();
async getDefaultKeyId() {
const defaultKey = await this._baseApis.getAccountDataFromServer(
// In it's pure javascript days, this was relying on some proper Javascript-style
// type-abuse where sometimes we'd pass in a fake client object with just the account
// data methods implemented, which is all this class needs unless you use the secret
// sharing code, so it was fine. As a low-touch TypeScript migration, this now has
// an extra, optional param for a real matrix client, so you can not pass it as long
// as you don't request any secrets.
// A better solution would probably be to split this class up into secret storage and
// secret sharing which are really two separate things, even though they share an MSC.
constructor(
private readonly accountDataAdapter: IAccountDataClient,
private readonly cryptoCallbacks: ICryptoCallbacks,
private readonly baseApis?: MatrixClient,
) {}
public async getDefaultKeyId(): Promise<string> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer<{ key: string }>(
'm.secret_storage.default_key',
);
if (!defaultKey) return null;
return defaultKey.key;
}
setDefaultKeyId(keyId) {
return new Promise(async (resolve, reject) => {
const listener = (ev) => {
public setDefaultKeyId(keyId: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const listener = (ev: MatrixEvent): void => {
if (
ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId
) {
this._baseApis.removeListener('accountData', listener);
this.accountDataAdapter.removeListener('accountData', listener);
resolve();
}
};
this._baseApis.on('accountData', listener);
this.accountDataAdapter.on('accountData', listener);
try {
await this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
} catch (e) {
this._baseApis.removeListener('accountData', listener);
this.accountDataAdapter.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
).catch(e => {
this.accountDataAdapter.removeListener('accountData', listener);
reject(e);
}
});
});
}
@@ -85,10 +127,14 @@ export class SecretStorage extends EventEmitter {
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
async addKey(algorithm, opts, keyId) {
const keyInfo = { algorithm };
public async addKey(
algorithm: string,
opts: IAddSecretStorageKeyOpts,
keyId?: string,
): Promise<SecretStorageKeyObject> {
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
if (!opts) opts = {};
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
if (opts.name) {
keyInfo.name = opts.name;
@@ -99,25 +145,25 @@ export class SecretStorage extends EventEmitter {
keyInfo.passphrase = opts.passphrase;
}
if (opts.key) {
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
const { iv, mac } = await calculateKeyCheck(opts.key);
keyInfo.iv = iv;
keyInfo.mac = mac;
}
} else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
throw new Error(`Unknown key algorithm ${algorithm}`);
}
if (!keyId) {
do {
keyId = randomString(32);
} while (
await this._baseApis.getAccountDataFromServer(
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
`m.secret_storage.key.${keyId}`,
)
);
}
await this._baseApis.setAccountData(
await this.accountDataAdapter.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
);
@@ -134,8 +180,9 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/
async getKey(keyId) {
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
@@ -143,7 +190,7 @@ export class SecretStorage extends EventEmitter {
return null;
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
@@ -156,8 +203,8 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
return !!(await this.getKey(keyId));
public async hasKey(keyId?: string): Promise<boolean> {
return Boolean(await this.getKey(keyId));
}
/**
@@ -168,10 +215,10 @@ export class SecretStorage extends EventEmitter {
*
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) {
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
const { mac } = await calculateKeyCheck(key, info.iv);
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
} else {
// if we have no information, we have to assume the key is right
@@ -182,10 +229,6 @@ export class SecretStorage extends EventEmitter {
}
}
static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}
/**
* Store an encrypted secret on the server
*
@@ -194,8 +237,8 @@ export class SecretStorage extends EventEmitter {
* @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key.
*/
async store(name, secret, keys) {
const encrypted = {};
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
const encrypted: Record<string, IEncryptedPayload> = {};
if (!keys) {
const defaultKeyId = await this.getDefaultKeyId();
@@ -211,7 +254,7 @@ export class SecretStorage extends EventEmitter {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) {
@@ -221,7 +264,7 @@ export class SecretStorage extends EventEmitter {
// encrypt secret, based on the algorithm
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const keys = { [keyId]: keyInfo };
const [, encryption] = await this._getSecretStorageKey(keys, name);
const [, encryption] = await this.getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
} else {
logger.warn("unknown algorithm for secret storage key " + keyId
@@ -231,34 +274,7 @@ export class SecretStorage extends EventEmitter {
}
// save encrypted secret
await this._baseApis.setAccountData(name, { encrypted });
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
*
* @param {string} name The name of the secret
* @param {object} secretInfo The account data object
* @returns {object} The fixed object or null if no fix was performed
*/
async _fixupStoredSecret(name, secretInfo) {
// We assume the secret was only stored passthrough for 1
// key - this was all the broken code supported.
const keys = Object.keys(secretInfo);
if (
keys.length === 1 && keys[0] !== 'encrypted' &&
secretInfo[keys[0]].passthrough
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
logger.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
}
}
return null;
await this.accountDataAdapter.setAccountData(name, { encrypted });
}
/**
@@ -268,25 +284,23 @@ export class SecretStorage extends EventEmitter {
*
* @return {string} the contents of the secret
*/
async get(name) {
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
public async get(name: string): Promise<string> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) {
return;
}
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
}
throw new Error("Content is not encrypted!");
}
// get possible keys to decrypt
const keys = {};
const keys: Record<string, ISecretStorageKeyInfo> = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
const keyInfo = (
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
)
);
const encInfo = secretInfo.encrypted[keyId];
// only use keys we understand the encryption algorithm of
@@ -302,11 +316,11 @@ export class SecretStorage extends EventEmitter {
`the keys it is encrypted with are for a supported algorithm`);
}
let keyId;
let keyId: string;
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId];
@@ -331,16 +345,12 @@ export class SecretStorage extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStored(name, checkKey) {
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
// check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return null;
}
return null;
}
if (checkKey === undefined) checkKey = true;
@@ -350,7 +360,7 @@ export class SecretStorage extends EventEmitter {
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue;
@@ -371,45 +381,48 @@ export class SecretStorage extends EventEmitter {
*
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
*
* @return {string} the contents of the secret
*/
request(name, devices) {
const requestId = this._baseApis.makeTxnId();
public request(name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
const requestControl = this._requests[requestId] = {
let resolve: (s: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
this.requests.set(requestId, {
name,
devices,
};
const promise = new Promise((resolve, reject) => {
requestControl.resolve = resolve;
requestControl.reject = reject;
resolve,
reject,
});
const cancel = (reason) => {
const cancel = (reason: string) => {
// send cancellation event
const cancelData = {
action: "request_cancellation",
requesting_device_id: this._baseApis.deviceId,
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
for (const device of devices) {
toDevice[device] = cancelData;
}
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
});
// and reject the promise so that anyone waiting on it will be
// notified
requestControl.reject(new Error(reason || "Cancelled"));
reject(new Error(reason || "Cancelled"));
};
// send request to devices
const requestData = {
name,
action: "request",
requesting_device_id: this._baseApis.deviceId,
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
@@ -417,21 +430,21 @@ export class SecretStorage extends EventEmitter {
toDevice[device] = requestData;
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
});
return {
request_id: requestId,
requestId,
promise,
cancel,
};
}
async _onRequestReceived(event) {
public async onRequestReceived(event: MatrixEvent): Promise<void> {
const sender = event.getSender();
const content = event.getContent();
if (sender !== this._baseApis.getUserId()
if (sender !== this.baseApis.getUserId()
|| !(content.name && content.action
&& content.requesting_device_id && content.request_id)) {
// ignore requests from anyone else, for now
@@ -440,34 +453,45 @@ export class SecretStorage extends EventEmitter {
const deviceId = content.requesting_device_id;
// check if it's a cancel
if (content.action === "request_cancellation") {
/*
Looks like we intended to emit events when we got cancelations, but
we never put anything in the _incomingRequests object, and the request
itself doesn't use events anyway so if we were to wire up cancellations,
they probably ought to use the same callback interface. I'm leaving them
disabled for now while converting this file to typescript.
if (this._incomingRequests[deviceId]
&& this._incomingRequests[deviceId][content.request_id]) {
logger.info("received request cancellation for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
logger.info(
"received request cancellation for secret (" + sender +
", " + deviceId + ", " + content.request_id + ")",
);
this.baseApis.emit("crypto.secrets.requestCancelled", {
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
});
}
*/
} else if (content.action === "request") {
if (deviceId === this._baseApis.deviceId) {
if (deviceId === this.baseApis.deviceId) {
// no point in trying to send ourself the secret
return;
}
// check if we have the secret
logger.info("received request for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
if (!this._cryptoCallbacks.onSecretRequested) {
logger.info(
"received request for secret (" + sender +
", " + deviceId + ", " + content.request_id + ")",
);
if (!this.cryptoCallbacks.onSecretRequested) {
return;
}
const secret = await this._cryptoCallbacks.onSecretRequested(
const secret = await this.cryptoCallbacks.onSecretRequested(
sender,
deviceId,
content.request_id,
content.name,
this._baseApis.checkDeviceTrust(sender, deviceId),
this.baseApis.checkDeviceTrust(sender, deviceId),
);
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
@@ -480,25 +504,25 @@ export class SecretStorage extends EventEmitter {
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.ensureOlmSessionsForDevices(
this._baseApis._crypto._olmDevice,
this._baseApis,
this.baseApis.crypto.olmDevice,
this.baseApis,
{
[sender]: [
this._baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId),
],
},
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._baseApis.getUserId(),
this._baseApis.deviceId,
this._baseApis._crypto._olmDevice,
this.baseApis.getUserId(),
this.baseApis.deviceId,
this.baseApis.crypto.olmDevice,
sender,
this._baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId),
payload,
);
const contentMap = {
@@ -508,26 +532,26 @@ export class SecretStorage extends EventEmitter {
};
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
} else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
}
}
}
_onSecretReceived(event) {
if (event.getSender() !== this._baseApis.getUserId()) {
public onSecretReceived(event: MatrixEvent): void {
if (event.getSender() !== this.baseApis.getUserId()) {
// we shouldn't be receiving secrets from anyone else, so ignore
// because someone could be trying to send us bogus data
return;
}
const content = event.getContent();
logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id];
const requestControl = this.requests.get(content.request_id);
if (requestControl) {
// make sure that the device that sent it is one of the devices that
// we requested from
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey(),
);
@@ -550,12 +574,15 @@ export class SecretStorage extends EventEmitter {
}
}
async _getSecretStorageKey(keys, name) {
if (!this._cryptoCallbacks.getSecretStorageKey) {
private async getSecretStorageKey(
keys: Record<string, ISecretStorageKeyInfo>,
name: string,
): Promise<[string, IDecryptors]> {
if (!this.cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
@@ -571,10 +598,10 @@ export class SecretStorage extends EventEmitter {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: async function(secret) {
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
return await encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo) {
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
return await decryptAES(encInfo, privateKey, name);
},
};
+59 -34
View File
@@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { BinaryLike } from "crypto";
import { getCrypto } from '../utils';
import { decodeBase64, encodeBase64 } from './olmlib';
@@ -21,7 +22,14 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
// salt for HKDF, with 8 bytes of zeros
const zerosalt = new Uint8Array(8);
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
iv?: string;
ciphertext?: string;
mac?: string;
}
/**
* encrypt a string in Node.js
@@ -31,7 +39,7 @@ const zerosalt = new Uint8Array(8);
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data, key, name, ivStr) {
async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
@@ -42,25 +50,27 @@ async function encryptNode(data, key, name, ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
const ciphertext = cipher.update(data, "utf-8", "base64")
+ cipher.final("base64");
const ciphertext = Buffer.concat([
cipher.update(data, "utf8"),
cipher.final(),
]);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(ciphertext, "base64").digest("base64");
.update(ciphertext).digest("base64");
return {
iv: encodeBase64(iv),
ciphertext: ciphertext,
ciphertext: ciphertext.toString("base64"),
mac: hmac,
};
}
@@ -75,7 +85,7 @@ async function encryptNode(data, key, name, ivStr) {
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptNode(data, key, name) {
async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
@@ -84,7 +94,8 @@ async function decryptNode(data, key, name) {
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, '');
.update(Buffer.from(data.ciphertext, "base64"))
.digest("base64").replace(/=+$/g, '');
if (hmac !== data.mac.replace(/=+$/g, '')) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
@@ -93,21 +104,20 @@ async function decryptNode(data, key, name) {
const decipher = crypto.createDecipheriv(
"aes-256-ctr", aesKey, decodeBase64(data.iv),
);
return decipher.update(data.ciphertext, "base64", "utf-8")
+ decipher.final("utf-8");
return decipher.update(data.ciphertext, "base64", "utf8")
+ decipher.final("utf8");
}
function deriveKeysNode(key, name) {
function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] {
const crypto = getCrypto();
const prk = crypto.createHmac("sha256", zerosalt)
.update(key).digest();
const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest();
const b = Buffer.alloc(1, 1);
const aesKey = crypto.createHmac("sha256", prk)
.update(name, "utf-8").update(b).digest();
.update(name, "utf8").update(b).digest();
b[0] = 2;
const hmacKey = crypto.createHmac("sha256", prk)
.update(aesKey).update(name, "utf-8").update(b).digest();
.update(aesKey).update(name, "utf8").update(b).digest();
return [aesKey, hmacKey];
}
@@ -120,19 +130,19 @@ function deriveKeysNode(key, name) {
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data, key, name, ivStr) {
async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const encodedData = new TextEncoder().encode(data);
@@ -170,7 +180,7 @@ async function encryptBrowser(data, key, name, ivStr) {
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptBrowser(data, key, name) {
async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const ciphertext = decodeBase64(data.ciphertext);
@@ -197,7 +207,7 @@ async function decryptBrowser(data, key, name) {
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeysBrowser(key, name) {
async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await subtleCrypto.importKey(
'raw',
key,
@@ -208,7 +218,9 @@ async function deriveKeysBrowser(key, name) {
const keybits = await subtleCrypto.deriveBits(
{
name: "HKDF",
salt: zerosalt,
salt: zeroSalt,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
info: (new TextEncoder().encode(name)),
hash: "SHA-256",
},
@@ -241,11 +253,24 @@ async function deriveKeysBrowser(key, name) {
return await Promise.all([aesProm, hmacProm]);
}
export function encryptAES(...args) {
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr);
}
export function decryptAES(...args) {
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
}
// string of zeroes, for calculating the key check
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/** Calculate the MAC for checking the key.
*
* @param {Uint8Array} key the key to use
* @param {string} [iv] The initialization vector as a base64-encoded string.
* If omitted, a random initialization vector will be created.
* @return {Promise<object>} An object that contains, `mac` and `iv` properties.
*/
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
return encryptAES(ZERO_STR, key, "", iv);
}
@@ -1,5 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,13 +20,23 @@ limitations under the License.
* @module
*/
import { MatrixClient } from "../../client";
import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../..";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
export const ENCRYPTION_CLASSES = {};
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {};
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
/**
* map of registered encryption algorithm classes. Map from string to {@link
@@ -34,7 +44,17 @@ export const ENCRYPTION_CLASSES = {};
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
export const DECRYPTION_CLASSES = {};
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
export interface IParams {
userId: string;
deviceId: string;
crypto: Crypto;
olmDevice: OlmDevice;
baseApis: MatrixClient;
roomId: string;
config: IRoomEncryption & object;
}
/**
* base type for encryption implementations
@@ -46,18 +66,25 @@ export const DECRYPTION_CLASSES = {};
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {MatrixClient} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
export class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
export abstract class EncryptionAlgorithm {
protected readonly userId: string;
protected readonly deviceId: string;
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId: string;
constructor(params: IParams) {
this.userId = params.userId;
this.deviceId = params.deviceId;
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
@@ -66,21 +93,22 @@ export class EncryptionAlgorithm {
*
* @param {module:models/room} room the room the event is in
*/
prepareToEncrypt(room) {
}
public prepareToEncrypt(room: Room): void {}
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
* @public
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
* @param {object} content event content
*
* @return {Promise} Promise which resolves to the new event body
*/
public abstract encryptMessage(room: Room, eventType: string, content: object): Promise<object>;
/**
* Called when the membership of a member of the room changes.
@@ -89,9 +117,18 @@ export class EncryptionAlgorithm {
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
* @public
* @abstract
*/
onRoomMembership(event, member, oldMembership) {
}
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
public reshareKeyWithDevice?(
senderKey: string,
sessionId: string,
userId: string,
device: DeviceInfo,
): Promise<void>;
public forceDiscardSession?(): void;
}
/**
@@ -102,17 +139,23 @@ export class EncryptionAlgorithm {
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {MatrixClient} baseApis base matrix api interface
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
export class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
export abstract class DecryptionAlgorithm {
protected readonly userId: string;
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId: string;
constructor(params: DecryptionClassParams) {
this.userId = params.userId;
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
@@ -127,6 +170,7 @@ export class DecryptionAlgorithm {
* resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
*/
public abstract decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
/**
* Handle a key event
@@ -135,7 +179,7 @@ export class DecryptionAlgorithm {
*
* @param {module:models/event.MatrixEvent} params event key event
*/
onRoomKeyEvent(params) {
public onRoomKeyEvent(params: MatrixEvent): void {
// ignore by default
}
@@ -143,8 +187,9 @@ export class DecryptionAlgorithm {
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
* @param {object} opts object
*/
importRoomKey(session) {
public async importRoomKey(session: IMegolmSessionData, opts: object): Promise<void> {
// ignore by default
}
@@ -155,7 +200,7 @@ export class DecryptionAlgorithm {
* @return {Promise<boolean>} true if we have the keys and could (theoretically) share
* them; else false.
*/
hasKeysForKeyRequest(keyRequest) {
public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
return Promise.resolve(false);
}
@@ -164,7 +209,7 @@ export class DecryptionAlgorithm {
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
*/
shareKeysWithDevice(keyRequest) {
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
@@ -174,9 +219,13 @@ export class DecryptionAlgorithm {
*
* @param {string} senderKey the sender's key
*/
async retryDecryptionFromSender(senderKey) {
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
// ignore by default
return false;
}
public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
public sendSharedHistoryInboundSessions?(devicesByUser: Record<string, DeviceInfo[]>): Promise<void>;
}
/**
@@ -191,22 +240,21 @@ export class DecryptionAlgorithm {
* @extends Error
*/
export class DecryptionError extends Error {
constructor(code, msg, details) {
public readonly detailedString: string;
constructor(public readonly code: string, msg: string, details?: Record<string, string>) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
this.detailedString = _detailedStringForDecryptionError(this, details);
this.detailedString = detailedStringForDecryptionError(this, details);
}
}
function _detailedStringForDecryptionError(err, details) {
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string {
let result = err.name + '[msg: ' + err.message;
if (details) {
result += ', ' +
Object.keys(details).map(
(k) => k + ': ' + details[k],
).join(', ');
result += ', ' + Object.keys(details).map((k) => k + ': ' + details[k]).join(', ');
}
result += ']';
@@ -224,7 +272,7 @@ function _detailedStringForDecryptionError(err, details) {
* @extends Error
*/
export class UnknownDeviceError extends Error {
constructor(msg, devices) {
constructor(msg: string, public readonly devices: Record<string, Record<string, object>>) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
@@ -244,7 +292,11 @@ export class UnknownDeviceError extends Error {
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
export function registerAlgorithm(algorithm, encryptor, decryptor) {
export function registerAlgorithm(
algorithm: string,
encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
): void {
ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor;
}
@@ -1,6 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-361
View File
@@ -1,361 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
import { logger } from '../../logger';
import * as utils from "../../utils";
import { polyfillSuper } from "../../utils";
import * as olmlib from "../olmlib";
import { DeviceInfo } from "../deviceinfo";
import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
registerAlgorithm,
} from "./base";
const DeviceVerification = DeviceInfo.DeviceVerification;
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/EncryptionAlgorithm}
*/
function OlmEncryption(params) {
polyfillSuper(this, EncryptionAlgorithm, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, EncryptionAlgorithm);
/**
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._sessionPrepared) {
// prep already done
return Promise.resolve();
}
const self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
}).finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
const members = await room.getEncryptionTargetMembers();
const users = members.map(function(u) {
return u.userId;
});
const self = this;
await this._ensureSession(users);
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const promises = [];
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
}
}
return await Promise.all(promises).then(() => encryptedContent);
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/DecryptionAlgorithm}
*/
function OlmDecryption(params) {
polyfillSuper(this, DecryptionAlgorithm, params);
}
utils.inherits(OlmDecryption, DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
OlmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let payloadString;
try {
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
},
);
}
const payload = JSON.parse(payloadString);
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
throw new DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this._olmDevice.deviceEd25519Key,
},
);
}
// check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
},
);
}
const claimedKeys = payload.keys || {};
return {
clearEvent: payload,
senderCurve25519Key: deviceKey,
claimedEd25519Key: claimedKeys.ed25519 || null,
};
};
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = async function(
theirDeviceIdentityKey, message,
) {
// This is a wrapper that serialises decryptions of prekey messages, because
// otherwise we race between deciding we have no active sessions for the message
// and creating a new one, which we can only do once because it removes the OTK.
if (message.type !== 0) {
// not a prekey message: we can safely just try & decrypt it
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
} else {
const myPromise = this._olmDevice._olmPrekeyPromise.then(() => {
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
});
// we want the error, but don't propagate it to the next decryption
this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
return await myPromise;
}
};
OlmDecryption.prototype._reallyDecryptMessage = async function(
theirDeviceIdentityKey, message,
) {
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
theirDeviceIdentityKey,
);
// try each session in turn.
const decryptionErrors = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
logger.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
return payload;
} catch (e) {
const foundSession = await this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
if (foundSession) {
// decryption failed, but it was a prekey message matching this
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message,
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
}
}
if (message.type !== 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.length === 0) {
throw new Error("No existing sessions");
}
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors),
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
let res;
try {
res = await this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors),
);
}
logger.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
return res.payload;
};
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+355
View File
@@ -0,0 +1,355 @@
/*
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
import { logger } from '../../logger';
import * as olmlib from "../olmlib";
import { DeviceInfo } from "../deviceinfo";
import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
registerAlgorithm,
} from "./base";
import { Room } from '../../models/room';
import { MatrixEvent } from "../..";
import { IEventDecryptionResult } from "../index";
const DeviceVerification = DeviceInfo.DeviceVerification;
interface IMessage {
type: number;
body: string;
}
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/EncryptionAlgorithm}
*/
class OlmEncryption extends EncryptionAlgorithm {
private sessionPrepared = false;
private prepPromise: Promise<void> = null;
/**
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {Promise} Promise which resolves when setup is complete
*/
private ensureSession(roomMembers: string[]): Promise<void> {
if (this.prepPromise) {
// prep already in progress
return this.prepPromise;
}
if (this.sessionPrepared) {
// prep already done
return Promise.resolve();
}
this.prepPromise = this.crypto.downloadKeys(roomMembers).then((res) => {
return this.crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(() => {
this.sessionPrepared = true;
}).finally(() => {
this.prepPromise = null;
});
return this.prepPromise;
}
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {Promise} Promise which resolves to the new event body
*/
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
const members = await room.getEncryptionTargetMembers();
const users = members.map(function(u) {
return u.userId;
});
await this.ensureSession(users);
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this.olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const promises = [];
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = this.crypto.getStoredDevicesForUser(userId);
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == this.olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId, this.deviceId, this.olmDevice,
userId, deviceInfo, payloadFields,
),
);
}
}
return await Promise.all(promises).then(() => encryptedContent);
}
}
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/DecryptionAlgorithm}
*/
class OlmDecryption extends DecryptionAlgorithm {
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) {
throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this.olmDevice.deviceCurve25519Key];
let payloadString;
try {
payloadString = await this.decryptMessage(deviceKey, message);
} catch (e) {
throw new DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
},
);
}
const payload = JSON.parse(payloadString);
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this.userId) {
throw new DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
throw new DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this.olmDevice.deviceEd25519Key,
},
);
}
// check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.getRoomId(),
},
);
}
const claimedKeys = payload.keys || {};
return {
clearEvent: payload,
senderCurve25519Key: deviceKey,
claimedEd25519Key: claimedKeys.ed25519 || null,
};
}
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
*/
private async decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
// This is a wrapper that serialises decryptions of prekey messages, because
// otherwise we race between deciding we have no active sessions for the message
// and creating a new one, which we can only do once because it removes the OTK.
if (message.type !== 0) {
// not a prekey message: we can safely just try & decrypt it
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
} else {
const myPromise = this.olmDevice.olmPrekeyPromise.then(() => {
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
});
// we want the error, but don't propagate it to the next decryption
this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {});
return await myPromise;
}
}
private async reallyDecryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
const decryptionErrors: Record<string, string> = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
const payload = await this.olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
logger.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
return payload;
} catch (e) {
const foundSession = await this.olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
if (foundSession) {
// decryption failed, but it was a prekey message matching this
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message,
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
}
}
if (message.type !== 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.length === 0) {
throw new Error("No existing sessions");
}
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors),
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
let res;
try {
res = await this.olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors),
);
}
logger.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
return res.payload;
}
}
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+142
View File
@@ -0,0 +1,142 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupInfo } from "./keybackup";
// TODO: Merge this with crypto.js once converted
export enum CrossSigningKey {
Master = "master",
SelfSigning = "self_signing",
UserSigning = "user_signing",
}
export interface IEncryptedEventInfo {
/**
* whether the event is encrypted (if not encrypted, some of the other properties may not be set)
*/
encrypted: boolean;
/**
* the sender's key
*/
senderKey: string;
/**
* the algorithm used to encrypt the event
*/
algorithm: string;
/**
* whether we can be sure that the owner of the senderKey sent the event
*/
authenticated: boolean;
/**
* the sender's device information, if available
*/
sender?: DeviceInfo;
/**
* if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
*/
mismatchedSender: boolean;
}
export interface IRecoveryKey {
keyInfo?: IAddSecretStorageKeyOpts;
privateKey: Uint8Array;
encodedPrivateKey?: string;
}
export interface ICreateSecretStorageOpts {
/**
* Function called to await a secret storage key creation flow.
* Returns:
* {Promise<Object>} Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
createSecretStorageKey?: () => Promise<IRecoveryKey>;
/**
* The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
*/
keyBackupInfo?: IKeyBackupInfo;
/**
* If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
*/
setupNewKeyBackup?: boolean;
/**
* Reset even if keys already exist.
*/
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
export interface ISecretStorageKeyInfo {
name: string;
algorithm: string;
// technically the below are specific to AES keys. If we ever introduce another type,
// we can split into separate interfaces.
iv: string;
mac: string;
passphrase: IPassphraseInfo;
}
export interface ISecretStorageKey {
keyId: string;
keyInfo: ISecretStorageKeyInfo;
}
export interface IPassphraseInfo {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
bits?: number;
}
export interface IAddSecretStorageKeyOpts {
pubkey: string;
passphrase?: IPassphraseInfo;
name?: string;
key: Uint8Array;
}
export interface IImportOpts {
stage: string; // TODO: Enum
successes: number;
failures: number;
total: number;
}
export interface IImportRoomKeysOpts {
progressCallback?: (stage: IImportOpts) => void;
untrusted?: boolean;
source?: string; // TODO: Enum
}
+815
View File
@@ -0,0 +1,815 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module crypto/backup
*
* Classes for dealing with key backup.
*/
import { MatrixClient } from "../client";
import { logger } from "../logger";
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase';
import { sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
type AuthData = IKeyBackupInfo["auth_data"];
type SigInfo = {
deviceId: string;
valid?: boolean | null; // true: valid, false: invalid, null: cannot attempt validation
device?: DeviceInfo | null;
crossSigningId?: boolean;
deviceTrust?: DeviceTrustLevel;
};
export type TrustInfo = {
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
sigs: SigInfo[];
// eslint-disable-next-line camelcase
trusted_locally?: boolean;
};
export interface IKeyBackupCheck {
backupInfo: IKeyBackupInfo;
trustInfo: TrustInfo;
}
/* eslint-disable camelcase */
export interface IPreparedKeyBackupVersion {
algorithm: string;
auth_data: AuthData;
recovery_key: string;
privateKey: Uint8Array;
}
/* eslint-enable camelcase */
/** A function used to get the secret key for a backup.
*/
type GetKey = () => Promise<ArrayLike<number>>;
interface BackupAlgorithmClass {
algorithmName: string;
// initialize from an existing backup
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
// prepare a brand new backup
prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]>;
checkBackupVersion(info: IKeyBackupInfo): void;
}
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
free(): void;
}
export interface IKeyBackup {
rooms: {
[roomId: string]: {
sessions: {
[sessionId: string]: IKeyBackupSession;
};
};
};
}
/**
* Manages the key backup.
*/
export class BackupManager {
private algorithm: BackupAlgorithm | undefined;
public backupInfo: IKeyBackupInfo | undefined; // The info dict from /room_keys/version
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
private sendingBackups: boolean; // Are we currently sending backups?
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
this.checkedForBackup = false;
this.sendingBackups = false;
}
public get version(): string | undefined {
return this.backupInfo && this.backupInfo.version;
}
/**
* Performs a quick check to ensure that the backup info looks sane.
*
* Throws an error if a problem is detected.
*
* @param {IKeyBackupInfo} info the key backup info
*/
public static checkBackupVersion(info: IKeyBackupInfo): void {
const Algorithm = algorithmsByName[info.algorithm];
if (!Algorithm) {
throw new Error("Unknown backup algorithm: " + info.algorithm);
}
if (!(typeof info.auth_data === "object")) {
throw new Error("Invalid backup data returned");
}
return Algorithm.checkBackupVersion(info);
}
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
const Algorithm = algorithmsByName[info.algorithm];
if (!Algorithm) {
throw new Error("Unknown backup algorithm");
}
return await Algorithm.init(info.auth_data, getKey);
}
public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
this.backupInfo = info;
if (this.algorithm) {
this.algorithm.free();
}
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
this.baseApis.emit('crypto.keyBackupStatus', true);
// There may be keys left over from a partially completed backup, so
// schedule a send to check.
this.scheduleKeyBackupSend();
}
/**
* Disable backing up of keys.
*/
public disableKeyBackup(): void {
if (this.algorithm) {
this.algorithm.free();
}
this.algorithm = undefined;
this.backupInfo = undefined;
this.baseApis.emit('crypto.keyBackupStatus', false);
}
public getKeyBackupEnabled(): boolean | null {
if (!this.checkedForBackup) {
return null;
}
return Boolean(this.algorithm);
}
public async prepareKeyBackupVersion(
key?: string | Uint8Array | null,
algorithm?: string | undefined,
// eslint-disable-next-line camelcase
): Promise<IPreparedKeyBackupVersion> {
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
if (!Algorithm) {
throw new Error("Unknown backup algorithm");
}
const [privateKey, authData] = await Algorithm.prepare(key);
const recoveryKey = encodeRecoveryKey(privateKey);
return {
algorithm: Algorithm.algorithmName,
auth_data: authData,
recovery_key: recoveryKey,
privateKey,
};
}
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<void> {
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
}
/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
* one of the user's verified devices, start backing up
* to it.
*/
public async checkAndStart(): Promise<IKeyBackupCheck> {
logger.log("Checking key backup status...");
if (this.baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest");
this.checkedForBackup = true;
return null;
}
let backupInfo: IKeyBackupInfo;
try {
backupInfo = await this.baseApis.getKeyBackupVersion();
} catch (e) {
logger.log("Error checking for active key backup", e);
if (e.httpStatus === 404) {
// 404 is returned when the key backup does not exist, so that
// counts as successfully checking.
this.checkedForBackup = true;
}
return null;
}
this.checkedForBackup = true;
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
await this.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup");
this.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
this.disableKeyBackup();
await this.enableKeyBackup(backupInfo);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
// because we don't remember what backup version we uploaded keys to:
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log("Backup version " + backupInfo.version + " still current");
}
}
return { backupInfo, trustInfo };
}
/**
* Forces a re-check of the key backup and enables/disables it
* as appropriate.
*
* @return {Object} Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*/
public async checkKeyBackup(): Promise<IKeyBackupCheck> {
this.checkedForBackup = false;
return this.checkAndStart();
}
/**
* Check if the given backup info is trusted.
*
* @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
* deviceId: [string],
* device: [DeviceInfo || null],
* ]
* }
*/
public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise<TrustInfo> {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
};
if (
!backupInfo ||
!backupInfo.algorithm ||
!backupInfo.auth_data ||
!backupInfo.auth_data.signatures
) {
logger.info("Key backup is absent or missing required data");
return ret;
}
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
}
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
if (keyIdParts[0] !== 'ed25519') {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue;
}
// Could be a cross-signing master key, but just say this is the device
// ID for backwards compat
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
// first check to see if it's from our cross-signing key
const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId();
if (crossSigningId === sigInfo.deviceId) {
sigInfo.crossSigningId = true;
try {
await verifySignature(
this.baseApis.crypto.olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
sigInfo.deviceId,
crossSigningId,
);
sigInfo.valid = true;
} catch (e) {
logger.warn(
"Bad signature from cross signing key " + crossSigningId, e,
);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
continue;
}
// Now look for a sig from a device
// At some point this can probably go away and we'll just support
// it being signed by the cross-signing master key
const device = this.baseApis.crypto.deviceList.getStoredDevice(
this.baseApis.getUserId(), sigInfo.deviceId,
);
if (device) {
sigInfo.device = device;
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
this.baseApis.getUserId(), sigInfo.deviceId,
);
try {
await verifySignature(
this.baseApis.crypto.olmDevice,
backupInfo.auth_data,
this.baseApis.getUserId(),
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
logger.info(
"Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
logger.info("Ignoring signature from unknown key " + keyId);
}
ret.sigs.push(sigInfo);
}
ret.usable = ret.sigs.some((s) => {
return (
s.valid && (
(s.device && s.deviceTrust.isVerified()) ||
(s.crossSigningId)
)
);
});
ret.usable = ret.usable || ret.trusted_locally;
return ret;
}
/**
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
*/
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
if (this.sendingBackups) return;
this.sendingBackups = true;
try {
// wait between 0 and `maxDelay` seconds, to avoid backup
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
await sleep(delay, undefined);
let numFailures = 0; // number of consecutive failures
for (;;) {
if (!this.algorithm) {
return;
}
try {
const numBackedUp =
await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
if (numBackedUp === 0) {
// no sessions left needing backup: we're done
return;
}
numFailures = 0;
} catch (err) {
numFailures++;
logger.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Re-check key backup status on error, so we can be
// sure to present the current situation when asked.
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
throw err;
}
}
}
if (numFailures) {
// exponential backoff if we have failures
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
}
}
} finally {
this.sendingBackups = false;
}
}
/**
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
*/
private async backupPendingKeys(limit: number): Promise<number> {
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} };
}
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
);
sessionData.algorithm = MEGOLM_ALGORITHM;
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
MEGOLM_ALGORITHM, session.senderKey,
);
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
rooms[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
session_data: await this.algorithm.encryptSession(sessionData),
};
}
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
return sessions.length;
}
public async backupGroupSession(
senderKey: string, sessionId: string,
): Promise<void> {
await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
if (this.backupInfo) {
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
}
// if this.backupInfo is not set, then the keys will be backed up when
// this.enableKeyBackup is called
}
/**
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*/
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
}
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
* (which will be equal to the number of sessions in the store).
*/
public async flagAllGroupSessionsForBackup(): Promise<number> {
await this.baseApis.crypto.cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP,
],
(txn) => {
this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) {
this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn);
}
});
},
);
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
return remaining;
}
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/
public countSessionsNeedingBackup(): Promise<number> {
return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
}
}
export class Curve25519 implements BackupAlgorithm {
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
constructor(
public authData: ICurve25519AuthData,
private publicKey: any, // FIXME: PkEncryption
private getKey: () => Promise<Uint8Array>,
) {}
public static async init(
authData: AuthData,
getKey: () => Promise<Uint8Array>,
): Promise<Curve25519> {
if (!authData || !("public_key" in authData)) {
throw new Error("auth_data missing required information");
}
const publicKey = new global.Olm.PkEncryption();
publicKey.set_recipient_key(authData.public_key);
return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
}
public static async prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
const decryption = new global.Olm.PkDecryption();
try {
const authData: Partial<ICurve25519AuthData> = {};
if (!key) {
authData.public_key = decryption.generate_key();
} else if (key instanceof Uint8Array) {
authData.public_key = decryption.init_with_private_key(key);
} else {
const derivation = await keyFromPassphrase(key);
authData.private_key_salt = derivation.salt;
authData.private_key_iterations = derivation.iterations;
authData.public_key = decryption.init_with_private_key(derivation.key);
}
const publicKey = new global.Olm.PkEncryption();
publicKey.set_recipient_key(authData.public_key);
return [
decryption.get_private_key(),
authData as AuthData,
];
} finally {
decryption.free();
}
}
public static checkBackupVersion(info: IKeyBackupInfo): void {
if (!("public_key" in info.auth_data)) {
throw new Error("Invalid backup data returned");
}
}
public get untrusted() { return true; }
public async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(
sessions: Record<string, IKeyBackupSession>,
): Promise<Record<string, any>[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
const backupPubKey = decryption.init_with_private_key(privKey);
if (backupPubKey !== this.authData.public_key) {
// eslint-disable-next-line no-throw-literal
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
}
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(decryption.decrypt(
sessionData.session_data.ephemeral,
sessionData.session_data.mac,
sessionData.session_data.ciphertext,
));
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
}
}
return keys;
} finally {
decryption.free();
}
}
public async keyMatches(key: Uint8Array): Promise<boolean> {
const decryption = new global.Olm.PkDecryption();
let pubKey;
try {
pubKey = decryption.init_with_private_key(key);
} finally {
decryption.free();
}
return pubKey === this.authData.public_key;
}
public free(): void {
this.publicKey.free();
}
}
function randomBytes(size: number): Uint8Array {
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
if (crypto) {
// nodejs version
return crypto.randomBytes(size);
}
if (window?.crypto) {
// browser version
const buf = new Uint8Array(size);
window.crypto.getRandomValues(buf);
return buf;
}
throw new Error("No usable crypto implementation");
}
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
export class Aes256 implements BackupAlgorithm {
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
constructor(
public readonly authData: IAes256AuthData,
private readonly key: Uint8Array,
) {}
public static async init(
authData: IAes256AuthData,
getKey: () => Promise<Uint8Array>,
): Promise<Aes256> {
if (!authData) {
throw new Error("auth_data missing");
}
const key = await getKey();
if (authData.mac) {
const { mac } = await calculateKeyCheck(key, authData.iv);
if (authData.mac.replace(/=+$/g, '') !== mac.replace(/=+/g, '')) {
throw new Error("Key does not match");
}
}
return new Aes256(authData, key);
}
public static async prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
let outKey: Uint8Array;
const authData: Partial<IAes256AuthData> = {};
if (!key) {
outKey = randomBytes(32);
} else if (key instanceof Uint8Array) {
outKey = new Uint8Array(key);
} else {
const derivation = await keyFromPassphrase(key);
authData.private_key_salt = derivation.salt;
authData.private_key_iterations = derivation.iterations;
outKey = derivation.key;
}
const { iv, mac } = await calculateKeyCheck(outKey);
authData.iv = iv;
authData.mac = mac;
return [outKey, authData as AuthData];
}
public static checkBackupVersion(info: IKeyBackupInfo): void {
if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
throw new Error("Invalid backup data returned");
}
}
public get untrusted() { return false; }
async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
}
}
return keys;
}
async keyMatches(key: Uint8Array): Promise<boolean> {
if (this.authData.mac) {
const { mac } = await calculateKeyCheck(key, this.authData.iv);
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
} else {
// if we have no information, we have to assume the key is right
return true;
}
}
public free(): void {
this.key.fill(0);
}
}
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
[Curve25519.algorithmName]: Curve25519,
[Aes256.algorithmName]: Aes256,
};
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
+41 -26
View File
@@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,16 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import anotherjson from "another-json";
import { decodeBase64, encodeBase64 } from './olmlib';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "./api";
import { Crypto } from "./index";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
interface DeviceKeys {
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase
algorithm: string;
account: string; // pickle
};
}
export interface IDehydratedDeviceKeyInfo {
passphrase?: string;
}
export interface IDeviceKeys {
algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
@@ -31,7 +46,7 @@ interface DeviceKeys {
signatures?: Signatures;
}
interface OneTimeKey {
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
@@ -47,20 +62,20 @@ export class DehydrationManager {
private key: Uint8Array;
private keyInfo: {[props: string]: any};
private deviceDisplayName: string;
constructor(private crypto) {
constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache();
}
async getDehydrationKeyFromCache(): Promise<void> {
return await this.crypto._cryptoStore.doTxn(
return await this.crypto.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.getSecretStorePrivateKey(
this.crypto.cryptoStore.getSecretStorePrivateKey(
txn,
async (result) => {
if (result) {
const { key, keyInfo, deviceDisplayName, time } = result;
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo;
@@ -101,11 +116,11 @@ export class DehydrationManager {
this.timeoutId = undefined;
}
// clear storage
await this.crypto._cryptoStore.doTxn(
await this.crypto.cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey(
this.crypto.cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration", null,
);
},
@@ -145,15 +160,15 @@ export class DehydrationManager {
this.timeoutId = undefined;
}
try {
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto._cryptoStore.doTxn(
await this.crypto.cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey(
this.crypto.cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration",
{
keyInfo: this.keyInfo,
@@ -192,7 +207,7 @@ export class DehydrationManager {
}
logger.log("Uploading account to server");
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
undefined,
"PUT",
"/dehydrated_device",
@@ -209,10 +224,10 @@ export class DehydrationManager {
// send the keys to the server
const deviceId = dehydrateResult.device_id;
logger.log("Preparing device keys", deviceId);
const deviceKeys: DeviceKeys = {
algorithms: this.crypto._supportedAlgorithms,
const deviceKeys: IDeviceKeys = {
algorithms: this.crypto.supportedAlgorithms,
device_id: deviceId,
user_id: this.crypto._userId,
user_id: this.crypto.userId,
keys: {
[`ed25519:${deviceId}`]: e2eKeys.ed25519,
[`curve25519:${deviceId}`]: e2eKeys.curve25519,
@@ -220,21 +235,21 @@ export class DehydrationManager {
};
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
deviceKeys.signatures = {
[this.crypto._userId]: {
[this.crypto.userId]: {
[`ed25519:${deviceId}`]: deviceSignature,
},
};
if (this.crypto._crossSigningInfo.getId("self_signing")) {
await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing");
if (this.crypto.crossSigningInfo.getId("self_signing")) {
await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
}
logger.log("Preparing one-time keys");
const oneTimeKeys = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: OneTimeKey = { key };
const k: IOneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto._userId]: {
[this.crypto.userId]: {
[`ed25519:${deviceId}`]: signature,
},
};
@@ -244,10 +259,10 @@ export class DehydrationManager {
logger.log("Preparing fallback keys");
const fallbackKeys = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: OneTimeKey = { key, fallback: true };
const k: IOneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto._userId]: {
[this.crypto.userId]: {
[`ed25519:${deviceId}`]: signature,
},
};
@@ -255,7 +270,7 @@ export class DehydrationManager {
}
logger.log("Uploading keys to server");
await this.crypto._baseApis._http.authedRequest(
await this.crypto.baseApis.http.authedRequest(
undefined,
"POST",
"/keys/upload/" + encodeURI(deviceId),
@@ -279,7 +294,7 @@ export class DehydrationManager {
}
}
private stop() {
public stop() {
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
-168
View File
@@ -1,168 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module crypto/deviceinfo
*/
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
export function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.known = false;
this.unsigned = {};
this.signatures = {};
}
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
signatures: this.signatures,
};
};
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
DeviceInfo.prototype.getIdentityKey = function() {
return this.keys["curve25519:" + this.deviceId];
};
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayName = function() {
return this.unsigned.device_display_name || null;
};
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
DeviceInfo.prototype.isBlocked = function() {
return this.verified == DeviceVerification.BLOCKED;
};
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
DeviceInfo.prototype.isVerified = function() {
return this.verified == DeviceVerification.VERIFIED;
};
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
*/
DeviceInfo.prototype.isUnverified = function() {
return this.verified == DeviceVerification.UNVERIFIED;
};
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
*/
DeviceInfo.prototype.isKnown = function() {
return this.known == true;
};
/**
* @enum
*/
DeviceInfo.DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
const DeviceVerification = DeviceInfo.DeviceVerification;
+177
View File
@@ -0,0 +1,177 @@
/*
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISignatures } from "../@types/signed";
/**
* @module crypto/deviceinfo
*/
export interface IDevice {
keys: Record<string, string>;
algorithms: string[];
verified: DeviceVerification;
known: boolean;
unsigned?: Record<string, any>;
signatures?: ISignatures;
}
enum DeviceVerification {
Blocked = -1,
Unverified = 0,
Verified = 1,
}
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
export class DeviceInfo {
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
public static fromStorage(obj: Partial<IDevice>, deviceId: string): DeviceInfo {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
}
/**
* @enum
*/
public static DeviceVerification = {
VERIFIED: DeviceVerification.Verified,
UNVERIFIED: DeviceVerification.Unverified,
BLOCKED: DeviceVerification.Blocked,
};
public algorithms: string[];
public keys: Record<string, string> = {};
public verified = DeviceVerification.Unverified;
public known = false;
public unsigned: Record<string, any> = {};
public signatures: ISignatures = {};
constructor(public readonly deviceId: string) {}
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
public toStorage(): IDevice {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
signatures: this.signatures,
};
}
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
public getFingerprint(): string {
return this.keys["ed25519:" + this.deviceId];
}
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
public getIdentityKey(): string {
return this.keys["curve25519:" + this.deviceId];
}
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
public getDisplayName(): string | null {
return this.unsigned.device_display_name || null;
}
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
public isBlocked(): boolean {
return this.verified == DeviceVerification.Blocked;
}
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
public isVerified(): boolean {
return this.verified == DeviceVerification.Verified;
}
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
*/
public isUnverified(): boolean {
return this.verified == DeviceVerification.Unverified;
}
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
*/
public isKnown(): boolean {
return this.known === true;
}
}
-4000
View File
File diff suppressed because it is too large Load Diff
+3807
View File
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +15,30 @@ limitations under the License.
*/
import { randomString } from '../randomstring';
import { getCrypto } from '../utils';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256;
export async function keyFromAuthData(authData, password) {
/* eslint-disable camelcase */
interface IAuthData {
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
/* eslint-enable camelcase */
interface IKey {
key: Uint8Array;
salt: string;
iterations: number;
}
export async function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!global.Olm) {
throw new Error("Olm is not available");
}
@@ -40,7 +57,7 @@ export async function keyFromAuthData(authData, password) {
);
}
export async function keyFromPassphrase(password) {
export async function keyFromPassphrase(password: string): Promise<IKey> {
if (!global.Olm) {
throw new Error("Olm is not available");
}
@@ -52,11 +69,26 @@ export async function keyFromPassphrase(password) {
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
export async function deriveKey(
password: string,
salt: string,
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
return subtleCrypto
? deriveKeyBrowser(password, salt, iterations, numBits)
: deriveKeyNode(password, salt, iterations, numBits);
}
async function deriveKeyBrowser(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
// TODO: Implement this for node
throw new Error("Password-based backup is not avaiable on this platform");
}
@@ -81,3 +113,17 @@ export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BI
return new Uint8Array(keybits);
}
async function deriveKeyNode(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
}
+71
View File
@@ -0,0 +1,71 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISigned } from "../@types/signed";
export interface IKeyBackupSession {
first_message_index: number; // eslint-disable-line camelcase
forwarded_count: number; // eslint-disable-line camelcase
is_verified: boolean; // eslint-disable-line camelcase
session_data: { // eslint-disable-line camelcase
ciphertext: string;
ephemeral: string;
mac: string;
iv: string;
};
}
export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
/* eslint-disable camelcase */
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
export interface IAes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
export interface IKeyBackupInfo {
algorithm: string;
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within
}
/* eslint-enable camelcase */
export interface IKeyBackupPrepareOpts {
secureSecretStorage: boolean;
}
export interface IKeyBackupRestoreResult {
total: number;
imported: number;
}
export interface IKeyBackupRestoreOpts {
cacheCompleteCallback?: () => void;
progressCallback?: (progress: { stage: string }) => void;
}
+87 -45
View File
@@ -1,7 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,24 +20,41 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
import { logger } from '../logger';
import * as utils from "../utils";
import anotherjson from "another-json";
import { Logger } from "loglevel";
import type { PkSigning } from "@matrix-org/olm";
import { OlmDevice } from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import { IOneTimeKey } from "./dehydration";
import { MatrixClient } from "../client";
enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2",
Megolm = "m.megolm.v1.aes-sha2",
MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
}
/**
* matrix algorithm tag for olm
*/
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
export const OLM_ALGORITHM = Algorithm.Olm;
/**
* matrix algorithm tag for megolm
*/
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
export const MEGOLM_ALGORITHM = Algorithm.Megolm;
/**
* matrix algorithm tag for megolm backups
*/
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
export interface IOlmSessionResult {
device: DeviceInfo;
sessionId?: string;
}
/**
* Encrypt an event payload for an Olm device
@@ -58,9 +73,13 @@ export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
* has been encrypted into `resultsObject`
*/
export async function encryptMessageForDevice(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
resultsObject: Record<string, string>,
ourUserId: string,
ourDeviceId: string,
olmDevice: OlmDevice,
recipientUserId: string,
recipientDevice: DeviceInfo,
payloadFields: Record<string, any>,
) {
const deviceKey = recipientDevice.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
@@ -77,6 +96,7 @@ export async function encryptMessageForDevice(
const payload = {
sender: ourUserId,
// TODO this appears to no longer be used whatsoever
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
@@ -105,7 +125,7 @@ export async function encryptMessageForDevice(
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
Object.assign(payload, payloadFields);
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
@@ -118,7 +138,7 @@ export async function encryptMessageForDevice(
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
@@ -129,7 +149,9 @@ export async function encryptMessageForDevice(
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
*/
export async function getExistingOlmSessions(
olmDevice, baseApis, devicesByUser,
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
) {
const devicesWithoutSession = {};
const sessions = {};
@@ -168,7 +190,7 @@ export async function getExistingOlmSessions(
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
* @param {MatrixClient} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
@@ -189,23 +211,30 @@ export async function getExistingOlmSessions(
* {@link module:crypto~OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
) {
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
force = false,
otkTimeout?: number,
failedServers?: string[],
log: Logger = logger,
): Promise<Record<string, Record<string, IOlmSessionResult>>> {
if (typeof force === "number") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
log = failedServers;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - backwards compatibility
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
if (!log) {
log = logger;
}
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
const resolveSession: Record<string, (sessionId?: string) => void> = {};
// Mark all sessions this task intends to update as in progress. It is
// important to do this for all devices this task cares about in a single
@@ -222,14 +251,14 @@ export async function ensureOlmSessionsForDevices(
continue;
}
if (!olmDevice._sessionsInProgress[key]) {
if (!olmDevice.sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
resolveSession[key] = (...args) => {
delete olmDevice._sessionsInProgress[key];
resolve(...args);
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
resolveSession[key] = (v: any) => {
delete olmDevice.sessionsInProgress[key];
resolve(v);
};
});
}
@@ -261,9 +290,7 @@ export async function ensureOlmSessionsForDevices(
}
const forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key], log,
);
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
if (sessionId !== null && resolveSession[key]) {
// we found a session, but we had marked the session as
// in-progress, so resolve it now, which will unmark it and
@@ -375,7 +402,12 @@ export async function ensureOlmSessionsForDevices(
return result;
}
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
async function _verifyKeyAndStartSession(
olmDevice: OlmDevice,
oneTimeKey: IOneTimeKey,
userId: string,
deviceInfo: DeviceInfo,
): Promise<string> {
const deviceId = deviceInfo.deviceId;
try {
await verifySignature(
@@ -407,6 +439,11 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
return sid;
}
export interface IObject {
unsigned?: object;
signatures?: object;
}
/**
* Verify the signature on an object
*
@@ -424,7 +461,11 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
* or rejects with an Error if it is bad.
*/
export async function verifySignature(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
olmDevice: OlmDevice,
obj: IOneTimeKey | IObject,
signingUserId: string,
signingDeviceId: string,
signingKey: string,
) {
const signKeyId = "ed25519:" + signingDeviceId;
const signatures = obj.signatures || {};
@@ -434,10 +475,11 @@ export async function verifySignature(
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
// prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson
const mangledObj = Object.assign({}, obj);
delete mangledObj.unsigned;
if ("unsigned" in mangledObj) {
delete mangledObj.unsigned;
}
delete mangledObj.signatures;
const json = anotherjson.stringify(mangledObj);
@@ -453,14 +495,14 @@ export async function verifySignature(
* @param {Olm.PkSigning|Uint8Array} key the signing object or the private key
* seed
* @param {string} userId The user ID who owns the signing key
* @param {string} pubkey The public key (ignored if key is a seed)
* @param {string} pubKey The public key (ignored if key is a seed)
* @returns {string} the signature for the object
*/
export function pkSign(obj, key, userId, pubkey) {
export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning();
pubkey = keyObj.init_with_seed(key);
pubKey = keyObj.init_with_seed(key);
key = keyObj;
createdKey = true;
}
@@ -472,7 +514,7 @@ export function pkSign(obj, key, userId, pubkey) {
const mysigs = sigs[userId] || {};
sigs[userId] = mysigs;
return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj));
return mysigs['ed25519:' + pubKey] = key.sign(anotherjson.stringify(obj));
} finally {
obj.signatures = sigs;
if (unsigned) obj.unsigned = unsigned;
@@ -485,11 +527,11 @@ export function pkSign(obj, key, userId, pubkey) {
/**
* Verify a signed JSON object
* @param {Object} obj Object to verify
* @param {string} pubkey The public key to use to verify
* @param {string} pubKey The public key to use to verify
* @param {string} userId The user ID who signed the object
*/
export function pkVerify(obj, pubkey, userId) {
const keyId = "ed25519:" + pubkey;
export function pkVerify(obj: IObject, pubKey: string, userId: string) {
const keyId = "ed25519:" + pubKey;
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
throw new Error("No signature");
}
@@ -500,7 +542,7 @@ export function pkVerify(obj, pubkey, userId) {
const unsigned = obj.unsigned;
if (obj.unsigned) delete obj.unsigned;
try {
util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature);
util.ed25519_verify(pubKey, anotherjson.stringify(obj), signature);
} finally {
obj.signatures = sigs;
if (unsigned) obj.unsigned = unsigned;
@@ -513,7 +555,7 @@ export function pkVerify(obj, pubkey, userId) {
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64.
*/
export function encodeBase64(uint8Array) {
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64");
}
@@ -522,7 +564,7 @@ export function encodeBase64(uint8Array) {
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array) {
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/=+$/g, '');
}
@@ -531,6 +573,6 @@ export function encodeUnpaddedBase64(uint8Array) {
* @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data.
*/
export function decodeBase64(base64) {
export function decodeBase64(base64: string): Uint8Array {
return Buffer.from(base64, "base64");
}
@@ -20,7 +20,7 @@ import bs58 from 'bs58';
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
export function encodeRecoveryKey(key: ArrayLike<number>): string {
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
@@ -35,8 +35,8 @@ export function encodeRecoveryKey(key) {
return base58key.match(/.{1,4}/g).join(" ");
}
export function decodeRecoveryKey(recoverykey) {
const result = bs58.decode(recoverykey.replace(/ /g, ''));
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
const result = bs58.decode(recoveryKey.replace(/ /g, ''));
let parity = 0;
for (const b of result) {
-34
View File
@@ -1,34 +0,0 @@
/**
* Internal module. Defintions for storage for the crypto module
*
* @module
*/
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
/**
* Represents an outgoing room key request
*
* @typedef {Object} OutgoingRoomKeyRequest
*
* @property {string} requestId unique id for this request. Used for both
* an id within the request for later pairing with a cancellation, and for
* the transaction id when sending the to_device messages to our local
* server.
*
* @property {string?} cancellationTxnId
* transaction id for the cancellation, if any
*
* @property {Array<{userId: string, deviceId: string}>} recipients
* list of recipients for the request
*
* @property {module:crypto~RoomKeyRequestBody} requestBody
* parameters for the request.
*
* @property {Number} state current state of this request (states are defined
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
*/
+205
View File
@@ -0,0 +1,205 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { TrackingStatus } from "../DeviceList";
import { IRoomEncryption } from "../RoomList";
import { IDevice } from "../deviceinfo";
import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Definitions for storage for the crypto module
*
* @module
*/
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
export interface CryptoStore {
startup(): Promise<CryptoStore>;
deleteAllData(): Promise<void>;
getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]>;
updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null>;
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string) => void);
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void;
getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void;
getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void;
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
// Inbound Group Sessions
getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void;
getAllEndToEndInboundGroupSessions(
txn: unknown,
func: (session: ISession | null) => void,
): void;
addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void;
storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void;
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void;
// Device Data
getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
countSessionsNeedingBackup(txn?: unknown): Promise<number>;
unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: unknown,
): Promise<[senderKey: string, sessionId: string][]>;
// Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
}
export type Mode = "readonly" | "readwrite";
export interface ISession {
senderKey: string;
sessionId: string;
sessionData?: InboundGroupSessionData;
}
export interface ISessionInfo {
deviceKey?: string;
sessionId?: string;
session?: string;
lastReceivedMessageTs?: number;
}
export interface IDeviceData {
devices: {
[ userId: string ]: {
[ deviceId: string ]: IDevice;
};
};
trackingStatus: {
[ userId: string ]: TrackingStatus;
};
crossSigningInfo?: Record<string, ICrossSigningInfo>;
syncToken?: string;
}
export interface IProblem {
type: string;
fixed: boolean;
time: number;
}
export interface IWithheld {
// eslint-disable-next-line camelcase
room_id: string;
code: string;
reason: string;
}
/**
* Represents an outgoing room key request
*
* @typedef {Object} OutgoingRoomKeyRequest
*
* @property {string} requestId unique id for this request. Used for both
* an id within the request for later pairing with a cancellation, and for
* the transaction id when sending the to_device messages to our local
* server.
*
* @property {string?} cancellationTxnId
* transaction id for the cancellation, if any
*
* @property {Array<{userId: string, deviceId: string}>} recipients
* list of recipients for the request
*
* @property {module:crypto~RoomKeyRequestBody} requestBody
* parameters for the request.
*
* @property {Number} state current state of this request (states are defined
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
*/
export interface OutgoingRoomKeyRequest {
requestId: string;
requestTxnId?: string;
cancellationTxnId?: string;
recipients: IRoomKeyRequestRecipient[];
requestBody: IRoomKeyRequestBody;
state: RoomKeyRequestState;
}
@@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,8 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../../logger';
import { logger, PrefixedLogger } from '../../logger';
import * as utils from "../../utils";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
export const VERSION = 10;
const PROFILE_TRANSACTIONS = false;
@@ -29,23 +43,31 @@ const PROFILE_TRANSACTIONS = false;
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class Backend {
export class Backend implements CryptoStore {
private nextTxnId = 0;
/**
* @param {IDBDatabase} db
*/
constructor(db) {
this._db = db;
this._nextTxnId = 0;
constructor(private db: IDBDatabase) {
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.onversionchange = () => {
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
db.close();
};
}
public async startup(): Promise<CryptoStore> {
// No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
// by passing us a ready IDBDatabase instance
return this;
}
public async deleteAllData(): Promise<void> {
throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
@@ -56,11 +78,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
const requestBody = request.requestBody;
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
txn.onerror = reject;
// first see if we already have an entry for this request.
@@ -99,9 +121,9 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
txn.onerror = reject;
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
@@ -122,7 +144,12 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found.
*/
_getOutgoingRoomKeyRequest(txn, requestBody, callback) {
// eslint-disable-next-line @typescript-eslint/naming-convention
private _getOutgoingRoomKeyRequest(
txn: IDBTransaction,
requestBody: IRoomKeyRequestBody,
callback: (req: OutgoingRoomKeyRequest | null) => void,
): void {
const store = txn.objectStore("outgoingRoomKeyRequests");
const idx = store.index("session");
@@ -131,8 +158,8 @@ export class Backend {
requestBody.session_id,
]);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
// no match found
callback(null);
@@ -162,7 +189,7 @@ export class Backend {
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
if (wantedStates.length === 0) {
return Promise.resolve(null);
}
@@ -195,7 +222,7 @@ export class Backend {
cursorReq.onsuccess = onsuccess;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
@@ -210,19 +237,23 @@ export class Backend {
* @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const index = store.index("state");
const request = index.getAll(wantedState);
request.onsuccess = (ev) => resolve(ev.target.result);
request.onerror = (ev) => reject(ev.target.error);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
let stateIndex = 0;
const results = [];
@@ -248,7 +279,7 @@ export class Backend {
}
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
@@ -270,7 +301,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result = null;
function onsuccess(ev) {
@@ -291,9 +326,8 @@ export class Backend {
result = data;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
@@ -307,12 +341,14 @@ export class Backend {
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
public deleteOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
return;
}
@@ -326,12 +362,12 @@ export class Backend {
}
cursor.delete();
};
return promiseifyTxn(txn);
return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
}
// Olm Account
getAccount(txn, func) {
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -343,12 +379,12 @@ export class Backend {
};
}
storeAccount(txn, newData) {
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
const objectStore = txn.objectStore("account");
objectStore.put(newData, "-");
objectStore.put(accountPickle, "-");
}
getCrossSigningKeys(txn, func) {
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() {
@@ -360,7 +396,11 @@ export class Backend {
};
}
getSecretStorePrivateKey(txn, func, type) {
public getSecretStorePrivateKey(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
@@ -372,19 +412,19 @@ export class Backend {
};
}
storeCrossSigningKeys(txn, keys) {
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
storeSecretStorePrivateKey(txn, type, key) {
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
countEndToEndSessions(txn, func) {
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
@@ -396,7 +436,11 @@ export class Backend {
};
}
getEndToEndSessions(deviceKey, txn, func) {
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
@@ -419,7 +463,12 @@ export class Backend {
};
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() {
@@ -438,7 +487,7 @@ export class Backend {
};
}
getAllEndToEndSessions(txn, func) {
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -456,7 +505,12 @@ export class Backend {
};
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
@@ -466,8 +520,8 @@ export class Backend {
});
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const txn = this._db.transaction("session_problems", "readwrite");
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
objectStore.put({
deviceKey,
@@ -478,13 +532,13 @@ export class Backend {
return promiseifyTxn(txn);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
let result;
const txn = this._db.transaction("session_problems", "readwrite");
const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = (event) => {
req.onsuccess = () => {
const problems = req.result;
if (!problems.length) {
result = null;
@@ -511,14 +565,14 @@ export class Backend {
}
// FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) {
const txn = this._db.transaction("notified_error_devices", "readwrite");
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const txn = this.db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices");
const ret = [];
const ret: IOlmDevice[] = [];
await Promise.all(devices.map((device) => {
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const { userId, deviceInfo } = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
@@ -536,9 +590,14 @@ export class Backend {
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
let session: InboundGroupSessionData | boolean = false;
let withheld: IWithheld | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
@@ -549,7 +608,7 @@ export class Backend {
session = null;
}
if (withheld !== false) {
func(session, withheld);
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -566,7 +625,7 @@ export class Backend {
withheld = null;
}
if (session !== false) {
func(session, withheld);
func(session as InboundGroupSessionData, withheld as IWithheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -574,7 +633,7 @@ export class Backend {
};
}
getAllEndToEndInboundGroupSessions(txn, func) {
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
@@ -600,7 +659,12 @@ export class Backend {
};
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
@@ -623,23 +687,31 @@ export class Backend {
};
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -651,24 +723,24 @@ export class Backend {
};
}
storeEndToEndDeviceData(deviceData, txn) {
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-");
}
storeEndToEndRoom(roomId, roomInfo, txn) {
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId);
}
getEndToEndRooms(txn, func) {
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key] = cursor.value;
rooms[cursor.key as string] = cursor.value;
cursor.continue();
} else {
try {
@@ -682,11 +754,11 @@ export class Backend {
// session backups
getSessionsNeedingBackup(limit) {
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(
const txn = this.db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
"readonly",
);
@@ -716,9 +788,9 @@ export class Backend {
});
}
countSessionsNeedingBackup(txn) {
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly");
txn = this.db.transaction("sessions_needing_backup", "readonly");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => {
@@ -728,12 +800,12 @@ export class Backend {
});
}
unmarkSessionsNeedingBackup(sessions, txn) {
public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
txn = this.db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
@@ -742,12 +814,12 @@ export class Backend {
}));
}
markSessionsNeedingBackup(sessions, txn) {
public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
txn = this.db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
@@ -759,9 +831,14 @@ export class Backend {
}));
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
public addSharedHistoryInboundGroupSession(
roomId: string,
senderKey: string,
sessionId: string,
txn?: IDBTransaction,
): void {
if (!txn) {
txn = this._db.transaction(
txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readwrite",
);
}
@@ -774,9 +851,12 @@ export class Backend {
};
}
getSharedHistoryInboundGroupSessions(roomId, txn) {
public getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
if (!txn) {
txn = this._db.transaction(
txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readonly",
);
}
@@ -791,16 +871,21 @@ export class Backend {
});
}
doTxn(mode, stores, func, log = logger) {
public doTxn<T>(
mode: Mode,
stores: Iterable<string>,
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
let startTime;
let description;
if (PROFILE_TRANSACTIONS) {
const txnId = this._nextTxnId++;
const txnId = this.nextTxnId++;
startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`);
}
const txn = this._db.transaction(stores, mode);
const txn = this.db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
if (PROFILE_TRANSACTIONS) {
@@ -818,7 +903,7 @@ export class Backend {
}
}
export function upgradeDatabase(db, oldVersion) {
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
@@ -874,7 +959,7 @@ export function upgradeDatabase(db, oldVersion) {
// Expand as needed.
}
function createDatabase(db) {
function createDatabase(db: IDBDatabase): void {
const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
@@ -887,15 +972,19 @@ function createDatabase(db) {
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
interface IWrappedIDBTransaction extends IDBTransaction {
_mx_abortexception: Error; // eslint-disable-line camelcase
}
/*
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn, e) {
function abortWithException(txn: IDBTransaction, e: Error) {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
txn._mx_abortexception = e;
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
try {
txn.abort();
} catch (e) {
@@ -904,28 +993,28 @@ function abortWithException(txn, e) {
}
}
function promiseifyTxn(txn) {
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T> {
return new Promise((resolve, reject) => {
txn.oncomplete = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
}
resolve();
resolve(null);
};
txn.onerror = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
reject(txn.error);
}
};
txn.onabort = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
reject(txn.error);
}
};
});
@@ -1,7 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../../logger';
import { logger, PrefixedLogger } from '../../logger';
import { LocalStorageCryptoStore } from './localStorage-crypto-store';
import { MemoryCryptoStore } from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import { InvalidCryptoStoreError } from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. indexeddb storage for e2e.
@@ -35,23 +49,30 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class IndexedDBCryptoStore {
export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_ACCOUNT = 'account';
public static STORE_SESSIONS = 'sessions';
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
public static STORE_DEVICE_DATA = 'device_data';
public static STORE_ROOMS = 'rooms';
public static STORE_BACKUP = 'sessions_needing_backup';
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
private backendPromise: Promise<CryptoStore> = null;
private backend: CryptoStore = null;
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(indexedDB, dbName) {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
/**
* Ensure the database exists and is up-to-date, or fall back to
@@ -62,25 +83,23 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
startup() {
if (this._backendPromise) {
return this._backendPromise;
public startup(): Promise<CryptoStore> {
if (this.backendPromise) {
return this.backendPromise;
}
this._backendPromise = new Promise((resolve, reject) => {
if (!this._indexedDB) {
this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
logger.log(`connecting to indexeddb ${this._dbName}`);
logger.log(`connecting to indexeddb ${this.dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
);
const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const db = req.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
@@ -93,13 +112,13 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error);
reject(req.error);
};
req.onsuccess = (r) => {
const db = r.target.result;
req.onsuccess = () => {
const db = req.result;
logger.log(`connected to indexeddb ${this._dbName}`);
logger.log(`connected to indexeddb ${this.dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).then((backend) => {
@@ -114,9 +133,7 @@ export class IndexedDBCryptoStore {
],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
}).then(() => backend,
);
}).catch((e) => {
if (e.name === 'VersionError') {
@@ -126,7 +143,7 @@ export class IndexedDBCryptoStore {
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
}
logger.warn(
`unable to connect to indexeddb ${this._dbName}` +
`unable to connect to indexeddb ${this.dbName}` +
`: falling back to localStorage store: ${e}`,
);
@@ -139,10 +156,11 @@ export class IndexedDBCryptoStore {
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
this.backend = backend;
return backend as CryptoStore;
});
return this._backendPromise;
return this.backendPromise;
}
/**
@@ -150,15 +168,15 @@ export class IndexedDBCryptoStore {
*
* @returns {Promise} resolves when the store has been cleared.
*/
deleteAllData() {
return new Promise((resolve, reject) => {
if (!this._indexedDB) {
public deleteAllData(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => {
logger.log(
@@ -168,11 +186,11 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error);
reject(req.error);
};
req.onsuccess = () => {
logger.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
}).catch((e) => {
@@ -193,8 +211,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
return this.backend.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -207,8 +225,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._backend.getOutgoingRoomKeyRequest(requestBody);
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -221,8 +239,8 @@ export class IndexedDBCryptoStore {
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
@@ -232,8 +250,8 @@ export class IndexedDBCryptoStore {
* @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
}
/**
@@ -246,8 +264,12 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._backend.getOutgoingRoomKeyRequestsByTarget(
public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
@@ -264,8 +286,12 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._backend.updateOutgoingRoomKeyRequest(
public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
@@ -279,8 +305,11 @@ export class IndexedDBCryptoStore {
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
public deleteOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -292,8 +321,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backend.getAccount(txn, func);
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) {
this.backend.getAccount(txn, func);
}
/**
@@ -301,10 +330,10 @@ export class IndexedDBCryptoStore {
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store.
* @param {string} accountPickle The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backend.storeAccount(txn, newData);
public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this.backend.storeAccount(txn, accountPickle);
}
/**
@@ -315,8 +344,8 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backend.getCrossSigningKeys(txn, func);
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
this.backend.getCrossSigningKeys(txn, func);
}
/**
@@ -324,8 +353,12 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
public getSecretStorePrivateKey(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -334,8 +367,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backend.storeCrossSigningKeys(txn, keys);
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this.backend.storeCrossSigningKeys(txn, keys);
}
/**
@@ -345,8 +378,8 @@ export class IndexedDBCryptoStore {
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
this.backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -356,8 +389,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backend.countEndToEndSessions(txn, func);
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this.backend.countEndToEndSessions(txn, func);
}
/**
@@ -372,8 +405,13 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -387,8 +425,12 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backend.getEndToEndSessions(deviceKey, txn, func);
public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -398,8 +440,8 @@ export class IndexedDBCryptoStore {
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backend.getAllEndToEndSessions(txn, func);
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
this.backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -409,22 +451,25 @@ export class IndexedDBCryptoStore {
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this.backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backend.filterOutNotifiedErrorDevices(devices);
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this.backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -438,10 +483,13 @@ export class IndexedDBCryptoStore {
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
}
/**
@@ -451,8 +499,11 @@ export class IndexedDBCryptoStore {
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
public getAllEndToEndInboundGroupSessions(
txn: IDBTransaction,
func: (session: ISession | null) => void,
): void {
this.backend.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -464,10 +515,13 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
/**
@@ -479,18 +533,22 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
}
// End-to-end device tracking
@@ -505,8 +563,8 @@ export class IndexedDBCryptoStore {
* @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backend.storeEndToEndDeviceData(deviceData, txn);
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this.backend.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -516,8 +574,8 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backend.getEndToEndDeviceData(txn, func);
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
this.backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -528,8 +586,8 @@ export class IndexedDBCryptoStore {
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this.backend.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -537,20 +595,20 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backend.getEndToEndRooms(txn, func);
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this.backend.getEndToEndRooms(txn, func);
}
// session backups
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* @param {number} limit The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._backend.getSessionsNeedingBackup(limit);
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this.backend.getSessionsNeedingBackup(limit);
}
/**
@@ -558,8 +616,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._backend.countSessionsNeedingBackup(txn);
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this.backend.countSessionsNeedingBackup(txn);
}
/**
@@ -568,8 +626,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -578,8 +636,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._backend.markSessionsNeedingBackup(sessions, txn);
public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this.backend.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -589,10 +647,13 @@ export class IndexedDBCryptoStore {
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional)
*/
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
this._backend.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
public addSharedHistoryInboundGroupSession(
roomId: string,
senderKey: string,
sessionId: string,
txn?: IDBTransaction,
): void {
this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
}
/**
@@ -601,8 +662,11 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
*/
getSharedHistoryInboundGroupSessions(roomId, txn) {
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
public getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
}
/**
@@ -627,18 +691,7 @@ export class IndexedDBCryptoStore {
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func, log) {
return this._backend.doTxn(mode, stores, func, log);
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise<T> {
return this.backend.doTxn(mode, stores, func, log);
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
@@ -1,6 +1,5 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,6 +16,12 @@ limitations under the License.
import { logger } from '../../logger';
import { MemoryCryptoStore } from './memory-crypto-store';
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -38,23 +43,23 @@ const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.w
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) {
function keyEndToEndSessions(deviceKey: string): string {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndSessionProblems(deviceKey) {
function keyEndToEndSessionProblems(deviceKey: string): string {
return E2E_PREFIX + "session.problems/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
function keyEndToEndRoomsPrefix(roomId: string): string {
return KEY_ROOMS_PREFIX + roomId;
}
@@ -62,24 +67,23 @@ function keyEndToEndRoomsPrefix(roomId) {
* @implements {module:crypto/store/base~CryptoStore}
*/
export class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
this.store = webStore;
}
static exists(webStore) {
const length = webStore.length;
public static exists(store: Storage): boolean {
const length = store.length;
for (let i = 0; i < length; i++) {
if (webStore.key(i).startsWith(E2E_PREFIX)) {
if (store.key(i).startsWith(E2E_PREFIX)) {
return true;
}
}
return false;
}
constructor(private readonly store: Storage) {
super();
}
// Olm Sessions
countEndToEndSessions(txn, func) {
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
let count = 0;
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
@@ -87,9 +91,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(count);
}
_getEndToEndSessions(deviceKey, txn, func) {
// eslint-disable-next-line @typescript-eslint/naming-convention
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {};
const fixedSessions: Record<string, ISessionInfo> = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
@@ -105,16 +110,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return fixedSessions;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {});
}
getEndToEndSessions(deviceKey, txn, func) {
public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this._getEndToEndSessions(deviceKey) || {});
}
getAllEndToEndSessions(txn, func) {
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
@@ -125,17 +139,15 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = sessionInfo;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
problems.push({ type, fixed, time: Date.now() });
problems.sort((a, b) => {
return a.time - b.time;
@@ -143,9 +155,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
if (!problems.length) {
return null;
}
@@ -162,9 +174,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices =
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
for (const device of devices) {
@@ -187,7 +198,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
func(
getJsonItem(
this.store,
@@ -200,7 +216,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
@@ -219,7 +235,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const existing = getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -231,7 +252,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
setJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -239,9 +265,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key: string,
sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void {
setJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
@@ -249,25 +278,19 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
));
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
func(getJsonItem(this.store, KEY_DEVICE_DATA));
}
storeEndToEndDeviceData(deviceData, txn) {
setJsonItem(
this.store, KEY_DEVICE_DATA, deviceData,
);
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
}
storeEndToEndRoom(roomId, roomInfo, txn) {
setJsonItem(
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
}
getEndToEndRooms(txn, func) {
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result = {};
const prefix = keyEndToEndRoomsPrefix('');
@@ -281,9 +304,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(result);
}
getSessionsNeedingBackup(limit) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
for (const session in sessionsNeedingBackup) {
@@ -309,13 +331,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
public countSessionsNeedingBackup(): Promise<number> {
const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
@@ -327,7 +348,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
@@ -344,52 +365,46 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
public deleteAllData(): Promise<void> {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve();
}
// Olm account
getAccount(txn, func) {
const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT);
func(account);
public getAccount(txn: unknown, func: (accountPickle: string) => void): void {
const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(accountPickle);
}
storeAccount(txn, newData) {
setJsonItem(
this.store, KEY_END_TO_END_ACCOUNT, newData,
);
public storeAccount(txn: unknown, accountPickle: string): void {
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
}
getCrossSigningKeys(txn, func) {
const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS);
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const key = getJsonItem<IEncryptedPayload>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
}
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, key,
);
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}
doTxn(mode, stores, func) {
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}
function getJsonItem(store, key) {
function getJsonItem<T>(store: Storage, key: string): T | null {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
@@ -401,6 +416,6 @@ function getJsonItem(store, key) {
return null;
}
function setJsonItem(store, key, val) {
function setJsonItem<T>(store: Storage, key: string, val: T): void {
store.setItem(key, JSON.stringify(val));
}

Some files were not shown because too many files have changed in this diff Show More