2121from .message import EncryptedKeyMaterial , KeyExchange , Message , PlainKeyMaterial
2222from .session import Initiation , Session
2323from .storage import NothingException , Storage
24- from .types import AsyncFramework , DeviceInformation , OMEMOException , TrustLevel
24+ from .types import AsyncFramework , DeviceInformation , DeviceList , OMEMOException , SignedLabel , TrustLevel
2525
2626
2727__all__ = [
@@ -877,13 +877,13 @@ async def _delete_bundle(namespace: str, device_id: int) -> None:
877877
878878 @staticmethod
879879 @abstractmethod
880- async def _upload_device_list (namespace : str , device_list : Dict [ int , Optional [ str ]] ) -> None :
880+ async def _upload_device_list (namespace : str , device_list : DeviceList ) -> None :
881881 """
882882 Upload the device list for this XMPP account.
883883
884884 Args:
885885 namespace: The XML namespace to execute this operation under.
886- device_list: The device list to upload. Mapping from device id to optional label.
886+ device_list: The device list to upload. Mapping from device id to optional signed label.
887887
888888 Raises:
889889 UnknownNamespace: if the namespace is unknown.
@@ -900,7 +900,7 @@ async def _upload_device_list(namespace: str, device_list: Dict[int, Optional[st
900900
901901 @staticmethod
902902 @abstractmethod
903- async def _download_device_list (namespace : str , bare_jid : str ) -> Dict [ int , Optional [ str ]] :
903+ async def _download_device_list (namespace : str , bare_jid : str ) -> DeviceList :
904904 """
905905 Download the device list of a specific XMPP account.
906906
@@ -909,7 +909,7 @@ async def _download_device_list(namespace: str, bare_jid: str) -> Dict[int, Opti
909909 bare_jid: The bare JID of the XMPP account.
910910
911911 Returns:
912- The device list as a dictionary, mapping the device ids to their optional label.
912+ The device list as a dictionary, mapping the device ids to their optional signed label.
913913
914914 Raises:
915915 UnknownNamespace: if the namespace is unknown.
@@ -1010,20 +1010,15 @@ async def _send_message(message: Message, bare_jid: str) -> None:
10101010 # device list management #
10111011 ##########################
10121012
1013- async def update_device_list (
1014- self ,
1015- namespace : str ,
1016- bare_jid : str ,
1017- device_list : Dict [int , Optional [str ]]
1018- ) -> None :
1013+ async def update_device_list (self , namespace : str , bare_jid : str , device_list : DeviceList ) -> None :
10191014 """
10201015 Update the device list of a specific bare JID, e.g. after receiving an update for the XMPP account
10211016 from `PEP <https://xmpp.org/extensions/xep-0163.html>`__.
10221017
10231018 Args:
10241019 namespace: The XML namespace to execute this operation under.
10251020 bare_jid: The bare JID of the XMPP account.
1026- device_list: The updated device list. Mapping from device id to optional label.
1021+ device_list: The updated device list. Mapping from device id to optional signed label.
10271022
10281023 Raises:
10291024 UnknownNamespace: if the backend to handle the message is not currently loaded.
@@ -1041,8 +1036,9 @@ async def update_device_list(
10411036
10421037 storage = self .__storage
10431038
1044- # This isn't strictly necessary, but good for consistency
1045- if namespace not in frozenset (backend .namespace for backend in self .__backends ):
1039+ # Find the backend to handle this device list update
1040+ backend = next (filter (lambda backend : backend .namespace == namespace , self .__backends ), None )
1041+ if backend is None :
10461042 raise UnknownNamespace (f"The backend handling the namespace { namespace } is not currently loaded." )
10471043
10481044 # Copy to make sure the original is not modified
@@ -1066,16 +1062,25 @@ async def update_device_list(
10661062 )
10671063
10681064 # Add this device to the device list and publish it
1069- device_list [ self . __own_device_id ] = (await storage .load_optional (
1065+ own_label = (await storage .load_optional (
10701066 f"/devices/{ self .__own_bare_jid } /{ self .__own_device_id } /label" ,
10711067 str
10721068 )).from_just ()
1069+
1070+ device_list [self .__own_device_id ] = None if own_label is None else SignedLabel (
1071+ label = own_label ,
1072+ signature = await backend .sign_own_label (own_label )
1073+ )
1074+
10731075 await self ._upload_device_list (namespace , device_list )
10741076
10751077 # Add new device information entries for new devices
10761078 for device_id in new_devices :
10771079 await storage .store (f"/devices/{ bare_jid } /{ device_id } /active" , { namespace : True })
1078- await storage .store (f"/devices/{ bare_jid } /{ device_id } /label" , device_list [device_id ])
1080+ # Device label processing is deferred until after all basic information has been processed, since
1081+ # the identity keys and thus bundle data is required to verify label signatures. This extended
1082+ # information is better fetched in bulk as the final step.
1083+ await storage .store (f"/devices/{ bare_jid } /{ device_id } /label" , None )
10791084 await storage .store (f"/devices/{ bare_jid } /{ device_id } /namespaces" , [ namespace ])
10801085
10811086 # Update namespaces, label and status for previously known devices
@@ -1087,24 +1092,12 @@ async def update_device_list(
10871092
10881093 active = (await storage .load_dict (f"/devices/{ bare_jid } /{ device_id } /active" , bool )).from_just ()
10891094
1090- if device_id in device_list :
1095+ if device_id in new_device_list :
10911096 # Update the status if required
10921097 if namespace not in active or active [namespace ] is False :
10931098 active [namespace ] = True
10941099 await storage .store (f"/devices/{ bare_jid } /{ device_id } /active" , active )
10951100
1096- # Update the label if required. Even though loading the value first isn't strictly required,
1097- # it is done under the assumption that loading values is cheaper than writing.
1098- label = (await storage .load_optional (
1099- f"/devices/{ bare_jid } /{ device_id } /label" ,
1100- str
1101- )).from_just ()
1102-
1103- # Don't interpret ``None`` as "no label set" here. Instead, interpret ``None`` as "the backend
1104- # doesn't support labels".
1105- if device_list [device_id ] is not None and device_list [device_id ] != label :
1106- await storage .store (f"/devices/{ bare_jid } /{ device_id } /label" , device_list [device_id ])
1107-
11081101 # Add the namespace if required
11091102 if namespace not in namespaces :
11101103 namespaces .add (namespace )
@@ -1116,11 +1109,34 @@ async def update_device_list(
11161109 active [namespace ] = False
11171110 await storage .store (f"/devices/{ bare_jid } /{ device_id } /active" , active )
11181111
1119- # If there are unknown devices in the new device list, update the list of known devices. Do this as
1120- # the last step to ensure data consistency.
1112+ # If there are unknown devices in the new device list, update the list of known devices. Do this after
1113+ # processing all data except for device labels to ensure data consistency.
11211114 if len (new_devices ) > 0 :
11221115 await storage .store (f"/devices/{ bare_jid } /list" , list (new_device_list | old_device_list ))
11231116
1117+ # If the backend supports labels, process new and updated labels now that all basic information has
1118+ # been processed.
1119+ if backend .supports_labels :
1120+ for device_information in await self .get_device_information (bare_jid ):
1121+ device_id = device_information .device_id
1122+ if device_id in new_device_list :
1123+ signed_label = device_list [device_id ]
1124+ new_label = None if signed_label is None else signed_label .label
1125+ if new_label != device_information .label :
1126+ signature_valid = signed_label is None or await backend .verify_label_signature (
1127+ signed_label .label ,
1128+ signed_label .signature ,
1129+ device_information .identity_key
1130+ )
1131+
1132+ if signature_valid :
1133+ await storage .store (f"/devices/{ bare_jid } /{ device_id } /label" , new_label )
1134+ else :
1135+ logging .getLogger (SessionManager .LOG_TAG ).warning (
1136+ f"In device list update for { bare_jid } under namespace { namespace } : ignored"
1137+ f" device label for device { device_id } without valid signature: { new_label } "
1138+ )
1139+
11241140 logging .getLogger (SessionManager .LOG_TAG ).debug ("Device list update processed." )
11251141
11261142 async def refresh_device_list (self , namespace : str , bare_jid : str ) -> None :
@@ -1363,7 +1379,10 @@ async def set_own_label(self, own_label: Optional[str]) -> None:
13631379 # However, one PEP node fetch per backend isn't super expensive and it's nice to avoid the code to
13641380 # load the cached device list.
13651381 device_list = await self ._download_device_list (backend .namespace , self .__own_bare_jid )
1366- device_list [self .__own_device_id ] = own_label
1382+ device_list [self .__own_device_id ] = None if own_label is None else SignedLabel (
1383+ label = own_label ,
1384+ signature = await backend .sign_own_label (own_label )
1385+ )
13671386 await self ._upload_device_list (backend .namespace , device_list )
13681387
13691388 async def get_device_information (self , bare_jid : str ) -> FrozenSet [DeviceInformation ]:
0 commit comments