@@ -238,6 +238,11 @@ export class Appservice extends EventEmitter {
238
238
private eventProcessors : { [ eventType : string ] : IPreprocessor [ ] } = { } ;
239
239
private pendingTransactions = new Map < string , Promise < void > > ( ) ;
240
240
241
+ /**
242
+ * A cache of intents for the purposes of decrypting rooms
243
+ */
244
+ private cryptoClientForRoomId : LRU . LRUCache < string , MatrixClient > ;
245
+
241
246
/**
242
247
* Creates a new application service.
243
248
* @param {IAppserviceOptions } options The options for the application service.
@@ -256,6 +261,11 @@ export class Appservice extends EventEmitter {
256
261
ttl : options . intentOptions . maxAgeMs ,
257
262
} ) ;
258
263
264
+ this . cryptoClientForRoomId = new LRU . LRUCache ( {
265
+ max : options . intentOptions . maxCached ,
266
+ ttl : options . intentOptions . maxAgeMs ,
267
+ } ) ;
268
+
259
269
this . registration = options . registration ;
260
270
261
271
// If protocol is not defined, define an empty array.
@@ -658,6 +668,75 @@ export class Appservice extends EventEmitter {
658
668
return providedToken === this . registration . hs_token ;
659
669
}
660
670
671
+ private async decryptAppserviceEvent ( roomId : string , encrypted : EncryptedRoomEvent ) : ReturnType < Appservice [ "processEvent" ] > {
672
+ const existingClient = this . cryptoClientForRoomId . get ( roomId ) ;
673
+ const decryptFn = async ( client : MatrixClient ) => {
674
+ // Also fetches state in order to decrypt room. We should throw if the client is confused.
675
+ if ( ! await client . crypto . isRoomEncrypted ( roomId ) ) {
676
+ throw new Error ( "Client detected that the room is not encrypted." ) ;
677
+ }
678
+ let event = ( await client . crypto . decryptRoomEvent ( encrypted , roomId ) ) . raw ;
679
+ event = await this . processEvent ( event ) ;
680
+ this . cryptoClientForRoomId . set ( roomId , client ) ;
681
+ // For logging purposes: show that the event was decrypted
682
+ LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] } ` ) ;
683
+ return event ;
684
+ } ;
685
+ // 1. Try cached client
686
+ if ( existingClient ) {
687
+ try {
688
+ return await decryptFn ( existingClient ) ;
689
+ } catch ( error ) {
690
+ LogService . debug ( "Appservice" , `Failed to decrypt via cached client ${ await existingClient . getUserId ( ) } ` , error ) ;
691
+ LogService . warn ( "Appservice" , `Cached client was not able to decrypt ${ roomId } ${ encrypted . eventId } - trying other intents` ) ;
692
+ }
693
+ }
694
+ this . cryptoClientForRoomId . delete ( roomId ) ;
695
+ // 2. Try the bot client
696
+ if ( this . botClient . crypto ?. isReady ) {
697
+ try {
698
+ return await decryptFn ( this . botClient ) ;
699
+ } catch ( error ) {
700
+ LogService . debug ( "Appservice" , `Failed to decrypt via bot client` , error ) ;
701
+ LogService . warn ( "Appservice" , `Bot client was not able to decrypt ${ roomId } ${ encrypted . eventId } - trying other intents` ) ;
702
+ }
703
+ }
704
+
705
+ const userIdsInRoom = ( await this . botClient . getJoinedRoomMembers ( roomId ) ) . filter ( u => this . isNamespacedUser ( u ) ) ;
706
+ // 3. Try existing clients with crypto enabled.
707
+ for ( const intentCacheEntry of this . intentsCache . entries ( ) ) {
708
+ const [ userId , intent ] = intentCacheEntry as [ string , Intent ] ;
709
+ if ( ! userIdsInRoom . includes ( userId ) ) {
710
+ // Not in this room.
711
+ continue ;
712
+ }
713
+ // Is this client crypto enabled?
714
+ if ( ! intent . underlyingClient . crypto ?. isReady ) {
715
+ continue ;
716
+ }
717
+ try {
718
+ return await decryptFn ( intent . underlyingClient ) ;
719
+ } catch ( error ) {
720
+ LogService . debug ( "Appservice" , `Failed to decrypt via ${ userId } ` , error ) ;
721
+ LogService . warn ( "Appservice" , `Existing encrypted client was not able to decrypt ${ roomId } ${ encrypted . eventId } - trying other intents` ) ;
722
+ }
723
+ }
724
+
725
+ // 4. Try to enable crypto on any client to decrypt it.
726
+ // We deliberately do not enable crypto on every client for performance reasons.
727
+ const userInRoom = this . intentsCache . find ( ( intent , userId ) => ! intent . underlyingClient . crypto ?. isReady && userIdsInRoom . includes ( userId ) ) ;
728
+ if ( ! userInRoom ) {
729
+ throw Error ( 'No users in room, cannot decrypt' ) ;
730
+ }
731
+ try {
732
+ await userInRoom . enableEncryption ( ) ;
733
+ return await decryptFn ( userInRoom . underlyingClient ) ;
734
+ } catch ( error ) {
735
+ LogService . debug ( "Appservice" , `Failed to decrypt via random user ${ userInRoom . userId } ` , error ) ;
736
+ throw new Error ( "Unable to decrypt event" , { cause : error } ) ;
737
+ }
738
+ }
739
+
661
740
private async handleTransaction ( txnId : string , body : Record < string , unknown > ) {
662
741
// Process all the crypto stuff first to ensure that future transactions (if not this one)
663
742
// will decrypt successfully. We start with EDUs because we need structures to put counts
@@ -804,39 +883,11 @@ export class Appservice extends EventEmitter {
804
883
try {
805
884
const encrypted = new EncryptedRoomEvent ( event ) ;
806
885
const roomId = event [ 'room_id' ] ;
807
- try {
808
- event = ( await this . botClient . crypto . decryptRoomEvent ( encrypted , roomId ) ) . raw ;
809
- event = await this . processEvent ( event ) ;
810
- this . emit ( "room.decrypted_event" , roomId , event ) ;
811
-
812
- // For logging purposes: show that the event was decrypted
813
- LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] } ` ) ;
814
- } catch ( e1 ) {
815
- LogService . warn ( "Appservice" , `Bot client was not able to decrypt ${ roomId } ${ event [ 'event_id' ] } - trying other intents` ) ;
816
-
817
- let tryUserId : string ;
818
- try {
819
- // TODO: This could be more efficient
820
- const userIdsInRoom = await this . botClient . getJoinedRoomMembers ( roomId ) ;
821
- tryUserId = userIdsInRoom . find ( u => this . isNamespacedUser ( u ) ) ;
822
- } catch ( e ) {
823
- LogService . error ( "Appservice" , "Failed to get members of room - cannot decrypt message" ) ;
824
- }
825
-
826
- if ( tryUserId ) {
827
- const intent = this . getIntentForUserId ( tryUserId ) ;
828
-
829
- event = ( await intent . underlyingClient . crypto . decryptRoomEvent ( encrypted , roomId ) ) . raw ;
830
- event = await this . processEvent ( event ) ;
831
- this . emit ( "room.decrypted_event" , roomId , event ) ;
832
-
833
- // For logging purposes: show that the event was decrypted
834
- LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] } ` ) ;
835
- } else {
836
- // noinspection ExceptionCaughtLocallyJS
837
- throw e1 ;
838
- }
839
- }
886
+ event = await this . decryptAppserviceEvent ( roomId , encrypted ) ;
887
+ this . emit ( "room.decrypted_event" , roomId , event ) ;
888
+
889
+ // For logging purposes: show that the event was decrypted
890
+ LogService . info ( "Appservice" , `Processing decrypted event of type ${ event [ "type" ] } ` ) ;
840
891
} catch ( e ) {
841
892
LogService . error ( "Appservice" , `Decryption error on ${ event [ 'room_id' ] } ${ event [ 'event_id' ] } ` , e ) ;
842
893
this . emit ( "room.failed_decryption" , event [ 'room_id' ] , event , e ) ;
0 commit comments