diff --git a/lib/src/omemo/decryption_result.dart b/lib/src/omemo/decryption_result.dart index e924c6a..d865e23 100644 --- a/lib/src/omemo/decryption_result.dart +++ b/lib/src/omemo/decryption_result.dart @@ -3,7 +3,15 @@ import 'package:omemo_dart/src/errors.dart'; @immutable class DecryptionResult { - const DecryptionResult(this.payload, this.error); + const DecryptionResult(this.payload, this.usedOpkId, this.error); + + /// The decrypted payload or null, if it was an empty OMEMO message. final String? payload; + + /// In case a key exchange has been performed: The id of the used OPK. Useful for + /// replacing the OPK after a message catch-up. + final int? usedOpkId; + + /// The error that occurred during decryption or null, if no error occurred. final OmemoError? error; } diff --git a/lib/src/omemo/device.dart b/lib/src/omemo/device.dart index 14e7a7d..c944fca 100644 --- a/lib/src/omemo/device.dart +++ b/lib/src/omemo/device.dart @@ -35,7 +35,16 @@ class OmemoDevice { final opks = {}; for (var i = 0; i < opkAmount; i++) { - opks[i] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); + // Generate unique ids for each key + while (true) { + final opkId = generateRandom32BitNumber(); + if (opks.containsKey(opkId)) { + continue; + } + + opks[opkId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); + break; + } } return OmemoDevice(jid, id, ik, spk, spkId, signature, null, null, opks); @@ -72,7 +81,18 @@ class OmemoDevice { /// a new Device object that copies over everything but replaces said key. @internal Future replaceOnetimePrekey(int id) async { - opks[id] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); + opks.remove(id); + + // Generate a new unique id for the OPK. + while (true) { + final newId = generateRandom32BitNumber(); + if (opks.containsKey(newId)) { + continue; + } + + opks[newId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); + break; + } return OmemoDevice( jid, diff --git a/lib/src/omemo/omemo.dart b/lib/src/omemo/omemo.dart index dd054d0..89c2148 100644 --- a/lib/src/omemo/omemo.dart +++ b/lib/src/omemo/omemo.dart @@ -351,6 +351,7 @@ class OmemoManager { final key = stanza.keys.firstWhereOrNull((key) => key.rid == deviceId); if (key == null) { return DecryptionResult( + null, null, NotEncryptedForDeviceError(), ); @@ -359,6 +360,7 @@ class OmemoManager { // Protobuf will happily parse this and return bogus data. if (key.value.isEmpty) { return DecryptionResult( + null, null, MalformedEncryptedKeyError(), ); @@ -396,6 +398,7 @@ class OmemoManager { spk = device.oldSpk!; } else { return DecryptionResult( + null, null, UnknownSignedPrekeyError(), ); @@ -438,7 +441,7 @@ class OmemoManager { final error = keyAndHmac.get(); _log.warning('Failed to decrypt symmetric key: $error'); - return DecryptionResult(null, error); + return DecryptionResult(null, null, error); } Result result; @@ -452,6 +455,7 @@ class OmemoManager { _log.warning('Decrypting payload failed: $error'); return DecryptionResult( + null, null, error, ); @@ -503,6 +507,7 @@ class OmemoManager { return DecryptionResult( result.get(), + kexMessage.pkId, null, ); } else { @@ -517,6 +522,7 @@ class OmemoManager { await _sendOmemoHeartbeat(stanza.bareSenderJid); return DecryptionResult( + null, null, NoSessionWithDeviceError(), ); @@ -540,7 +546,7 @@ class OmemoManager { if (keyAndHmac.isType()) { final error = keyAndHmac.get(); _log.warning('Failed to decrypt symmetric key: $error'); - return DecryptionResult(null, error); + return DecryptionResult(null, null, error); } Result result; @@ -553,6 +559,7 @@ class OmemoManager { final error = result.get(); _log.warning('Failed to decrypt message: $error'); return DecryptionResult( + null, null, error, ); @@ -586,6 +593,7 @@ class OmemoManager { return DecryptionResult( result.get(), null, + null, ); } } @@ -958,7 +966,33 @@ class OmemoManager { @visibleForTesting OmemoDoubleRatchet? getRatchet(RatchetMapKey key) => _ratchetMap[key]; - /// Trust management functions + /// Replaces the OPK with id [opkId] and commits the new device to storage. This + /// function should not be called. It's only useful for rotating OPKs after message + /// catch-up, because in that case the OPKs are not rotated automatically. + Future replaceOnetimePrekey(int opkId) async { + await _deviceLock.synchronized(() async { + // Replace OPK + await _device.replaceOnetimePrekey(opkId); + + // Commit the device + await commitDevice(_device); + }); + } + + /// Replaces the SPK of our device and commits it to storage. + Future replaceSignedPrekey() async { + await _deviceLock.synchronized(() async { + // Replace SPK + await _device.replaceSignedPrekey(); + + // Commit the device + await commitDevice(_device); + }); + } + + /// Acquire a lock for interacting with the trust manager for modifying the trust + /// state of [jid]. [callback] is called from within the critical section with the + /// trust manager as its parameter. Future withTrustManager( String jid, Future Function(TrustManager) callback,