Compare commits
	
		
			38 Commits
		
	
	
		
			0ffc0b067a
			...
			3c20d8ac56
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3c20d8ac56 | |||
| 96ca643d26 | |||
| f54a90d5bb | |||
| 9214e964df | |||
| 8b91c07fb8 | |||
| 499817313d | |||
| b4241db9b6 | |||
| 7f60140583 | |||
| ddb4483d4a | |||
| c086579b57 | |||
| b986096aa0 | |||
| bb5ef414f2 | |||
| 207215cc5f | |||
| 3829c6c11b | |||
| 4baf8187e1 | |||
| fe2b090ea0 | |||
| 65c0975a77 | |||
| 234fee167f | |||
| ed0701bdcd | |||
| dad85b8467 | |||
| 4fb25a3ab7 | |||
| 28e7ad59b0 | |||
| e6c792a8ac | |||
| 0b2d6f0a97 | |||
| 4ed2d3dec3 | |||
| 3d953f0acb | |||
| b0bba4fe82 | |||
| da11e60f79 | |||
| 6e734ec0c3 | |||
| 6c301ab88f | |||
| f1ec8d1793 | |||
| af33ed51d1 | |||
| c7ded4c824 | |||
| 87a985fee0 | |||
| c483585d0b | |||
| f6f0e145cc | |||
| d2558ea9fa | |||
| 50f6513c6f | 
							
								
								
									
										12
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -54,3 +54,15 @@ | |||||||
| ## 0.4.3 | ## 0.4.3 | ||||||
| 
 | 
 | ||||||
| - Fix bug that causes ratchets to be unable to decrypt anything after receiving a heartbeat with a completely new session | - Fix bug that causes ratchets to be unable to decrypt anything after receiving a heartbeat with a completely new session | ||||||
|  | 
 | ||||||
|  | ## 0.5.0 | ||||||
|  | 
 | ||||||
|  | This version is a complete rework of omemo_dart! | ||||||
|  | 
 | ||||||
|  | - Removed events from `OmemoManager` | ||||||
|  | - Removed `OmemoSessionManager` | ||||||
|  | - Removed serialization/deserialization code | ||||||
|  | - Replace exceptions with errors inside a result type | ||||||
|  | - Ratchets and trust data is now loaded and cached on demand | ||||||
|  | - Accessing the trust manager must happen via `withTrustManager` | ||||||
|  | - Overriding the base implementations is replaced by providing callback functions | ||||||
							
								
								
									
										30
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								README.md
									
									
									
									
									
								
							| @ -28,22 +28,32 @@ Include `omemo_dart` in your `pubspec.yaml` like this: | |||||||
| dependencies: | dependencies: | ||||||
|   omemo_dart: |   omemo_dart: | ||||||
|     hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub |     hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub | ||||||
|     version: ^0.4.3 |     version: ^0.5.0 | ||||||
|   # [...] |   # [...] | ||||||
| 
 | 
 | ||||||
| # [...] | # [...] | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Contributing | ### Example | ||||||
| 
 | 
 | ||||||
| Due to issues with `protobuf`, `omemo_dart` reimplements the Protobuf encoding for the required | This repository includes a documented ["example"](./example/omemo_dart_example.dart) that explains the basic usage of the library while | ||||||
| OMEMO messages. As such, `protobuf` is only a dependency for testing that the serialisation and | leaving out the XMPP specific bits. For a more functional and integrated example, see the `omemo_client.dart` example from | ||||||
| deserialisation of the custom implementation. In order to run tests, you need the Protbuf | [moxxmpp](https://codeberg.org/moxxy/moxxmpp). | ||||||
| compiler. After that, making sure that | 
 | ||||||
| the [Dart Protobuf compiler addon](https://pub.dev/packages/protoc_plugin) and the | ### Persistence | ||||||
| Protobuf compiler itself is in your PATH, | 
 | ||||||
| run `protoc -I=./protobuf/ --dart_out=lib/protobuf/ ./protobuf/schema.proto` in the | By default, `omemo_dart` uses in-memory implementations for everything. For a real-world application, this is unsuitable as OMEMO devices would be constantly added. | ||||||
| repository's root to generate the real Protobuf bindings. | In order to allow persistence, your application needs to keep track of the following: | ||||||
|  | 
 | ||||||
|  | - The `OmemoDevice` assigned to the `OmemoManager` | ||||||
|  | - `JID -> [int]`: The device list for each JID | ||||||
|  | - `(JID, device) -> Ratchet`: The actual ratchet | ||||||
|  | 
 | ||||||
|  | If you also use the `BlindTrustBeforeVerificationTrustManager`, you additionally need to keep track of: | ||||||
|  | 
 | ||||||
|  | - `(JID, device) -> (int, bool)`: The trust level and the enablement state | ||||||
|  | 
 | ||||||
|  | ## Contributing | ||||||
| 
 | 
 | ||||||
| When submitting a PR, please run the linter using `dart analyze` and make sure that all | When submitting a PR, please run the linter using `dart analyze` and make sure that all | ||||||
| tests still pass using `dart test`. | tests still pass using `dart test`. | ||||||
|  | |||||||
| @ -9,8 +9,5 @@ linter: | |||||||
| 
 | 
 | ||||||
| analyzer: | analyzer: | ||||||
|   exclude: |   exclude: | ||||||
|     - "lib/protobuf/*.dart" |     - "lib/src/protobuf/*.dart" | ||||||
|     # TODO: Remove once OmemoSessionManager is gone |  | ||||||
|     - "test/omemo_test.dart" |  | ||||||
|     - "example/omemo_dart_example.dart" |     - "example/omemo_dart_example.dart" | ||||||
|     - "test/serialisation_test.dart" |  | ||||||
|  | |||||||
| @ -35,6 +35,10 @@ void main() async { | |||||||
|     // This needs to be wired into your XMPP library's OMEMO implementation. |     // This needs to be wired into your XMPP library's OMEMO implementation. | ||||||
|     // For simplicity, we use an empty function and imagine it works. |     // For simplicity, we use an empty function and imagine it works. | ||||||
|     (jid) async {}, |     (jid) async {}, | ||||||
|  |     // This function is called whenever our own device bundle has to be republished to our PEP node. | ||||||
|  |     // This needs to be wired into your XMPP library's OMEMO implementation. | ||||||
|  |     // For simplicity, we use an empty function and imagine it works. | ||||||
|  |     (device) async {}, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   // Alice now wants to chat with Bob at his bare Jid "bob@other.server". To make things |   // Alice now wants to chat with Bob at his bare Jid "bob@other.server". To make things | ||||||
| @ -42,7 +46,7 @@ void main() async { | |||||||
|   // request it using PEP and then convert the device bundle into a OmemoBundle object. |   // request it using PEP and then convert the device bundle into a OmemoBundle object. | ||||||
|   final bobManager = OmemoManager( |   final bobManager = OmemoManager( | ||||||
|     await OmemoDevice.generateNewDevice(bobJid), |     await OmemoDevice.generateNewDevice(bobJid), | ||||||
|     MemoryBTBVTrustManager(), |     BlindTrustBeforeVerificationTrustManager(), | ||||||
|     (result, recipient) async => {}, |     (result, recipient) async => {}, | ||||||
|     (jid) async => [], |     (jid) async => [], | ||||||
|     (jid, id) async => null, |     (jid, id) async => null, | ||||||
| @ -145,6 +149,11 @@ void main() async { | |||||||
|       /// The text of the <payload /> element, if it exists. If not, then the message might be |       /// The text of the <payload /> element, if it exists. If not, then the message might be | ||||||
|       /// a hearbeat, where no payload is sent. In that case, use null. |       /// a hearbeat, where no payload is sent. In that case, use null. | ||||||
|       payload, |       payload, | ||||||
|  | 
 | ||||||
|  |       /// Since we did not receive this message due to a catch-up mechanism, like MAM, we | ||||||
|  |       /// set this to false. If we, however, did use a catch-up mechanism, we must set this | ||||||
|  |       /// to true to prevent the OPKs from being replaced. | ||||||
|  |       false, | ||||||
|     ), |     ), | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,9 +8,10 @@ export 'src/omemo/bundle.dart'; | |||||||
| export 'src/omemo/device.dart'; | export 'src/omemo/device.dart'; | ||||||
| export 'src/omemo/encrypted_key.dart'; | export 'src/omemo/encrypted_key.dart'; | ||||||
| export 'src/omemo/encryption_result.dart'; | export 'src/omemo/encryption_result.dart'; | ||||||
| export 'src/omemo/events.dart'; | export 'src/omemo/errors.dart'; | ||||||
| export 'src/omemo/fingerprint.dart'; | export 'src/omemo/fingerprint.dart'; | ||||||
| export 'src/omemo/omemomanager.dart'; | export 'src/omemo/omemo.dart'; | ||||||
|  | export 'src/omemo/ratchet_data.dart'; | ||||||
| export 'src/omemo/ratchet_map_key.dart'; | export 'src/omemo/ratchet_map_key.dart'; | ||||||
| export 'src/omemo/stanza.dart'; | export 'src/omemo/stanza.dart'; | ||||||
| export 'src/trust/base.dart'; | export 'src/trust/base.dart'; | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								lib/src/common/constants.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/src/common/constants.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | /// The overarching assumption is that we use Ed25519 keys for the identity keys | ||||||
|  | const omemoX3DHInfoString = 'OMEMO X3DH'; | ||||||
|  | 
 | ||||||
|  | /// The info used for when encrypting the AES key for the actual payload. | ||||||
|  | const omemoPayloadInfoString = 'OMEMO Payload'; | ||||||
|  | 
 | ||||||
|  | /// Info string for ENCRYPT | ||||||
|  | const encryptHkdfInfoString = 'OMEMO Message Key Material'; | ||||||
|  | 
 | ||||||
|  | /// Amount of messages we may skip per session | ||||||
|  | const maxSkip = 1000; | ||||||
| @ -1,5 +1,7 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'package:cryptography/cryptography.dart'; | import 'package:cryptography/cryptography.dart'; | ||||||
|  | import 'package:moxlib/moxlib.dart'; | ||||||
|  | import 'package:omemo_dart/src/errors.dart'; | ||||||
| import 'package:omemo_dart/src/keys.dart'; | import 'package:omemo_dart/src/keys.dart'; | ||||||
| 
 | 
 | ||||||
| /// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then | /// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then | ||||||
| @ -92,7 +94,7 @@ Future<List<int>> aes256CbcEncrypt( | |||||||
| 
 | 
 | ||||||
| /// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as | /// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as | ||||||
| /// the encryption key and [iv] as the IV. Returns the ciphertext. | /// the encryption key and [iv] as the IV. Returns the ciphertext. | ||||||
| Future<List<int>> aes256CbcDecrypt( | Future<Result<MalformedCiphertextError, List<int>>> aes256CbcDecrypt( | ||||||
|   List<int> ciphertext, |   List<int> ciphertext, | ||||||
|   List<int> key, |   List<int> key, | ||||||
|   List<int> iv, |   List<int> iv, | ||||||
| @ -100,13 +102,19 @@ Future<List<int>> aes256CbcDecrypt( | |||||||
|   final algorithm = AesCbc.with256bits( |   final algorithm = AesCbc.with256bits( | ||||||
|     macAlgorithm: MacAlgorithm.empty, |     macAlgorithm: MacAlgorithm.empty, | ||||||
|   ); |   ); | ||||||
|   return algorithm.decrypt( |   try { | ||||||
|     NoMacSecretBox( |     return Result( | ||||||
|       ciphertext, |       await algorithm.decrypt( | ||||||
|       nonce: iv, |         NoMacSecretBox( | ||||||
|     ), |           ciphertext, | ||||||
|     secretKey: SecretKey(key), |           nonce: iv, | ||||||
|   ); |         ), | ||||||
|  |         secretKey: SecretKey(key), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } catch (ex) { | ||||||
|  |     return Result(MalformedCiphertextError(ex)); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes. | /// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes. | ||||||
|  | |||||||
| @ -1,60 +0,0 @@ | |||||||
| import 'package:omemo_dart/src/crypto.dart'; |  | ||||||
| import 'package:omemo_dart/src/errors.dart'; |  | ||||||
| import 'package:omemo_dart/src/helpers.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_message.dart'; |  | ||||||
| 
 |  | ||||||
| /// Info string for ENCRYPT |  | ||||||
| const encryptHkdfInfoString = 'OMEMO Message Key Material'; |  | ||||||
| 
 |  | ||||||
| /// Signals ENCRYPT function as specified by OMEMO 0.8.3. |  | ||||||
| /// Encrypt [plaintext] using the message key [mk], given associated_data [associatedData] |  | ||||||
| /// and the AD output from the X3DH [sessionAd]. |  | ||||||
| Future<List<int>> encrypt( |  | ||||||
|   List<int> mk, |  | ||||||
|   List<int> plaintext, |  | ||||||
|   List<int> associatedData, |  | ||||||
|   List<int> sessionAd, |  | ||||||
| ) async { |  | ||||||
|   // Generate encryption, authentication key and IV |  | ||||||
|   final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); |  | ||||||
|   final ciphertext = |  | ||||||
|       await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv); |  | ||||||
| 
 |  | ||||||
|   final header = |  | ||||||
|       OmemoMessage.fromBuffer(associatedData.sublist(sessionAd.length)) |  | ||||||
|         ..ciphertext = ciphertext; |  | ||||||
|   final headerBytes = header.writeToBuffer(); |  | ||||||
|   final hmacInput = concat([sessionAd, headerBytes]); |  | ||||||
|   final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey); |  | ||||||
|   final message = OmemoAuthenticatedMessage() |  | ||||||
|     ..mac = hmacResult |  | ||||||
|     ..message = headerBytes; |  | ||||||
|   return message.writeToBuffer(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Signals DECRYPT function as specified by OMEMO 0.8.3. |  | ||||||
| /// Decrypt [ciphertext] with the message key [mk], given the associated_data [associatedData] |  | ||||||
| /// and the AD output from the X3DH. |  | ||||||
| Future<List<int>> decrypt( |  | ||||||
|   List<int> mk, |  | ||||||
|   List<int> ciphertext, |  | ||||||
|   List<int> associatedData, |  | ||||||
|   List<int> sessionAd, |  | ||||||
| ) async { |  | ||||||
|   // Generate encryption, authentication key and IV |  | ||||||
|   final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); |  | ||||||
| 
 |  | ||||||
|   // Assumption ciphertext is a OMEMOAuthenticatedMessage |  | ||||||
|   final message = OmemoAuthenticatedMessage.fromBuffer(ciphertext); |  | ||||||
|   final header = OmemoMessage.fromBuffer(message.message!); |  | ||||||
| 
 |  | ||||||
|   final hmacInput = concat([sessionAd, header.writeToBuffer()]); |  | ||||||
|   final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey); |  | ||||||
| 
 |  | ||||||
|   if (!listsEqual(hmacResult, message.mac!)) { |  | ||||||
|     throw InvalidMessageHMACException(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return aes256CbcDecrypt(header.ciphertext!, keys.encryptionKey, keys.iv); |  | ||||||
| } |  | ||||||
| @ -1,47 +1,24 @@ | |||||||
| import 'dart:convert'; |  | ||||||
| import 'package:cryptography/cryptography.dart'; | import 'package:cryptography/cryptography.dart'; | ||||||
| import 'package:hex/hex.dart'; | import 'package:hex/hex.dart'; | ||||||
| import 'package:meta/meta.dart'; | import 'package:meta/meta.dart'; | ||||||
|  | import 'package:moxlib/moxlib.dart'; | ||||||
|  | import 'package:omemo_dart/src/common/constants.dart'; | ||||||
| import 'package:omemo_dart/src/crypto.dart'; | import 'package:omemo_dart/src/crypto.dart'; | ||||||
| import 'package:omemo_dart/src/double_ratchet/crypto.dart'; |  | ||||||
| import 'package:omemo_dart/src/double_ratchet/kdf.dart'; | import 'package:omemo_dart/src/double_ratchet/kdf.dart'; | ||||||
| import 'package:omemo_dart/src/errors.dart'; | import 'package:omemo_dart/src/errors.dart'; | ||||||
| import 'package:omemo_dart/src/helpers.dart'; | import 'package:omemo_dart/src/helpers.dart'; | ||||||
| import 'package:omemo_dart/src/keys.dart'; | import 'package:omemo_dart/src/keys.dart'; | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_message.dart'; | import 'package:omemo_dart/src/protobuf/schema.pb.dart'; | ||||||
| 
 |  | ||||||
| /// Amount of messages we may skip per session |  | ||||||
| const maxSkip = 1000; |  | ||||||
| 
 |  | ||||||
| class RatchetStep { |  | ||||||
|   const RatchetStep(this.header, this.ciphertext); |  | ||||||
|   final OmemoMessage header; |  | ||||||
|   final List<int> ciphertext; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| @immutable | @immutable | ||||||
| class SkippedKey { | class SkippedKey { | ||||||
|   const SkippedKey(this.dh, this.n); |   const SkippedKey(this.dh, this.n); | ||||||
| 
 | 
 | ||||||
|   factory SkippedKey.fromJson(Map<String, dynamic> data) { |   /// The DH public key for which we skipped a message key. | ||||||
|     return SkippedKey( |  | ||||||
|       OmemoPublicKey.fromBytes( |  | ||||||
|         base64.decode(data['public']! as String), |  | ||||||
|         KeyPairType.x25519, |  | ||||||
|       ), |  | ||||||
|       data['n']! as int, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   final OmemoPublicKey dh; |   final OmemoPublicKey dh; | ||||||
|   final int n; |  | ||||||
| 
 | 
 | ||||||
|   Future<Map<String, dynamic>> toJson() async { |   /// The associated number of the message key we skipped. | ||||||
|     return { |   final int n; | ||||||
|       'public': base64.encode(await dh.getBytes()), |  | ||||||
|       'n': n, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) { |   bool operator ==(Object other) { | ||||||
| @ -52,6 +29,28 @@ class SkippedKey { | |||||||
|   int get hashCode => dh.hashCode ^ n.hashCode; |   int get hashCode => dh.hashCode ^ n.hashCode; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @immutable | ||||||
|  | class KeyExchangeData { | ||||||
|  |   const KeyExchangeData( | ||||||
|  |     this.pkId, | ||||||
|  |     this.spkId, | ||||||
|  |     this.ik, | ||||||
|  |     this.ek, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   /// The id of the used OPK. | ||||||
|  |   final int pkId; | ||||||
|  | 
 | ||||||
|  |   /// The id of the used SPK. | ||||||
|  |   final int spkId; | ||||||
|  | 
 | ||||||
|  |   /// The ephemeral key used while the key exchange. | ||||||
|  |   final OmemoPublicKey ek; | ||||||
|  | 
 | ||||||
|  |   /// The identity key used in the key exchange. | ||||||
|  |   final OmemoPublicKey ik; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| class OmemoDoubleRatchet { | class OmemoDoubleRatchet { | ||||||
|   OmemoDoubleRatchet( |   OmemoDoubleRatchet( | ||||||
|     this.dhs, // DHs |     this.dhs, // DHs | ||||||
| @ -66,77 +65,9 @@ class OmemoDoubleRatchet { | |||||||
|     this.sessionAd, |     this.sessionAd, | ||||||
|     this.mkSkipped, // MKSKIPPED |     this.mkSkipped, // MKSKIPPED | ||||||
|     this.acknowledged, |     this.acknowledged, | ||||||
|     this.kexTimestamp, |  | ||||||
|     this.kex, |     this.kex, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) { |  | ||||||
|     /* |  | ||||||
|     { |  | ||||||
|       'dhs': 'base/64/encoded', |  | ||||||
|       'dhs_pub': 'base/64/encoded', |  | ||||||
|       'dhr': null | 'base/64/encoded', |  | ||||||
|       'rk': 'base/64/encoded', |  | ||||||
|       'cks': null | 'base/64/encoded', |  | ||||||
|       'ckr': null | 'base/64/encoded', |  | ||||||
|       'ns': 0, |  | ||||||
|       'nr': 0, |  | ||||||
|       'pn': 0, |  | ||||||
|       'ik_pub': null | 'base/64/encoded', |  | ||||||
|       'session_ad': 'base/64/encoded', |  | ||||||
|       'acknowledged': true | false, |  | ||||||
|       'kex_timestamp': int, |  | ||||||
|       'kex': 'base/64/encoded', |  | ||||||
|       'mkskipped': [ |  | ||||||
|         { |  | ||||||
|           'key': 'base/64/encoded', |  | ||||||
|           'public': 'base/64/encoded', |  | ||||||
|           'n': 0 |  | ||||||
|         }, ... |  | ||||||
|       ] |  | ||||||
|     } |  | ||||||
|     */ |  | ||||||
|     // NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as |  | ||||||
|     //       such we need to convert the items by hand. |  | ||||||
|     final mkSkipped = Map<SkippedKey, List<int>>.fromEntries( |  | ||||||
|       (data['mkskipped']! as List<dynamic>) |  | ||||||
|           .map<MapEntry<SkippedKey, List<int>>>( |  | ||||||
|         (entry) { |  | ||||||
|           final map = entry as Map<String, dynamic>; |  | ||||||
|           final key = SkippedKey.fromJson(map); |  | ||||||
|           return MapEntry( |  | ||||||
|             key, |  | ||||||
|             base64.decode(map['key']! as String), |  | ||||||
|           ); |  | ||||||
|         }, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     return OmemoDoubleRatchet( |  | ||||||
|       OmemoKeyPair.fromBytes( |  | ||||||
|         base64.decode(data['dhs_pub']! as String), |  | ||||||
|         base64.decode(data['dhs']! as String), |  | ||||||
|         KeyPairType.x25519, |  | ||||||
|       ), |  | ||||||
|       decodeKeyIfNotNull(data, 'dhr', KeyPairType.x25519), |  | ||||||
|       base64.decode(data['rk']! as String), |  | ||||||
|       base64DecodeIfNotNull(data, 'cks'), |  | ||||||
|       base64DecodeIfNotNull(data, 'ckr'), |  | ||||||
|       data['ns']! as int, |  | ||||||
|       data['nr']! as int, |  | ||||||
|       data['pn']! as int, |  | ||||||
|       OmemoPublicKey.fromBytes( |  | ||||||
|         base64.decode(data['ik_pub']! as String), |  | ||||||
|         KeyPairType.ed25519, |  | ||||||
|       ), |  | ||||||
|       base64.decode(data['session_ad']! as String), |  | ||||||
|       mkSkipped, |  | ||||||
|       data['acknowledged']! as bool, |  | ||||||
|       data['kex_timestamp']! as int, |  | ||||||
|       data['kex'] as String?, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Sending DH keypair |   /// Sending DH keypair | ||||||
|   OmemoKeyPair dhs; |   OmemoKeyPair dhs; | ||||||
| 
 | 
 | ||||||
| @ -161,16 +92,14 @@ class OmemoDoubleRatchet { | |||||||
|   /// for verification purposes |   /// for verification purposes | ||||||
|   final OmemoPublicKey ik; |   final OmemoPublicKey ik; | ||||||
| 
 | 
 | ||||||
|  |   /// Associated data for this ratchet. | ||||||
|   final List<int> sessionAd; |   final List<int> sessionAd; | ||||||
| 
 | 
 | ||||||
|  |   /// List of skipped message keys. | ||||||
|   final Map<SkippedKey, List<int>> mkSkipped; |   final Map<SkippedKey, List<int>> mkSkipped; | ||||||
| 
 | 
 | ||||||
|   /// The point in time at which we performed the kex exchange to create this ratchet. |  | ||||||
|   /// Precision is milliseconds since epoch. |  | ||||||
|   int kexTimestamp; |  | ||||||
| 
 |  | ||||||
|   /// The key exchange that was used for initiating the session. |   /// The key exchange that was used for initiating the session. | ||||||
|   final String? kex; |   final KeyExchangeData kex; | ||||||
| 
 | 
 | ||||||
|   /// Indicates whether we received an empty OMEMO message after building a session with |   /// Indicates whether we received an empty OMEMO message after building a session with | ||||||
|   /// the device. |   /// the device. | ||||||
| @ -181,21 +110,22 @@ class OmemoDoubleRatchet { | |||||||
|   /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key. |   /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key. | ||||||
|   static Future<OmemoDoubleRatchet> initiateNewSession( |   static Future<OmemoDoubleRatchet> initiateNewSession( | ||||||
|     OmemoPublicKey spk, |     OmemoPublicKey spk, | ||||||
|  |     int spkId, | ||||||
|     OmemoPublicKey ik, |     OmemoPublicKey ik, | ||||||
|  |     OmemoPublicKey ownIk, | ||||||
|  |     OmemoPublicKey ek, | ||||||
|     List<int> sk, |     List<int> sk, | ||||||
|     List<int> ad, |     List<int> ad, | ||||||
|     int timestamp, |     int pkId, | ||||||
|   ) async { |   ) async { | ||||||
|     final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); |     final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); | ||||||
|     final dhr = spk; |     final rk = await kdfRk(sk, await omemoDH(dhs, spk, 0)); | ||||||
|     final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0)); |  | ||||||
|     final cks = rk; |  | ||||||
| 
 | 
 | ||||||
|     return OmemoDoubleRatchet( |     return OmemoDoubleRatchet( | ||||||
|       dhs, |       dhs, | ||||||
|       dhr, |       spk, | ||||||
|       rk, |       List.from(rk), | ||||||
|       cks, |       List.from(rk), | ||||||
|       null, |       null, | ||||||
|       0, |       0, | ||||||
|       0, |       0, | ||||||
| @ -204,8 +134,12 @@ class OmemoDoubleRatchet { | |||||||
|       ad, |       ad, | ||||||
|       {}, |       {}, | ||||||
|       false, |       false, | ||||||
|       timestamp, |       KeyExchangeData( | ||||||
|       '', |         pkId, | ||||||
|  |         spkId, | ||||||
|  |         ownIk, | ||||||
|  |         ek, | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -215,10 +149,12 @@ class OmemoDoubleRatchet { | |||||||
|   /// Alice's (the initiator's) IK public key. |   /// Alice's (the initiator's) IK public key. | ||||||
|   static Future<OmemoDoubleRatchet> acceptNewSession( |   static Future<OmemoDoubleRatchet> acceptNewSession( | ||||||
|     OmemoKeyPair spk, |     OmemoKeyPair spk, | ||||||
|  |     int spkId, | ||||||
|     OmemoPublicKey ik, |     OmemoPublicKey ik, | ||||||
|  |     int pkId, | ||||||
|  |     OmemoPublicKey ek, | ||||||
|     List<int> sk, |     List<int> sk, | ||||||
|     List<int> ad, |     List<int> ad, | ||||||
|     int kexTimestamp, |  | ||||||
|   ) async { |   ) async { | ||||||
|     return OmemoDoubleRatchet( |     return OmemoDoubleRatchet( | ||||||
|       spk, |       spk, | ||||||
| @ -233,72 +169,52 @@ class OmemoDoubleRatchet { | |||||||
|       ad, |       ad, | ||||||
|       {}, |       {}, | ||||||
|       true, |       true, | ||||||
|       kexTimestamp, |       KeyExchangeData( | ||||||
|       null, |         pkId, | ||||||
|  |         spkId, | ||||||
|  |         ik, | ||||||
|  |         ek, | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<Map<String, dynamic>> toJson() async { |   /// Performs a single ratchet step in case we received a new | ||||||
|     final mkSkippedSerialised = |   /// public key in [header]. | ||||||
|         List<Map<String, dynamic>>.empty(growable: true); |   Future<void> _dhRatchet(OMEMOMessage header) async { | ||||||
|     for (final entry in mkSkipped.entries) { |     pn = ns; | ||||||
|       final result = await entry.key.toJson(); |     ns = 0; | ||||||
|       result['key'] = base64.encode(entry.value); |     nr = 0; | ||||||
| 
 |     dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519); | ||||||
|       mkSkippedSerialised.add(result); |     final newRk1 = await kdfRk( | ||||||
|     } |       rk, | ||||||
| 
 |       await omemoDH( | ||||||
|     return { |         dhs, | ||||||
|       'dhs': base64.encode(await dhs.sk.getBytes()), |         dhr!, | ||||||
|       'dhs_pub': base64.encode(await dhs.pk.getBytes()), |         0, | ||||||
|       'dhr': dhr != null ? base64.encode(await dhr!.getBytes()) : null, |       ), | ||||||
|       'rk': base64.encode(rk), |  | ||||||
|       'cks': cks != null ? base64.encode(cks!) : null, |  | ||||||
|       'ckr': ckr != null ? base64.encode(ckr!) : null, |  | ||||||
|       'ns': ns, |  | ||||||
|       'nr': nr, |  | ||||||
|       'pn': pn, |  | ||||||
|       'ik_pub': base64.encode(await ik.getBytes()), |  | ||||||
|       'session_ad': base64.encode(sessionAd), |  | ||||||
|       'mkskipped': mkSkippedSerialised, |  | ||||||
|       'acknowledged': acknowledged, |  | ||||||
|       'kex_timestamp': kexTimestamp, |  | ||||||
|       'kex': kex, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Returns the OMEMO compatible fingerprint of the ratchet session. |  | ||||||
|   Future<String> getOmemoFingerprint() async { |  | ||||||
|     final curveKey = await ik.toCurve25519(); |  | ||||||
|     return HEX.encode(await curveKey.getBytes()); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Future<List<int>?> _trySkippedMessageKeys( |  | ||||||
|     OmemoMessage header, |  | ||||||
|     List<int> ciphertext, |  | ||||||
|   ) async { |  | ||||||
|     final key = SkippedKey( |  | ||||||
|       OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519), |  | ||||||
|       header.n!, |  | ||||||
|     ); |     ); | ||||||
|     if (mkSkipped.containsKey(key)) { |     rk = List.from(newRk1); | ||||||
|       final mk = mkSkipped[key]!; |     ckr = List.from(newRk1); | ||||||
|       mkSkipped.remove(key); |  | ||||||
| 
 | 
 | ||||||
|       return decrypt( |     dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); | ||||||
|         mk, |     final newRk2 = await kdfRk( | ||||||
|         ciphertext, |       rk, | ||||||
|         concat([sessionAd, header.writeToBuffer()]), |       await omemoDH( | ||||||
|         sessionAd, |         dhs, | ||||||
|       ); |         dhr!, | ||||||
|     } |         0, | ||||||
| 
 |       ), | ||||||
|     return null; |     ); | ||||||
|  |     rk = List.from(newRk2); | ||||||
|  |     cks = List.from(newRk2); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> _skipMessageKeys(int until) async { |   /// Skip (and keep track of) message keys until our receive counter is | ||||||
|  |   /// equal to [until]. If we would skip too many messages, returns | ||||||
|  |   /// a [SkippingTooManyKeysError]. If not, returns null. | ||||||
|  |   Future<OmemoError?> _skipMessageKeys(int until) async { | ||||||
|     if (nr + maxSkip < until) { |     if (nr + maxSkip < until) { | ||||||
|       throw SkippingTooManyMessagesException(); |       return SkippingTooManyKeysError(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (ckr != null) { |     if (ckr != null) { | ||||||
| @ -306,88 +222,140 @@ class OmemoDoubleRatchet { | |||||||
|         final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); |         final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); | ||||||
|         final mk = await kdfCk(ckr!, kdfCkNextMessageKey); |         final mk = await kdfCk(ckr!, kdfCkNextMessageKey); | ||||||
|         ckr = newCkr; |         ckr = newCkr; | ||||||
|  | 
 | ||||||
|         mkSkipped[SkippedKey(dhr!, nr)] = mk; |         mkSkipped[SkippedKey(dhr!, nr)] = mk; | ||||||
|         nr++; |         nr++; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     return null; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> _dhRatchet(OmemoMessage header) async { |   /// Decrypt [ciphertext] using keys derived from the message key [mk]. Also computes the | ||||||
|     pn = ns; |   /// HMAC from the [OMEMOMessage] embedded in [message]. | ||||||
|     ns = 0; |  | ||||||
|     nr = 0; |  | ||||||
|     dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519); |  | ||||||
| 
 |  | ||||||
|     final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); |  | ||||||
|     rk = List.from(newRk); |  | ||||||
|     ckr = List.from(newRk); |  | ||||||
|     dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); |  | ||||||
|     final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); |  | ||||||
|     rk = List.from(newNewRk); |  | ||||||
|     cks = List.from(newNewRk); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Encrypt [plaintext] using the Double Ratchet. |  | ||||||
|   Future<RatchetStep> ratchetEncrypt(List<int> plaintext) async { |  | ||||||
|     final newCks = await kdfCk(cks!, kdfCkNextChainKey); |  | ||||||
|     final mk = await kdfCk(cks!, kdfCkNextMessageKey); |  | ||||||
| 
 |  | ||||||
|     cks = newCks; |  | ||||||
|     final header = OmemoMessage() |  | ||||||
|       ..dhPub = await dhs.pk.getBytes() |  | ||||||
|       ..pn = pn |  | ||||||
|       ..n = ns; |  | ||||||
| 
 |  | ||||||
|     ns++; |  | ||||||
| 
 |  | ||||||
|     return RatchetStep( |  | ||||||
|       header, |  | ||||||
|       await encrypt( |  | ||||||
|         mk, |  | ||||||
|         plaintext, |  | ||||||
|         concat([sessionAd, header.writeToBuffer()]), |  | ||||||
|         sessionAd, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Decrypt a [ciphertext] that was sent with the header [header] using the Double |  | ||||||
|   /// Ratchet. Returns the decrypted (raw) plaintext. |  | ||||||
|   /// |   /// | ||||||
|   /// Throws an SkippingTooManyMessagesException if too many messages were to be skipped. |   /// If the computed HMAC does not match the HMAC in [message], returns | ||||||
|   Future<List<int>> ratchetDecrypt( |   /// [InvalidMessageHMACError]. If it matches, returns the decrypted | ||||||
|     OmemoMessage header, |   /// payload. | ||||||
|  |   Future<Result<OmemoError, List<int>>> _decrypt( | ||||||
|  |     OMEMOAuthenticatedMessage message, | ||||||
|     List<int> ciphertext, |     List<int> ciphertext, | ||||||
|  |     List<int> mk, | ||||||
|   ) async { |   ) async { | ||||||
|     // Check if we skipped too many messages |     final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); | ||||||
|     final plaintext = await _trySkippedMessageKeys(header, ciphertext); | 
 | ||||||
|     if (plaintext != null) { |     final hmacInput = concat([sessionAd, message.message]); | ||||||
|       return plaintext; |     final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey); | ||||||
|  |     if (!listsEqual(hmacResult, message.mac)) { | ||||||
|  |       return Result(InvalidMessageHMACError()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     final dhPubMatches = listsEqual( |     final plaintext = | ||||||
|       header.dhPub!, |         await aes256CbcDecrypt(ciphertext, keys.encryptionKey, keys.iv); | ||||||
|       (await dhr?.getBytes()) ?? <int>[], |     if (plaintext.isType<MalformedCiphertextError>()) { | ||||||
|  |       return Result(plaintext.get<MalformedCiphertextError>()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Result(plaintext.get<List<int>>()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Checks whether we could decrypt the payload in [header] with a skipped key. If yes, | ||||||
|  |   /// attempts to decrypt it. If not, returns null. | ||||||
|  |   /// | ||||||
|  |   /// If the decryption is successful, returns the plaintext payload. If an error occurs, like | ||||||
|  |   /// an [InvalidMessageHMACError], that is returned instead. | ||||||
|  |   Future<Result<OmemoError, List<int>?>> _trySkippedMessageKeys( | ||||||
|  |     OMEMOAuthenticatedMessage message, | ||||||
|  |     OMEMOMessage header, | ||||||
|  |   ) async { | ||||||
|  |     final key = SkippedKey( | ||||||
|  |       OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519), | ||||||
|  |       header.n, | ||||||
|     ); |     ); | ||||||
|     if (!dhPubMatches) { |     if (mkSkipped.containsKey(key)) { | ||||||
|       await _skipMessageKeys(header.pn!); |       final mk = mkSkipped[key]!; | ||||||
|  |       mkSkipped.remove(key); | ||||||
|  | 
 | ||||||
|  |       return _decrypt(message, header.ciphertext, mk); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return const Result(null); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Decrypt the payload (deeply) embedded in [message]. | ||||||
|  |   /// | ||||||
|  |   /// If everything goes well, returns the plaintext payload. If an error occurs, that | ||||||
|  |   /// is returned instead. | ||||||
|  |   Future<Result<OmemoError, List<int>>> ratchetDecrypt( | ||||||
|  |     OMEMOAuthenticatedMessage message, | ||||||
|  |   ) async { | ||||||
|  |     final header = OMEMOMessage.fromBuffer(message.message); | ||||||
|  | 
 | ||||||
|  |     // Try skipped keys | ||||||
|  |     final plaintextRaw = await _trySkippedMessageKeys(message, header); | ||||||
|  |     if (plaintextRaw.isType<OmemoError>()) { | ||||||
|  |       // Propagate the error | ||||||
|  |       return Result(plaintextRaw.get<OmemoError>()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     final plaintext = plaintextRaw.get<List<int>?>(); | ||||||
|  |     if (plaintext != null) { | ||||||
|  |       return Result(plaintext); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (dhr == null || !listsEqual(header.dhPub, await dhr!.getBytes())) { | ||||||
|  |       final skipResult1 = await _skipMessageKeys(header.pn); | ||||||
|  |       if (skipResult1 != null) { | ||||||
|  |         return Result(skipResult1); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       await _dhRatchet(header); |       await _dhRatchet(header); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await _skipMessageKeys(header.n!); |     final skipResult2 = await _skipMessageKeys(header.n); | ||||||
|     final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); |     if (skipResult2 != null) { | ||||||
|  |       return Result(skipResult2); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     final ck = await kdfCk(ckr!, kdfCkNextChainKey); | ||||||
|     final mk = await kdfCk(ckr!, kdfCkNextMessageKey); |     final mk = await kdfCk(ckr!, kdfCkNextMessageKey); | ||||||
|     ckr = newCkr; |     ckr = ck; | ||||||
|     nr++; |     nr++; | ||||||
| 
 | 
 | ||||||
|     return decrypt( |     return _decrypt(message, header.ciphertext, mk); | ||||||
|       mk, |  | ||||||
|       ciphertext, |  | ||||||
|       concat([sessionAd, header.writeToBuffer()]), |  | ||||||
|       sessionAd, |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /// Encrypt the payload [plaintext] using the double ratchet session. | ||||||
|  |   Future<OMEMOAuthenticatedMessage> ratchetEncrypt(List<int> plaintext) async { | ||||||
|  |     // Advance the ratchet | ||||||
|  |     final ck = await kdfCk(cks!, kdfCkNextChainKey); | ||||||
|  |     final mk = await kdfCk(cks!, kdfCkNextMessageKey); | ||||||
|  |     cks = ck; | ||||||
|  | 
 | ||||||
|  |     // Generate encryption, authentication key and IV | ||||||
|  |     final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); | ||||||
|  |     final ciphertext = | ||||||
|  |         await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv); | ||||||
|  | 
 | ||||||
|  |     // Fill-in the header and serialize it here so we do it only once | ||||||
|  |     final header = OMEMOMessage() | ||||||
|  |       ..dhPub = await dhs.pk.getBytes() | ||||||
|  |       ..pn = pn | ||||||
|  |       ..n = ns | ||||||
|  |       ..ciphertext = ciphertext; | ||||||
|  |     final headerBytes = header.writeToBuffer(); | ||||||
|  | 
 | ||||||
|  |     // Increment the send counter | ||||||
|  |     ns++; | ||||||
|  | 
 | ||||||
|  |     final newAd = concat([sessionAd, headerBytes]); | ||||||
|  |     final hmac = await truncatedHmac(newAd, keys.authenticationKey); | ||||||
|  |     return OMEMOAuthenticatedMessage() | ||||||
|  |       ..mac = hmac | ||||||
|  |       ..message = headerBytes; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Returns a copy of the ratchet. | ||||||
|   OmemoDoubleRatchet clone() { |   OmemoDoubleRatchet clone() { | ||||||
|     return OmemoDoubleRatchet( |     return OmemoDoubleRatchet( | ||||||
|       dhs, |       dhs, | ||||||
| @ -402,27 +370,16 @@ class OmemoDoubleRatchet { | |||||||
|       sessionAd, |       sessionAd, | ||||||
|       Map<SkippedKey, List<int>>.from(mkSkipped), |       Map<SkippedKey, List<int>>.from(mkSkipped), | ||||||
|       acknowledged, |       acknowledged, | ||||||
|       kexTimestamp, |  | ||||||
|       kex, |       kex, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   OmemoDoubleRatchet cloneWithKex(String kex) { |   /// Computes the fingerprint of the double ratchet, according to | ||||||
|     return OmemoDoubleRatchet( |   /// XEP-0384. | ||||||
|       dhs, |   Future<String> get fingerprint async { | ||||||
|       dhr, |     final curveKey = await ik.toCurve25519(); | ||||||
|       rk, |     return HEX.encode( | ||||||
|       cks != null ? List<int>.from(cks!) : null, |       await curveKey.getBytes(), | ||||||
|       ckr != null ? List<int>.from(ckr!) : null, |  | ||||||
|       ns, |  | ||||||
|       nr, |  | ||||||
|       pn, |  | ||||||
|       ik, |  | ||||||
|       sessionAd, |  | ||||||
|       Map<SkippedKey, List<int>>.from(mkSkipped), |  | ||||||
|       acknowledged, |  | ||||||
|       kexTimestamp, |  | ||||||
|       kex, |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -454,7 +411,6 @@ class OmemoDoubleRatchet { | |||||||
|         ns == other.ns && |         ns == other.ns && | ||||||
|         nr == other.nr && |         nr == other.nr && | ||||||
|         pn == other.pn && |         pn == other.pn && | ||||||
|         listsEqual(sessionAd, other.sessionAd) && |         listsEqual(sessionAd, other.sessionAd); | ||||||
|         kexTimestamp == other.kexTimestamp; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ const kdfRkInfoString = 'OMEMO Root Chain'; | |||||||
| const kdfCkNextMessageKey = 0x01; | const kdfCkNextMessageKey = 0x01; | ||||||
| const kdfCkNextChainKey = 0x02; | const kdfCkNextChainKey = 0x02; | ||||||
| 
 | 
 | ||||||
| /// Signals KDF_CK function as specified by OMEMO 0.8.0. | /// Signals KDF_CK function as specified by OMEMO 0.8.3. | ||||||
| Future<List<int>> kdfCk(List<int> ck, int constant) async { | Future<List<int>> kdfCk(List<int> ck, int constant) async { | ||||||
|   final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32); |   final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32); | ||||||
|   final result = await hkdf.deriveKey( |   final result = await hkdf.deriveKey( | ||||||
| @ -19,7 +19,7 @@ Future<List<int>> kdfCk(List<int> ck, int constant) async { | |||||||
|   return result.extractBytes(); |   return result.extractBytes(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Signals KDF_RK function as specified by OMEMO 0.8.0. | /// Signals KDF_RK function as specified by OMEMO 0.8.3. | ||||||
| Future<List<int>> kdfRk(List<int> rk, List<int> dhOut) async { | Future<List<int>> kdfRk(List<int> rk, List<int> dhOut) async { | ||||||
|   final algorithm = Hkdf( |   final algorithm = Hkdf( | ||||||
|     hmac: Hmac(Sha256()), |     hmac: Hmac(Sha256()), | ||||||
|  | |||||||
| @ -1,61 +1,39 @@ | |||||||
| abstract class OmemoException {} | abstract class OmemoError {} | ||||||
| 
 | 
 | ||||||
| /// Triggered during X3DH if the signature if the SPK does verify to the actual SPK. | /// Triggered during X3DH if the signature if the SPK does verify to the actual SPK. | ||||||
| class InvalidSignatureException extends OmemoException implements Exception { | class InvalidKeyExchangeSignatureError extends OmemoError {} | ||||||
|   String errMsg() => |  | ||||||
|       'The signature of the SPK does not match the provided signature'; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC. | /// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC. | ||||||
| /// Triggered by the Session Manager if the computed HMAC does not match the attached HMAC. | class InvalidMessageHMACError extends OmemoError {} | ||||||
| class InvalidMessageHMACException extends OmemoException implements Exception { |  | ||||||
|   String errMsg() => 'The computed HMAC does not match the provided HMAC'; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Triggered by the Double Ratchet if skipping messages would cause skipping more than | /// Triggered by the Double Ratchet if skipping messages would cause skipping more than | ||||||
| /// MAXSKIP messages | /// MAXSKIP messages | ||||||
| class SkippingTooManyMessagesException extends OmemoException | class SkippingTooManyKeysError extends OmemoError {} | ||||||
|     implements Exception { |  | ||||||
|   String errMsg() => 'Skipping messages would cause a skip bigger than MAXSKIP'; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Triggered by the Session Manager if the message key is not encrypted for the device. | /// Triggered by the Session Manager if the message key is not encrypted for the device. | ||||||
| class NotEncryptedForDeviceException extends OmemoException | class NotEncryptedForDeviceError extends OmemoError {} | ||||||
|     implements Exception { |  | ||||||
|   String errMsg() => 'Not encrypted for this device'; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered by the Session Manager when there is no key for decrypting the message. |  | ||||||
| class NoDecryptionKeyException extends OmemoException implements Exception { |  | ||||||
|   String errMsg() => 'No key available for decrypting the message'; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Triggered by the Session Manager when the identifier of the used Signed Prekey | /// Triggered by the Session Manager when the identifier of the used Signed Prekey | ||||||
| /// is neither the current SPK's identifier nor the old one's. | /// is neither the current SPK's identifier nor the old one's. | ||||||
| class UnknownSignedPrekeyException extends OmemoException implements Exception { | class UnknownSignedPrekeyError extends OmemoError {} | ||||||
|   String errMsg() => 'Unknown Signed Prekey used.'; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered by the Session Manager when the received Key Exchange message does not meet |  | ||||||
| /// the requirement that a key exchange, given that the ratchet already exists, must be |  | ||||||
| /// sent after its creation. |  | ||||||
| class InvalidKeyExchangeException extends OmemoException implements Exception { |  | ||||||
|   String errMsg() => 'The key exchange was sent before the last kex finished'; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered by the Session Manager when a message's sequence number is smaller than we |  | ||||||
| /// expect it to be. |  | ||||||
| class MessageAlreadyDecryptedException extends OmemoException |  | ||||||
|     implements Exception { |  | ||||||
|   String errMsg() => 'The message has already been decrypted'; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| /// Triggered by the OmemoManager when we could not encrypt a message as we have | /// Triggered by the OmemoManager when we could not encrypt a message as we have | ||||||
| /// no key material available. That happens, for example, when we want to create a | /// no key material available. That happens, for example, when we want to create a | ||||||
| /// ratchet session with a JID we had no session with but fetching the device bundle | /// ratchet session with a JID we had no session with but fetching the device bundle | ||||||
| /// failed. | /// failed. | ||||||
| class NoKeyMaterialAvailableException extends OmemoException | class NoKeyMaterialAvailableError extends OmemoError {} | ||||||
|     implements Exception { | 
 | ||||||
|   String errMsg() => | /// A non-key-exchange message was received that was encrypted for our device, but we have no ratchet with | ||||||
|       'No key material available to create a ratchet session with'; | /// the device that sent the message. | ||||||
|  | class NoSessionWithDeviceError extends OmemoError {} | ||||||
|  | 
 | ||||||
|  | /// Caused when the AES-256 CBC decryption failed. | ||||||
|  | class MalformedCiphertextError extends OmemoError { | ||||||
|  |   MalformedCiphertextError(this.ex); | ||||||
|  | 
 | ||||||
|  |   /// The exception that was raised while decryption. | ||||||
|  |   final Object ex; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /// Caused by an empty <key /> element | ||||||
|  | class MalformedEncryptedKeyError extends OmemoError {} | ||||||
|  | |||||||
| @ -1,7 +1,5 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
| import 'package:cryptography/cryptography.dart'; |  | ||||||
| import 'package:omemo_dart/src/keys.dart'; |  | ||||||
| 
 | 
 | ||||||
| /// Flattens [inputs] and concatenates the elements. | /// Flattens [inputs] and concatenates the elements. | ||||||
| List<int> concat(List<List<int>> inputs) { | List<int> concat(List<List<int>> inputs) { | ||||||
| @ -43,41 +41,35 @@ int generateRandom32BitNumber() { | |||||||
|   return Random.secure().nextInt(4294967295 /*pow(2, 32) - 1*/); |   return Random.secure().nextInt(4294967295 /*pow(2, 32) - 1*/); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| OmemoPublicKey? decodeKeyIfNotNull( | /// Describes the differences between two lists in terms of its items. | ||||||
|   Map<String, dynamic> map, | class ListDiff<T> { | ||||||
|   String key, |   ListDiff(this.added, this.removed); | ||||||
|   KeyPairType type, |  | ||||||
| ) { |  | ||||||
|   if (map[key] == null) return null; |  | ||||||
| 
 | 
 | ||||||
|   return OmemoPublicKey.fromBytes( |   /// The items that were added. | ||||||
|     base64.decode(map[key]! as String), |   final List<T> added; | ||||||
|     type, | 
 | ||||||
|   ); |   /// The items that were removed. | ||||||
|  |   final List<T> removed; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| List<int>? base64DecodeIfNotNull(Map<String, dynamic> map, String key) { | extension AppendToListOrCreateExtension<K, V> on Map<K, List<V>> { | ||||||
|   if (map[key] == null) return null; |   /// Create or append [value] to the list identified with key [key]. | ||||||
| 
 |   void appendOrCreate(K key, V value, {bool checkExistence = false}) { | ||||||
|   return base64.decode(map[key]! as String); |     if (containsKey(key)) { | ||||||
|  |       if (!checkExistence) { | ||||||
|  |         this[key]!.add(value); | ||||||
|  |       } | ||||||
|  |       if (!this[key]!.contains(value)) { | ||||||
|  |         this[key]!.add(value); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this[key] = [value]; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| String? base64EncodeIfNotNull(List<int>? bytes) { | extension StringFromBase64Extension on String { | ||||||
|   if (bytes == null) return null; |   /// Base64-decode this string. Useful for doing `someString?.fromBase64()` instead | ||||||
| 
 |   /// of `someString != null ? base64Decode(someString) : null`. | ||||||
|   return base64.encode(bytes); |   List<int> fromBase64() => base64Decode(this); | ||||||
| } |  | ||||||
| 
 |  | ||||||
| OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) { |  | ||||||
|   if (pk == null || sk == null) return null; |  | ||||||
| 
 |  | ||||||
|   return OmemoKeyPair.fromBytes( |  | ||||||
|     base64.decode(pk), |  | ||||||
|     base64.decode(sk), |  | ||||||
|     type, |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| int getTimestamp() { |  | ||||||
|   return DateTime.now().millisecondsSinceEpoch; |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,2 +0,0 @@ | |||||||
| /// The info used for when encrypting the AES key for the actual payload. |  | ||||||
| const omemoPayloadInfoString = 'OMEMO Payload'; |  | ||||||
| @ -3,7 +3,15 @@ import 'package:omemo_dart/src/errors.dart'; | |||||||
| 
 | 
 | ||||||
| @immutable | @immutable | ||||||
| class DecryptionResult { | 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; |   final String? payload; | ||||||
|   final OmemoException? error; | 
 | ||||||
|  |   /// 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; | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,76 +22,6 @@ class OmemoDevice { | |||||||
|     this.opks, |     this.opks, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   /// Deserialize the Device |  | ||||||
|   factory OmemoDevice.fromJson(Map<String, dynamic> data) { |  | ||||||
|     // NOTE: We use the way OpenSSH names their keys, meaning that ik is the Identity |  | ||||||
|     //       Keypair's private key, while ik_pub refers to the Identity Keypair's public |  | ||||||
|     //       key. |  | ||||||
|     /* |  | ||||||
|     { |  | ||||||
|       'jid': 'alice@...', |  | ||||||
|       'id': 123, |  | ||||||
|       'ik': 'base/64/encoded', |  | ||||||
|       'ik_pub': 'base/64/encoded', |  | ||||||
|       'spk': 'base/64/encoded', |  | ||||||
|       'spk_pub': 'base/64/encoded', |  | ||||||
|       'spk_id': 123, |  | ||||||
|       'spk_sig': 'base/64/encoded', |  | ||||||
|       'old_spk': 'base/64/encoded', |  | ||||||
|       'old_spk_pub': 'base/64/encoded', |  | ||||||
|       'old_spk_id': 122, |  | ||||||
|       'opks': [ |  | ||||||
|         { |  | ||||||
|           'id': 0, |  | ||||||
|           'public': 'base/64/encoded', |  | ||||||
|           'private': 'base/64/encoded' |  | ||||||
|         }, ... |  | ||||||
|       ] |  | ||||||
|     } |  | ||||||
|     */ |  | ||||||
|     // NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as |  | ||||||
|     //       such we need to convert the items by hand. |  | ||||||
|     final opks = Map<int, OmemoKeyPair>.fromEntries( |  | ||||||
|       (data['opks']! as List<dynamic>).map<MapEntry<int, OmemoKeyPair>>( |  | ||||||
|         (opk) { |  | ||||||
|           final map = opk as Map<String, dynamic>; |  | ||||||
|           return MapEntry( |  | ||||||
|             map['id']! as int, |  | ||||||
|             OmemoKeyPair.fromBytes( |  | ||||||
|               base64.decode(map['public']! as String), |  | ||||||
|               base64.decode(map['private']! as String), |  | ||||||
|               KeyPairType.x25519, |  | ||||||
|             ), |  | ||||||
|           ); |  | ||||||
|         }, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     return OmemoDevice( |  | ||||||
|       data['jid']! as String, |  | ||||||
|       data['id']! as int, |  | ||||||
|       OmemoKeyPair.fromBytes( |  | ||||||
|         base64.decode(data['ik_pub']! as String), |  | ||||||
|         base64.decode(data['ik']! as String), |  | ||||||
|         KeyPairType.ed25519, |  | ||||||
|       ), |  | ||||||
|       OmemoKeyPair.fromBytes( |  | ||||||
|         base64.decode(data['spk_pub']! as String), |  | ||||||
|         base64.decode(data['spk']! as String), |  | ||||||
|         KeyPairType.x25519, |  | ||||||
|       ), |  | ||||||
|       data['spk_id']! as int, |  | ||||||
|       base64.decode(data['spk_sig']! as String), |  | ||||||
|       decodeKeyPairIfNotNull( |  | ||||||
|         data['old_spk_pub'] as String?, |  | ||||||
|         data['old_spk'] as String?, |  | ||||||
|         KeyPairType.x25519, |  | ||||||
|       ), |  | ||||||
|       data['old_spk_id'] as int?, |  | ||||||
|       opks, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Generate a completely new device, i.e. cryptographic identity. |   /// Generate a completely new device, i.e. cryptographic identity. | ||||||
|   static Future<OmemoDevice> generateNewDevice( |   static Future<OmemoDevice> generateNewDevice( | ||||||
|     String jid, { |     String jid, { | ||||||
| @ -105,7 +35,16 @@ class OmemoDevice { | |||||||
| 
 | 
 | ||||||
|     final opks = <int, OmemoKeyPair>{}; |     final opks = <int, OmemoKeyPair>{}; | ||||||
|     for (var i = 0; i < opkAmount; i++) { |     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); |     return OmemoDevice(jid, id, ik, spk, spkId, signature, null, null, opks); | ||||||
| @ -142,7 +81,18 @@ class OmemoDevice { | |||||||
|   /// a new Device object that copies over everything but replaces said key. |   /// a new Device object that copies over everything but replaces said key. | ||||||
|   @internal |   @internal | ||||||
|   Future<OmemoDevice> replaceOnetimePrekey(int id) async { |   Future<OmemoDevice> 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( |     return OmemoDevice( | ||||||
|       jid, |       jid, | ||||||
| @ -221,34 +171,6 @@ class OmemoDevice { | |||||||
|     return HEX.encode(await curveKey.getBytes()); |     return HEX.encode(await curveKey.getBytes()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Serialise the device information. |  | ||||||
|   Future<Map<String, dynamic>> toJson() async { |  | ||||||
|     /// Serialise the OPKs |  | ||||||
|     final serialisedOpks = List<Map<String, dynamic>>.empty(growable: true); |  | ||||||
|     for (final entry in opks.entries) { |  | ||||||
|       serialisedOpks.add({ |  | ||||||
|         'id': entry.key, |  | ||||||
|         'public': base64.encode(await entry.value.pk.getBytes()), |  | ||||||
|         'private': base64.encode(await entry.value.sk.getBytes()), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       'jid': jid, |  | ||||||
|       'id': id, |  | ||||||
|       'ik': base64.encode(await ik.sk.getBytes()), |  | ||||||
|       'ik_pub': base64.encode(await ik.pk.getBytes()), |  | ||||||
|       'spk': base64.encode(await spk.sk.getBytes()), |  | ||||||
|       'spk_pub': base64.encode(await spk.pk.getBytes()), |  | ||||||
|       'spk_id': spkId, |  | ||||||
|       'spk_sig': base64.encode(spkSignature), |  | ||||||
|       'old_spk': base64EncodeIfNotNull(await oldSpk?.sk.getBytes()), |  | ||||||
|       'old_spk_pub': base64EncodeIfNotNull(await oldSpk?.pk.getBytes()), |  | ||||||
|       'old_spk_id': oldSpkId, |  | ||||||
|       'opks': serialisedOpks, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @visibleForTesting |   @visibleForTesting | ||||||
|   Future<bool> equals(OmemoDevice other) async { |   Future<bool> equals(OmemoDevice other) async { | ||||||
|     var opksMatch = true; |     var opksMatch = true; | ||||||
|  | |||||||
| @ -1,12 +1,23 @@ | |||||||
|  | import 'dart:convert'; | ||||||
|  | 
 | ||||||
| import 'package:meta/meta.dart'; | import 'package:meta/meta.dart'; | ||||||
| 
 | 
 | ||||||
| /// EncryptedKey is the intermediary format of a <key /> element in the OMEMO message's | /// EncryptedKey is the intermediary format of a <key /> element in the OMEMO message's | ||||||
| /// <keys /> header. | /// <keys /> header. | ||||||
| @immutable | @immutable | ||||||
| class EncryptedKey { | class EncryptedKey { | ||||||
|   const EncryptedKey(this.jid, this.rid, this.value, this.kex); |   const EncryptedKey(this.rid, this.value, this.kex); | ||||||
|   final String jid; | 
 | ||||||
|  |   /// The id of the device the key is encrypted for. | ||||||
|   final int rid; |   final int rid; | ||||||
|  | 
 | ||||||
|  |   /// The base64-encoded payload. | ||||||
|   final String value; |   final String value; | ||||||
|  | 
 | ||||||
|  |   /// Flag indicating whether the payload is a OMEMOKeyExchange (true) or | ||||||
|  |   /// an OMEMOAuthenticatedMessage (false). | ||||||
|   final bool kex; |   final bool kex; | ||||||
|  | 
 | ||||||
|  |   /// The base64-decoded payload. | ||||||
|  |   List<int> get data => base64Decode(value); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import 'package:meta/meta.dart'; | import 'package:meta/meta.dart'; | ||||||
| import 'package:omemo_dart/src/errors.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/encrypted_key.dart'; | import 'package:omemo_dart/src/omemo/encrypted_key.dart'; | ||||||
| import 'package:omemo_dart/src/omemo/ratchet_map_key.dart'; | import 'package:omemo_dart/src/omemo/errors.dart'; | ||||||
| 
 | 
 | ||||||
| @immutable | @immutable | ||||||
| class EncryptionResult { | class EncryptionResult { | ||||||
| @ -9,7 +8,7 @@ class EncryptionResult { | |||||||
|     this.ciphertext, |     this.ciphertext, | ||||||
|     this.encryptedKeys, |     this.encryptedKeys, | ||||||
|     this.deviceEncryptionErrors, |     this.deviceEncryptionErrors, | ||||||
|     this.jidEncryptionErrors, |     this.canSend, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   /// The actual message that was encrypted. |   /// The actual message that was encrypted. | ||||||
| @ -17,17 +16,12 @@ class EncryptionResult { | |||||||
| 
 | 
 | ||||||
|   /// Mapping of the device Id to the key for decrypting ciphertext, encrypted |   /// Mapping of the device Id to the key for decrypting ciphertext, encrypted | ||||||
|   /// for the ratchet with said device Id. |   /// for the ratchet with said device Id. | ||||||
|   final List<EncryptedKey> encryptedKeys; |   final Map<String, List<EncryptedKey>> encryptedKeys; | ||||||
| 
 | 
 | ||||||
|   /// Mapping of a ratchet map keys to a possible exception. |   /// Mapping of a JID to | ||||||
|   final Map<RatchetMapKey, OmemoException> deviceEncryptionErrors; |   final Map<String, List<EncryptToJidError>> deviceEncryptionErrors; | ||||||
| 
 | 
 | ||||||
|   /// Mapping of a JID to a possible exception. |   /// A flag indicating that the message could be sent like that, i.e. we were able | ||||||
|   final Map<String, OmemoException> jidEncryptionErrors; |   /// to encrypt to at-least one device per recipient. | ||||||
| 
 |   final bool canSend; | ||||||
|   /// True if the encryption was a success. This means that we could encrypt for |  | ||||||
|   /// at least one ratchet. |  | ||||||
|   bool isSuccess(int numberOfRecipients) => |  | ||||||
|       encryptedKeys.isNotEmpty && |  | ||||||
|       jidEncryptionErrors.length < numberOfRecipients; |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								lib/src/omemo/errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lib/src/omemo/errors.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import 'package:omemo_dart/src/errors.dart'; | ||||||
|  | 
 | ||||||
|  | /// Returned on encryption, if encryption failed for some reason. | ||||||
|  | class EncryptToJidError extends OmemoError { | ||||||
|  |   EncryptToJidError(this.device, this.error); | ||||||
|  | 
 | ||||||
|  |   /// The device the error occurred with | ||||||
|  |   final int? device; | ||||||
|  | 
 | ||||||
|  |   /// The actual error. | ||||||
|  |   final OmemoError error; | ||||||
|  | } | ||||||
| @ -1,44 +0,0 @@ | |||||||
| import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/device.dart'; |  | ||||||
| 
 |  | ||||||
| abstract class OmemoEvent {} |  | ||||||
| 
 |  | ||||||
| /// Triggered when a ratchet has been modified |  | ||||||
| class RatchetModifiedEvent extends OmemoEvent { |  | ||||||
|   RatchetModifiedEvent( |  | ||||||
|     this.jid, |  | ||||||
|     this.deviceId, |  | ||||||
|     this.ratchet, |  | ||||||
|     this.added, |  | ||||||
|     this.replaced, |  | ||||||
|   ); |  | ||||||
|   final String jid; |  | ||||||
|   final int deviceId; |  | ||||||
|   final OmemoDoubleRatchet ratchet; |  | ||||||
| 
 |  | ||||||
|   /// Indicates whether the ratchet has just been created (true) or just modified (false). |  | ||||||
|   final bool added; |  | ||||||
| 
 |  | ||||||
|   /// Indicates whether the ratchet has been replaced (true) or not. |  | ||||||
|   final bool replaced; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered when a ratchet has been removed and should be removed from storage. |  | ||||||
| class RatchetRemovedEvent extends OmemoEvent { |  | ||||||
|   RatchetRemovedEvent(this.jid, this.deviceId); |  | ||||||
|   final String jid; |  | ||||||
|   final int deviceId; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered when the device map has been modified |  | ||||||
| class DeviceListModifiedEvent extends OmemoEvent { |  | ||||||
|   DeviceListModifiedEvent(this.list); |  | ||||||
|   final Map<String, List<int>> list; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Triggered by the OmemoSessionManager when our own device bundle was modified |  | ||||||
| /// and thus should be republished. |  | ||||||
| class DeviceModifiedEvent extends OmemoEvent { |  | ||||||
|   DeviceModifiedEvent(this.device); |  | ||||||
|   final OmemoDevice device; |  | ||||||
| } |  | ||||||
							
								
								
									
										1040
									
								
								lib/src/omemo/omemo.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1040
									
								
								lib/src/omemo/omemo.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,854 +0,0 @@ | |||||||
| import 'dart:async'; |  | ||||||
| import 'dart:collection'; |  | ||||||
| import 'dart:convert'; |  | ||||||
| import 'package:collection/collection.dart'; |  | ||||||
| import 'package:cryptography/cryptography.dart'; |  | ||||||
| import 'package:hex/hex.dart'; |  | ||||||
| import 'package:logging/logging.dart'; |  | ||||||
| import 'package:meta/meta.dart'; |  | ||||||
| import 'package:omemo_dart/src/crypto.dart'; |  | ||||||
| import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; |  | ||||||
| import 'package:omemo_dart/src/errors.dart'; |  | ||||||
| import 'package:omemo_dart/src/helpers.dart'; |  | ||||||
| import 'package:omemo_dart/src/keys.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/bundle.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/constants.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/decryption_result.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/device.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/encrypted_key.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/encryption_result.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/events.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/fingerprint.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/ratchet_map_key.dart'; |  | ||||||
| import 'package:omemo_dart/src/omemo/stanza.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/trust/base.dart'; |  | ||||||
| import 'package:omemo_dart/src/x3dh/x3dh.dart'; |  | ||||||
| import 'package:synchronized/synchronized.dart'; |  | ||||||
| 
 |  | ||||||
| class _InternalDecryptionResult { |  | ||||||
|   const _InternalDecryptionResult( |  | ||||||
|     this.ratchetCreated, |  | ||||||
|     this.ratchetReplaced, |  | ||||||
|     this.payload, |  | ||||||
|   ) : assert( |  | ||||||
|           !ratchetCreated || !ratchetReplaced, |  | ||||||
|           'Ratchet must be either replaced or created', |  | ||||||
|         ); |  | ||||||
|   final bool ratchetCreated; |  | ||||||
|   final bool ratchetReplaced; |  | ||||||
|   final String? payload; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class OmemoManager { |  | ||||||
|   OmemoManager( |  | ||||||
|     this._device, |  | ||||||
|     this._trustManager, |  | ||||||
|     this.sendEmptyOmemoMessageImpl, |  | ||||||
|     this.fetchDeviceListImpl, |  | ||||||
|     this.fetchDeviceBundleImpl, |  | ||||||
|     this.subscribeToDeviceListNodeImpl, |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   final Logger _log = Logger('OmemoManager'); |  | ||||||
| 
 |  | ||||||
|   /// Functions for connecting with the OMEMO library |  | ||||||
| 
 |  | ||||||
|   /// Send an empty OMEMO:2 message using the encrypted payload @result to |  | ||||||
|   /// @recipientJid. |  | ||||||
|   final Future<void> Function(EncryptionResult result, String recipientJid) |  | ||||||
|       sendEmptyOmemoMessageImpl; |  | ||||||
| 
 |  | ||||||
|   /// Fetch the list of device ids associated with @jid. If the device list cannot be |  | ||||||
|   /// fetched, return null. |  | ||||||
|   final Future<List<int>?> Function(String jid) fetchDeviceListImpl; |  | ||||||
| 
 |  | ||||||
|   /// Fetch the device bundle for the device with id @id of jid. If it cannot be fetched, return null. |  | ||||||
|   final Future<OmemoBundle?> Function(String jid, int id) fetchDeviceBundleImpl; |  | ||||||
| 
 |  | ||||||
|   /// Subscribe to the device list PEP node of @jid. |  | ||||||
|   final Future<void> Function(String jid) subscribeToDeviceListNodeImpl; |  | ||||||
| 
 |  | ||||||
|   /// Map bare JID to its known devices |  | ||||||
|   Map<String, List<int>> _deviceList = {}; |  | ||||||
| 
 |  | ||||||
|   /// Map bare JIDs to whether we already requested the device list once |  | ||||||
|   final Map<String, bool> _deviceListRequested = {}; |  | ||||||
| 
 |  | ||||||
|   /// Map bare a ratchet key to its ratchet. Note that this is also locked by |  | ||||||
|   /// _ratchetCriticalSectionLock. |  | ||||||
|   Map<RatchetMapKey, OmemoDoubleRatchet> _ratchetMap = {}; |  | ||||||
| 
 |  | ||||||
|   /// Map bare JID to whether we already tried to subscribe to the device list node. |  | ||||||
|   final Map<String, bool> _subscriptionMap = {}; |  | ||||||
| 
 |  | ||||||
|   /// For preventing a race condition in encryption/decryption |  | ||||||
|   final Map<String, Queue<Completer<void>>> _ratchetCriticalSectionQueue = {}; |  | ||||||
|   final Lock _ratchetCriticalSectionLock = Lock(); |  | ||||||
| 
 |  | ||||||
|   /// The OmemoManager's trust management |  | ||||||
|   final TrustManager _trustManager; |  | ||||||
|   TrustManager get trustManager => _trustManager; |  | ||||||
| 
 |  | ||||||
|   /// Our own keys... |  | ||||||
|   final Lock _deviceLock = Lock(); |  | ||||||
|   // ignore: prefer_final_fields |  | ||||||
|   OmemoDevice _device; |  | ||||||
| 
 |  | ||||||
|   /// The event bus of the session manager |  | ||||||
|   final StreamController<OmemoEvent> _eventStreamController = |  | ||||||
|       StreamController<OmemoEvent>.broadcast(); |  | ||||||
|   Stream<OmemoEvent> get eventStream => _eventStreamController.stream; |  | ||||||
| 
 |  | ||||||
|   /// Enter the critical section for performing cryptographic operations on the ratchets |  | ||||||
|   Future<void> _enterRatchetCriticalSection(String jid) async { |  | ||||||
|     final completer = await _ratchetCriticalSectionLock.synchronized(() { |  | ||||||
|       if (_ratchetCriticalSectionQueue.containsKey(jid)) { |  | ||||||
|         final c = Completer<void>(); |  | ||||||
|         _ratchetCriticalSectionQueue[jid]!.addLast(c); |  | ||||||
|         return c; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       _ratchetCriticalSectionQueue[jid] = Queue(); |  | ||||||
|       return null; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (completer != null) { |  | ||||||
|       await completer.future; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Leave the critical section for the ratchets. |  | ||||||
|   Future<void> _leaveRatchetCriticalSection(String jid) async { |  | ||||||
|     await _ratchetCriticalSectionLock.synchronized(() { |  | ||||||
|       if (_ratchetCriticalSectionQueue.containsKey(jid)) { |  | ||||||
|         if (_ratchetCriticalSectionQueue[jid]!.isEmpty) { |  | ||||||
|           _ratchetCriticalSectionQueue.remove(jid); |  | ||||||
|         } else { |  | ||||||
|           _ratchetCriticalSectionQueue[jid]!.removeFirst().complete(); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   Future<String?> _decryptAndVerifyHmac( |  | ||||||
|     List<int>? ciphertext, |  | ||||||
|     List<int> keyAndHmac, |  | ||||||
|   ) async { |  | ||||||
|     // Empty OMEMO messages should just have the key decrypted and/or session set up. |  | ||||||
|     if (ciphertext == null) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final key = keyAndHmac.sublist(0, 32); |  | ||||||
|     final hmac = keyAndHmac.sublist(32, 48); |  | ||||||
|     final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString); |  | ||||||
|     final computedHmac = |  | ||||||
|         await truncatedHmac(ciphertext, derivedKeys.authenticationKey); |  | ||||||
|     if (!listsEqual(hmac, computedHmac)) { |  | ||||||
|       throw InvalidMessageHMACException(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return utf8.decode( |  | ||||||
|       await aes256CbcDecrypt( |  | ||||||
|         ciphertext, |  | ||||||
|         derivedKeys.encryptionKey, |  | ||||||
|         derivedKeys.iv, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Add a session [ratchet] with the [deviceId] to the internal tracking state. |  | ||||||
|   /// NOTE: Must be called from within the ratchet critical section. |  | ||||||
|   void _addSession(String jid, int deviceId, OmemoDoubleRatchet ratchet) { |  | ||||||
|     // Add the bundle Id |  | ||||||
|     if (!_deviceList.containsKey(jid)) { |  | ||||||
|       _deviceList[jid] = [deviceId]; |  | ||||||
| 
 |  | ||||||
|       // Commit the device map |  | ||||||
|       _eventStreamController.add(DeviceListModifiedEvent(_deviceList)); |  | ||||||
|     } else { |  | ||||||
|       // Prevent having the same device multiple times in the list |  | ||||||
|       if (!_deviceList[jid]!.contains(deviceId)) { |  | ||||||
|         _deviceList[jid]!.add(deviceId); |  | ||||||
| 
 |  | ||||||
|         // Commit the device map |  | ||||||
|         _eventStreamController.add(DeviceListModifiedEvent(_deviceList)); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Add the ratchet session |  | ||||||
|     final key = RatchetMapKey(jid, deviceId); |  | ||||||
|     _ratchetMap[key] = ratchet; |  | ||||||
| 
 |  | ||||||
|     // Commit the ratchet |  | ||||||
|     _eventStreamController |  | ||||||
|         .add(RatchetModifiedEvent(jid, deviceId, ratchet, true, false)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Build a new session with the user at [jid] with the device [deviceId] using data |  | ||||||
|   /// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey |  | ||||||
|   /// identifier an UnknownSignedPrekeyException will be thrown. |  | ||||||
|   Future<OmemoDoubleRatchet> _addSessionFromKeyExchange( |  | ||||||
|     String jid, |  | ||||||
|     int deviceId, |  | ||||||
|     OmemoKeyExchange kex, |  | ||||||
|   ) async { |  | ||||||
|     // Pick the correct SPK |  | ||||||
|     final device = await getDevice(); |  | ||||||
|     OmemoKeyPair spk; |  | ||||||
|     if (kex.spkId == _device.spkId) { |  | ||||||
|       spk = _device.spk; |  | ||||||
|     } else if (kex.spkId == _device.oldSpkId) { |  | ||||||
|       spk = _device.oldSpk!; |  | ||||||
|     } else { |  | ||||||
|       throw UnknownSignedPrekeyException(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final kexResult = await x3dhFromInitialMessage( |  | ||||||
|       X3DHMessage( |  | ||||||
|         OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), |  | ||||||
|         OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519), |  | ||||||
|         kex.pkId!, |  | ||||||
|       ), |  | ||||||
|       spk, |  | ||||||
|       device.opks.values.elementAt(kex.pkId!), |  | ||||||
|       device.ik, |  | ||||||
|     ); |  | ||||||
|     final ratchet = await OmemoDoubleRatchet.acceptNewSession( |  | ||||||
|       spk, |  | ||||||
|       OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), |  | ||||||
|       kexResult.sk, |  | ||||||
|       kexResult.ad, |  | ||||||
|       getTimestamp(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Notify the trust manager |  | ||||||
|     await trustManager.onNewSession(jid, deviceId); |  | ||||||
| 
 |  | ||||||
|     return ratchet; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device |  | ||||||
|   /// [deviceId] from the bundle [bundle]. |  | ||||||
|   @visibleForTesting |  | ||||||
|   Future<OmemoKeyExchange> addSessionFromBundle( |  | ||||||
|     String jid, |  | ||||||
|     int deviceId, |  | ||||||
|     OmemoBundle bundle, |  | ||||||
|   ) async { |  | ||||||
|     final device = await getDevice(); |  | ||||||
|     final kexResult = await x3dhFromBundle( |  | ||||||
|       bundle, |  | ||||||
|       device.ik, |  | ||||||
|     ); |  | ||||||
|     final ratchet = await OmemoDoubleRatchet.initiateNewSession( |  | ||||||
|       bundle.spk, |  | ||||||
|       bundle.ik, |  | ||||||
|       kexResult.sk, |  | ||||||
|       kexResult.ad, |  | ||||||
|       getTimestamp(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await _trustManager.onNewSession(jid, deviceId); |  | ||||||
|     _addSession(jid, deviceId, ratchet); |  | ||||||
| 
 |  | ||||||
|     return OmemoKeyExchange() |  | ||||||
|       ..pkId = kexResult.opkId |  | ||||||
|       ..spkId = bundle.spkId |  | ||||||
|       ..ik = await device.ik.pk.getBytes() |  | ||||||
|       ..ek = await kexResult.ek.pk.getBytes(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// In case a decryption error occurs, the Double Ratchet spec says to just restore |  | ||||||
|   /// the ratchet to its old state. As such, this function restores the ratchet at |  | ||||||
|   /// [mapKey] with [oldRatchet]. |  | ||||||
|   /// NOTE: Must be called from within the ratchet critical section |  | ||||||
|   void _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) { |  | ||||||
|     _log.finest( |  | ||||||
|       'Restoring ratchet ${mapKey.jid}:${mapKey.deviceId} to ${oldRatchet.nr}', |  | ||||||
|     ); |  | ||||||
|     _ratchetMap[mapKey] = oldRatchet; |  | ||||||
| 
 |  | ||||||
|     // Commit the ratchet |  | ||||||
|     _eventStreamController.add( |  | ||||||
|       RatchetModifiedEvent( |  | ||||||
|         mapKey.jid, |  | ||||||
|         mapKey.deviceId, |  | ||||||
|         oldRatchet, |  | ||||||
|         false, |  | ||||||
|         false, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the |  | ||||||
|   /// <keys /> element with a "jid" attribute matching our own. [senderJid] refers to the |  | ||||||
|   /// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the |  | ||||||
|   /// <encrypted /> element. |  | ||||||
|   /// [timestamp] refers to the time the message was sent. This might be either what the |  | ||||||
|   /// server tells you via "XEP-0203: Delayed Delivery" or the point in time at which |  | ||||||
|   /// you received the stanza, if no Delayed Delivery element was found. |  | ||||||
|   /// |  | ||||||
|   /// If the received message is an empty OMEMO message, i.e. there is no <payload /> |  | ||||||
|   /// element, then [ciphertext] must be set to null. In this case, this function |  | ||||||
|   /// will return null as there is no message to be decrypted. This, however, is used |  | ||||||
|   /// to set up sessions or advance the ratchets. |  | ||||||
|   Future<_InternalDecryptionResult> _decryptMessage( |  | ||||||
|     List<int>? ciphertext, |  | ||||||
|     String senderJid, |  | ||||||
|     int senderDeviceId, |  | ||||||
|     List<EncryptedKey> keys, |  | ||||||
|     int timestamp, |  | ||||||
|   ) async { |  | ||||||
|     // Try to find a session we can decrypt with. |  | ||||||
|     var device = await getDevice(); |  | ||||||
|     final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id); |  | ||||||
|     if (rawKey == null) { |  | ||||||
|       throw NotEncryptedForDeviceException(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final decodedRawKey = base64.decode(rawKey.value); |  | ||||||
|     List<int>? keyAndHmac; |  | ||||||
|     OmemoAuthenticatedMessage authMessage; |  | ||||||
|     OmemoMessage? message; |  | ||||||
| 
 |  | ||||||
|     // If the ratchet already existed, we store it. If it didn't, oldRatchet will stay |  | ||||||
|     // null. |  | ||||||
|     final ratchetKey = RatchetMapKey(senderJid, senderDeviceId); |  | ||||||
|     final oldRatchet = getRatchet(ratchetKey)?.clone(); |  | ||||||
|     if (rawKey.kex) { |  | ||||||
|       final kex = OmemoKeyExchange.fromBuffer(decodedRawKey); |  | ||||||
|       authMessage = kex.message!; |  | ||||||
|       message = OmemoMessage.fromBuffer(authMessage.message!); |  | ||||||
| 
 |  | ||||||
|       // Guard against old key exchanges |  | ||||||
|       if (oldRatchet != null) { |  | ||||||
|         _log.finest( |  | ||||||
|           'KEX for existent ratchet ${ratchetKey.toJsonKey()}. ${oldRatchet.kexTimestamp} > $timestamp: ${oldRatchet.kexTimestamp > timestamp}', |  | ||||||
|         ); |  | ||||||
|         if (oldRatchet.kexTimestamp > timestamp) { |  | ||||||
|           throw InvalidKeyExchangeException(); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       final r = |  | ||||||
|           await _addSessionFromKeyExchange(senderJid, senderDeviceId, kex); |  | ||||||
| 
 |  | ||||||
|       // Try to decrypt with the new ratchet r |  | ||||||
|       try { |  | ||||||
|         keyAndHmac = |  | ||||||
|             await r.ratchetDecrypt(message, authMessage.writeToBuffer()); |  | ||||||
|         final result = await _decryptAndVerifyHmac(ciphertext, keyAndHmac); |  | ||||||
| 
 |  | ||||||
|         // Add the new ratchet |  | ||||||
|         _addSession(senderJid, senderDeviceId, r); |  | ||||||
| 
 |  | ||||||
|         // Replace the OPK |  | ||||||
|         await _deviceLock.synchronized(() async { |  | ||||||
|           device = await device.replaceOnetimePrekey(kex.pkId!); |  | ||||||
| 
 |  | ||||||
|           // Commit the device |  | ||||||
|           _eventStreamController.add(DeviceModifiedEvent(device)); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         // Commit the ratchet |  | ||||||
|         _eventStreamController.add( |  | ||||||
|           RatchetModifiedEvent( |  | ||||||
|             senderJid, |  | ||||||
|             senderDeviceId, |  | ||||||
|             r, |  | ||||||
|             oldRatchet == null, |  | ||||||
|             oldRatchet != null, |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         return _InternalDecryptionResult( |  | ||||||
|           oldRatchet == null, |  | ||||||
|           oldRatchet != null, |  | ||||||
|           result, |  | ||||||
|         ); |  | ||||||
|       } catch (ex) { |  | ||||||
|         _log.finest('Kex failed due to $ex. Not proceeding with kex.'); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey); |  | ||||||
|       message = OmemoMessage.fromBuffer(authMessage.message!); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final devices = _deviceList[senderJid]; |  | ||||||
|     if (devices?.contains(senderDeviceId) != true) { |  | ||||||
|       throw NoDecryptionKeyException(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // TODO(PapaTutuWawa): When receiving a message that is not an OMEMOKeyExchange from a device there is no session with, clients SHOULD create a session with that device and notify it about the new session by responding with an empty OMEMO message as per Sending a message. |  | ||||||
| 
 |  | ||||||
|     // We can guarantee that the ratchet exists at this point in time |  | ||||||
|     final ratchet = getRatchet(ratchetKey)!; |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       if (rawKey.kex) { |  | ||||||
|         keyAndHmac = |  | ||||||
|             await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer()); |  | ||||||
|       } else { |  | ||||||
|         keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey); |  | ||||||
|       } |  | ||||||
|     } catch (_) { |  | ||||||
|       _restoreRatchet(ratchetKey, oldRatchet!); |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Commit the ratchet |  | ||||||
|     _eventStreamController.add( |  | ||||||
|       RatchetModifiedEvent( |  | ||||||
|         senderJid, |  | ||||||
|         senderDeviceId, |  | ||||||
|         ratchet, |  | ||||||
|         false, |  | ||||||
|         false, |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       return _InternalDecryptionResult( |  | ||||||
|         false, |  | ||||||
|         false, |  | ||||||
|         await _decryptAndVerifyHmac(ciphertext, keyAndHmac), |  | ||||||
|       ); |  | ||||||
|     } catch (_) { |  | ||||||
|       _restoreRatchet(ratchetKey, oldRatchet!); |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Returns, if it exists, the ratchet associated with [key]. |  | ||||||
|   /// NOTE: Must be called from within the ratchet critical section. |  | ||||||
|   @visibleForTesting |  | ||||||
|   OmemoDoubleRatchet? getRatchet(RatchetMapKey key) => _ratchetMap[key]; |  | ||||||
| 
 |  | ||||||
|   /// Figure out what bundles we have to still build a session with. |  | ||||||
|   Future<List<OmemoBundle>> _fetchNewBundles(String jid) async { |  | ||||||
|     // Check if we already requested the device list for [jid] |  | ||||||
|     List<int> bundlesToFetch; |  | ||||||
|     if (!_deviceListRequested.containsKey(jid) || |  | ||||||
|         !_deviceList.containsKey(jid)) { |  | ||||||
|       // We don't have an up-to-date version of the device list |  | ||||||
|       final newDeviceList = await fetchDeviceListImpl(jid); |  | ||||||
|       if (newDeviceList == null) return []; |  | ||||||
| 
 |  | ||||||
|       _deviceList[jid] = newDeviceList; |  | ||||||
|       bundlesToFetch = newDeviceList.where((id) { |  | ||||||
|         return !_ratchetMap.containsKey(RatchetMapKey(jid, id)) || |  | ||||||
|             _deviceList[jid]?.contains(id) == false; |  | ||||||
|       }).toList(); |  | ||||||
| 
 |  | ||||||
|       // Trigger an event with the new device list |  | ||||||
|       _eventStreamController.add(DeviceListModifiedEvent(_deviceList)); |  | ||||||
|     } else { |  | ||||||
|       // We already have an up-to-date version of the device list |  | ||||||
|       bundlesToFetch = _deviceList[jid]! |  | ||||||
|           .where((id) => !_ratchetMap.containsKey(RatchetMapKey(jid, id))) |  | ||||||
|           .toList(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (bundlesToFetch.isNotEmpty) { |  | ||||||
|       _log.finest('Fetching bundles $bundlesToFetch for $jid'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final device = await getDevice(); |  | ||||||
|     final newBundles = List<OmemoBundle>.empty(growable: true); |  | ||||||
|     for (final id in bundlesToFetch) { |  | ||||||
|       if (jid == device.jid && id == device.id) continue; |  | ||||||
| 
 |  | ||||||
|       final bundle = await fetchDeviceBundleImpl(jid, id); |  | ||||||
|       if (bundle != null) newBundles.add(bundle); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return newBundles; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Encrypt the key [plaintext] for all known bundles of the Jids in [jids]. Returns a |  | ||||||
|   /// map that maps the device Id to the ciphertext of [plaintext]. |  | ||||||
|   /// |  | ||||||
|   /// If [plaintext] is null, then the result will be an empty OMEMO message, i.e. one that |  | ||||||
|   /// does not contain a <payload /> element. This means that the ciphertext attribute of |  | ||||||
|   /// the result will be null as well. |  | ||||||
|   /// NOTE: Must be called within the ratchet critical section |  | ||||||
|   Future<EncryptionResult> _encryptToJids( |  | ||||||
|     List<String> jids, |  | ||||||
|     String? plaintext, |  | ||||||
|   ) async { |  | ||||||
|     final encryptedKeys = List<EncryptedKey>.empty(growable: true); |  | ||||||
| 
 |  | ||||||
|     var ciphertext = const <int>[]; |  | ||||||
|     var keyPayload = const <int>[]; |  | ||||||
|     if (plaintext != null) { |  | ||||||
|       // Generate the key and encrypt the plaintext |  | ||||||
|       final key = generateRandomBytes(32); |  | ||||||
|       final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString); |  | ||||||
|       ciphertext = await aes256CbcEncrypt( |  | ||||||
|         utf8.encode(plaintext), |  | ||||||
|         keys.encryptionKey, |  | ||||||
|         keys.iv, |  | ||||||
|       ); |  | ||||||
|       final hmac = await truncatedHmac(ciphertext, keys.authenticationKey); |  | ||||||
|       keyPayload = concat([key, hmac]); |  | ||||||
|     } else { |  | ||||||
|       keyPayload = List<int>.filled(32, 0x0); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final kex = <RatchetMapKey, OmemoKeyExchange>{}; |  | ||||||
|     for (final jid in jids) { |  | ||||||
|       for (final newSession in await _fetchNewBundles(jid)) { |  | ||||||
|         kex[RatchetMapKey(jid, newSession.id)] = await addSessionFromBundle( |  | ||||||
|           newSession.jid, |  | ||||||
|           newSession.id, |  | ||||||
|           newSession, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // We assume that the user already checked if the session exists |  | ||||||
|     final deviceEncryptionErrors = <RatchetMapKey, OmemoException>{}; |  | ||||||
|     final jidEncryptionErrors = <String, OmemoException>{}; |  | ||||||
|     for (final jid in jids) { |  | ||||||
|       final devices = _deviceList[jid]; |  | ||||||
|       if (devices == null) { |  | ||||||
|         _log.severe('Device list does not exist for $jid.'); |  | ||||||
|         jidEncryptionErrors[jid] = NoKeyMaterialAvailableException(); |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if (!_subscriptionMap.containsKey(jid)) { |  | ||||||
|         unawaited(subscribeToDeviceListNodeImpl(jid)); |  | ||||||
|         _subscriptionMap[jid] = true; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       for (final deviceId in devices) { |  | ||||||
|         // Empty OMEMO messages are allowed to bypass trust |  | ||||||
|         if (plaintext != null) { |  | ||||||
|           // Only encrypt to devices that are trusted |  | ||||||
|           if (!(await _trustManager.isTrusted(jid, deviceId))) continue; |  | ||||||
| 
 |  | ||||||
|           // Only encrypt to devices that are enabled |  | ||||||
|           if (!(await _trustManager.isEnabled(jid, deviceId))) continue; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final ratchetKey = RatchetMapKey(jid, deviceId); |  | ||||||
|         var ratchet = _ratchetMap[ratchetKey]; |  | ||||||
|         if (ratchet == null) { |  | ||||||
|           _log.severe('Ratchet ${ratchetKey.toJsonKey()} does not exist.'); |  | ||||||
|           deviceEncryptionErrors[ratchetKey] = |  | ||||||
|               NoKeyMaterialAvailableException(); |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final ciphertext = |  | ||||||
|             (await ratchet.ratchetEncrypt(keyPayload)).ciphertext; |  | ||||||
| 
 |  | ||||||
|         if (kex.containsKey(ratchetKey)) { |  | ||||||
|           // The ratchet did not exist |  | ||||||
|           final k = kex[ratchetKey]! |  | ||||||
|             ..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext); |  | ||||||
|           final buffer = base64.encode(k.writeToBuffer()); |  | ||||||
|           encryptedKeys.add( |  | ||||||
|             EncryptedKey( |  | ||||||
|               jid, |  | ||||||
|               deviceId, |  | ||||||
|               buffer, |  | ||||||
|               true, |  | ||||||
|             ), |  | ||||||
|           ); |  | ||||||
| 
 |  | ||||||
|           ratchet = ratchet.cloneWithKex(buffer); |  | ||||||
|           _ratchetMap[ratchetKey] = ratchet; |  | ||||||
|         } else if (!ratchet.acknowledged) { |  | ||||||
|           // The ratchet exists but is not acked |  | ||||||
|           if (ratchet.kex != null) { |  | ||||||
|             final oldKex = |  | ||||||
|                 OmemoKeyExchange.fromBuffer(base64.decode(ratchet.kex!)) |  | ||||||
|                   ..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext); |  | ||||||
| 
 |  | ||||||
|             encryptedKeys.add( |  | ||||||
|               EncryptedKey( |  | ||||||
|                 jid, |  | ||||||
|                 deviceId, |  | ||||||
|                 base64.encode(oldKex.writeToBuffer()), |  | ||||||
|                 true, |  | ||||||
|               ), |  | ||||||
|             ); |  | ||||||
|           } else { |  | ||||||
|             // The ratchet is not acked but we don't have the old key exchange |  | ||||||
|             _log.warning( |  | ||||||
|               'Ratchet for $jid:$deviceId is not acked but the kex attribute is null', |  | ||||||
|             ); |  | ||||||
|             encryptedKeys.add( |  | ||||||
|               EncryptedKey( |  | ||||||
|                 jid, |  | ||||||
|                 deviceId, |  | ||||||
|                 base64.encode(ciphertext), |  | ||||||
|                 false, |  | ||||||
|               ), |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           // The ratchet exists and is acked |  | ||||||
|           encryptedKeys.add( |  | ||||||
|             EncryptedKey( |  | ||||||
|               jid, |  | ||||||
|               deviceId, |  | ||||||
|               base64.encode(ciphertext), |  | ||||||
|               false, |  | ||||||
|             ), |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Commit the ratchet |  | ||||||
|         _eventStreamController |  | ||||||
|             .add(RatchetModifiedEvent(jid, deviceId, ratchet, false, false)); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return EncryptionResult( |  | ||||||
|       plaintext != null ? ciphertext : null, |  | ||||||
|       encryptedKeys, |  | ||||||
|       deviceEncryptionErrors, |  | ||||||
|       jidEncryptionErrors, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Call when receiving an OMEMO:2 encrypted stanza. Will handle everything and |  | ||||||
|   /// decrypt it. |  | ||||||
|   Future<DecryptionResult> onIncomingStanza(OmemoIncomingStanza stanza) async { |  | ||||||
|     await _enterRatchetCriticalSection(stanza.bareSenderJid); |  | ||||||
| 
 |  | ||||||
|     if (!_subscriptionMap.containsKey(stanza.bareSenderJid)) { |  | ||||||
|       unawaited(subscribeToDeviceListNodeImpl(stanza.bareSenderJid)); |  | ||||||
|       _subscriptionMap[stanza.bareSenderJid] = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     final ratchetKey = |  | ||||||
|         RatchetMapKey(stanza.bareSenderJid, stanza.senderDeviceId); |  | ||||||
|     final _InternalDecryptionResult result; |  | ||||||
|     try { |  | ||||||
|       result = await _decryptMessage( |  | ||||||
|         stanza.payload != null ? base64.decode(stanza.payload!) : null, |  | ||||||
|         stanza.bareSenderJid, |  | ||||||
|         stanza.senderDeviceId, |  | ||||||
|         stanza.keys, |  | ||||||
|         stanza.timestamp, |  | ||||||
|       ); |  | ||||||
|     } on OmemoException catch (ex) { |  | ||||||
|       await _leaveRatchetCriticalSection(stanza.bareSenderJid); |  | ||||||
|       return DecryptionResult( |  | ||||||
|         null, |  | ||||||
|         ex, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Check if the ratchet is acked |  | ||||||
|     final ratchet = getRatchet(ratchetKey); |  | ||||||
|     assert( |  | ||||||
|       ratchet != null, |  | ||||||
|       'We decrypted the message, so the ratchet must exist', |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     if (ratchet!.acknowledged) { |  | ||||||
|       // Ratchet is acknowledged |  | ||||||
|       if (ratchet.nr > 53 || result.ratchetCreated || result.ratchetReplaced) { |  | ||||||
|         await sendEmptyOmemoMessageImpl( |  | ||||||
|           await _encryptToJids( |  | ||||||
|             [stanza.bareSenderJid], |  | ||||||
|             null, |  | ||||||
|           ), |  | ||||||
|           stanza.bareSenderJid, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Ratchet is acked |  | ||||||
|       await _leaveRatchetCriticalSection(stanza.bareSenderJid); |  | ||||||
|       return DecryptionResult( |  | ||||||
|         result.payload, |  | ||||||
|         null, |  | ||||||
|       ); |  | ||||||
|     } else { |  | ||||||
|       // Ratchet is not acked. |  | ||||||
|       // Mark as acked and send an empty OMEMO message. |  | ||||||
|       await ratchetAcknowledged( |  | ||||||
|         stanza.bareSenderJid, |  | ||||||
|         stanza.senderDeviceId, |  | ||||||
|         enterCriticalSection: false, |  | ||||||
|       ); |  | ||||||
|       await sendEmptyOmemoMessageImpl( |  | ||||||
|         await _encryptToJids( |  | ||||||
|           [stanza.bareSenderJid], |  | ||||||
|           null, |  | ||||||
|         ), |  | ||||||
|         stanza.bareSenderJid, |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       await _leaveRatchetCriticalSection(stanza.bareSenderJid); |  | ||||||
|       return DecryptionResult( |  | ||||||
|         result.payload, |  | ||||||
|         null, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Call when sending out an encrypted stanza. Will handle everything and |  | ||||||
|   /// encrypt it. |  | ||||||
|   Future<EncryptionResult> onOutgoingStanza(OmemoOutgoingStanza stanza) async { |  | ||||||
|     _log.finest('Waiting to enter critical section'); |  | ||||||
|     await _enterRatchetCriticalSection(stanza.recipientJids.first); |  | ||||||
|     _log.finest('Entered critical section'); |  | ||||||
| 
 |  | ||||||
|     final result = _encryptToJids( |  | ||||||
|       stanza.recipientJids, |  | ||||||
|       stanza.payload, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await _leaveRatchetCriticalSection(stanza.recipientJids.first); |  | ||||||
| 
 |  | ||||||
|     return result; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Sends a hearbeat message as specified by XEP-0384 to [jid]. |  | ||||||
|   Future<void> sendOmemoHeartbeat(String jid) async { |  | ||||||
|     // TODO(Unknown): Include some error handling |  | ||||||
|     final result = await _encryptToJids( |  | ||||||
|       [jid], |  | ||||||
|       null, |  | ||||||
|     ); |  | ||||||
|     await sendEmptyOmemoMessageImpl(result, jid); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Mark the ratchet for device [deviceId] from [jid] as acked. |  | ||||||
|   Future<void> ratchetAcknowledged( |  | ||||||
|     String jid, |  | ||||||
|     int deviceId, { |  | ||||||
|     bool enterCriticalSection = true, |  | ||||||
|   }) async { |  | ||||||
|     if (enterCriticalSection) await _enterRatchetCriticalSection(jid); |  | ||||||
| 
 |  | ||||||
|     final key = RatchetMapKey(jid, deviceId); |  | ||||||
|     if (_ratchetMap.containsKey(key)) { |  | ||||||
|       final ratchet = _ratchetMap[key]!..acknowledged = true; |  | ||||||
| 
 |  | ||||||
|       // Commit it |  | ||||||
|       _eventStreamController |  | ||||||
|           .add(RatchetModifiedEvent(jid, deviceId, ratchet, false, false)); |  | ||||||
|     } else { |  | ||||||
|       _log.severe( |  | ||||||
|         'Attempted to acknowledge ratchet ${key.toJsonKey()}, even though it does not exist', |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (enterCriticalSection) await _leaveRatchetCriticalSection(jid); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Generates an entirely new device. May be useful when the user wants to reset their cryptographic |  | ||||||
|   /// identity. Triggers an event to commit it to storage. |  | ||||||
|   Future<void> regenerateDevice() async { |  | ||||||
|     await _deviceLock.synchronized(() async { |  | ||||||
|       _device = await OmemoDevice.generateNewDevice(_device.jid); |  | ||||||
| 
 |  | ||||||
|       // Commit it |  | ||||||
|       _eventStreamController.add(DeviceModifiedEvent(_device)); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Returns the device used for encryption and decryption. |  | ||||||
|   Future<OmemoDevice> getDevice() => _deviceLock.synchronized(() => _device); |  | ||||||
| 
 |  | ||||||
|   /// Returns the id of the device used for encryption and decryption. |  | ||||||
|   Future<int> getDeviceId() async => (await getDevice()).id; |  | ||||||
| 
 |  | ||||||
|   /// Directly aquire the current device as a OMEMO device bundle. |  | ||||||
|   Future<OmemoBundle> getDeviceBundle() async => (await getDevice()).toBundle(); |  | ||||||
| 
 |  | ||||||
|   /// Directly aquire the current device's fingerprint. |  | ||||||
|   Future<String> getDeviceFingerprint() async => |  | ||||||
|       (await getDevice()).getFingerprint(); |  | ||||||
| 
 |  | ||||||
|   /// Returns the fingerprints for all devices of [jid] that we have a session with. |  | ||||||
|   /// If there are not sessions with [jid], then returns null. |  | ||||||
|   Future<List<DeviceFingerprint>?> getFingerprintsForJid(String jid) async { |  | ||||||
|     if (!_deviceList.containsKey(jid)) return null; |  | ||||||
| 
 |  | ||||||
|     await _enterRatchetCriticalSection(jid); |  | ||||||
| 
 |  | ||||||
|     final fingerprintKeys = _deviceList[jid]! |  | ||||||
|         .map((id) => RatchetMapKey(jid, id)) |  | ||||||
|         .where((key) => _ratchetMap.containsKey(key)); |  | ||||||
| 
 |  | ||||||
|     final fingerprints = List<DeviceFingerprint>.empty(growable: true); |  | ||||||
|     for (final key in fingerprintKeys) { |  | ||||||
|       final curveKey = await _ratchetMap[key]!.ik.toCurve25519(); |  | ||||||
|       fingerprints.add( |  | ||||||
|         DeviceFingerprint( |  | ||||||
|           key.deviceId, |  | ||||||
|           HEX.encode(await curveKey.getBytes()), |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     await _leaveRatchetCriticalSection(jid); |  | ||||||
|     return fingerprints; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Ensures that the device list is fetched again on the next message sending. |  | ||||||
|   void onNewConnection() { |  | ||||||
|     _deviceListRequested.clear(); |  | ||||||
|     _subscriptionMap.clear(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Sets the device list for [jid] to [devices]. Triggers a DeviceListModifiedEvent. |  | ||||||
|   void onDeviceListUpdate(String jid, List<int> devices) { |  | ||||||
|     _deviceList[jid] = devices; |  | ||||||
|     _deviceListRequested[jid] = true; |  | ||||||
| 
 |  | ||||||
|     // Trigger an event |  | ||||||
|     _eventStreamController.add(DeviceListModifiedEvent(_deviceList)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   void initialize( |  | ||||||
|     Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap, |  | ||||||
|     Map<String, List<int>> deviceList, |  | ||||||
|   ) { |  | ||||||
|     _deviceList = deviceList; |  | ||||||
|     _ratchetMap = ratchetMap; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Removes all ratchets for JID [jid]. This also removes all trust decisions for |  | ||||||
|   /// [jid] from the trust manager. This function triggers a RatchetRemovedEvent for |  | ||||||
|   /// every removed ratchet and a DeviceListModifiedEvent afterwards. Behaviour for |  | ||||||
|   /// the trust manager is dependent on its implementation. |  | ||||||
|   Future<void> removeAllRatchets(String jid) async { |  | ||||||
|     await _enterRatchetCriticalSection(jid); |  | ||||||
| 
 |  | ||||||
|     for (final deviceId in _deviceList[jid]!) { |  | ||||||
|       // Remove the ratchet and commit it |  | ||||||
|       _ratchetMap.remove(RatchetMapKey(jid, deviceId)); |  | ||||||
|       _eventStreamController.add(RatchetRemovedEvent(jid, deviceId)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Remove the devices from the device list cache and commit it |  | ||||||
|     _deviceList.remove(jid); |  | ||||||
|     _deviceListRequested.remove(jid); |  | ||||||
|     _eventStreamController.add(DeviceListModifiedEvent(_deviceList)); |  | ||||||
| 
 |  | ||||||
|     // Remove trust decisions |  | ||||||
|     await _trustManager.removeTrustDecisionsForJid(jid); |  | ||||||
| 
 |  | ||||||
|     await _leaveRatchetCriticalSection(jid); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Replaces the internal device with [newDevice]. Does not trigger an event. |  | ||||||
|   Future<void> replaceDevice(OmemoDevice newDevice) async { |  | ||||||
|     await _deviceLock.synchronized(() { |  | ||||||
|       _device = newDevice; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										100
									
								
								lib/src/omemo/queue.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								lib/src/omemo/queue.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:collection'; | ||||||
|  | 
 | ||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  | import 'package:synchronized/synchronized.dart'; | ||||||
|  | 
 | ||||||
|  | extension UtilAllMethodsList<T> on List<T> { | ||||||
|  |   void removeAll(List<T> values) { | ||||||
|  |     for (final value in values) { | ||||||
|  |       remove(value); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   bool containsAll(List<T> values) { | ||||||
|  |     for (final value in values) { | ||||||
|  |       if (!contains(value)) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _RatchetAccessQueueEntry { | ||||||
|  |   _RatchetAccessQueueEntry( | ||||||
|  |     this.jids, | ||||||
|  |     this.completer, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   final List<String> jids; | ||||||
|  |   final Completer<void> completer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class RatchetAccessQueue { | ||||||
|  |   final Queue<_RatchetAccessQueueEntry> _queue = Queue(); | ||||||
|  | 
 | ||||||
|  |   @visibleForTesting | ||||||
|  |   final List<String> runningOperations = List<String>.empty(growable: true); | ||||||
|  | 
 | ||||||
|  |   final Lock lock = Lock(); | ||||||
|  | 
 | ||||||
|  |   bool canBypass(List<String> jids) { | ||||||
|  |     for (final jid in jids) { | ||||||
|  |       if (runningOperations.contains(jid)) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> enterCriticalSection(List<String> jids) async { | ||||||
|  |     final completer = await lock.synchronized<Completer<void>?>(() { | ||||||
|  |       if (canBypass(jids)) { | ||||||
|  |         runningOperations.addAll(jids); | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       final completer = Completer<void>(); | ||||||
|  |       _queue.add( | ||||||
|  |         _RatchetAccessQueueEntry( | ||||||
|  |           jids, | ||||||
|  |           completer, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       return completer; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await completer?.future; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> leaveCriticalSection(List<String> jids) async { | ||||||
|  |     await lock.synchronized(() { | ||||||
|  |       runningOperations.removeAll(jids); | ||||||
|  | 
 | ||||||
|  |       while (_queue.isNotEmpty) { | ||||||
|  |         if (canBypass(_queue.first.jids)) { | ||||||
|  |           final head = _queue.removeFirst(); | ||||||
|  |           runningOperations.addAll(head.jids); | ||||||
|  |           head.completer.complete(); | ||||||
|  |         } else { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<T> synchronized<T>( | ||||||
|  |     List<String> jids, | ||||||
|  |     Future<T> Function() function, | ||||||
|  |   ) async { | ||||||
|  |     await enterCriticalSection(jids); | ||||||
|  |     final result = await function(); | ||||||
|  |     await leaveCriticalSection(jids); | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								lib/src/omemo/ratchet_data.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/src/omemo/ratchet_data.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; | ||||||
|  | 
 | ||||||
|  | class OmemoRatchetData { | ||||||
|  |   const OmemoRatchetData( | ||||||
|  |     this.jid, | ||||||
|  |     this.id, | ||||||
|  |     this.ratchet, | ||||||
|  |     this.added, | ||||||
|  |     this.replaced, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   /// The JID we have the ratchet with. | ||||||
|  |   final String jid; | ||||||
|  | 
 | ||||||
|  |   /// The device id we have the ratchet with. | ||||||
|  |   final int id; | ||||||
|  | 
 | ||||||
|  |   /// The actual double ratchet to commit. | ||||||
|  |   final OmemoDoubleRatchet ratchet; | ||||||
|  | 
 | ||||||
|  |   /// Indicates whether the ratchet has just been created (true) or just modified (false). | ||||||
|  |   final bool added; | ||||||
|  | 
 | ||||||
|  |   /// Indicates whether the ratchet has been replaced (true) or not. | ||||||
|  |   final bool replaced; | ||||||
|  | } | ||||||
| @ -5,9 +5,9 @@ class OmemoIncomingStanza { | |||||||
|   const OmemoIncomingStanza( |   const OmemoIncomingStanza( | ||||||
|     this.bareSenderJid, |     this.bareSenderJid, | ||||||
|     this.senderDeviceId, |     this.senderDeviceId, | ||||||
|     this.timestamp, |  | ||||||
|     this.keys, |     this.keys, | ||||||
|     this.payload, |     this.payload, | ||||||
|  |     this.isCatchup, | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   /// The bare JID of the sender of the stanza. |   /// The bare JID of the sender of the stanza. | ||||||
| @ -16,14 +16,14 @@ class OmemoIncomingStanza { | |||||||
|   /// The device ID of the sender. |   /// The device ID of the sender. | ||||||
|   final int senderDeviceId; |   final int senderDeviceId; | ||||||
| 
 | 
 | ||||||
|   /// The timestamp when the stanza was received. |   /// The included encrypted keys for our own JID | ||||||
|   final int timestamp; |  | ||||||
| 
 |  | ||||||
|   /// The included encrypted keys |  | ||||||
|   final List<EncryptedKey> keys; |   final List<EncryptedKey> keys; | ||||||
| 
 | 
 | ||||||
|   /// The string payload included in the <encrypted /> element. |   /// The string payload included in the <encrypted /> element. | ||||||
|   final String? payload; |   final String? payload; | ||||||
|  | 
 | ||||||
|  |   /// Flag indicating whether the message was received due to a catchup. | ||||||
|  |   final bool isCatchup; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Describes a stanza that is to be sent out | /// Describes a stanza that is to be sent out | ||||||
| @ -37,5 +37,5 @@ class OmemoOutgoingStanza { | |||||||
|   final List<String> recipientJids; |   final List<String> recipientJids; | ||||||
| 
 | 
 | ||||||
|   /// The serialised XML data that should be encrypted. |   /// The serialised XML data that should be encrypted. | ||||||
|   final String payload; |   final String? payload; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,38 +0,0 @@ | |||||||
| import 'package:omemo_dart/src/helpers.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/protobuf.dart'; |  | ||||||
| 
 |  | ||||||
| class OmemoAuthenticatedMessage { |  | ||||||
|   OmemoAuthenticatedMessage(); |  | ||||||
| 
 |  | ||||||
|   factory OmemoAuthenticatedMessage.fromBuffer(List<int> data) { |  | ||||||
|     var i = 0; |  | ||||||
| 
 |  | ||||||
|     // required bytes mac     = 1; |  | ||||||
|     if (data[0] != fieldId(1, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final mac = data.sublist(2, i + 2 + data[1]); |  | ||||||
|     i += data[1] + 2; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(2, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final message = data.sublist(i + 2, i + 2 + data[i + 1]); |  | ||||||
| 
 |  | ||||||
|     return OmemoAuthenticatedMessage() |  | ||||||
|       ..mac = mac |  | ||||||
|       ..message = message; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   List<int>? mac; |  | ||||||
|   List<int>? message; |  | ||||||
| 
 |  | ||||||
|   List<int> writeToBuffer() { |  | ||||||
|     return concat([ |  | ||||||
|       [fieldId(1, fieldTypeByteArray), mac!.length], |  | ||||||
|       mac!, |  | ||||||
|       [fieldId(2, fieldTypeByteArray), message!.length], |  | ||||||
|       message!, |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,71 +0,0 @@ | |||||||
| import 'package:omemo_dart/src/helpers.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/protobuf.dart'; |  | ||||||
| 
 |  | ||||||
| class OmemoKeyExchange { |  | ||||||
|   OmemoKeyExchange(); |  | ||||||
| 
 |  | ||||||
|   factory OmemoKeyExchange.fromBuffer(List<int> data) { |  | ||||||
|     var i = 0; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(1, fieldTypeUint32)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     var decoded = decodeVarint(data, 1); |  | ||||||
|     final pkId = decoded.n; |  | ||||||
|     i += decoded.length + 1; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(2, fieldTypeUint32)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     decoded = decodeVarint(data, i + 1); |  | ||||||
|     final spkId = decoded.n; |  | ||||||
|     i += decoded.length + 1; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(3, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final ik = data.sublist(i + 2, i + 2 + data[i + 1]); |  | ||||||
|     i += 2 + data[i + 1]; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(4, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final ek = data.sublist(i + 2, i + 2 + data[i + 1]); |  | ||||||
|     i += 2 + data[i + 1]; |  | ||||||
| 
 |  | ||||||
|     if (data[i] != fieldId(5, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final message = OmemoAuthenticatedMessage.fromBuffer(data.sublist(i + 2)); |  | ||||||
| 
 |  | ||||||
|     return OmemoKeyExchange() |  | ||||||
|       ..pkId = pkId |  | ||||||
|       ..spkId = spkId |  | ||||||
|       ..ik = ik |  | ||||||
|       ..ek = ek |  | ||||||
|       ..message = message; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   int? pkId; |  | ||||||
|   int? spkId; |  | ||||||
|   List<int>? ik; |  | ||||||
|   List<int>? ek; |  | ||||||
|   OmemoAuthenticatedMessage? message; |  | ||||||
| 
 |  | ||||||
|   List<int> writeToBuffer() { |  | ||||||
|     final msg = message!.writeToBuffer(); |  | ||||||
|     return concat([ |  | ||||||
|       [fieldId(1, fieldTypeUint32)], |  | ||||||
|       encodeVarint(pkId!), |  | ||||||
|       [fieldId(2, fieldTypeUint32)], |  | ||||||
|       encodeVarint(spkId!), |  | ||||||
|       [fieldId(3, fieldTypeByteArray), ik!.length], |  | ||||||
|       ik!, |  | ||||||
|       [fieldId(4, fieldTypeByteArray), ek!.length], |  | ||||||
|       ek!, |  | ||||||
|       [fieldId(5, fieldTypeByteArray), msg.length], |  | ||||||
|       msg, |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,75 +0,0 @@ | |||||||
| import 'package:omemo_dart/src/helpers.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/protobuf.dart'; |  | ||||||
| 
 |  | ||||||
| class OmemoMessage { |  | ||||||
|   OmemoMessage(); |  | ||||||
| 
 |  | ||||||
|   factory OmemoMessage.fromBuffer(List<int> data) { |  | ||||||
|     var i = 0; |  | ||||||
| 
 |  | ||||||
|     // required uint32 n          = 1; |  | ||||||
|     if (data[0] != fieldId(1, fieldTypeUint32)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     var decode = decodeVarint(data, 1); |  | ||||||
|     final n = decode.n; |  | ||||||
|     i += decode.length + 1; |  | ||||||
| 
 |  | ||||||
|     // required uint32 pn         = 2; |  | ||||||
|     if (data[i] != fieldId(2, fieldTypeUint32)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     decode = decodeVarint(data, i + 1); |  | ||||||
|     final pn = decode.n; |  | ||||||
|     i += decode.length + 1; |  | ||||||
| 
 |  | ||||||
|     // required bytes  dh_pub     = 3; |  | ||||||
|     if (data[i] != fieldId(3, fieldTypeByteArray)) { |  | ||||||
|       throw Exception(); |  | ||||||
|     } |  | ||||||
|     final dhPub = data.sublist(i + 2, i + 2 + data[i + 1]); |  | ||||||
|     i += 2 + data[i + 1]; |  | ||||||
| 
 |  | ||||||
|     // optional bytes  ciphertext = 4; |  | ||||||
|     List<int>? ciphertext; |  | ||||||
|     if (i < data.length) { |  | ||||||
|       if (data[i] != fieldId(4, fieldTypeByteArray)) { |  | ||||||
|         throw Exception(); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       ciphertext = data.sublist(i + 2, i + 2 + data[i + 1]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return OmemoMessage() |  | ||||||
|       ..n = n |  | ||||||
|       ..pn = pn |  | ||||||
|       ..dhPub = dhPub |  | ||||||
|       ..ciphertext = ciphertext; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   int? n; |  | ||||||
|   int? pn; |  | ||||||
|   List<int>? dhPub; |  | ||||||
|   List<int>? ciphertext; |  | ||||||
| 
 |  | ||||||
|   List<int> writeToBuffer() { |  | ||||||
|     final data = concat([ |  | ||||||
|       [fieldId(1, fieldTypeUint32)], |  | ||||||
|       encodeVarint(n!), |  | ||||||
|       [fieldId(2, fieldTypeUint32)], |  | ||||||
|       encodeVarint(pn!), |  | ||||||
|       [fieldId(3, fieldTypeByteArray), dhPub!.length], |  | ||||||
|       dhPub!, |  | ||||||
|     ]); |  | ||||||
| 
 |  | ||||||
|     if (ciphertext != null) { |  | ||||||
|       return concat([ |  | ||||||
|         data, |  | ||||||
|         [fieldId(4, fieldTypeByteArray), ciphertext!.length], |  | ||||||
|         ciphertext!, |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return data; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,64 +0,0 @@ | |||||||
| /// Masks the 7 LSB |  | ||||||
| const lsb7Mask = 0x7F; |  | ||||||
| 
 |  | ||||||
| /// Constant for setting the MSB |  | ||||||
| const msb = 1 << 7; |  | ||||||
| 
 |  | ||||||
| /// Field types |  | ||||||
| const fieldTypeUint32 = 0; |  | ||||||
| const fieldTypeByteArray = 2; |  | ||||||
| 
 |  | ||||||
| int fieldId(int number, int type) { |  | ||||||
|   return (number << 3) | type; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class VarintDecode { |  | ||||||
|   const VarintDecode(this.n, this.length); |  | ||||||
|   final int n; |  | ||||||
|   final int length; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Decode a Varint that begins at [input]'s index [offset]. |  | ||||||
| VarintDecode decodeVarint(List<int> input, int offset) { |  | ||||||
|   // The return value |  | ||||||
|   var n = 0; |  | ||||||
|   // The byte offset counter |  | ||||||
|   var i = 0; |  | ||||||
| 
 |  | ||||||
|   // Iterate until the MSB of the byte is 0 |  | ||||||
|   while (true) { |  | ||||||
|     // Mask only the 7 LSB and "move" them accordingly |  | ||||||
|     n += (input[offset + i] & lsb7Mask) << (7 * i); |  | ||||||
| 
 |  | ||||||
|     // Break if we reached the end |  | ||||||
|     if (input[offset + i] & 1 << 7 == 0) { |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     i++; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return VarintDecode(n, i + 1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Encodes the integer [i] into a Varint. |  | ||||||
| List<int> encodeVarint(int i) { |  | ||||||
|   assert(i >= 0, "Two's complement is not implemented"); |  | ||||||
|   final ret = List<int>.empty(growable: true); |  | ||||||
| 
 |  | ||||||
|   // Thanks to https://github.com/hathibelagal-dev/LEB128 for the trick with toRadixString! |  | ||||||
|   final numSevenBlocks = (i.toRadixString(2).length / 7).ceil(); |  | ||||||
|   for (var j = 0; j < numSevenBlocks; j++) { |  | ||||||
|     // The 7 LSB of the byte we're creating |  | ||||||
|     final x = (i & (lsb7Mask << j * 7)) >> j * 7; |  | ||||||
| 
 |  | ||||||
|     if (j == numSevenBlocks - 1) { |  | ||||||
|       // If we were to shift further, we only get zero, so we're at the end |  | ||||||
|       ret.add(x); |  | ||||||
|     } else { |  | ||||||
|       // We still have at least one bit more to go, so set the MSB to 1 |  | ||||||
|       ret.add(x + msb); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return ret; |  | ||||||
| } |  | ||||||
| @ -22,5 +22,5 @@ class AlwaysTrustingTrustManager extends TrustManager { | |||||||
|   Future<void> removeTrustDecisionsForJid(String jid) async {} |   Future<void> removeTrustDecisionsForJid(String jid) async {} | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<Map<String, dynamic>> toJson() async => <String, dynamic>{}; |   Future<void> loadTrustData(String jid) async {} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | import 'package:meta/meta.dart'; | ||||||
|  | 
 | ||||||
| /// The base class for managing trust in OMEMO sessions. | /// The base class for managing trust in OMEMO sessions. | ||||||
| // ignore: one_member_abstracts | // ignore: one_member_abstracts | ||||||
| abstract class TrustManager { | abstract class TrustManager { | ||||||
| @ -7,6 +9,7 @@ abstract class TrustManager { | |||||||
| 
 | 
 | ||||||
|   /// Called by the OmemoSessionManager when a new session has been built. Should set |   /// Called by the OmemoSessionManager when a new session has been built. Should set | ||||||
|   /// a default trust state to [jid]'s device with identifier [deviceId]. |   /// a default trust state to [jid]'s device with identifier [deviceId]. | ||||||
|  |   @internal | ||||||
|   Future<void> onNewSession(String jid, int deviceId); |   Future<void> onNewSession(String jid, int deviceId); | ||||||
| 
 | 
 | ||||||
|   /// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption. |   /// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption. | ||||||
| @ -17,9 +20,14 @@ abstract class TrustManager { | |||||||
|   /// if [enabled] is false. |   /// if [enabled] is false. | ||||||
|   Future<void> setEnabled(String jid, int deviceId, bool enabled); |   Future<void> setEnabled(String jid, int deviceId, bool enabled); | ||||||
| 
 | 
 | ||||||
|   /// Serialize the trust manager to JSON. |  | ||||||
|   Future<Map<String, dynamic>> toJson(); |  | ||||||
| 
 |  | ||||||
|   /// Removes all trust decisions for [jid]. |   /// Removes all trust decisions for [jid]. | ||||||
|  |   @internal | ||||||
|   Future<void> removeTrustDecisionsForJid(String jid); |   Future<void> removeTrustDecisionsForJid(String jid); | ||||||
|  | 
 | ||||||
|  |   // ignore: comment_references | ||||||
|  |   /// Called from within the [OmemoManager]. | ||||||
|  |   /// Loads the trust data for the JID [jid] from persistent storage | ||||||
|  |   /// into the internal cache, if applicable. | ||||||
|  |   @internal | ||||||
|  |   Future<void> loadTrustData(String jid); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,71 +1,116 @@ | |||||||
| import 'package:meta/meta.dart'; | import 'package:meta/meta.dart'; | ||||||
|  | import 'package:omemo_dart/src/helpers.dart'; | ||||||
| import 'package:omemo_dart/src/omemo/ratchet_map_key.dart'; | import 'package:omemo_dart/src/omemo/ratchet_map_key.dart'; | ||||||
| import 'package:omemo_dart/src/trust/base.dart'; | import 'package:omemo_dart/src/trust/base.dart'; | ||||||
| import 'package:synchronized/synchronized.dart'; | 
 | ||||||
|  | @immutable | ||||||
|  | class BTBVTrustData { | ||||||
|  |   const BTBVTrustData( | ||||||
|  |     this.jid, | ||||||
|  |     this.device, | ||||||
|  |     this.state, | ||||||
|  |     this.enabled, | ||||||
|  |     this.trusted, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   /// The JID in question. | ||||||
|  |   final String jid; | ||||||
|  | 
 | ||||||
|  |   /// The device (ratchet) in question. | ||||||
|  |   final int device; | ||||||
|  | 
 | ||||||
|  |   /// The trust state of the ratchet. | ||||||
|  |   final BTBVTrustState state; | ||||||
|  | 
 | ||||||
|  |   /// Flag indicating whether the ratchet is enabled (true) or not (false). | ||||||
|  |   final bool enabled; | ||||||
|  | 
 | ||||||
|  |   /// Flag indicating whether the ratchet is trusted. For loading and commiting a ratchet, this field | ||||||
|  |   /// contains an arbitrary value. | ||||||
|  |   /// When using [BlindTrustBeforeVerificationTrustManager.getDevicesTrust], this flag will be true if | ||||||
|  |   /// the ratchet is trusted and false if not. | ||||||
|  |   final bool trusted; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// A callback for when a trust decision is to be commited to persistent storage. | ||||||
|  | typedef BTBVTrustCommitCallback = Future<void> Function(BTBVTrustData data); | ||||||
|  | 
 | ||||||
|  | /// A stub-implementation of [BTBVTrustCommitCallback]. | ||||||
|  | Future<void> btbvCommitStub(BTBVTrustData _) async {} | ||||||
|  | 
 | ||||||
|  | /// A callback for when all trust decisions for a JID should be removed from persistent storage. | ||||||
|  | typedef BTBVRemoveTrustForJidCallback = Future<void> Function(String jid); | ||||||
|  | 
 | ||||||
|  | /// A stub-implementation of [BTBVRemoveTrustForJidCallback]. | ||||||
|  | Future<void> btbvRemoveTrustStub(String _) async {} | ||||||
|  | 
 | ||||||
|  | /// A callback for when trust data should be loaded. | ||||||
|  | typedef BTBVLoadDataCallback = Future<List<BTBVTrustData>> Function(String jid); | ||||||
|  | 
 | ||||||
|  | /// A stub-implementation for [BTBVLoadDataCallback]. | ||||||
|  | Future<List<BTBVTrustData>> btbvLoadDataStub(String _) async => []; | ||||||
| 
 | 
 | ||||||
| /// Every device is in either of those two trust states: | /// Every device is in either of those two trust states: | ||||||
| /// - notTrusted: The device is absolutely not trusted | /// - notTrusted: The device is absolutely not trusted | ||||||
| /// - blindTrust: The fingerprint is not verified using OOB means | /// - blindTrust: The fingerprint is not verified using OOB means | ||||||
| /// - verified: The fingerprint has been verified using OOB means | /// - verified: The fingerprint has been verified using OOB means | ||||||
| enum BTBVTrustState { | enum BTBVTrustState { | ||||||
|   notTrusted, // = 1 |   notTrusted(1), | ||||||
|   blindTrust, // = 2 |   blindTrust(2), | ||||||
|   verified, // = 3 |   verified(3); | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| int _trustToInt(BTBVTrustState state) { |   const BTBVTrustState(this.value); | ||||||
|   switch (state) { |  | ||||||
|     case BTBVTrustState.notTrusted: |  | ||||||
|       return 1; |  | ||||||
|     case BTBVTrustState.blindTrust: |  | ||||||
|       return 2; |  | ||||||
|     case BTBVTrustState.verified: |  | ||||||
|       return 3; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| BTBVTrustState _trustFromInt(int i) { |   factory BTBVTrustState.fromInt(int value) { | ||||||
|   switch (i) { |     switch (value) { | ||||||
|     case 1: |       case 1: | ||||||
|       return BTBVTrustState.notTrusted; |         return BTBVTrustState.notTrusted; | ||||||
|     case 2: |       case 2: | ||||||
|       return BTBVTrustState.blindTrust; |         return BTBVTrustState.blindTrust; | ||||||
|     case 3: |       case 3: | ||||||
|       return BTBVTrustState.verified; |         return BTBVTrustState.verified; | ||||||
|     default: |       // TODO(Unknown): Should we handle this better? | ||||||
|       return BTBVTrustState.notTrusted; |       default: | ||||||
|  |         return BTBVTrustState.notTrusted; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /// The value backing the trust state. | ||||||
|  |   final int value; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A TrustManager that implements the idea of Blind Trust Before Verification. | /// A TrustManager that implements the idea of Blind Trust Before Verification. | ||||||
| /// See https://gultsch.de/trust.html for more details. | /// See https://gultsch.de/trust.html for more details. | ||||||
| abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager { | class BlindTrustBeforeVerificationTrustManager extends TrustManager { | ||||||
|   BlindTrustBeforeVerificationTrustManager({ |   BlindTrustBeforeVerificationTrustManager({ | ||||||
|     Map<RatchetMapKey, BTBVTrustState>? trustCache, |     this.loadData = btbvLoadDataStub, | ||||||
|     Map<RatchetMapKey, bool>? enablementCache, |     this.commit = btbvCommitStub, | ||||||
|     Map<String, List<int>>? devices, |     this.removeTrust = btbvRemoveTrustStub, | ||||||
|   })  : trustCache = trustCache ?? {}, |   }); | ||||||
|         enablementCache = enablementCache ?? {}, |  | ||||||
|         devices = devices ?? {}, |  | ||||||
|         _lock = Lock(); |  | ||||||
| 
 | 
 | ||||||
|   /// The cache for mapping a RatchetMapKey to its trust state |   /// The cache for mapping a RatchetMapKey to its trust state | ||||||
|   @visibleForTesting |   @visibleForTesting | ||||||
|   @protected |   @protected | ||||||
|   final Map<RatchetMapKey, BTBVTrustState> trustCache; |   final Map<RatchetMapKey, BTBVTrustState> trustCache = {}; | ||||||
| 
 | 
 | ||||||
|   /// The cache for mapping a RatchetMapKey to whether it is enabled or not |   /// The cache for mapping a RatchetMapKey to whether it is enabled or not | ||||||
|   @visibleForTesting |   @visibleForTesting | ||||||
|   @protected |   @protected | ||||||
|   final Map<RatchetMapKey, bool> enablementCache; |   final Map<RatchetMapKey, bool> enablementCache = {}; | ||||||
| 
 | 
 | ||||||
|   /// Mapping of Jids to their device identifiers |   /// Mapping of Jids to their device identifiers | ||||||
|   @visibleForTesting |   @visibleForTesting | ||||||
|   @protected |   @protected | ||||||
|   final Map<String, List<int>> devices; |   final Map<String, List<int>> devices = {}; | ||||||
| 
 | 
 | ||||||
|   /// The lock for devices and trustCache |   /// Callback for loading trust data. | ||||||
|   final Lock _lock; |   final BTBVLoadDataCallback loadData; | ||||||
|  | 
 | ||||||
|  |   /// Callback for commiting trust data to persistent storage. | ||||||
|  |   final BTBVTrustCommitCallback commit; | ||||||
|  | 
 | ||||||
|  |   /// Callback for removing trust data for a JID. | ||||||
|  |   final BTBVRemoveTrustForJidCallback removeTrust; | ||||||
| 
 | 
 | ||||||
|   /// Returns true if [jid] has at least one device that is verified. If not, returns false. |   /// Returns true if [jid] has at least one device that is verified. If not, returns false. | ||||||
|   /// Note that this function accesses devices and trustCache, which requires that the |   /// Note that this function accesses devices and trustCache, which requires that the | ||||||
| @ -80,69 +125,72 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager { | |||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<bool> isTrusted(String jid, int deviceId) async { |   Future<bool> isTrusted(String jid, int deviceId) async { | ||||||
|     var returnValue = false; |     final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)]; | ||||||
|     await _lock.synchronized(() async { |     if (trustCacheValue == BTBVTrustState.notTrusted) { | ||||||
|       final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)]; |       return false; | ||||||
|       if (trustCacheValue == BTBVTrustState.notTrusted) { |     } else if (trustCacheValue == BTBVTrustState.verified) { | ||||||
|         returnValue = false; |       // The key is verified, so it's safe. | ||||||
|         return; |       return true; | ||||||
|       } else if (trustCacheValue == BTBVTrustState.verified) { |     } else { | ||||||
|         // The key is verified, so it's safe. |       if (_hasAtLeastOneVerifiedDevice(jid)) { | ||||||
|         returnValue = true; |         // Do not trust if there is at least one device with full trust | ||||||
|         return; |         return false; | ||||||
|       } else { |       } else { | ||||||
|         if (_hasAtLeastOneVerifiedDevice(jid)) { |         // We have not verified a key from [jid], so it is blind trust all the way. | ||||||
|           // Do not trust if there is at least one device with full trust |         return true; | ||||||
|           returnValue = false; |  | ||||||
|           return; |  | ||||||
|         } else { |  | ||||||
|           // We have not verified a key from [jid], so it is blind trust all the way. |  | ||||||
|           returnValue = true; |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }); |     } | ||||||
| 
 |  | ||||||
|     return returnValue; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> onNewSession(String jid, int deviceId) async { |   Future<void> onNewSession(String jid, int deviceId) async { | ||||||
|     await _lock.synchronized(() async { |     final key = RatchetMapKey(jid, deviceId); | ||||||
|       final key = RatchetMapKey(jid, deviceId); |     if (_hasAtLeastOneVerifiedDevice(jid)) { | ||||||
|       if (_hasAtLeastOneVerifiedDevice(jid)) { |       trustCache[key] = BTBVTrustState.notTrusted; | ||||||
|         trustCache[key] = BTBVTrustState.notTrusted; |       enablementCache[key] = false; | ||||||
|         enablementCache[key] = false; |     } else { | ||||||
|       } else { |       trustCache[key] = BTBVTrustState.blindTrust; | ||||||
|         trustCache[key] = BTBVTrustState.blindTrust; |       enablementCache[key] = true; | ||||||
|         enablementCache[key] = true; |     } | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       if (devices.containsKey(jid)) { |     // Append to the device list | ||||||
|         devices[jid]!.add(deviceId); |     devices.appendOrCreate(jid, deviceId, checkExistence: true); | ||||||
|       } else { |  | ||||||
|         devices[jid] = List<int>.from([deviceId]); |  | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       // Commit the state |     // Commit the state | ||||||
|       await commitState(); |     await commit( | ||||||
|     }); |       BTBVTrustData( | ||||||
|  |         jid, | ||||||
|  |         deviceId, | ||||||
|  |         trustCache[key]!, | ||||||
|  |         enablementCache[key]!, | ||||||
|  |         false, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Returns a mapping from the device identifiers of [jid] to their trust state. If |   /// Returns a mapping from the device identifiers of [jid] to their trust state. If | ||||||
|   /// there are no devices known for [jid], then an empty map is returned. |   /// there are no devices known for [jid], then an empty map is returned. | ||||||
|   Future<Map<int, BTBVTrustState>> getDevicesTrust(String jid) async { |   Future<Map<int, BTBVTrustData>> getDevicesTrust(String jid) async { | ||||||
|     return _lock.synchronized(() async { |     final map = <int, BTBVTrustData>{}; | ||||||
|       final map = <int, BTBVTrustState>{}; |  | ||||||
| 
 | 
 | ||||||
|       if (!devices.containsKey(jid)) return map; |     if (!devices.containsKey(jid)) return map; | ||||||
| 
 | 
 | ||||||
|       for (final deviceId in devices[jid]!) { |     for (final deviceId in devices[jid]!) { | ||||||
|         map[deviceId] = trustCache[RatchetMapKey(jid, deviceId)]!; |       final key = RatchetMapKey(jid, deviceId); | ||||||
|  |       if (!trustCache.containsKey(key) || !enablementCache.containsKey(key)) { | ||||||
|  |         continue; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return map; |       map[deviceId] = BTBVTrustData( | ||||||
|     }); |         jid, | ||||||
|  |         deviceId, | ||||||
|  |         trustCache[key]!, | ||||||
|  |         enablementCache[key]!, | ||||||
|  |         await isTrusted(jid, deviceId), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return map; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Sets the trust of [jid]'s device with identifier [deviceId] to [state]. |   /// Sets the trust of [jid]'s device with identifier [deviceId] to [state]. | ||||||
| @ -151,108 +199,71 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager { | |||||||
|     int deviceId, |     int deviceId, | ||||||
|     BTBVTrustState state, |     BTBVTrustState state, | ||||||
|   ) async { |   ) async { | ||||||
|     await _lock.synchronized(() async { |     final key = RatchetMapKey(jid, deviceId); | ||||||
|       trustCache[RatchetMapKey(jid, deviceId)] = state; |     trustCache[key] = state; | ||||||
| 
 | 
 | ||||||
|       // Commit the state |     // Commit the state | ||||||
|       await commitState(); |     await commit( | ||||||
|     }); |       BTBVTrustData( | ||||||
|  |         jid, | ||||||
|  |         deviceId, | ||||||
|  |         state, | ||||||
|  |         enablementCache[key]!, | ||||||
|  |         false, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<bool> isEnabled(String jid, int deviceId) async { |   Future<bool> isEnabled(String jid, int deviceId) async { | ||||||
|     return _lock.synchronized(() async { |     final value = enablementCache[RatchetMapKey(jid, deviceId)]; | ||||||
|       final value = enablementCache[RatchetMapKey(jid, deviceId)]; |  | ||||||
| 
 | 
 | ||||||
|       if (value == null) return false; |     if (value == null) return false; | ||||||
|       return value; |     return value; | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> setEnabled(String jid, int deviceId, bool enabled) async { |   Future<void> setEnabled(String jid, int deviceId, bool enabled) async { | ||||||
|     await _lock.synchronized(() async { |     final key = RatchetMapKey(jid, deviceId); | ||||||
|       enablementCache[RatchetMapKey(jid, deviceId)] = enabled; |     enablementCache[key] = enabled; | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     // Commit the state |     // Commit the state | ||||||
|     await commitState(); |     await commit( | ||||||
|   } |       BTBVTrustData( | ||||||
| 
 |         jid, | ||||||
|   @override |         deviceId, | ||||||
|   Future<Map<String, dynamic>> toJson() async { |         trustCache[key]!, | ||||||
|     return { |         enabled, | ||||||
|       'devices': devices, |         false, | ||||||
|       'trust': trustCache.map( |  | ||||||
|         (key, value) => MapEntry( |  | ||||||
|           key.toJsonKey(), |  | ||||||
|           _trustToInt(value), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       'enable': |  | ||||||
|           enablementCache.map((key, value) => MapEntry(key.toJsonKey(), value)), |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// From a serialized version of a BTBV trust manager, extract the device list. |  | ||||||
|   /// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on. |  | ||||||
|   static Map<String, List<int>> deviceListFromJson(Map<String, dynamic> json) { |  | ||||||
|     return (json['devices']! as Map<String, dynamic>).map<String, List<int>>( |  | ||||||
|       (key, value) => MapEntry( |  | ||||||
|         key, |  | ||||||
|         (value as List<dynamic>).map<int>((i) => i as int).toList(), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// From a serialized version of a BTBV trust manager, extract the trust cache. |  | ||||||
|   /// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on. |  | ||||||
|   static Map<RatchetMapKey, BTBVTrustState> trustCacheFromJson( |  | ||||||
|     Map<String, dynamic> json, |  | ||||||
|   ) { |  | ||||||
|     return (json['trust']! as Map<String, dynamic>) |  | ||||||
|         .map<RatchetMapKey, BTBVTrustState>( |  | ||||||
|       (key, value) => MapEntry( |  | ||||||
|         RatchetMapKey.fromJsonKey(key), |  | ||||||
|         _trustFromInt(value as int), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// From a serialized version of a BTBV trust manager, extract the enable cache. |  | ||||||
|   /// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on. |  | ||||||
|   static Map<RatchetMapKey, bool> enableCacheFromJson( |  | ||||||
|     Map<String, dynamic> json, |  | ||||||
|   ) { |  | ||||||
|     return (json['enable']! as Map<String, dynamic>).map<RatchetMapKey, bool>( |  | ||||||
|       (key, value) => MapEntry( |  | ||||||
|         RatchetMapKey.fromJsonKey(key), |  | ||||||
|         value as bool, |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> removeTrustDecisionsForJid(String jid) async { |   Future<void> removeTrustDecisionsForJid(String jid) async { | ||||||
|     await _lock.synchronized(() async { |     // Clear the caches | ||||||
|       devices.remove(jid); |     for (final device in devices[jid]!) { | ||||||
|       await commitState(); |       final key = RatchetMapKey(jid, device); | ||||||
|     }); |       trustCache.remove(key); | ||||||
|  |       enablementCache.remove(key); | ||||||
|  |     } | ||||||
|  |     devices.remove(jid); | ||||||
|  | 
 | ||||||
|  |     // Commit the state | ||||||
|  |     await removeTrust(jid); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Called when the state of the trust manager has been changed. Allows the user to |   @override | ||||||
|   /// commit the trust state to persistent storage. |   Future<void> loadTrustData(String jid) async { | ||||||
|   @visibleForOverriding |     for (final result in await loadData(jid)) { | ||||||
|   Future<void> commitState(); |       final key = RatchetMapKey(jid, result.device); | ||||||
|  |       trustCache[key] = result.state; | ||||||
|  |       enablementCache[key] = result.enabled; | ||||||
|  |       devices.appendOrCreate(jid, result.device, checkExistence: true); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   @visibleForTesting |   @visibleForTesting | ||||||
|   BTBVTrustState getDeviceTrust(String jid, int deviceId) => |   BTBVTrustState getDeviceTrust(String jid, int deviceId) => | ||||||
|       trustCache[RatchetMapKey(jid, deviceId)]!; |       trustCache[RatchetMapKey(jid, deviceId)]!; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /// A BTBV TrustManager that does not commit its state to persistent storage. Well suited |  | ||||||
| /// for testing. |  | ||||||
| class MemoryBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager { |  | ||||||
|   @override |  | ||||||
|   Future<void> commitState() async {} |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -22,5 +22,5 @@ class NeverTrustingTrustManager extends TrustManager { | |||||||
|   Future<void> removeTrustDecisionsForJid(String jid) async {} |   Future<void> removeTrustDecisionsForJid(String jid) async {} | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<Map<String, dynamic>> toJson() async => <String, dynamic>{}; |   Future<void> loadTrustData(String jid) async {} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,15 +1,14 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
| import 'package:cryptography/cryptography.dart'; | import 'package:cryptography/cryptography.dart'; | ||||||
|  | import 'package:moxlib/moxlib.dart'; | ||||||
|  | import 'package:omemo_dart/src/common/constants.dart'; | ||||||
| import 'package:omemo_dart/src/crypto.dart'; | import 'package:omemo_dart/src/crypto.dart'; | ||||||
| import 'package:omemo_dart/src/errors.dart'; | import 'package:omemo_dart/src/errors.dart'; | ||||||
| import 'package:omemo_dart/src/helpers.dart'; | import 'package:omemo_dart/src/helpers.dart'; | ||||||
| import 'package:omemo_dart/src/keys.dart'; | import 'package:omemo_dart/src/keys.dart'; | ||||||
| import 'package:omemo_dart/src/omemo/bundle.dart'; | import 'package:omemo_dart/src/omemo/bundle.dart'; | ||||||
| 
 | 
 | ||||||
| /// The overarching assumption is that we use Ed25519 keys for the identity keys |  | ||||||
| const omemoX3DHInfoString = 'OMEMO X3DH'; |  | ||||||
| 
 |  | ||||||
| /// Performed by Alice | /// Performed by Alice | ||||||
| class X3DHAliceResult { | class X3DHAliceResult { | ||||||
|   const X3DHAliceResult(this.ek, this.sk, this.opkId, this.ad); |   const X3DHAliceResult(this.ek, this.sk, this.opkId, this.ad); | ||||||
| @ -70,7 +69,8 @@ Future<List<int>> kdf(List<int> km) async { | |||||||
| 
 | 
 | ||||||
| /// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key | /// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key | ||||||
| /// pair [ik]. | /// pair [ik]. | ||||||
| Future<X3DHAliceResult> x3dhFromBundle( | Future<Result<InvalidKeyExchangeSignatureError, X3DHAliceResult>> | ||||||
|  |     x3dhFromBundle( | ||||||
|   OmemoBundle bundle, |   OmemoBundle bundle, | ||||||
|   OmemoKeyPair ik, |   OmemoKeyPair ik, | ||||||
| ) async { | ) async { | ||||||
| @ -84,7 +84,7 @@ Future<X3DHAliceResult> x3dhFromBundle( | |||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   if (!signatureValue) { |   if (!signatureValue) { | ||||||
|     throw InvalidSignatureException(); |     return Result(InvalidKeyExchangeSignatureError()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Generate EK |   // Generate EK | ||||||
| @ -106,7 +106,7 @@ Future<X3DHAliceResult> x3dhFromBundle( | |||||||
|     await bundle.ik.getBytes(), |     await bundle.ik.getBytes(), | ||||||
|   ]); |   ]); | ||||||
| 
 | 
 | ||||||
|   return X3DHAliceResult(ek, sk, opkId, ad); |   return Result(X3DHAliceResult(ek, sk, opkId, ad)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the | /// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| name: omemo_dart | name: omemo_dart | ||||||
| description: An XMPP library independent OMEMO library | description: An XMPP library independent OMEMO library | ||||||
| version: 0.4.3 | version: 0.5.0 | ||||||
| homepage: https://github.com/PapaTutuWawa/omemo_dart | homepage: https://github.com/PapaTutuWawa/omemo_dart | ||||||
| publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub | publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub | ||||||
| 
 | 
 | ||||||
| @ -13,12 +13,15 @@ dependencies: | |||||||
|   hex: ^0.2.0 |   hex: ^0.2.0 | ||||||
|   logging: ^1.0.2 |   logging: ^1.0.2 | ||||||
|   meta: ^1.7.0 |   meta: ^1.7.0 | ||||||
|  |   moxlib: | ||||||
|  |     version: ^0.2.0 | ||||||
|  |     hosted: https://git.polynom.me/api/packages/Moxxy/pub | ||||||
|   pinenacl: ^0.5.1 |   pinenacl: ^0.5.1 | ||||||
|  |   protobuf: ^2.1.0 | ||||||
|  |   protoc_plugin: ^20.0.1 | ||||||
|   synchronized: ^3.0.0+2 |   synchronized: ^3.0.0+2 | ||||||
| 
 | 
 | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   lints: ^2.0.0 |   lints: ^2.0.0 | ||||||
|   protobuf: ^2.1.0 |  | ||||||
|   protoc_plugin: ^20.0.1 |  | ||||||
|   test: ^1.21.0 |   test: ^1.21.0 | ||||||
|   very_good_analysis: ^3.0.1 |   very_good_analysis: ^3.0.1 | ||||||
|  | |||||||
| @ -1,39 +1,10 @@ | |||||||
| // ignore_for_file: avoid_print |  | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  | import 'dart:developer'; | ||||||
| import 'package:cryptography/cryptography.dart'; | import 'package:cryptography/cryptography.dart'; | ||||||
| import 'package:omemo_dart/omemo_dart.dart'; | import 'package:omemo_dart/omemo_dart.dart'; | ||||||
| import 'package:omemo_dart/protobuf/schema.pb.dart'; |  | ||||||
| import 'package:omemo_dart/src/double_ratchet/crypto.dart'; |  | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
| 
 | 
 | ||||||
| void main() { | void main() { | ||||||
|   test('Test encrypting and decrypting', () async { |  | ||||||
|     final sessionAd = List<int>.filled(32, 0x0); |  | ||||||
|     final mk = List<int>.filled(32, 0x1); |  | ||||||
|     final plaintext = utf8.encode('Hallo'); |  | ||||||
|     final header = OMEMOMessage() |  | ||||||
|       ..n = 0 |  | ||||||
|       ..pn = 0 |  | ||||||
|       ..dhPub = List<int>.empty(); |  | ||||||
|     final asd = concat([sessionAd, header.writeToBuffer()]); |  | ||||||
| 
 |  | ||||||
|     final ciphertext = await encrypt( |  | ||||||
|       mk, |  | ||||||
|       plaintext, |  | ||||||
|       asd, |  | ||||||
|       sessionAd, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     final decrypted = await decrypt( |  | ||||||
|       mk, |  | ||||||
|       ciphertext, |  | ||||||
|       asd, |  | ||||||
|       sessionAd, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     expect(decrypted, plaintext); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test the Double Ratchet', () async { |   test('Test the Double Ratchet', () async { | ||||||
|     // Generate keys |     // Generate keys | ||||||
|     const bobJid = 'bob@other.example.server'; |     const bobJid = 'bob@other.example.server'; | ||||||
| @ -57,7 +28,8 @@ void main() { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Alice does X3DH |     // Alice does X3DH | ||||||
|     final resultAlice = await x3dhFromBundle(bundleBob, ikAlice); |     final resultAliceRaw = await x3dhFromBundle(bundleBob, ikAlice); | ||||||
|  |     final resultAlice = resultAliceRaw.get<X3DHAliceResult>(); | ||||||
| 
 | 
 | ||||||
|     // Alice sends the inital message to Bob |     // Alice sends the inital message to Bob | ||||||
|     // ... |     // ... | ||||||
| @ -74,23 +46,28 @@ void main() { | |||||||
|       ikBob, |       ikBob, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     print('X3DH key exchange done'); |     log('X3DH key exchange done'); | ||||||
| 
 | 
 | ||||||
|     // Alice and Bob now share sk as a common secret and ad |     // Alice and Bob now share sk as a common secret and ad | ||||||
|     // Build a session |     // Build a session | ||||||
|     final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession( |     final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession( | ||||||
|       spkBob.pk, |       spkBob.pk, | ||||||
|  |       bundleBob.spkId, | ||||||
|       ikBob.pk, |       ikBob.pk, | ||||||
|  |       ikAlice.pk, | ||||||
|  |       resultAlice.ek.pk, | ||||||
|       resultAlice.sk, |       resultAlice.sk, | ||||||
|       resultAlice.ad, |       resultAlice.ad, | ||||||
|       0, |       resultAlice.opkId, | ||||||
|     ); |     ); | ||||||
|     final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( |     final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( | ||||||
|       spkBob, |       spkBob, | ||||||
|  |       bundleBob.spkId, | ||||||
|       ikAlice.pk, |       ikAlice.pk, | ||||||
|  |       2, | ||||||
|  |       resultAlice.ek.pk, | ||||||
|       resultBob.sk, |       resultBob.sk, | ||||||
|       resultBob.ad, |       resultBob.ad, | ||||||
|       0, |  | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd); |     expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd); | ||||||
| @ -98,40 +75,42 @@ void main() { | |||||||
|     for (var i = 0; i < 100; i++) { |     for (var i = 0; i < 100; i++) { | ||||||
|       final messageText = 'Hello, dear $i'; |       final messageText = 'Hello, dear $i'; | ||||||
| 
 | 
 | ||||||
|  |       log('${i + 1}/100'); | ||||||
|       if (i.isEven) { |       if (i.isEven) { | ||||||
|         // Alice encrypts a message |         // Alice encrypts a message | ||||||
|         final aliceRatchetResult = |         final aliceRatchetResult = | ||||||
|             await alicesRatchet.ratchetEncrypt(utf8.encode(messageText)); |             await alicesRatchet.ratchetEncrypt(utf8.encode(messageText)); | ||||||
|         print('Alice sent the message'); |         log('Alice sent the message'); | ||||||
| 
 | 
 | ||||||
|         // Alice sends it to Bob |         // Alice sends it to Bob | ||||||
|         // ... |         // ... | ||||||
| 
 | 
 | ||||||
|         // Bob tries to decrypt it |         // Bob tries to decrypt it | ||||||
|         final bobRatchetResult = await bobsRatchet.ratchetDecrypt( |         final bobRatchetResult = await bobsRatchet.ratchetDecrypt( | ||||||
|           aliceRatchetResult.header, |           aliceRatchetResult, | ||||||
|           aliceRatchetResult.ciphertext, |  | ||||||
|         ); |         ); | ||||||
|         print('Bob decrypted the message'); |         log('Bob decrypted the message'); | ||||||
| 
 | 
 | ||||||
|         expect(utf8.encode(messageText), bobRatchetResult); |         expect(bobRatchetResult.isType<List<int>>(), true); | ||||||
|  |         expect(bobRatchetResult.get<List<int>>(), utf8.encode(messageText)); | ||||||
|       } else { |       } else { | ||||||
|         // Bob sends a message to Alice |         // Bob sends a message to Alice | ||||||
|         final bobRatchetResult = |         final bobRatchetResult = | ||||||
|             await bobsRatchet.ratchetEncrypt(utf8.encode(messageText)); |             await bobsRatchet.ratchetEncrypt(utf8.encode(messageText)); | ||||||
|         print('Bob sent the message'); |         log('Bob sent the message'); | ||||||
| 
 | 
 | ||||||
|         // Bobs sends it to Alice |         // Bobs sends it to Alice | ||||||
|         // ... |         // ... | ||||||
| 
 | 
 | ||||||
|         // Alice tries to decrypt it |         // Alice tries to decrypt it | ||||||
|         final aliceRatchetResult = await alicesRatchet.ratchetDecrypt( |         final aliceRatchetResult = await alicesRatchet.ratchetDecrypt( | ||||||
|           bobRatchetResult.header, |           bobRatchetResult, | ||||||
|           bobRatchetResult.ciphertext, |  | ||||||
|         ); |         ); | ||||||
|         print('Alice decrypted the message'); |         log('Alice decrypted the message'); | ||||||
| 
 | 
 | ||||||
|         expect(utf8.encode(messageText), aliceRatchetResult); |         expect(aliceRatchetResult.isType<List<int>>(), true); | ||||||
|  |         expect(aliceRatchetResult.get<List<int>>(), utf8.encode(messageText)); | ||||||
|  |         expect(utf8.encode(messageText), aliceRatchetResult.get<List<int>>()); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  | |||||||
							
								
								
									
										2606
									
								
								test/omemo_test.dart
									
									
									
									
									
								
							
							
						
						
									
										2606
									
								
								test/omemo_test.dart
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,186 +0,0 @@ | |||||||
| import 'package:omemo_dart/protobuf/schema.pb.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/omemo_message.dart'; |  | ||||||
| import 'package:omemo_dart/src/protobuf/protobuf.dart'; |  | ||||||
| import 'package:test/test.dart'; |  | ||||||
| 
 |  | ||||||
| void main() { |  | ||||||
|   group('Base 128 Varints', () { |  | ||||||
|     test('Test simple parsing of Varints', () { |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[1], 0).n, |  | ||||||
|         1, |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[1], 0).length, |  | ||||||
|         1, |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[0x96, 0x01, 0x00], 0).n, |  | ||||||
|         150, |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[0x96, 0x01, 0x00], 0).length, |  | ||||||
|         2, |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[172, 2, 0x8], 0).n, |  | ||||||
|         300, |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         decodeVarint(<int>[172, 2, 0x8], 0).length, |  | ||||||
|         2, |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('Test encoding Varints', () { |  | ||||||
|       expect( |  | ||||||
|         encodeVarint(1), |  | ||||||
|         <int>[1], |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         encodeVarint(150), |  | ||||||
|         <int>[0x96, 0x01], |  | ||||||
|       ); |  | ||||||
|       expect( |  | ||||||
|         encodeVarint(300), |  | ||||||
|         <int>[172, 2], |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('Test some special cases', () { |  | ||||||
|       expect(decodeVarint(encodeVarint(1042464893), 0).n, 1042464893); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   group('OMEMOMessage', () { |  | ||||||
|     test('Decode a OMEMOMessage', () { |  | ||||||
|       final pbMessage = OMEMOMessage() |  | ||||||
|         ..n = 1 |  | ||||||
|         ..pn = 5 |  | ||||||
|         ..dhPub = <int>[1, 2, 3] |  | ||||||
|         ..ciphertext = <int>[4, 5, 6]; |  | ||||||
|       final serial = pbMessage.writeToBuffer(); |  | ||||||
|       final msg = OmemoMessage.fromBuffer(serial); |  | ||||||
| 
 |  | ||||||
|       expect(msg.n, 1); |  | ||||||
|       expect(msg.pn, 5); |  | ||||||
|       expect(msg.dhPub, <int>[1, 2, 3]); |  | ||||||
|       expect(msg.ciphertext, <int>[4, 5, 6]); |  | ||||||
|     }); |  | ||||||
|     test('Decode a OMEMOMessage without ciphertext', () { |  | ||||||
|       final pbMessage = OMEMOMessage() |  | ||||||
|         ..n = 1 |  | ||||||
|         ..pn = 5 |  | ||||||
|         ..dhPub = <int>[1, 2, 3]; |  | ||||||
|       final serial = pbMessage.writeToBuffer(); |  | ||||||
|       final msg = OmemoMessage.fromBuffer(serial); |  | ||||||
| 
 |  | ||||||
|       expect(msg.n, 1); |  | ||||||
|       expect(msg.pn, 5); |  | ||||||
|       expect(msg.dhPub, <int>[1, 2, 3]); |  | ||||||
|       expect(msg.ciphertext, null); |  | ||||||
|     }); |  | ||||||
|     test('Encode a OMEMOMessage', () { |  | ||||||
|       final m = OmemoMessage() |  | ||||||
|         ..n = 1 |  | ||||||
|         ..pn = 5 |  | ||||||
|         ..dhPub = <int>[1, 2, 3] |  | ||||||
|         ..ciphertext = <int>[4, 5, 6]; |  | ||||||
|       final serial = m.writeToBuffer(); |  | ||||||
|       final msg = OMEMOMessage.fromBuffer(serial); |  | ||||||
| 
 |  | ||||||
|       expect(msg.n, 1); |  | ||||||
|       expect(msg.pn, 5); |  | ||||||
|       expect(msg.dhPub, <int>[1, 2, 3]); |  | ||||||
|       expect(msg.ciphertext, <int>[4, 5, 6]); |  | ||||||
|     }); |  | ||||||
|     test('Encode a OMEMOMessage without ciphertext', () { |  | ||||||
|       final m = OmemoMessage() |  | ||||||
|         ..n = 1 |  | ||||||
|         ..pn = 5 |  | ||||||
|         ..dhPub = <int>[1, 2, 3]; |  | ||||||
|       final serial = m.writeToBuffer(); |  | ||||||
|       final msg = OMEMOMessage.fromBuffer(serial); |  | ||||||
| 
 |  | ||||||
|       expect(msg.n, 1); |  | ||||||
|       expect(msg.pn, 5); |  | ||||||
|       expect(msg.dhPub, <int>[1, 2, 3]); |  | ||||||
|       expect(msg.ciphertext, <int>[]); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   group('OMEMOAuthenticatedMessage', () { |  | ||||||
|     test('Test encoding a message', () { |  | ||||||
|       final msg = OmemoAuthenticatedMessage() |  | ||||||
|         ..mac = <int>[1, 2, 3] |  | ||||||
|         ..message = <int>[4, 5, 6]; |  | ||||||
|       final decoded = OMEMOAuthenticatedMessage.fromBuffer(msg.writeToBuffer()); |  | ||||||
| 
 |  | ||||||
|       expect(decoded.mac, <int>[1, 2, 3]); |  | ||||||
|       expect(decoded.message, <int>[4, 5, 6]); |  | ||||||
|     }); |  | ||||||
|     test('Test decoding a message', () { |  | ||||||
|       final msg = OMEMOAuthenticatedMessage() |  | ||||||
|         ..mac = <int>[1, 2, 3] |  | ||||||
|         ..message = <int>[4, 5, 6]; |  | ||||||
|       final bytes = msg.writeToBuffer(); |  | ||||||
|       final decoded = OmemoAuthenticatedMessage.fromBuffer(bytes); |  | ||||||
| 
 |  | ||||||
|       expect(decoded.mac, <int>[1, 2, 3]); |  | ||||||
|       expect(decoded.message, <int>[4, 5, 6]); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   group('OMEMOKeyExchange', () { |  | ||||||
|     test('Test encoding a message', () { |  | ||||||
|       final authMessage = OmemoAuthenticatedMessage() |  | ||||||
|         ..mac = <int>[5, 6, 8, 0] |  | ||||||
|         ..message = <int>[4, 5, 7, 3, 2]; |  | ||||||
|       final message = OmemoKeyExchange() |  | ||||||
|         ..pkId = 698 |  | ||||||
|         ..spkId = 245 |  | ||||||
|         ..ik = <int>[1, 4, 6] |  | ||||||
|         ..ek = <int>[4, 6, 7, 80] |  | ||||||
|         ..message = authMessage; |  | ||||||
|       final kex = OMEMOKeyExchange.fromBuffer(message.writeToBuffer()); |  | ||||||
| 
 |  | ||||||
|       expect(kex.pkId, 698); |  | ||||||
|       expect(kex.spkId, 245); |  | ||||||
|       expect(kex.ik, <int>[1, 4, 6]); |  | ||||||
|       expect(kex.ek, <int>[4, 6, 7, 80]); |  | ||||||
| 
 |  | ||||||
|       expect(kex.message.mac, <int>[5, 6, 8, 0]); |  | ||||||
|       expect(kex.message.message, <int>[4, 5, 7, 3, 2]); |  | ||||||
|     }); |  | ||||||
|     test('Test decoding a message', () { |  | ||||||
|       final message = OMEMOAuthenticatedMessage() |  | ||||||
|         ..mac = <int>[5, 6, 8, 0] |  | ||||||
|         ..message = <int>[4, 5, 7, 3, 2]; |  | ||||||
|       final kex = OMEMOKeyExchange() |  | ||||||
|         ..pkId = 698 |  | ||||||
|         ..spkId = 245 |  | ||||||
|         ..ik = <int>[1, 4, 6] |  | ||||||
|         ..ek = <int>[4, 6, 7, 80] |  | ||||||
|         ..message = message; |  | ||||||
|       final decoded = OmemoKeyExchange.fromBuffer(kex.writeToBuffer()); |  | ||||||
| 
 |  | ||||||
|       expect(decoded.pkId, 698); |  | ||||||
|       expect(decoded.spkId, 245); |  | ||||||
|       expect(decoded.ik, <int>[1, 4, 6]); |  | ||||||
|       expect(decoded.ek, <int>[4, 6, 7, 80]); |  | ||||||
| 
 |  | ||||||
|       expect(decoded.message!.mac, <int>[5, 6, 8, 0]); |  | ||||||
|       expect(decoded.message!.message, <int>[4, 5, 7, 3, 2]); |  | ||||||
|     }); |  | ||||||
|     test('Test decoding an issue', () { |  | ||||||
|       /* |  | ||||||
|       final data = 'CAAQfRogc2GwslU219dUkrMHNM4KdZRmuFnBTae+bQaJ+55IsAMiII7aZKj2sUpb6xR/3Ari7WZUmKFV0G6czUc4NMvjKDBaKnwKEM2ZpI8X3TgcxhxwENANnlsSaAgAEAAaICy8T9WPgLb7RdYd8/4JkrLF0RahEkC3ZaEfk5jw3dsLIkBMILzLyByweLgF4lCn0oNea+kbdrFr6rY7r/7WyI8hXEQz38QpnN+jyGGwC7Ga0dq70WuyqE7VpiFArQwqZh2G'; |  | ||||||
|       final kex = OmemoKeyExchange.fromBuffer(base64Decode(data)); |  | ||||||
| 
 |  | ||||||
|       expect(kex.spkId!, 1042464893); |  | ||||||
|       */ |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
							
								
								
									
										62
									
								
								test/queue_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								test/queue_test.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'package:omemo_dart/src/omemo/queue.dart'; | ||||||
|  | import 'package:test/test.dart'; | ||||||
|  | 
 | ||||||
|  | Future<void> testMethod( | ||||||
|  |   RatchetAccessQueue queue, | ||||||
|  |   List<String> data, | ||||||
|  |   int duration, | ||||||
|  | ) async { | ||||||
|  |   await queue.enterCriticalSection(data); | ||||||
|  | 
 | ||||||
|  |   await Future<void>.delayed(Duration(seconds: duration)); | ||||||
|  | 
 | ||||||
|  |   await queue.leaveCriticalSection(data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void main() { | ||||||
|  |   test('Test blocking due to conflicts', () async { | ||||||
|  |     final queue = RatchetAccessQueue(); | ||||||
|  | 
 | ||||||
|  |     unawaited(testMethod(queue, ['a', 'b', 'c'], 5)); | ||||||
|  |     unawaited(testMethod(queue, ['a'], 4)); | ||||||
|  | 
 | ||||||
|  |     await Future<void>.delayed(const Duration(seconds: 1)); | ||||||
|  |     expect( | ||||||
|  |       queue.runningOperations.containsAll(['a', 'b', 'c']), | ||||||
|  |       isTrue, | ||||||
|  |     ); | ||||||
|  |     expect(queue.runningOperations.length, 3); | ||||||
|  | 
 | ||||||
|  |     await Future<void>.delayed(const Duration(seconds: 4)); | ||||||
|  | 
 | ||||||
|  |     expect( | ||||||
|  |       queue.runningOperations.containsAll(['a']), | ||||||
|  |       isTrue, | ||||||
|  |     ); | ||||||
|  |     expect(queue.runningOperations.length, 1); | ||||||
|  | 
 | ||||||
|  |     await Future<void>.delayed(const Duration(seconds: 4)); | ||||||
|  |     expect(queue.runningOperations.length, 0); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Test not blocking due to no conflicts', () async { | ||||||
|  |     final queue = RatchetAccessQueue(); | ||||||
|  | 
 | ||||||
|  |     unawaited(testMethod(queue, ['a', 'b'], 5)); | ||||||
|  |     unawaited(testMethod(queue, ['c'], 5)); | ||||||
|  |     unawaited(testMethod(queue, ['d'], 5)); | ||||||
|  | 
 | ||||||
|  |     await Future<void>.delayed(const Duration(seconds: 1)); | ||||||
|  |     expect(queue.runningOperations.length, 4); | ||||||
|  |     expect( | ||||||
|  |       queue.runningOperations.containsAll([ | ||||||
|  |         'a', | ||||||
|  |         'b', | ||||||
|  |         'c', | ||||||
|  |         'd', | ||||||
|  |       ]), | ||||||
|  |       isTrue, | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -1,155 +0,0 @@ | |||||||
| import 'dart:convert'; |  | ||||||
| import 'package:omemo_dart/omemo_dart.dart'; |  | ||||||
| import 'package:omemo_dart/src/trust/always.dart'; |  | ||||||
| import 'package:test/test.dart'; |  | ||||||
| 
 |  | ||||||
| Map<String, dynamic> jsonify(Map<String, dynamic> map) { |  | ||||||
|   return jsonDecode(jsonEncode(map)) as Map<String, dynamic>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void main() { |  | ||||||
|   test('Test serialising and deserialising the Device', () async { |  | ||||||
|     // Generate a random session |  | ||||||
|     final oldSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       'user@test.server', |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 1, |  | ||||||
|     ); |  | ||||||
|     final oldDevice = await oldSession.getDevice(); |  | ||||||
|     final serialised = jsonify(await oldDevice.toJson()); |  | ||||||
| 
 |  | ||||||
|     final newDevice = OmemoDevice.fromJson(serialised); |  | ||||||
|     expect(await oldDevice.equals(newDevice), true); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test serialising and deserialising the Device after rotating the SPK', |  | ||||||
|       () async { |  | ||||||
|     // Generate a random session |  | ||||||
|     final oldSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       'user@test.server', |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 1, |  | ||||||
|     ); |  | ||||||
|     final oldDevice = |  | ||||||
|         await (await oldSession.getDevice()).replaceSignedPrekey(); |  | ||||||
|     final serialised = jsonify(await oldDevice.toJson()); |  | ||||||
| 
 |  | ||||||
|     final newDevice = OmemoDevice.fromJson(serialised); |  | ||||||
|     expect(await oldDevice.equals(newDevice), true); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test serialising and deserialising the OmemoDoubleRatchet', () async { |  | ||||||
|     // Generate a random ratchet |  | ||||||
|     const aliceJid = 'alice@server.example'; |  | ||||||
|     const bobJid = 'bob@other.server.example'; |  | ||||||
|     final aliceSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       aliceJid, |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 1, |  | ||||||
|     ); |  | ||||||
|     final bobSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       bobJid, |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 1, |  | ||||||
|     ); |  | ||||||
|     final aliceMessage = await aliceSession.encryptToJid( |  | ||||||
|       bobJid, |  | ||||||
|       'Hello Bob!', |  | ||||||
|       newSessions: [ |  | ||||||
|         await bobSession.getDeviceBundle(), |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|     await bobSession.decryptMessage( |  | ||||||
|       aliceMessage.ciphertext, |  | ||||||
|       aliceJid, |  | ||||||
|       await aliceSession.getDeviceId(), |  | ||||||
|       aliceMessage.encryptedKeys, |  | ||||||
|       getTimestamp(), |  | ||||||
|     ); |  | ||||||
|     final aliceOld = |  | ||||||
|         aliceSession.getRatchet(bobJid, await bobSession.getDeviceId()); |  | ||||||
|     final aliceSerialised = jsonify(await aliceOld.toJson()); |  | ||||||
|     final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised); |  | ||||||
| 
 |  | ||||||
|     expect(await aliceOld.equals(aliceNew), true); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test serialising and deserialising the OmemoSessionManager', () async { |  | ||||||
|     // Generate a random session |  | ||||||
|     final oldSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       'a@server', |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 4, |  | ||||||
|     ); |  | ||||||
|     final bobSession = await OmemoSessionManager.generateNewIdentity( |  | ||||||
|       'b@other.server', |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|       opkAmount: 4, |  | ||||||
|     ); |  | ||||||
|     await oldSession.addSessionFromBundle( |  | ||||||
|       'bob@localhost', |  | ||||||
|       await bobSession.getDeviceId(), |  | ||||||
|       await bobSession.getDeviceBundle(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Serialise and deserialise |  | ||||||
|     final serialised = jsonify(await oldSession.toJsonWithoutSessions()); |  | ||||||
|     final newSession = OmemoSessionManager.fromJsonWithoutSessions( |  | ||||||
|       serialised, |  | ||||||
|       // NOTE: At this point, we don't care about this attribute |  | ||||||
|       {}, |  | ||||||
|       AlwaysTrustingTrustManager(), |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     final oldDevice = await oldSession.getDevice(); |  | ||||||
|     final newDevice = await newSession.getDevice(); |  | ||||||
|     expect(await oldDevice.equals(newDevice), true); |  | ||||||
|     expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap()); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test serializing and deserializing RatchetMapKey', () { |  | ||||||
|     const test1 = RatchetMapKey('user@example.org', 1234); |  | ||||||
|     final result1 = RatchetMapKey.fromJsonKey(test1.toJsonKey()); |  | ||||||
|     expect(result1.jid, test1.jid); |  | ||||||
|     expect(result1.deviceId, test1.deviceId); |  | ||||||
| 
 |  | ||||||
|     const test2 = RatchetMapKey('user@example.org/hallo:welt', 3333); |  | ||||||
|     final result2 = RatchetMapKey.fromJsonKey(test2.toJsonKey()); |  | ||||||
|     expect(result2.jid, test2.jid); |  | ||||||
|     expect(result2.deviceId, test2.deviceId); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   test('Test serializing and deserializing the components of the BTBV manager', |  | ||||||
|       () async { |  | ||||||
|     // Caroline's BTBV manager |  | ||||||
|     final btbv = MemoryBTBVTrustManager(); |  | ||||||
|     // Example data |  | ||||||
|     const aliceJid = 'alice@some.server'; |  | ||||||
|     const bobJid = 'bob@other.server'; |  | ||||||
| 
 |  | ||||||
|     await btbv.onNewSession(aliceJid, 1); |  | ||||||
|     await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified); |  | ||||||
|     await btbv.onNewSession(aliceJid, 2); |  | ||||||
|     await btbv.onNewSession(bobJid, 3); |  | ||||||
|     await btbv.onNewSession(bobJid, 4); |  | ||||||
| 
 |  | ||||||
|     final serialized = jsonify(await btbv.toJson()); |  | ||||||
|     final deviceList = |  | ||||||
|         BlindTrustBeforeVerificationTrustManager.deviceListFromJson( |  | ||||||
|       serialized, |  | ||||||
|     ); |  | ||||||
|     expect(btbv.devices, deviceList); |  | ||||||
| 
 |  | ||||||
|     final trustCache = |  | ||||||
|         BlindTrustBeforeVerificationTrustManager.trustCacheFromJson( |  | ||||||
|       serialized, |  | ||||||
|     ); |  | ||||||
|     expect(btbv.trustCache, trustCache); |  | ||||||
| 
 |  | ||||||
|     final enableCache = |  | ||||||
|         BlindTrustBeforeVerificationTrustManager.enableCacheFromJson( |  | ||||||
|       serialized, |  | ||||||
|     ); |  | ||||||
|     expect(btbv.enablementCache, enableCache); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @ -4,7 +4,7 @@ import 'package:test/test.dart'; | |||||||
| void main() { | void main() { | ||||||
|   test('Test the Blind Trust Before Verification TrustManager', () async { |   test('Test the Blind Trust Before Verification TrustManager', () async { | ||||||
|     // Caroline's BTBV manager |     // Caroline's BTBV manager | ||||||
|     final btbv = MemoryBTBVTrustManager(); |     final btbv = BlindTrustBeforeVerificationTrustManager(); | ||||||
|     // Example data |     // Example data | ||||||
|     const aliceJid = 'alice@some.server'; |     const aliceJid = 'alice@some.server'; | ||||||
|     const bobJid = 'bob@other.server'; |     const bobJid = 'bob@other.server'; | ||||||
|  | |||||||
| @ -26,7 +26,8 @@ void main() { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Alice does X3DH |     // Alice does X3DH | ||||||
|     final resultAlice = await x3dhFromBundle(bundleBob, ikAlice); |     final resultAliceRaw = await x3dhFromBundle(bundleBob, ikAlice); | ||||||
|  |     final resultAlice = resultAliceRaw.get<X3DHAliceResult>(); | ||||||
| 
 | 
 | ||||||
|     // Alice sends the inital message to Bob |     // Alice sends the inital message to Bob | ||||||
|     // ... |     // ... | ||||||
| @ -68,18 +69,7 @@ void main() { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Alice does X3DH |     // Alice does X3DH | ||||||
|     var exception = false; |     final result = await x3dhFromBundle(bundleBob, ikAlice); | ||||||
|     try { |     expect(result.isType<InvalidKeyExchangeSignatureError>(), isTrue); | ||||||
|       await x3dhFromBundle(bundleBob, ikAlice); |  | ||||||
|     } catch (e) { |  | ||||||
|       exception = true; |  | ||||||
|       expect( |  | ||||||
|         e is InvalidSignatureException, |  | ||||||
|         true, |  | ||||||
|         reason: 'Expected InvalidSignatureException, but got $e', |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     expect(exception, true, reason: 'Expected test failure'); |  | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user