Line data Source code
1 : library msc_3814_dehydrated_devices; 2 : 3 : import 'dart:convert'; 4 : import 'dart:math'; 5 : 6 : import 'package:matrix/encryption.dart'; 7 : import 'package:matrix/matrix.dart'; 8 : import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/api.dart'; 9 : import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/model/dehydrated_device.dart'; 10 : import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/model/dehydrated_device_events.dart'; 11 : import 'package:matrix/src/utils/crypto/crypto.dart' as uc; 12 : 13 : extension DehydratedDeviceHandler on Client { 14 : static const Set<String> _oldDehydratedDeviceAlgorithms = { 15 : 'com.famedly.dehydrated_device.raw_olm_account', 16 : }; 17 : static const String _dehydratedDeviceAlgorithm = 18 : 'com.famedly.dehydrated_device.raw_olm_account.v2'; 19 : static const String _ssssSecretNameForDehydratedDevice = 'org.matrix.msc3814'; 20 : 21 : /// Restores the dehydrated device account and/or creates a new one, fetches the events and as such makes encrypted messages available while we were offline. 22 : /// Usually it only makes sense to call this when you just entered the SSSS passphrase or recovery key successfully. 23 1 : Future<void> dehydratedDeviceSetup(OpenSSSS secureStorage) async { 24 : try { 25 : // dehydrated devices need to be cross-signed 26 1 : if (!enableDehydratedDevices || 27 0 : !encryptionEnabled || 28 0 : this.encryption?.crossSigning.enabled != true) { 29 : return; 30 : } 31 : 32 : DehydratedDevice? device; 33 : try { 34 0 : device = await getDehydratedDevice(); 35 0 : } on MatrixException catch (e) { 36 0 : if (e.response?.statusCode == 400) { 37 0 : Logs().i('Dehydrated devices unsupported, skipping.'); 38 : return; 39 : } 40 : // No device, so we just create a new device. 41 0 : await _uploadNewDevice(secureStorage); 42 : return; 43 : } 44 : 45 : // Just throw away the old device if it is using an old algoritm. In the future we could try to still use it and then migrate it, but currently that is not worth the effort 46 : if (_oldDehydratedDeviceAlgorithms 47 0 : .contains(device.deviceData?.tryGet<String>('algorithm'))) { 48 0 : await _uploadNewDevice(secureStorage); 49 : return; 50 : } 51 : 52 : // Only handle devices we understand 53 : // In the future we might want to migrate to a newer format here 54 0 : if (device.deviceData?.tryGet<String>('algorithm') != 55 : _dehydratedDeviceAlgorithm) return; 56 : 57 : // Verify that the device is cross-signed 58 : final dehydratedDeviceIdentity = 59 0 : userDeviceKeys[userID]!.deviceKeys[device.deviceId]; 60 : if (dehydratedDeviceIdentity == null || 61 0 : !dehydratedDeviceIdentity.hasValidSignatureChain()) { 62 0 : Logs().w( 63 0 : 'Dehydrated device ${device.deviceId} is unknown or unverified, replacing it'); 64 0 : await _uploadNewDevice(secureStorage); 65 : return; 66 : } 67 : 68 : final pickleDeviceKey = 69 0 : await secureStorage.getStored(_ssssSecretNameForDehydratedDevice); 70 0 : final pickledDevice = device.deviceData?.tryGet<String>('device'); 71 : if (pickledDevice == null) { 72 0 : Logs() 73 0 : .w('Dehydrated device ${device.deviceId} is invalid, replacing it'); 74 0 : await _uploadNewDevice(secureStorage); 75 : return; 76 : } 77 : 78 : // Use a separate encryption object for the dehydrated device. 79 : // We need to be careful to not use the client.deviceId here and such. 80 0 : final encryption = Encryption(client: this); 81 : try { 82 0 : await encryption.init( 83 : pickledDevice, 84 0 : deviceId: device.deviceId, 85 : pickleKey: pickleDeviceKey, 86 : dehydratedDeviceAlgorithm: _dehydratedDeviceAlgorithm, 87 : ); 88 : 89 0 : if (dehydratedDeviceIdentity.curve25519Key != encryption.identityKey || 90 0 : dehydratedDeviceIdentity.ed25519Key != encryption.fingerprintKey) { 91 0 : Logs() 92 0 : .w('Invalid dehydrated device ${device.deviceId}, replacing it'); 93 0 : await encryption.dispose(); 94 0 : await _uploadNewDevice(secureStorage); 95 : return; 96 : } 97 : 98 : // Fetch the to_device messages sent to the picked device and handle them 1:1. 99 : DehydratedDeviceEvents? events; 100 : 101 : do { 102 0 : events = await getDehydratedDeviceEvents(device.deviceId, 103 0 : nextBatch: events?.nextBatch); 104 : 105 0 : for (final e in events.events ?? []) { 106 : // We are only interested in roomkeys, which ALWAYS need to be encrypted. 107 0 : if (e.type == EventTypes.Encrypted) { 108 0 : final decryptedEvent = await encryption.decryptToDeviceEvent(e); 109 : 110 0 : if (decryptedEvent.type == EventTypes.RoomKey) { 111 0 : await encryption.handleToDeviceEvent(decryptedEvent); 112 : } 113 : } 114 : } 115 0 : } while (events.events?.isNotEmpty == true); 116 : 117 : // make sure the sessions we just received get uploaded before we upload a new device (which deletes the old device). 118 : await this 119 0 : .encryption 120 0 : ?.keyManager 121 0 : .uploadInboundGroupSessions(skipIfInProgress: false); 122 : 123 0 : await _uploadNewDevice(secureStorage); 124 : } finally { 125 0 : await encryption.dispose(); 126 : } 127 : } catch (e) { 128 0 : Logs().w('Exception while handling dehydrated devices: ${e.toString()}'); 129 : return; 130 : } 131 : } 132 : 133 0 : Future<void> _uploadNewDevice(OpenSSSS secureStorage) async { 134 0 : final encryption = Encryption(client: this); 135 : 136 : try { 137 : String? pickleDeviceKey; 138 : try { 139 : pickleDeviceKey = 140 0 : await secureStorage.getStored(_ssssSecretNameForDehydratedDevice); 141 : } catch (_) { 142 0 : Logs().i('Dehydrated device key not found, creating new one.'); 143 0 : pickleDeviceKey = base64.encode(uc.secureRandomBytes(128)); 144 0 : await secureStorage.store( 145 : _ssssSecretNameForDehydratedDevice, pickleDeviceKey); 146 : } 147 : 148 : const chars = 149 : 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; 150 0 : final rnd = Random(); 151 : 152 0 : final deviceIdSuffix = String.fromCharCodes(Iterable.generate( 153 0 : 10, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); 154 0 : final String device = 'FAM$deviceIdSuffix'; 155 : 156 : // Generate a new olm account for the dehydrated device. 157 : try { 158 0 : await encryption.init( 159 : null, 160 : deviceId: device, 161 : pickleKey: pickleDeviceKey, 162 : dehydratedDeviceAlgorithm: _dehydratedDeviceAlgorithm, 163 : ); 164 0 : } on MatrixException catch (_) { 165 : // dehydrated devices unsupported, do noting. 166 0 : Logs().i('Dehydrated devices unsupported, skipping upload.'); 167 0 : await encryption.dispose(); 168 : return; 169 : } 170 : 171 0 : encryption.ourDeviceId = device; 172 0 : encryption.olmManager.ourDeviceId = device; 173 : 174 : // cross sign the device from our currently signed in device 175 0 : await updateUserDeviceKeys(additionalUsers: {userID!}); 176 0 : final keysToSign = <SignableKey>[ 177 0 : userDeviceKeys[userID]!.deviceKeys[device]!, 178 : ]; 179 0 : await this.encryption?.crossSigning.sign(keysToSign); 180 : } finally { 181 0 : await encryption.dispose(); 182 : } 183 : } 184 : }