| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | __filename__ = "devices.py" | 
					
						
							|  |  |  | __author__ = "Bob Mottram" | 
					
						
							|  |  |  | __license__ = "AGPL3+" | 
					
						
							| 
									
										
										
										
											2023-01-21 23:03:30 +00:00
										 |  |  | __version__ = "1.4.0" | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | __maintainer__ = "Bob Mottram" | 
					
						
							| 
									
										
										
										
											2021-09-10 16:14:50 +00:00
										 |  |  | __email__ = "bob@libreserver.org" | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | __status__ = "Production" | 
					
						
							| 
									
										
										
										
											2021-06-26 11:16:41 +00:00
										 |  |  | __module_group__ = "Security" | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-06 16:33:19 +00:00
										 |  |  | # REST API overview | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # To support Olm, the following APIs are required: | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Uploading keys for a device (current app) | 
					
						
							|  |  |  | #    POST /api/v1/crypto/keys/upload | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Querying available devices of people you want to establish a session with | 
					
						
							|  |  |  | #    POST /api/v1/crypto/keys/query | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Claiming a pre-key (one-time-key) for each device you want to establish | 
					
						
							|  |  |  | #    a session with | 
					
						
							|  |  |  | #    POST /api/v1/crypto/keys/claim | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Sending encrypted messages directly to specific devices of other people | 
					
						
							|  |  |  | #    POST /api/v1/crypto/delivery | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Collect encrypted messages addressed to the current device | 
					
						
							|  |  |  | #    GET /api/v1/crypto/encrypted_messages | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #  * Clear all encrypted messages addressed to the current device | 
					
						
							|  |  |  | #    POST /api/v1/crypto/encrypted_messages/clear | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2021-12-26 15:13:34 +00:00
										 |  |  | from utils import load_json | 
					
						
							| 
									
										
										
										
											2021-12-26 14:47:21 +00:00
										 |  |  | from utils import save_json | 
					
						
							| 
									
										
										
										
											2021-12-26 12:02:29 +00:00
										 |  |  | from utils import acct_dir | 
					
						
							| 
									
										
										
										
											2021-12-26 10:19:59 +00:00
										 |  |  | from utils import local_actor_url | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  | def e2e_eremove_device(base_dir: str, nickname: str, domain: str, | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |                        device_id: str) -> bool: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     """Unregisters a device for e2ee
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     person_dir = acct_dir(base_dir, nickname, domain) | 
					
						
							|  |  |  |     device_filename = person_dir + '/devices/' + device_id + '.json' | 
					
						
							|  |  |  |     if os.path.isfile(device_filename): | 
					
						
							| 
									
										
										
										
											2021-09-05 10:17:43 +00:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |             os.remove(device_filename) | 
					
						
							| 
									
										
										
										
											2021-11-25 18:42:38 +00:00
										 |  |  |         except OSError: | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |             print('EX: e2e_eremove_device unable to delete ' + device_filename) | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  | def e2e_evalid_device(deviceJson: {}) -> bool: | 
					
						
							| 
									
										
										
										
											2020-08-06 20:56:14 +00:00
										 |  |  |     """Returns true if the given json contains valid device keys
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if not isinstance(deviceJson, dict): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson.get('deviceId'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['deviceId'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson.get('type'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['type'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-08-06 21:23:17 +00:00
										 |  |  |     if not deviceJson.get('name'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['name'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-08-06 20:56:14 +00:00
										 |  |  |     if deviceJson['type'] != 'Device': | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson.get('claim'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['claim'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson.get('fingerprintKey'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['fingerprintKey'], dict): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson['fingerprintKey'].get('type'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['fingerprintKey']['type'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson['fingerprintKey'].get('publicKeyBase64'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['fingerprintKey']['publicKeyBase64'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson.get('identityKey'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['identityKey'], dict): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson['identityKey'].get('type'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['identityKey']['type'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not deviceJson['identityKey'].get('publicKeyBase64'): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not isinstance(deviceJson['identityKey']['publicKeyBase64'], str): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  | def e2e_eadd_device(base_dir: str, nickname: str, domain: str, | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |                     device_id: str, name: str, claim_url: str, | 
					
						
							|  |  |  |                     fingerprint_public_key: str, | 
					
						
							|  |  |  |                     identity_public_key: str, | 
					
						
							|  |  |  |                     fingerprint_key_type="Ed25519Key", | 
					
						
							|  |  |  |                     identity_key_type="Curve25519Key") -> bool: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     """Registers a device for e2ee
 | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     claim_url could be something like: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         http://localhost:3000/users/admin/claim?id=11119 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     if ' ' in device_id or '/' in device_id or \ | 
					
						
							|  |  |  |        '?' in device_id or '#' in device_id or \ | 
					
						
							|  |  |  |        '.' in device_id: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     person_dir = acct_dir(base_dir, nickname, domain) | 
					
						
							|  |  |  |     if not os.path.isdir(person_dir): | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     if not os.path.isdir(person_dir + '/devices'): | 
					
						
							|  |  |  |         os.mkdir(person_dir + '/devices') | 
					
						
							|  |  |  |     device_dict = { | 
					
						
							|  |  |  |         "deviceId": device_id, | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         "type": "Device", | 
					
						
							|  |  |  |         "name": name, | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |         "claim": claim_url, | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         "fingerprintKey": { | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |             "type": fingerprint_key_type, | 
					
						
							|  |  |  |             "publicKeyBase64": fingerprint_public_key | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         }, | 
					
						
							|  |  |  |         "identityKey": { | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |             "type": identity_key_type, | 
					
						
							|  |  |  |             "publicKeyBase64": identity_public_key | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     device_filename = person_dir + '/devices/' + device_id + '.json' | 
					
						
							|  |  |  |     return save_json(device_dict, device_filename) | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  | def e2e_edevices_collection(base_dir: str, nickname: str, domain: str, | 
					
						
							|  |  |  |                             domain_full: str, http_prefix: str) -> {}: | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |     """Returns a list of registered devices
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     person_dir = acct_dir(base_dir, nickname, domain) | 
					
						
							|  |  |  |     if not os.path.isdir(person_dir): | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |         return {} | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     person_id = local_actor_url(http_prefix, nickname, domain_full) | 
					
						
							|  |  |  |     if not os.path.isdir(person_dir + '/devices'): | 
					
						
							|  |  |  |         os.mkdir(person_dir + '/devices') | 
					
						
							|  |  |  |     device_list = [] | 
					
						
							|  |  |  |     for _, _, files in os.walk(person_dir + '/devices/'): | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |         for dev in files: | 
					
						
							|  |  |  |             if not dev.endswith('.json'): | 
					
						
							|  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |             device_filename = os.path.join(person_dir + '/devices', dev) | 
					
						
							|  |  |  |             dev_json = load_json(device_filename) | 
					
						
							|  |  |  |             if dev_json: | 
					
						
							|  |  |  |                 device_list.append(dev_json) | 
					
						
							| 
									
										
										
										
											2020-12-13 22:13:45 +00:00
										 |  |  |         break | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     devices_dict = { | 
					
						
							|  |  |  |         'id': person_id + '/collections/devices', | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |         'type': 'Collection', | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |         'totalItems': len(device_list), | 
					
						
							|  |  |  |         'items': device_list | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-01-02 12:32:48 +00:00
										 |  |  |     return devices_dict | 
					
						
							| 
									
										
										
										
											2020-08-05 12:47:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 21:55:09 +00:00
										 |  |  | def e2e_edecrypt_message_from_device(message_json: {}) -> str: | 
					
						
							| 
									
										
										
										
											2020-08-05 12:47:15 +00:00
										 |  |  |     """Locally decrypts a message on the device.
 | 
					
						
							|  |  |  |     This should probably be a link to a local script | 
					
						
							|  |  |  |     or native app, such that what the user sees isn't | 
					
						
							|  |  |  |     something which the server could get access to. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     # TODO | 
					
						
							|  |  |  |     #  { | 
					
						
							|  |  |  |     #    "type": "EncryptedMessage", | 
					
						
							|  |  |  |     #    "messageType": 0, | 
					
						
							|  |  |  |     #    "cipherText": "...", | 
					
						
							|  |  |  |     #    "digest": { | 
					
						
							|  |  |  |     #      "type": "Digest", | 
					
						
							|  |  |  |     #      "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#hmac-sha256", | 
					
						
							|  |  |  |     #      "digestValue": "5f6ad31acd64995483d75c7..." | 
					
						
							|  |  |  |     #    }, | 
					
						
							|  |  |  |     #    "messageFranking": "...", | 
					
						
							|  |  |  |     #    "attributedTo": { | 
					
						
							|  |  |  |     #      "type": "Device", | 
					
						
							|  |  |  |     #      "deviceId": "11119" | 
					
						
							|  |  |  |     #    }, | 
					
						
							|  |  |  |     #    "to": { | 
					
						
							|  |  |  |     #      "type": "Device", | 
					
						
							|  |  |  |     #      "deviceId": "11876" | 
					
						
							|  |  |  |     #    } | 
					
						
							|  |  |  |     #  } | 
					
						
							|  |  |  |     return '' |