fix: Migrate to custom protobuf
This commit is contained in:
parent
5c3cc424de
commit
9ed94c8f3a
@ -1,12 +1,12 @@
|
|||||||
import 'package:cryptography/cryptography.dart';
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:omemo_dart/protobuf/schema.pb.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/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';
|
||||||
|
|
||||||
/// Amount of messages we may skip per session
|
/// Amount of messages we may skip per session
|
||||||
const maxSkip = 1000;
|
const maxSkip = 1000;
|
||||||
@ -14,7 +14,7 @@ const maxSkip = 1000;
|
|||||||
class RatchetStep {
|
class RatchetStep {
|
||||||
|
|
||||||
const RatchetStep(this.header, this.ciphertext);
|
const RatchetStep(this.header, this.ciphertext);
|
||||||
final OMEMOMessage header;
|
final OmemoMessage header;
|
||||||
final List<int> ciphertext;
|
final List<int> ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +111,10 @@ class OmemoDoubleRatchet {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
header.n,
|
header.n!,
|
||||||
);
|
);
|
||||||
if (mkSkipped.containsKey(key)) {
|
if (mkSkipped.containsKey(key)) {
|
||||||
final mk = mkSkipped[key]!;
|
final mk = mkSkipped[key]!;
|
||||||
@ -142,11 +142,11 @@ class OmemoDoubleRatchet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _dhRatchet(OMEMOMessage header) async {
|
Future<void> _dhRatchet(OmemoMessage header) async {
|
||||||
pn = header.n;
|
pn = header.n!;
|
||||||
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 = newRk;
|
||||||
@ -163,7 +163,7 @@ class OmemoDoubleRatchet {
|
|||||||
final mk = await kdfCk(cks!, kdfCkNextMessageKey);
|
final mk = await kdfCk(cks!, kdfCkNextMessageKey);
|
||||||
|
|
||||||
cks = newCks;
|
cks = newCks;
|
||||||
final header = OMEMOMessage()
|
final header = OmemoMessage()
|
||||||
..dhPub = await dhs.pk.getBytes()
|
..dhPub = await dhs.pk.getBytes()
|
||||||
..pn = pn
|
..pn = pn
|
||||||
..n = ns;
|
..n = ns;
|
||||||
@ -180,7 +180,7 @@ class OmemoDoubleRatchet {
|
|||||||
/// Ratchet. Returns the decrypted (raw) plaintext.
|
/// Ratchet. Returns the decrypted (raw) plaintext.
|
||||||
///
|
///
|
||||||
/// Throws an SkippingTooManyMessagesException if too many messages were to be skipped.
|
/// Throws an SkippingTooManyMessagesException if too many messages were to be skipped.
|
||||||
Future<List<int>> ratchetDecrypt(OMEMOMessage header, List<int> ciphertext) async {
|
Future<List<int>> ratchetDecrypt(OmemoMessage header, List<int> ciphertext) async {
|
||||||
// Check if we skipped too many messages
|
// Check if we skipped too many messages
|
||||||
final plaintext = await _trySkippedMessageKeys(header, ciphertext);
|
final plaintext = await _trySkippedMessageKeys(header, ciphertext);
|
||||||
if (plaintext != null) {
|
if (plaintext != null) {
|
||||||
@ -188,11 +188,11 @@ class OmemoDoubleRatchet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (header.dhPub != await dhr?.getBytes()) {
|
if (header.dhPub != await dhr?.getBytes()) {
|
||||||
await _skipMessageKeys(header.pn);
|
await _skipMessageKeys(header.pn!);
|
||||||
await _dhRatchet(header);
|
await _dhRatchet(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _skipMessageKeys(header.n);
|
await _skipMessageKeys(header.n!);
|
||||||
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;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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:omemo_dart/protobuf/schema.pb.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';
|
||||||
@ -9,6 +8,9 @@ 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';
|
||||||
import 'package:omemo_dart/src/omemo/device.dart';
|
import 'package:omemo_dart/src/omemo/device.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/x3dh/x3dh.dart';
|
import 'package:omemo_dart/src/x3dh/x3dh.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ class OmemoSessionManager {
|
|||||||
|
|
||||||
/// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device
|
/// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device
|
||||||
/// [deviceId] from the bundle [bundle].
|
/// [deviceId] from the bundle [bundle].
|
||||||
Future<OMEMOKeyExchange> addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async {
|
Future<OmemoKeyExchange> addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async {
|
||||||
final kexResult = await x3dhFromBundle(
|
final kexResult = await x3dhFromBundle(
|
||||||
bundle,
|
bundle,
|
||||||
device.ik,
|
device.ik,
|
||||||
@ -93,9 +95,8 @@ class OmemoSessionManager {
|
|||||||
|
|
||||||
await addSession(jid, deviceId, ratchet);
|
await addSession(jid, deviceId, ratchet);
|
||||||
|
|
||||||
return OMEMOKeyExchange()
|
return OmemoKeyExchange()
|
||||||
..pkId = kexResult.opkId
|
..pkId = kexResult.opkId
|
||||||
// TODO(PapaTutuWawa): Fix
|
|
||||||
..spkId = 0
|
..spkId = 0
|
||||||
..ik = await device.ik.pk.getBytes()
|
..ik = await device.ik.pk.getBytes()
|
||||||
..ek = await kexResult.ek.pk.getBytes();
|
..ek = await kexResult.ek.pk.getBytes();
|
||||||
@ -104,15 +105,15 @@ 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].
|
/// from the key exchange [kex].
|
||||||
// TODO(PapaTutuWawa): Replace the OPK
|
// TODO(PapaTutuWawa): Replace the OPK
|
||||||
Future<void> addSessionFromKeyExchange(String jid, int deviceId, OMEMOKeyExchange kex) async {
|
Future<void> addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
|
||||||
final kexResult = await x3dhFromInitialMessage(
|
final kexResult = await x3dhFromInitialMessage(
|
||||||
X3DHMessage(
|
X3DHMessage(
|
||||||
OmemoPublicKey.fromBytes(kex.ik, KeyPairType.ed25519),
|
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
|
||||||
OmemoPublicKey.fromBytes(kex.ek, KeyPairType.x25519),
|
OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519),
|
||||||
kex.pkId,
|
kex.pkId!,
|
||||||
),
|
),
|
||||||
device.spk,
|
device.spk,
|
||||||
device.opks.values.elementAt(kex.pkId),
|
device.opks.values.elementAt(kex.pkId!),
|
||||||
device.ik,
|
device.ik,
|
||||||
);
|
);
|
||||||
final ratchet = await OmemoDoubleRatchet.acceptNewSession(
|
final ratchet = await OmemoDoubleRatchet.acceptNewSession(
|
||||||
@ -174,8 +175,8 @@ class OmemoSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final decodedRawKey = base64.decode(rawKey.value);
|
final decodedRawKey = base64.decode(rawKey.value);
|
||||||
final authMessage = OMEMOAuthenticatedMessage.fromBuffer(decodedRawKey);
|
final authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
|
||||||
final message = OMEMOMessage.fromBuffer(authMessage.message);
|
final message = OmemoMessage.fromBuffer(authMessage.message!);
|
||||||
|
|
||||||
final ratchet = _ratchetMap[senderDeviceId]!;
|
final ratchet = _ratchetMap[senderDeviceId]!;
|
||||||
final keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
|
final keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:omemo_dart/src/protobuf/protobuf.dart';
|
|||||||
|
|
||||||
class OmemoAuthenticatedMessage {
|
class OmemoAuthenticatedMessage {
|
||||||
|
|
||||||
const OmemoAuthenticatedMessage(this.mac, this.message);
|
OmemoAuthenticatedMessage();
|
||||||
|
|
||||||
factory OmemoAuthenticatedMessage.fromBuffer(List<int> data) {
|
factory OmemoAuthenticatedMessage.fromBuffer(List<int> data) {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@ -20,18 +20,20 @@ class OmemoAuthenticatedMessage {
|
|||||||
}
|
}
|
||||||
final message = data.sublist(i + 2, i + 2 + data[i + 1]);
|
final message = data.sublist(i + 2, i + 2 + data[i + 1]);
|
||||||
|
|
||||||
return OmemoAuthenticatedMessage(mac, message);
|
return OmemoAuthenticatedMessage()
|
||||||
|
..mac = mac
|
||||||
|
..message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<int> mac;
|
List<int>? mac;
|
||||||
final List<int> message;
|
List<int>? message;
|
||||||
|
|
||||||
List<int> writeToBuffer() {
|
List<int> writeToBuffer() {
|
||||||
return concat([
|
return concat([
|
||||||
[fieldId(1, fieldTypeByteArray), mac.length],
|
[fieldId(1, fieldTypeByteArray), mac!.length],
|
||||||
mac,
|
mac!,
|
||||||
[fieldId(2, fieldTypeByteArray), message.length],
|
[fieldId(2, fieldTypeByteArray), message!.length],
|
||||||
message,
|
message!,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import 'package:omemo_dart/src/protobuf/protobuf.dart';
|
|||||||
|
|
||||||
class OmemoKeyExchange {
|
class OmemoKeyExchange {
|
||||||
|
|
||||||
const OmemoKeyExchange(this.pkId, this.spkId, this.ik, this.ek, this.message);
|
OmemoKeyExchange();
|
||||||
|
|
||||||
factory OmemoKeyExchange.fromBuffer(List<int> data) {
|
factory OmemoKeyExchange.fromBuffer(List<int> data) {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@ -40,26 +40,31 @@ class OmemoKeyExchange {
|
|||||||
}
|
}
|
||||||
final message = OmemoAuthenticatedMessage.fromBuffer(data.sublist(i + 2));
|
final message = OmemoAuthenticatedMessage.fromBuffer(data.sublist(i + 2));
|
||||||
|
|
||||||
return OmemoKeyExchange(pkId, spkId, ik, ek, message);
|
return OmemoKeyExchange()
|
||||||
|
..pkId = pkId
|
||||||
|
..spkId = spkId
|
||||||
|
..ik = ik
|
||||||
|
..ek = ek
|
||||||
|
..message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int pkId;
|
int? pkId;
|
||||||
final int spkId;
|
int? spkId;
|
||||||
final List<int> ik;
|
List<int>? ik;
|
||||||
final List<int> ek;
|
List<int>? ek;
|
||||||
final OmemoAuthenticatedMessage message;
|
OmemoAuthenticatedMessage? message;
|
||||||
|
|
||||||
List<int> writeToBuffer() {
|
List<int> writeToBuffer() {
|
||||||
final msg = message.writeToBuffer();
|
final msg = message!.writeToBuffer();
|
||||||
return concat([
|
return concat([
|
||||||
[fieldId(1, fieldTypeUint32)],
|
[fieldId(1, fieldTypeUint32)],
|
||||||
encodeVarint(pkId),
|
encodeVarint(pkId!),
|
||||||
[fieldId(2, fieldTypeUint32)],
|
[fieldId(2, fieldTypeUint32)],
|
||||||
encodeVarint(spkId),
|
encodeVarint(spkId!),
|
||||||
[fieldId(3, fieldTypeByteArray), ik.length],
|
[fieldId(3, fieldTypeByteArray), ik!.length],
|
||||||
ik,
|
ik!,
|
||||||
[fieldId(4, fieldTypeByteArray), ek.length],
|
[fieldId(4, fieldTypeByteArray), ek!.length],
|
||||||
ek,
|
ek!,
|
||||||
[fieldId(5, fieldTypeByteArray), msg.length],
|
[fieldId(5, fieldTypeByteArray), msg.length],
|
||||||
msg,
|
msg,
|
||||||
]);
|
]);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:omemo_dart/src/protobuf/protobuf.dart';
|
|||||||
|
|
||||||
class OmemoMessage {
|
class OmemoMessage {
|
||||||
|
|
||||||
const OmemoMessage(this.n, this.pn, this.dhPub, this.ciphertext);
|
OmemoMessage();
|
||||||
|
|
||||||
factory OmemoMessage.fromBuffer(List<int> data) {
|
factory OmemoMessage.fromBuffer(List<int> data) {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
@ -41,28 +41,32 @@ class OmemoMessage {
|
|||||||
ciphertext = data.sublist(i + 2, i + 2 + data[i + 1]);
|
ciphertext = data.sublist(i + 2, i + 2 + data[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OmemoMessage(n, pn, dhPub, ciphertext);
|
return OmemoMessage()
|
||||||
|
..n = n
|
||||||
|
..pn = pn
|
||||||
|
..dhPub = dhPub
|
||||||
|
..ciphertext = ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int n;
|
int? n;
|
||||||
final int pn;
|
int? pn;
|
||||||
final List<int> dhPub;
|
List<int>? dhPub;
|
||||||
final List<int>? ciphertext;
|
List<int>? ciphertext;
|
||||||
|
|
||||||
List<int> writeToBuffer() {
|
List<int> writeToBuffer() {
|
||||||
final data = concat([
|
final data = concat([
|
||||||
[8],
|
[fieldId(1, fieldTypeUint32)],
|
||||||
encodeVarint(n),
|
encodeVarint(n!),
|
||||||
[16],
|
[fieldId(2, fieldTypeUint32)],
|
||||||
encodeVarint(pn),
|
encodeVarint(pn!),
|
||||||
[((3 << 3) | 2), dhPub.length],
|
[fieldId(3, fieldTypeByteArray), dhPub!.length],
|
||||||
dhPub,
|
dhPub!,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (ciphertext != null) {
|
if (ciphertext != null) {
|
||||||
return concat([
|
return concat([
|
||||||
data,
|
data,
|
||||||
[((4 << 3) | 2), ciphertext!.length],
|
[fieldId(4, fieldTypeByteArray), ciphertext!.length],
|
||||||
ciphertext!,
|
ciphertext!,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -79,12 +79,11 @@ void main() {
|
|||||||
expect(msg.ciphertext, null);
|
expect(msg.ciphertext, null);
|
||||||
});
|
});
|
||||||
test('Encode a OMEMOMessage', () {
|
test('Encode a OMEMOMessage', () {
|
||||||
const m = OmemoMessage(
|
final m = OmemoMessage()
|
||||||
1,
|
..n = 1
|
||||||
5,
|
..pn = 5
|
||||||
<int>[1, 2, 3],
|
..dhPub = <int>[1, 2, 3]
|
||||||
<int>[4, 5, 6],
|
..ciphertext = <int>[4, 5, 6];
|
||||||
);
|
|
||||||
final serial = m.writeToBuffer();
|
final serial = m.writeToBuffer();
|
||||||
final msg = OMEMOMessage.fromBuffer(serial);
|
final msg = OMEMOMessage.fromBuffer(serial);
|
||||||
|
|
||||||
@ -94,12 +93,10 @@ void main() {
|
|||||||
expect(msg.ciphertext, <int>[4, 5, 6]);
|
expect(msg.ciphertext, <int>[4, 5, 6]);
|
||||||
});
|
});
|
||||||
test('Encode a OMEMOMessage without ciphertext', () {
|
test('Encode a OMEMOMessage without ciphertext', () {
|
||||||
const m = OmemoMessage(
|
final m = OmemoMessage()
|
||||||
1,
|
..n = 1
|
||||||
5,
|
..pn = 5
|
||||||
<int>[1, 2, 3],
|
..dhPub = <int>[1, 2, 3];
|
||||||
null,
|
|
||||||
);
|
|
||||||
final serial = m.writeToBuffer();
|
final serial = m.writeToBuffer();
|
||||||
final msg = OMEMOMessage.fromBuffer(serial);
|
final msg = OMEMOMessage.fromBuffer(serial);
|
||||||
|
|
||||||
@ -112,7 +109,9 @@ void main() {
|
|||||||
|
|
||||||
group('OMEMOAuthenticatedMessage', () {
|
group('OMEMOAuthenticatedMessage', () {
|
||||||
test('Test encoding a message', () {
|
test('Test encoding a message', () {
|
||||||
const msg = OmemoAuthenticatedMessage(<int>[1, 2, 3], <int>[4, 5, 6]);
|
final msg = OmemoAuthenticatedMessage()
|
||||||
|
..mac = <int>[1, 2, 3]
|
||||||
|
..message = <int>[4, 5, 6];
|
||||||
final decoded = OMEMOAuthenticatedMessage.fromBuffer(msg.writeToBuffer());
|
final decoded = OMEMOAuthenticatedMessage.fromBuffer(msg.writeToBuffer());
|
||||||
|
|
||||||
expect(decoded.mac, <int>[1, 2, 3]);
|
expect(decoded.mac, <int>[1, 2, 3]);
|
||||||
@ -132,16 +131,15 @@ void main() {
|
|||||||
|
|
||||||
group('OMEMOKeyExchange', () {
|
group('OMEMOKeyExchange', () {
|
||||||
test('Test encoding a message', () {
|
test('Test encoding a message', () {
|
||||||
const message = OmemoKeyExchange(
|
final authMessage = OmemoAuthenticatedMessage()
|
||||||
698,
|
..mac = <int>[5, 6, 8, 0]
|
||||||
245,
|
..message = <int>[4, 5, 7, 3, 2];
|
||||||
<int>[1, 4, 6],
|
final message = OmemoKeyExchange()
|
||||||
<int>[4, 6, 7, 80],
|
..pkId = 698
|
||||||
OmemoAuthenticatedMessage(
|
..spkId = 245
|
||||||
<int>[5, 6, 8, 0],
|
..ik = <int>[1, 4, 6]
|
||||||
<int>[4, 5, 7, 3, 2],
|
..ek = <int>[4, 6, 7, 80]
|
||||||
),
|
..message = authMessage;
|
||||||
);
|
|
||||||
final kex = OMEMOKeyExchange.fromBuffer(message.writeToBuffer());
|
final kex = OMEMOKeyExchange.fromBuffer(message.writeToBuffer());
|
||||||
|
|
||||||
expect(kex.pkId, 698);
|
expect(kex.pkId, 698);
|
||||||
@ -169,8 +167,8 @@ void main() {
|
|||||||
expect(decoded.ik, <int>[1, 4, 6]);
|
expect(decoded.ik, <int>[1, 4, 6]);
|
||||||
expect(decoded.ek, <int>[4 ,6 ,7 , 80]);
|
expect(decoded.ek, <int>[4 ,6 ,7 , 80]);
|
||||||
|
|
||||||
expect(decoded.message.mac, <int>[5, 6, 8, 0]);
|
expect(decoded.message!.mac, <int>[5, 6, 8, 0]);
|
||||||
expect(decoded.message.message, <int>[4, 5, 7, 3, 2]);
|
expect(decoded.message!.message, <int>[4, 5, 7, 3, 2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user