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: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),
|
||||||
|
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 '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]!;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user