| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | __filename__ = "devices.py" | 
					
						
							|  |  |  | __author__ = "Bob Mottram" | 
					
						
							|  |  |  | __license__ = "AGPL3+" | 
					
						
							| 
									
										
										
										
											2021-01-26 10:07:42 +00:00
										 |  |  | __version__ = "1.2.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-25 16:17:53 +00:00
										 |  |  | def E2EEremoveDevice(base_dir: str, nickname: str, domain: str, | 
					
						
							| 
									
										
										
										
											2020-08-06 20:16:42 +00:00
										 |  |  |                      deviceId: str) -> bool: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     """Unregisters a device for e2ee
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2021-12-26 12:02:29 +00:00
										 |  |  |     personDir = acct_dir(base_dir, nickname, domain) | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     deviceFilename = personDir + '/devices/' + deviceId + '.json' | 
					
						
							|  |  |  |     if os.path.isfile(deviceFilename): | 
					
						
							| 
									
										
										
										
											2021-09-05 10:17:43 +00:00
										 |  |  |         try: | 
					
						
							|  |  |  |             os.remove(deviceFilename) | 
					
						
							| 
									
										
										
										
											2021-11-25 18:42:38 +00:00
										 |  |  |         except OSError: | 
					
						
							| 
									
										
										
										
											2021-10-29 18:48:15 +00:00
										 |  |  |             print('EX: E2EEremoveDevice unable to delete ' + deviceFilename) | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |         return True | 
					
						
							|  |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-06 20:56:14 +00:00
										 |  |  | def E2EEvalidDevice(deviceJson: {}) -> bool: | 
					
						
							|  |  |  |     """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-25 16:17:53 +00:00
										 |  |  | def E2EEaddDevice(base_dir: str, nickname: str, domain: str, | 
					
						
							| 
									
										
										
										
											2020-08-06 20:16:42 +00:00
										 |  |  |                   deviceId: str, name: str, claimUrl: str, | 
					
						
							|  |  |  |                   fingerprintPublicKey: str, | 
					
						
							|  |  |  |                   identityPublicKey: str, | 
					
						
							|  |  |  |                   fingerprintKeyType="Ed25519Key", | 
					
						
							|  |  |  |                   identityKeyType="Curve25519Key") -> bool: | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     """Registers a device for e2ee
 | 
					
						
							|  |  |  |     claimUrl could be something like: | 
					
						
							|  |  |  |         http://localhost:3000/users/admin/claim?id=11119 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if ' ' in deviceId or '/' in deviceId or \ | 
					
						
							|  |  |  |        '?' in deviceId or '#' in deviceId or \ | 
					
						
							|  |  |  |        '.' in deviceId: | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2021-12-26 12:02:29 +00:00
										 |  |  |     personDir = acct_dir(base_dir, nickname, domain) | 
					
						
							| 
									
										
										
										
											2020-08-05 14:06:04 +00:00
										 |  |  |     if not os.path.isdir(personDir): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     if not os.path.isdir(personDir + '/devices'): | 
					
						
							|  |  |  |         os.mkdir(personDir + '/devices') | 
					
						
							|  |  |  |     deviceDict = { | 
					
						
							|  |  |  |         "deviceId": deviceId, | 
					
						
							|  |  |  |         "type": "Device", | 
					
						
							|  |  |  |         "name": name, | 
					
						
							|  |  |  |         "claim": claimUrl, | 
					
						
							|  |  |  |         "fingerprintKey": { | 
					
						
							|  |  |  |             "type": fingerprintKeyType, | 
					
						
							|  |  |  |             "publicKeyBase64": fingerprintPublicKey | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         "identityKey": { | 
					
						
							|  |  |  |             "type": identityKeyType, | 
					
						
							|  |  |  |             "publicKeyBase64": identityPublicKey | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     deviceFilename = personDir + '/devices/' + deviceId + '.json' | 
					
						
							| 
									
										
										
										
											2021-12-26 14:47:21 +00:00
										 |  |  |     return save_json(deviceDict, deviceFilename) | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-25 16:17:53 +00:00
										 |  |  | def E2EEdevicesCollection(base_dir: str, nickname: str, domain: str, | 
					
						
							| 
									
										
										
										
											2021-12-26 10:00:46 +00:00
										 |  |  |                           domain_full: str, http_prefix: str) -> {}: | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |     """Returns a list of registered devices
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2021-12-26 12:02:29 +00:00
										 |  |  |     personDir = acct_dir(base_dir, nickname, domain) | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |     if not os.path.isdir(personDir): | 
					
						
							|  |  |  |         return {} | 
					
						
							| 
									
										
										
										
											2021-12-26 10:19:59 +00:00
										 |  |  |     personId = local_actor_url(http_prefix, nickname, domain_full) | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |     if not os.path.isdir(personDir + '/devices'): | 
					
						
							|  |  |  |         os.mkdir(personDir + '/devices') | 
					
						
							|  |  |  |     deviceList = [] | 
					
						
							|  |  |  |     for subdir, dirs, files in os.walk(personDir + '/devices/'): | 
					
						
							|  |  |  |         for dev in files: | 
					
						
							|  |  |  |             if not dev.endswith('.json'): | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             deviceFilename = os.path.join(personDir + '/devices', dev) | 
					
						
							| 
									
										
										
										
											2021-12-26 15:13:34 +00:00
										 |  |  |             devJson = load_json(deviceFilename) | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  |             if devJson: | 
					
						
							|  |  |  |                 deviceList.append(devJson) | 
					
						
							| 
									
										
										
										
											2020-12-13 22:13:45 +00:00
										 |  |  |         break | 
					
						
							| 
									
										
										
										
											2020-08-05 12:05:39 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     devicesDict = { | 
					
						
							|  |  |  |         'id': personId + '/collections/devices', | 
					
						
							|  |  |  |         'type': 'Collection', | 
					
						
							|  |  |  |         'totalItems': len(deviceList), | 
					
						
							|  |  |  |         'items': deviceList | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return devicesDict | 
					
						
							| 
									
										
										
										
											2020-08-05 12:47:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-25 23:51:19 +00:00
										 |  |  | def E2EEdecryptMessageFromDevice(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 '' |