Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afad5056c0 | |||
| c68471349a | |||
| 1472624b1d | |||
| 0826d043d5 | |||
| 2aa3674c4b | |||
| 7c3a9a75df |
1
.pubignore
Normal file
1
.pubignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lib/protobuf
|
||||||
@@ -119,6 +119,8 @@ void main() async {
|
|||||||
aliceDevice.id,
|
aliceDevice.id,
|
||||||
// The deserialised keys
|
// The deserialised keys
|
||||||
keys,
|
keys,
|
||||||
|
// Since the message was not delayed, we use the current time
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
);
|
);
|
||||||
|
|
||||||
// All Bob has to do now is replace the OMEMO wrapper element
|
// All Bob has to do now is replace the OMEMO wrapper element
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class OmemoDoubleRatchet {
|
|||||||
this.sessionAd,
|
this.sessionAd,
|
||||||
this.mkSkipped, // MKSKIPPED
|
this.mkSkipped, // MKSKIPPED
|
||||||
this.acknowledged,
|
this.acknowledged,
|
||||||
|
this.kexTimestamp,
|
||||||
|
this.kex,
|
||||||
);
|
);
|
||||||
|
|
||||||
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
|
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
|
||||||
@@ -82,9 +84,11 @@ class OmemoDoubleRatchet {
|
|||||||
'ns': 0,
|
'ns': 0,
|
||||||
'nr': 0,
|
'nr': 0,
|
||||||
'pn': 0,
|
'pn': 0,
|
||||||
'ik_pub': 'base/64/encoded',
|
'ik_pub': null | 'base/64/encoded',
|
||||||
'session_ad': 'base/64/encoded',
|
'session_ad': 'base/64/encoded',
|
||||||
'acknowledged': true | false,
|
'acknowledged': true | false,
|
||||||
|
'kex_timestamp': int,
|
||||||
|
'kex': 'base/64/encoded',
|
||||||
'mkskipped': [
|
'mkskipped': [
|
||||||
{
|
{
|
||||||
'key': 'base/64/encoded',
|
'key': 'base/64/encoded',
|
||||||
@@ -129,6 +133,8 @@ class OmemoDoubleRatchet {
|
|||||||
base64.decode(data['session_ad']! as String),
|
base64.decode(data['session_ad']! as String),
|
||||||
mkSkipped,
|
mkSkipped,
|
||||||
data['acknowledged']! as bool,
|
data['acknowledged']! as bool,
|
||||||
|
data['kex_timestamp']! as int,
|
||||||
|
data['kex'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +166,13 @@ class OmemoDoubleRatchet {
|
|||||||
|
|
||||||
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.
|
||||||
|
final String? 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.
|
||||||
bool acknowledged;
|
bool acknowledged;
|
||||||
@@ -167,7 +180,7 @@ class OmemoDoubleRatchet {
|
|||||||
/// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that
|
/// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that
|
||||||
/// was obtained using a X3DH and the associated data [ad] that was also obtained through
|
/// was obtained using a X3DH and the associated data [ad] that was also obtained through
|
||||||
/// 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(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async {
|
static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int timestamp) async {
|
||||||
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
|
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
|
||||||
final dhr = spk;
|
final dhr = spk;
|
||||||
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
|
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
|
||||||
@@ -186,6 +199,8 @@ class OmemoDoubleRatchet {
|
|||||||
ad,
|
ad,
|
||||||
{},
|
{},
|
||||||
false,
|
false,
|
||||||
|
timestamp,
|
||||||
|
'',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +208,7 @@ class OmemoDoubleRatchet {
|
|||||||
/// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and
|
/// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and
|
||||||
/// the associated data [ad] that was also obtained through a X3DH. [ik] refers to
|
/// the associated data [ad] that was also obtained through a X3DH. [ik] refers to
|
||||||
/// Alice's (the initiator's) IK public key.
|
/// Alice's (the initiator's) IK public key.
|
||||||
static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async {
|
static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int kexTimestamp) async {
|
||||||
return OmemoDoubleRatchet(
|
return OmemoDoubleRatchet(
|
||||||
spk,
|
spk,
|
||||||
null,
|
null,
|
||||||
@@ -207,6 +222,8 @@ class OmemoDoubleRatchet {
|
|||||||
ad,
|
ad,
|
||||||
{},
|
{},
|
||||||
false,
|
false,
|
||||||
|
kexTimestamp,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +250,8 @@ class OmemoDoubleRatchet {
|
|||||||
'session_ad': base64.encode(sessionAd),
|
'session_ad': base64.encode(sessionAd),
|
||||||
'mkskipped': mkSkippedSerialised,
|
'mkskipped': mkSkippedSerialised,
|
||||||
'acknowledged': acknowledged,
|
'acknowledged': acknowledged,
|
||||||
|
'kex_timestamp': kexTimestamp,
|
||||||
|
'kex': kex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,18 +287,18 @@ class OmemoDoubleRatchet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _dhRatchet(OmemoMessage header) async {
|
Future<void> _dhRatchet(OmemoMessage header) async {
|
||||||
pn = header.n!;
|
pn = ns;
|
||||||
ns = 0;
|
ns = 0;
|
||||||
nr = 0;
|
nr = 0;
|
||||||
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
|
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
|
||||||
|
|
||||||
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
||||||
rk = newRk;
|
rk = List.from(newRk);
|
||||||
ckr = newRk;
|
ckr = List.from(newRk);
|
||||||
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
|
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
|
||||||
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
||||||
rk = newNewRk;
|
rk = List.from(newNewRk);
|
||||||
cks = newNewRk;
|
cks = List.from(newNewRk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt [plaintext] using the Double Ratchet.
|
/// Encrypt [plaintext] using the Double Ratchet.
|
||||||
@@ -313,8 +332,8 @@ class OmemoDoubleRatchet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final dhPubMatches = listsEqual(
|
final dhPubMatches = listsEqual(
|
||||||
header.dhPub ?? <int>[],
|
header.dhPub!,
|
||||||
await dhr?.getBytes() ?? <int>[],
|
(await dhr?.getBytes()) ?? <int>[],
|
||||||
);
|
);
|
||||||
if (!dhPubMatches) {
|
if (!dhPubMatches) {
|
||||||
await _skipMessageKeys(header.pn!);
|
await _skipMessageKeys(header.pn!);
|
||||||
@@ -330,12 +349,64 @@ class OmemoDoubleRatchet {
|
|||||||
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
|
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OmemoDoubleRatchet clone() {
|
||||||
|
return OmemoDoubleRatchet(
|
||||||
|
dhs,
|
||||||
|
dhr,
|
||||||
|
rk,
|
||||||
|
cks != null ?
|
||||||
|
List<int>.from(cks!) :
|
||||||
|
null,
|
||||||
|
ckr != null ?
|
||||||
|
List<int>.from(ckr!) :
|
||||||
|
null,
|
||||||
|
ns,
|
||||||
|
nr,
|
||||||
|
pn,
|
||||||
|
ik,
|
||||||
|
sessionAd,
|
||||||
|
Map<SkippedKey, List<int>>.from(mkSkipped),
|
||||||
|
acknowledged,
|
||||||
|
kexTimestamp,
|
||||||
|
kex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OmemoDoubleRatchet cloneWithKex(String kex) {
|
||||||
|
return OmemoDoubleRatchet(
|
||||||
|
dhs,
|
||||||
|
dhr,
|
||||||
|
rk,
|
||||||
|
cks != null ?
|
||||||
|
List<int>.from(cks!) :
|
||||||
|
null,
|
||||||
|
ckr != null ?
|
||||||
|
List<int>.from(ckr!) :
|
||||||
|
null,
|
||||||
|
ns,
|
||||||
|
nr,
|
||||||
|
pn,
|
||||||
|
ik,
|
||||||
|
sessionAd,
|
||||||
|
Map<SkippedKey, List<int>>.from(mkSkipped),
|
||||||
|
acknowledged,
|
||||||
|
kexTimestamp,
|
||||||
|
kex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
Future<bool> equals(OmemoDoubleRatchet other) async {
|
Future<bool> equals(OmemoDoubleRatchet other) async {
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
final dhrMatch = dhr == null ?
|
||||||
final dhrMatch = dhr == null ? other.dhr == null : await dhr!.equals(other.dhr!);
|
other.dhr == null :
|
||||||
final ckrMatch = ckr == null ? other.ckr == null : listsEqual(ckr!, other.ckr!);
|
// ignore: invalid_use_of_visible_for_testing_member
|
||||||
final cksMatch = cks == null ? other.cks == null : listsEqual(cks!, other.cks!);
|
other.dhr != null && await dhr!.equals(other.dhr!);
|
||||||
|
final ckrMatch = ckr == null ?
|
||||||
|
other.ckr == null :
|
||||||
|
other.ckr != null && listsEqual(ckr!, other.ckr!);
|
||||||
|
final cksMatch = cks == null ?
|
||||||
|
other.cks == null :
|
||||||
|
other.cks != null && listsEqual(cks!, other.cks!);
|
||||||
|
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
// ignore: invalid_use_of_visible_for_testing_member
|
||||||
final dhsMatch = await dhs.equals(other.dhs);
|
final dhsMatch = await dhs.equals(other.dhs);
|
||||||
@@ -351,6 +422,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,16 @@ class NoDecryptionKeyException implements Exception {
|
|||||||
class UnknownSignedPrekeyException implements Exception {
|
class UnknownSignedPrekeyException implements Exception {
|
||||||
String errMsg() => 'Unknown Signed Prekey used.';
|
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 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 implements Exception {
|
||||||
|
String errMsg() => 'The message has already been decrypted';
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,3 +73,7 @@ OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) {
|
|||||||
type,
|
type,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getTimestamp() {
|
||||||
|
return DateTime.now().millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ class OmemoSessionManager {
|
|||||||
bundle.ik,
|
bundle.ik,
|
||||||
kexResult.sk,
|
kexResult.sk,
|
||||||
kexResult.ad,
|
kexResult.ad,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _trustManager.onNewSession(jid, deviceId);
|
await _trustManager.onNewSession(jid, deviceId);
|
||||||
@@ -169,7 +170,7 @@ class OmemoSessionManager {
|
|||||||
/// Build a new session with the user at [jid] with the device [deviceId] using data
|
/// 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
|
/// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey
|
||||||
/// identifier an UnknownSignedPrekeyException will be thrown.
|
/// identifier an UnknownSignedPrekeyException will be thrown.
|
||||||
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
|
Future<OmemoDoubleRatchet> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
|
||||||
// Pick the correct SPK
|
// Pick the correct SPK
|
||||||
final device = await getDevice();
|
final device = await getDevice();
|
||||||
final spk = await _lock.synchronized(() async {
|
final spk = await _lock.synchronized(() async {
|
||||||
@@ -200,10 +201,10 @@ class OmemoSessionManager {
|
|||||||
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
|
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
|
||||||
kexResult.sk,
|
kexResult.sk,
|
||||||
kexResult.ad,
|
kexResult.ad,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _trustManager.onNewSession(jid, deviceId);
|
return ratchet;
|
||||||
await _addSession(jid, deviceId, ratchet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [encryptToJids] but only for one Jid [jid].
|
/// Like [encryptToJids] but only for one Jid [jid].
|
||||||
@@ -240,7 +241,11 @@ class OmemoSessionManager {
|
|||||||
final kex = <int, OmemoKeyExchange>{};
|
final kex = <int, OmemoKeyExchange>{};
|
||||||
if (newSessions != null) {
|
if (newSessions != null) {
|
||||||
for (final newSession in newSessions) {
|
for (final newSession in newSessions) {
|
||||||
kex[newSession.id] = await addSessionFromBundle(newSession.jid, newSession.id, newSession);
|
kex[newSession.id] = await addSessionFromBundle(
|
||||||
|
newSession.jid,
|
||||||
|
newSession.id,
|
||||||
|
newSession,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,24 +263,53 @@ class OmemoSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ratchetKey = RatchetMapKey(jid, deviceId);
|
final ratchetKey = RatchetMapKey(jid, deviceId);
|
||||||
final ratchet = _ratchetMap[ratchetKey]!;
|
var ratchet = _ratchetMap[ratchetKey]!;
|
||||||
final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
|
final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
|
||||||
|
|
||||||
// Commit the ratchet
|
|
||||||
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
|
|
||||||
|
|
||||||
if (kex.isNotEmpty && kex.containsKey(deviceId)) {
|
if (kex.isNotEmpty && kex.containsKey(deviceId)) {
|
||||||
|
// The ratchet did not exist
|
||||||
final k = kex[deviceId]!
|
final k = kex[deviceId]!
|
||||||
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
|
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
|
||||||
|
final buffer = base64.encode(k.writeToBuffer());
|
||||||
encryptedKeys.add(
|
encryptedKeys.add(
|
||||||
EncryptedKey(
|
EncryptedKey(
|
||||||
jid,
|
jid,
|
||||||
deviceId,
|
deviceId,
|
||||||
base64.encode(k.writeToBuffer()),
|
buffer,
|
||||||
true,
|
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 {
|
} else {
|
||||||
|
// The ratchet exists and is acked
|
||||||
encryptedKeys.add(
|
encryptedKeys.add(
|
||||||
EncryptedKey(
|
EncryptedKey(
|
||||||
jid,
|
jid,
|
||||||
@@ -285,6 +319,9 @@ class OmemoSessionManager {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the ratchet
|
||||||
|
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -300,7 +337,7 @@ class OmemoSessionManager {
|
|||||||
/// [mapKey] with [oldRatchet].
|
/// [mapKey] with [oldRatchet].
|
||||||
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
|
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
|
||||||
await _lock.synchronized(() {
|
await _lock.synchronized(() {
|
||||||
_log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId}');
|
_log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId} to ${oldRatchet.nr}');
|
||||||
_ratchetMap[mapKey] = oldRatchet;
|
_ratchetMap[mapKey] = oldRatchet;
|
||||||
|
|
||||||
// Commit the ratchet
|
// Commit the ratchet
|
||||||
@@ -314,16 +351,38 @@ class OmemoSessionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the
|
/// 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
|
/// <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
|
/// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the
|
||||||
/// <encrypted /> element.
|
/// <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 />
|
/// 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
|
/// 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
|
/// will return null as there is no message to be decrypted. This, however, is used
|
||||||
/// to set up sessions or advance the ratchets.
|
/// to set up sessions or advance the ratchets.
|
||||||
Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys) async {
|
Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys, int timestamp) async {
|
||||||
// Try to find a session we can decrypt with.
|
// Try to find a session we can decrypt with.
|
||||||
var device = await getDevice();
|
var device = await getDevice();
|
||||||
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
|
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
|
||||||
@@ -333,24 +392,55 @@ class OmemoSessionManager {
|
|||||||
|
|
||||||
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
|
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
|
||||||
final decodedRawKey = base64.decode(rawKey.value);
|
final decodedRawKey = base64.decode(rawKey.value);
|
||||||
|
List<int>? keyAndHmac;
|
||||||
OmemoAuthenticatedMessage authMessage;
|
OmemoAuthenticatedMessage authMessage;
|
||||||
OmemoDoubleRatchet? oldRatchet;
|
OmemoDoubleRatchet? oldRatchet;
|
||||||
|
OmemoMessage? message;
|
||||||
if (rawKey.kex) {
|
if (rawKey.kex) {
|
||||||
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
|
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
|
||||||
// null.
|
// null.
|
||||||
oldRatchet = await _getRatchet(ratchetKey);
|
final oldRatchet = (await _getRatchet(ratchetKey))?.clone();
|
||||||
|
|
||||||
// TODO(PapaTutuWawa): Only do this when we should
|
|
||||||
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
|
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
|
||||||
await _addSessionFromKeyExchange(
|
|
||||||
senderJid,
|
|
||||||
senderDeviceId,
|
|
||||||
kex,
|
|
||||||
);
|
|
||||||
|
|
||||||
authMessage = kex.message!;
|
authMessage = kex.message!;
|
||||||
|
message = OmemoMessage.fromBuffer(authMessage.message!);
|
||||||
|
|
||||||
|
// Guard against old key exchanges
|
||||||
|
if (oldRatchet != null) {
|
||||||
|
_log.finest('KEX for existent ratchet. ${oldRatchet.pn}');
|
||||||
|
if (oldRatchet.kexTimestamp > timestamp) {
|
||||||
|
throw InvalidKeyExchangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to decrypt it
|
||||||
|
try {
|
||||||
|
final decrypted = await oldRatchet.ratchetDecrypt(message, authMessage.writeToBuffer());
|
||||||
|
|
||||||
|
// Commit the ratchet
|
||||||
|
_eventStreamController.add(
|
||||||
|
RatchetModifiedEvent(
|
||||||
|
senderJid,
|
||||||
|
senderDeviceId,
|
||||||
|
oldRatchet,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final plaintext = await _decryptAndVerifyHmac(
|
||||||
|
ciphertext,
|
||||||
|
decrypted,
|
||||||
|
);
|
||||||
|
await _addSession(senderJid, senderDeviceId, oldRatchet);
|
||||||
|
return plaintext;
|
||||||
|
} catch (_) {
|
||||||
|
_log.finest('Failed to use old ratchet with KEX for existing ratchet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final r = await _addSessionFromKeyExchange(senderJid, senderDeviceId, kex);
|
||||||
|
await _trustManager.onNewSession(senderJid, senderDeviceId);
|
||||||
|
await _addSession(senderJid, senderDeviceId, r);
|
||||||
|
|
||||||
// Replace the OPK
|
// Replace the OPK
|
||||||
|
// TODO(PapaTutuWawa): Replace the OPK when we know that the KEX worked
|
||||||
await _deviceLock.synchronized(() async {
|
await _deviceLock.synchronized(() async {
|
||||||
device = await device.replaceOnetimePrekey(kex.pkId!);
|
device = await device.replaceOnetimePrekey(kex.pkId!);
|
||||||
|
|
||||||
@@ -359,6 +449,7 @@ class OmemoSessionManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
|
authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
|
||||||
|
message = OmemoMessage.fromBuffer(authMessage.message!);
|
||||||
}
|
}
|
||||||
|
|
||||||
final devices = _deviceMap[senderJid];
|
final devices = _deviceMap[senderJid];
|
||||||
@@ -369,11 +460,9 @@ class OmemoSessionManager {
|
|||||||
throw NoDecryptionKeyException();
|
throw NoDecryptionKeyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = OmemoMessage.fromBuffer(authMessage.message!);
|
|
||||||
List<int>? keyAndHmac;
|
|
||||||
// We can guarantee that the ratchet exists at this point in time
|
// We can guarantee that the ratchet exists at this point in time
|
||||||
final ratchet = (await _getRatchet(ratchetKey))!;
|
final ratchet = (await _getRatchet(ratchetKey))!;
|
||||||
oldRatchet ??= ratchet ;
|
oldRatchet ??= ratchet.clone();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (rawKey.kex) {
|
if (rawKey.kex) {
|
||||||
@@ -389,24 +478,12 @@ class OmemoSessionManager {
|
|||||||
// Commit the ratchet
|
// Commit the ratchet
|
||||||
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
|
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
|
||||||
|
|
||||||
// Empty OMEMO messages should just have the key decrypted and/or session set up.
|
try {
|
||||||
if (ciphertext == null) {
|
return _decryptAndVerifyHmac(ciphertext, keyAndHmac);
|
||||||
return null;
|
} catch (_) {
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
// TODO(PapaTutuWawa): I am unsure if we should restore the ratchet here
|
|
||||||
await _restoreRatchet(ratchetKey, oldRatchet);
|
await _restoreRatchet(ratchetKey, oldRatchet);
|
||||||
throw InvalidMessageHMACException();
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
|
|
||||||
return utf8.decode(plaintext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of hex-encoded fingerprints we have for sessions with [jid].
|
/// Returns the list of hex-encoded fingerprints we have for sessions with [jid].
|
||||||
|
|||||||
@@ -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.3.1
|
version: 0.3.2
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -84,12 +84,14 @@ void main() {
|
|||||||
ikBob.pk,
|
ikBob.pk,
|
||||||
resultAlice.sk,
|
resultAlice.sk,
|
||||||
resultAlice.ad,
|
resultAlice.ad,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
|
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
|
||||||
spkBob,
|
spkBob,
|
||||||
ikAlice.pk,
|
ikAlice.pk,
|
||||||
resultBob.sk,
|
resultBob.sk,
|
||||||
resultBob.ad,
|
resultBob.ad,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
|
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messagePlaintext, bobMessage);
|
expect(messagePlaintext, bobMessage);
|
||||||
// The ratchet should be modified two times: Once for when the ratchet is created and
|
// The ratchet should be modified two times: Once for when the ratchet is created and
|
||||||
@@ -105,6 +106,10 @@ void main() {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ratchets are acked
|
||||||
|
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
|
||||||
|
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
|
||||||
|
|
||||||
// Bob responds to Alice
|
// Bob responds to Alice
|
||||||
const bobResponseText = 'Oh, hello Alice!';
|
const bobResponseText = 'Oh, hello Alice!';
|
||||||
final bobResponseMessage = await bobSession.encryptToJid(
|
final bobResponseMessage = await bobSession.encryptToJid(
|
||||||
@@ -121,6 +126,7 @@ void main() {
|
|||||||
bobJid,
|
bobJid,
|
||||||
await bobSession.getDeviceId(),
|
await bobSession.getDeviceId(),
|
||||||
bobResponseMessage.encryptedKeys,
|
bobResponseMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(bobResponseText, aliceReceivedMessage);
|
expect(bobResponseText, aliceReceivedMessage);
|
||||||
});
|
});
|
||||||
@@ -170,9 +176,14 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messagePlaintext, bobMessage);
|
expect(messagePlaintext, bobMessage);
|
||||||
|
|
||||||
|
// Ratchets are acked
|
||||||
|
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
|
||||||
|
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
|
||||||
|
|
||||||
// Bob responds to Alice
|
// Bob responds to Alice
|
||||||
const bobResponseText = 'Oh, hello Alice!';
|
const bobResponseText = 'Oh, hello Alice!';
|
||||||
final bobResponseMessage = await bobSession.encryptToJid(
|
final bobResponseMessage = await bobSession.encryptToJid(
|
||||||
@@ -189,6 +200,7 @@ void main() {
|
|||||||
bobJid,
|
bobJid,
|
||||||
await bobSession.getDeviceId(),
|
await bobSession.getDeviceId(),
|
||||||
bobResponseMessage.encryptedKeys,
|
bobResponseMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(bobResponseText, aliceReceivedMessage);
|
expect(bobResponseText, aliceReceivedMessage);
|
||||||
|
|
||||||
@@ -245,6 +257,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession1.getDeviceId(),
|
await aliceSession1.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messagePlaintext, bobMessage);
|
expect(messagePlaintext, bobMessage);
|
||||||
|
|
||||||
@@ -254,6 +267,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession1.getDeviceId(),
|
await aliceSession1.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messagePlaintext, aliceMessage2);
|
expect(messagePlaintext, aliceMessage2);
|
||||||
});
|
});
|
||||||
@@ -294,6 +308,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(bobMessage, null);
|
expect(bobMessage, null);
|
||||||
|
|
||||||
@@ -371,6 +386,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messagePlaintext, bobMessage);
|
expect(messagePlaintext, bobMessage);
|
||||||
});
|
});
|
||||||
@@ -437,8 +453,13 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ratchets are acked
|
||||||
|
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
|
||||||
|
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
|
||||||
|
|
||||||
for (var i = 0; i < 100; i++) {
|
for (var i = 0; i < 100; i++) {
|
||||||
final messageText = 'Test Message #$i';
|
final messageText = 'Test Message #$i';
|
||||||
// Bob responds to Alice
|
// Bob responds to Alice
|
||||||
@@ -456,6 +477,7 @@ void main() {
|
|||||||
bobJid,
|
bobJid,
|
||||||
await bobSession.getDeviceId(),
|
await bobSession.getDeviceId(),
|
||||||
bobResponseMessage.encryptedKeys,
|
bobResponseMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(messageText, aliceReceivedMessage);
|
expect(messageText, aliceReceivedMessage);
|
||||||
}
|
}
|
||||||
@@ -610,6 +632,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg1.encryptedKeys,
|
msg1.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
final aliceRatchet1 = aliceSession.getRatchet(
|
final aliceRatchet1 = aliceSession.getRatchet(
|
||||||
bobJid,
|
bobJid,
|
||||||
@@ -634,6 +657,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg2.encryptedKeys,
|
msg2.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
final aliceRatchet2 = aliceSession.getRatchet(
|
final aliceRatchet2 = aliceSession.getRatchet(
|
||||||
bobJid,
|
bobJid,
|
||||||
@@ -653,7 +677,7 @@ void main() {
|
|||||||
expect(await bobRatchet1.equals(bobRatchet2), false);
|
expect(await bobRatchet1.equals(bobRatchet2), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Test receiving an old message that contains a KEX', () async {
|
test('Test resending key exchanges', () async {
|
||||||
const aliceJid = 'alice@server.example';
|
const aliceJid = 'alice@server.example';
|
||||||
const bobJid = 'bob@other.server.example';
|
const bobJid = 'bob@other.server.example';
|
||||||
// Alice and Bob generate their sessions
|
// Alice and Bob generate their sessions
|
||||||
@@ -676,40 +700,145 @@ void main() {
|
|||||||
await bobSession.getDeviceBundle(),
|
await bobSession.getDeviceBundle(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
// The first message should be a kex message
|
||||||
|
expect(msg1.encryptedKeys.first.kex, true);
|
||||||
|
|
||||||
await bobSession.decryptMessage(
|
await bobSession.decryptMessage(
|
||||||
msg1.ciphertext,
|
msg1.ciphertext,
|
||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg1.encryptedKeys,
|
msg1.encryptedKeys,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Alice is impatient and immediately sends another message before the original one
|
||||||
|
// can be acknowledged by Bob
|
||||||
|
final msg2 = await aliceSession.encryptToJid(
|
||||||
|
bobJid,
|
||||||
|
"Why don't you answer?",
|
||||||
|
);
|
||||||
|
expect(msg2.encryptedKeys.first.kex, true);
|
||||||
|
|
||||||
|
await bobSession.decryptMessage(
|
||||||
|
msg2.ciphertext,
|
||||||
|
aliceJid,
|
||||||
|
await aliceSession.getDeviceId(),
|
||||||
|
msg2.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Test receiving old messages including a KEX', () async {
|
||||||
|
const aliceJid = 'alice@server.example';
|
||||||
|
const bobJid = 'bob@other.server.example';
|
||||||
|
// Alice and Bob generate their sessions
|
||||||
|
final aliceSession = await OmemoSessionManager.generateNewIdentity(
|
||||||
|
aliceJid,
|
||||||
|
AlwaysTrustingTrustManager(),
|
||||||
|
opkAmount: 1,
|
||||||
|
);
|
||||||
|
final bobSession = await OmemoSessionManager.generateNewIdentity(
|
||||||
|
bobJid,
|
||||||
|
AlwaysTrustingTrustManager(),
|
||||||
|
opkAmount: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
final bobsReceivedMessages = List<EncryptionResult>.empty(growable: true);
|
||||||
|
final bobsReceivedMessagesTimestamps = List<int>.empty(growable: true);
|
||||||
|
|
||||||
|
// Alice sends Bob a message
|
||||||
|
final msg1 = await aliceSession.encryptToJid(
|
||||||
|
bobJid,
|
||||||
|
'Hallo Welt',
|
||||||
|
newSessions: [
|
||||||
|
await bobSession.getDeviceBundle(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
bobsReceivedMessages.add(msg1);
|
||||||
|
final t1 = getTimestamp();
|
||||||
|
bobsReceivedMessagesTimestamps.add(t1);
|
||||||
|
|
||||||
|
await bobSession.decryptMessage(
|
||||||
|
msg1.ciphertext,
|
||||||
|
aliceJid,
|
||||||
|
await aliceSession.getDeviceId(),
|
||||||
|
msg1.encryptedKeys,
|
||||||
|
t1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ratchets are acked
|
||||||
|
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
|
||||||
|
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
|
||||||
|
|
||||||
// Bob responds
|
// Bob responds
|
||||||
final msg2 = await bobSession.encryptToJid(
|
final msg2 = await bobSession.encryptToJid(
|
||||||
aliceJid,
|
aliceJid,
|
||||||
'Hello!',
|
'Hello!',
|
||||||
);
|
);
|
||||||
|
|
||||||
await aliceSession.decryptMessage(
|
await aliceSession.decryptMessage(
|
||||||
msg2.ciphertext,
|
msg2.ciphertext,
|
||||||
bobJid,
|
bobJid,
|
||||||
await bobSession.getDeviceId(),
|
await bobSession.getDeviceId(),
|
||||||
msg2.encryptedKeys,
|
msg2.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Due to some issue with the transport protocol, the first message Bob received is
|
// Send some messages between the two
|
||||||
// received again
|
for (var i = 0; i < 100; i++) {
|
||||||
try {
|
final msg = await aliceSession.encryptToJid(
|
||||||
await bobSession.decryptMessage(
|
bobJid,
|
||||||
msg1.ciphertext,
|
'Hello $i',
|
||||||
|
);
|
||||||
|
bobsReceivedMessages.add(msg);
|
||||||
|
final t = getTimestamp();
|
||||||
|
bobsReceivedMessagesTimestamps.add(t);
|
||||||
|
final result = await bobSession.decryptMessage(
|
||||||
|
msg.ciphertext,
|
||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg1.encryptedKeys,
|
msg.encryptedKeys,
|
||||||
|
t,
|
||||||
);
|
);
|
||||||
expect(true, false);
|
|
||||||
} on InvalidMessageHMACException {
|
expect(result, 'Hello $i');
|
||||||
// NOOP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Due to some issue with the transport protocol, the messages to Bob are received
|
||||||
|
// again.
|
||||||
|
final ratchetPreError = bobSession
|
||||||
|
.getRatchet(aliceJid, await aliceSession.getDeviceId())
|
||||||
|
.clone();
|
||||||
|
var invalidKex = 0;
|
||||||
|
var errorCounter = 0;
|
||||||
|
for (var i = 0; i < bobsReceivedMessages.length; i++) {
|
||||||
|
final msg = bobsReceivedMessages[i];
|
||||||
|
try {
|
||||||
|
await bobSession.decryptMessage(
|
||||||
|
msg.ciphertext,
|
||||||
|
aliceJid,
|
||||||
|
await aliceSession.getDeviceId(),
|
||||||
|
msg.encryptedKeys,
|
||||||
|
bobsReceivedMessagesTimestamps[i],
|
||||||
|
);
|
||||||
|
expect(true, false);
|
||||||
|
} on InvalidMessageHMACException catch (_) {
|
||||||
|
errorCounter++;
|
||||||
|
} on InvalidKeyExchangeException catch (_) {
|
||||||
|
invalidKex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final ratchetPostError = bobSession
|
||||||
|
.getRatchet(aliceJid, await aliceSession.getDeviceId())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// The 100 messages including the initial KEX message
|
||||||
|
expect(invalidKex, 1);
|
||||||
|
expect(errorCounter, 100);
|
||||||
|
expect(await ratchetPreError.equals(ratchetPostError), true);
|
||||||
|
|
||||||
|
|
||||||
final msg3 = await aliceSession.encryptToJid(
|
final msg3 = await aliceSession.encryptToJid(
|
||||||
bobJid,
|
bobJid,
|
||||||
'Are you okay?',
|
'Are you okay?',
|
||||||
@@ -719,12 +848,13 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg3.encryptedKeys,
|
msg3.encryptedKeys,
|
||||||
|
104,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result, 'Are you okay?');
|
expect(result, 'Are you okay?');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Test receiving an old message that does not contain a KEX', () async {
|
test("Test ignoring a new KEX when we haven't acket it yet", () async {
|
||||||
const aliceJid = 'alice@server.example';
|
const aliceJid = 'alice@server.example';
|
||||||
const bobJid = 'bob@other.server.example';
|
const bobJid = 'bob@other.server.example';
|
||||||
// Alice and Bob generate their sessions
|
// Alice and Bob generate their sessions
|
||||||
@@ -736,7 +866,7 @@ void main() {
|
|||||||
final bobSession = await OmemoSessionManager.generateNewIdentity(
|
final bobSession = await OmemoSessionManager.generateNewIdentity(
|
||||||
bobJid,
|
bobJid,
|
||||||
AlwaysTrustingTrustManager(),
|
AlwaysTrustingTrustManager(),
|
||||||
opkAmount: 2,
|
opkAmount: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alice sends Bob a message
|
// Alice sends Bob a message
|
||||||
@@ -747,50 +877,70 @@ void main() {
|
|||||||
await bobSession.getDeviceBundle(),
|
await bobSession.getDeviceBundle(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
expect(msg1.encryptedKeys.first.kex, true);
|
||||||
|
|
||||||
await bobSession.decryptMessage(
|
await bobSession.decryptMessage(
|
||||||
msg1.ciphertext,
|
msg1.ciphertext,
|
||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg1.encryptedKeys,
|
msg1.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bob responds
|
// Alice sends another message before the ack can reach us
|
||||||
final msg2 = await bobSession.encryptToJid(
|
final msg2 = await aliceSession.encryptToJid(
|
||||||
aliceJid,
|
|
||||||
'Hello!',
|
|
||||||
);
|
|
||||||
await aliceSession.decryptMessage(
|
|
||||||
msg2.ciphertext,
|
|
||||||
bobJid,
|
bobJid,
|
||||||
await bobSession.getDeviceId(),
|
'ANSWER ME!',
|
||||||
|
);
|
||||||
|
expect(msg2.encryptedKeys.first.kex, true);
|
||||||
|
|
||||||
|
await bobSession.decryptMessage(
|
||||||
|
msg2.ciphertext,
|
||||||
|
aliceJid,
|
||||||
|
await aliceSession.getDeviceId(),
|
||||||
msg2.encryptedKeys,
|
msg2.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Due to some issue with the transport protocol, the first message Alice received is
|
// Now the acks reach us
|
||||||
// received again.
|
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
|
||||||
try {
|
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
|
||||||
await aliceSession.decryptMessage(
|
|
||||||
msg2.ciphertext,
|
|
||||||
bobJid,
|
|
||||||
await bobSession.getDeviceId(),
|
|
||||||
msg2.encryptedKeys,
|
|
||||||
);
|
|
||||||
expect(true, false);
|
|
||||||
} catch (_) {
|
|
||||||
// NOOP
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Alice sends another message
|
||||||
final msg3 = await aliceSession.encryptToJid(
|
final msg3 = await aliceSession.encryptToJid(
|
||||||
bobJid,
|
bobJid,
|
||||||
'Are you okay?',
|
"You read the message, didn't you?",
|
||||||
);
|
);
|
||||||
final result = await bobSession.decryptMessage(
|
expect(msg3.encryptedKeys.first.kex, false);
|
||||||
|
|
||||||
|
await bobSession.decryptMessage(
|
||||||
msg3.ciphertext,
|
msg3.ciphertext,
|
||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
msg3.encryptedKeys,
|
msg3.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result, 'Are you okay?');
|
for (var i = 0; i < 100; i++) {
|
||||||
|
final messageText = 'Test Message #$i';
|
||||||
|
// Bob responds to Alice
|
||||||
|
final bobResponseMessage = await bobSession.encryptToJid(
|
||||||
|
aliceJid,
|
||||||
|
messageText,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bob sends the message to Alice
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Alice decrypts it
|
||||||
|
final aliceReceivedMessage = await aliceSession.decryptMessage(
|
||||||
|
bobResponseMessage.ciphertext,
|
||||||
|
bobJid,
|
||||||
|
await bobSession.getDeviceId(),
|
||||||
|
bobResponseMessage.encryptedKeys,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(messageText, aliceReceivedMessage);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ void main() {
|
|||||||
aliceJid,
|
aliceJid,
|
||||||
await aliceSession.getDeviceId(),
|
await aliceSession.getDeviceId(),
|
||||||
aliceMessage.encryptedKeys,
|
aliceMessage.encryptedKeys,
|
||||||
|
getTimestamp(),
|
||||||
);
|
);
|
||||||
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
|
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
|
||||||
final aliceSerialised = jsonify(await aliceOld.toJson());
|
final aliceSerialised = jsonify(await aliceOld.toJson());
|
||||||
|
|||||||
Reference in New Issue
Block a user