feat: Serialise the Double Ratchet
This commit is contained in:
parent
cd77996db4
commit
fdc3985a8d
@ -1,7 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:omemo_dart/src/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/errors.dart';
|
||||
import 'package:omemo_dart/src/helpers.dart';
|
||||
@ -22,9 +24,27 @@ class RatchetStep {
|
||||
class SkippedKey {
|
||||
|
||||
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 int n;
|
||||
|
||||
Future<Map<String, dynamic>> toJson() async {
|
||||
return {
|
||||
'public': base64.encode(await dh.getBytes()),
|
||||
'n': n,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SkippedKey && other.dh == dh && other.n == n;
|
||||
@ -46,8 +66,55 @@ class OmemoDoubleRatchet {
|
||||
this.nr, // Nr
|
||||
this.pn, // Pn
|
||||
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
|
||||
OmemoKeyPair dhs;
|
||||
|
||||
@ -70,7 +137,7 @@ class OmemoDoubleRatchet {
|
||||
|
||||
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
|
||||
/// was obtained using a X3DH and the associated data [ad] that was also obtained through
|
||||
@ -91,6 +158,7 @@ class OmemoDoubleRatchet {
|
||||
0,
|
||||
0,
|
||||
ad,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
@ -108,9 +176,34 @@ class OmemoDoubleRatchet {
|
||||
0,
|
||||
0,
|
||||
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 {
|
||||
final key = SkippedKey(
|
||||
OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519),
|
||||
|
18
lib/src/double_ratchet/helpers.dart
Normal file
18
lib/src/double_ratchet/helpers.dart
Normal 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);
|
||||
}
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography/cryptography.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';
|
||||
@ -60,6 +61,7 @@ class OmemoSessionManager {
|
||||
final Lock _lock;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Mapping of a bare Jid to its Device Ids
|
||||
@ -274,4 +276,7 @@ class OmemoSessionManager {
|
||||
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
|
||||
return utf8.decode(plaintext);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
OmemoDoubleRatchet getRatchet(int deviceId) => _ratchetMap[deviceId]!;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import 'package:omemo_dart/omemo_dart.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Test serialising and deserialising Device', () async {
|
||||
test('Test serialising and deserialising the Device', () async {
|
||||
// Generate a random session
|
||||
final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1);
|
||||
final oldDevice = await oldSession.getDevice();
|
||||
@ -21,4 +21,50 @@ void main() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user