feat: Serialise the Double Ratchet

This commit is contained in:
PapaTutuWawa 2022-08-05 18:14:10 +02:00
parent cd77996db4
commit fdc3985a8d
4 changed files with 166 additions and 4 deletions

View File

@ -1,7 +1,9 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.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/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/helpers.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';
@ -22,9 +24,27 @@ class RatchetStep {
class SkippedKey { class SkippedKey {
const SkippedKey(this.dh, this.n); const SkippedKey(this.dh, this.n);
factory SkippedKey.fromJson(Map<String, dynamic> data) {
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; final int n;
Future<Map<String, dynamic>> toJson() async {
return {
'public': base64.encode(await dh.getBytes()),
'n': n,
};
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SkippedKey && other.dh == dh && other.n == n; return other is SkippedKey && other.dh == dh && other.n == n;
@ -46,8 +66,55 @@ class OmemoDoubleRatchet {
this.nr, // Nr this.nr, // Nr
this.pn, // Pn this.pn, // Pn
this.sessionAd, this.sessionAd,
this.mkSkipped,
); );
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,
'session_ad': 'base/64/encoded',
'mkskipped': [
{
'key': 'base/64/encoded',
'public': 'base/64/encoded',
'n': 0
}, ...
]
}
*/
final mkSkipped = <SkippedKey, List<int>>{};
for (final entry in data['mkskipped']! as List<Map<String, dynamic>>) {
final key = SkippedKey.fromJson(entry);
mkSkipped[key] = base64.decode(entry['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,
base64.decode(data['session_ad']! as String),
mkSkipped,
);
}
/// Sending DH keypair /// Sending DH keypair
OmemoKeyPair dhs; OmemoKeyPair dhs;
@ -70,7 +137,7 @@ class OmemoDoubleRatchet {
final List<int> sessionAd; final List<int> sessionAd;
final Map<SkippedKey, List<int>> mkSkipped = {}; final Map<SkippedKey, List<int>> mkSkipped;
/// 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
@ -91,6 +158,7 @@ class OmemoDoubleRatchet {
0, 0,
0, 0,
ad, ad,
{},
); );
} }
@ -108,9 +176,34 @@ class OmemoDoubleRatchet {
0, 0,
0, 0,
ad, ad,
{},
); );
} }
Future<Map<String, dynamic>> toJson() async {
final mkSkippedSerialised = List<Map<String, dynamic>>.empty(growable: true);
for (final entry in mkSkipped.entries) {
final result = await entry.key.toJson();
result['key'] = base64.encode(entry.value);
mkSkippedSerialised.add(result);
}
return {
'dhs': base64.encode(await dhs.sk.getBytes()),
'dhs_pub': base64.encode(await dhs.pk.getBytes()),
'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,
'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised,
};
}
Future<List<int>?> _trySkippedMessageKeys(OmemoMessage header, List<int> ciphertext) async { Future<List<int>?> _trySkippedMessageKeys(OmemoMessage header, List<int> ciphertext) async {
final key = SkippedKey( final key = SkippedKey(
OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519), OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519),

View File

@ -0,0 +1,18 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/src/keys.dart';
OmemoPublicKey? decodeKeyIfNotNull(Map<String, dynamic> map, String key, KeyPairType type) {
if (map[key] == null) return null;
return OmemoPublicKey.fromBytes(
base64.decode(map[key]! as String),
type,
);
}
List<int>? base64DecodeIfNotNull(Map<String, dynamic> map, String key) {
if (map[key] == null) return null;
return base64.decode(map[key]! as String);
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:meta/meta.dart';
import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/errors.dart';
@ -60,6 +61,7 @@ class OmemoSessionManager {
final Lock _lock; final Lock _lock;
/// Mapping of the Device Id to its OMEMO session /// Mapping of the Device Id to its OMEMO session
// TODO(PapaTutuWawa): Make this map use a tuple (Jid, Id) as a key
final Map<int, OmemoDoubleRatchet> _ratchetMap; final Map<int, OmemoDoubleRatchet> _ratchetMap;
/// Mapping of a bare Jid to its Device Ids /// Mapping of a bare Jid to its Device Ids
@ -274,4 +276,7 @@ class OmemoSessionManager {
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv); final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
return utf8.decode(plaintext); return utf8.decode(plaintext);
} }
@visibleForTesting
OmemoDoubleRatchet getRatchet(int deviceId) => _ratchetMap[deviceId]!;
} }

View File

@ -2,7 +2,7 @@ import 'package:omemo_dart/omemo_dart.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test('Test serialising and deserialising Device', () async { test('Test serialising and deserialising the Device', () async {
// Generate a random session // Generate a random session
final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1);
final oldDevice = await oldSession.getDevice(); final oldDevice = await oldSession.getDevice();
@ -21,4 +21,50 @@ void main() {
expect(await newDevice.opks[entry.key]!.equals(entry.value), true); expect(await newDevice.opks[entry.key]!.equals(entry.value), 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(opkAmount: 1);
final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1);
final aliceMessage = await aliceSession.encryptToJid(
bobJid,
'Hello Bob!',
newSessions: [
await (await bobSession.getDevice()).toBundle(),
],
);
await bobSession.decryptMessage(
aliceMessage.ciphertext,
aliceJid,
(await aliceSession.getDevice()).id,
aliceMessage.encryptedKeys,
);
final aliceOld = aliceSession.getRatchet((await bobSession.getDevice()).id);
final aliceSerialised = await aliceOld.toJson();
final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised);
expect(await aliceOld.dhs.equals(aliceNew.dhs), true);
if (aliceOld.dhr == null) {
expect(aliceNew.dhr, null);
} else {
expect(await aliceOld.dhr!.equals(aliceNew.dhr!), true);
}
expect(listsEqual(aliceOld.rk, aliceNew.rk), true);
if (aliceOld.cks == null) {
expect(aliceNew.cks, null);
} else {
expect(listsEqual(aliceOld.cks!, aliceNew.cks!), true);
}
if (aliceOld.ckr == null) {
expect(aliceNew.ckr, null);
} else {
expect(listsEqual(aliceOld.ckr!, aliceNew.ckr!), true);
}
expect(aliceOld.ns, aliceNew.ns);
expect(aliceOld.nr, aliceNew.nr);
expect(aliceOld.pn, aliceNew.pn);
expect(listsEqual(aliceOld.sessionAd, aliceNew.sessionAd), true);
});
} }