347 lines
14 KiB
Dart
347 lines
14 KiB
Dart
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:fixnum/fixnum.dart';
|
|
import 'package:libsignal_protocol_dart/src/ecc/curve.dart';
|
|
import 'package:libsignal_protocol_dart/src/invalid_key_exception.dart';
|
|
import 'package:libsignal_protocol_dart/src/protocol/ciphertext_message.dart';
|
|
import 'package:libsignal_protocol_dart/src/protocol/pre_key_signal_message.dart';
|
|
import 'package:libsignal_protocol_dart/src/protocol/signal_message.dart';
|
|
import 'package:libsignal_protocol_dart/src/session_builder.dart';
|
|
import 'package:libsignal_protocol_dart/src/session_cipher.dart';
|
|
import 'package:libsignal_protocol_dart/src/signal_protocol_address.dart';
|
|
import 'package:libsignal_protocol_dart/src/state/pre_key_bundle.dart';
|
|
import 'package:libsignal_protocol_dart/src/state/pre_key_record.dart';
|
|
import 'package:libsignal_protocol_dart/src/state/signal_protocol_store.dart';
|
|
import 'package:libsignal_protocol_dart/src/state/signed_pre_key_record.dart';
|
|
import 'package:libsignal_protocol_dart/src/untrusted_identity_exception.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'test_in_memory_identity_key_store.dart';
|
|
import 'test_in_memory_signal_protocol_store.dart';
|
|
|
|
void main() {
|
|
const aliceAddress = SignalProtocolAddress('+14151111111', 1);
|
|
const bobAddress = SignalProtocolAddress('+14152222222', 1);
|
|
|
|
test('testBasicPreKeyV2', () async {
|
|
final aliceStore = TestInMemorySignalProtocolStore();
|
|
final aliceSessionBuilder =
|
|
SessionBuilder.fromSignalStore(aliceStore, bobAddress);
|
|
|
|
final bobStore = TestInMemorySignalProtocolStore();
|
|
final bobPreKeyPair = Curve.generateKeyPair();
|
|
final bobPreKey = PreKeyBundle(
|
|
await bobStore.getLocalRegistrationId(),
|
|
1,
|
|
31337,
|
|
bobPreKeyPair.publicKey,
|
|
0,
|
|
null,
|
|
null,
|
|
await bobStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
try {
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
throw AssertionError('Should fail with missing unsigned prekey!');
|
|
} on InvalidKeyException {
|
|
// Good!
|
|
return;
|
|
}
|
|
});
|
|
|
|
Future<void> runInteraction(
|
|
SignalProtocolStore aliceStore, SignalProtocolStore bobStore) async {
|
|
final aliceSessionCipher = SessionCipher.fromStore(aliceStore, bobAddress);
|
|
final bobSessionCipher = SessionCipher.fromStore(bobStore, aliceAddress);
|
|
|
|
const originalMessage = 'smert ze smert';
|
|
final aliceMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(originalMessage)));
|
|
|
|
assert(aliceMessage.getType() == CiphertextMessage.whisperType);
|
|
|
|
var plaintext = await bobSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(aliceMessage.serialize()));
|
|
assert(String.fromCharCodes(plaintext) == originalMessage);
|
|
|
|
final bobMessage = await bobSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(originalMessage)));
|
|
|
|
assert(bobMessage.getType() == CiphertextMessage.whisperType);
|
|
|
|
plaintext = await aliceSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(bobMessage.serialize()));
|
|
assert(String.fromCharCodes(plaintext) == originalMessage);
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final loopingMessage =
|
|
'''What do we mean by saying that existence precedes essence?
|
|
We mean that man first of all exists, encounters himself,
|
|
surges up in the world--and defines himself aftward. $i''';
|
|
final aliceLoopingMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(loopingMessage)));
|
|
|
|
final loopingPlaintext = await bobSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(aliceLoopingMessage.serialize()));
|
|
assert(String.fromCharCodes(loopingPlaintext) == loopingMessage);
|
|
}
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final loopingMessage =
|
|
'''What do we mean by saying that existence precedes essence?
|
|
We mean that man first of all exists, encounters himself,
|
|
surges up in the world--and defines himself aftward. $i''';
|
|
final bobLoopingMessage = await bobSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(loopingMessage)));
|
|
|
|
final loopingPlaintext = await aliceSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(bobLoopingMessage.serialize()));
|
|
assert(String.fromCharCodes(loopingPlaintext) == loopingMessage);
|
|
}
|
|
|
|
final Set<(String, CiphertextMessage)> aliceOutOfOrderMessages =
|
|
HashSet<(String, CiphertextMessage)>();
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final loopingMessage =
|
|
'''What do we mean by saying that existence precedes essence?
|
|
We mean that man first of all exists, encounters himself,
|
|
surges up in the world--and defines himself aftward. $i''';
|
|
final aliceLoopingMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(loopingMessage)));
|
|
|
|
aliceOutOfOrderMessages.add((loopingMessage, aliceLoopingMessage));
|
|
}
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final loopingMessage =
|
|
'''What do we mean by saying that existence precedes essence?
|
|
We mean that man first of all exists, encounters himself,
|
|
surges up in the world--and defines himself aftward. $i''';
|
|
final aliceLoopingMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(loopingMessage)));
|
|
|
|
final loopingPlaintext = await bobSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(aliceLoopingMessage.serialize()));
|
|
assert(String.fromCharCodes(loopingPlaintext) == loopingMessage);
|
|
}
|
|
|
|
for (var i = 0; i < 10; i++) {
|
|
final loopingMessage = 'You can only desire based on what you know: $i';
|
|
final bobLoopingMessage = await bobSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(loopingMessage)));
|
|
|
|
final loopingPlaintext = await aliceSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(bobLoopingMessage.serialize()));
|
|
assert(String.fromCharCodes(loopingPlaintext) == loopingMessage);
|
|
}
|
|
|
|
for (final aliceOutOfOrderMessage in aliceOutOfOrderMessages) {
|
|
final outOfOrderPlaintext = await bobSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(aliceOutOfOrderMessage.$2.serialize()));
|
|
assert(String.fromCharCodes(outOfOrderPlaintext) ==
|
|
(aliceOutOfOrderMessage.$1));
|
|
}
|
|
}
|
|
|
|
test('testBasicPreKeyV3', () async {
|
|
var aliceStore = TestInMemorySignalProtocolStore();
|
|
var aliceSessionBuilder =
|
|
SessionBuilder.fromSignalStore(aliceStore, bobAddress);
|
|
|
|
final bobStore = TestInMemorySignalProtocolStore();
|
|
var bobPreKeyPair = Curve.generateKeyPair();
|
|
var bobSignedPreKeyPair = Curve.generateKeyPair();
|
|
var bobSignedPreKeySignature = Curve.calculateSignature(
|
|
await bobStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPrivateKey()),
|
|
bobSignedPreKeyPair.publicKey.serialize());
|
|
|
|
var bobPreKey = PreKeyBundle(
|
|
await bobStore.getLocalRegistrationId(),
|
|
1,
|
|
31337,
|
|
bobPreKeyPair.publicKey,
|
|
22,
|
|
bobSignedPreKeyPair.publicKey,
|
|
bobSignedPreKeySignature,
|
|
await bobStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
|
|
assert(await aliceStore.containsSession(bobAddress));
|
|
assert(await aliceStore
|
|
.loadSession(bobAddress)
|
|
.then((value) => value.sessionState.getSessionVersion()) ==
|
|
3);
|
|
|
|
const originalMessage = "L'homme est condamné à être libre";
|
|
var aliceSessionCipher = SessionCipher.fromStore(aliceStore, bobAddress);
|
|
var outgoingMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(originalMessage)));
|
|
assert(outgoingMessage.getType() == CiphertextMessage.prekeyType);
|
|
|
|
final incomingMessage = PreKeySignalMessage(outgoingMessage.serialize());
|
|
await bobStore.storePreKey(
|
|
31337, PreKeyRecord(bobPreKey.getPreKeyId()!, bobPreKeyPair));
|
|
await bobStore.storeSignedPreKey(
|
|
22,
|
|
SignedPreKeyRecord(22, Int64(DateTime.now().millisecondsSinceEpoch),
|
|
bobSignedPreKeyPair, bobSignedPreKeySignature));
|
|
|
|
final bobSessionCipher = SessionCipher.fromStore(bobStore, aliceAddress);
|
|
var plaintext = await bobSessionCipher.decryptWithCallback(incomingMessage,
|
|
(plaintext) async {
|
|
final result = utf8.decode(plaintext, allowMalformed: true);
|
|
assert(originalMessage == result);
|
|
assert(!await bobStore.containsSession(aliceAddress));
|
|
});
|
|
|
|
assert(await bobStore.containsSession(aliceAddress));
|
|
assert(await bobStore
|
|
.loadSession(aliceAddress)
|
|
.then((value) => value.sessionState.getSessionVersion()) ==
|
|
3);
|
|
assert(originalMessage == utf8.decode(plaintext, allowMalformed: true));
|
|
|
|
final bobOutgoingMessage = await bobSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(originalMessage)));
|
|
assert(bobOutgoingMessage.getType() == CiphertextMessage.whisperType);
|
|
|
|
final alicePlaintext = await aliceSessionCipher.decryptFromSignal(
|
|
SignalMessage.fromSerialized(bobOutgoingMessage.serialize()));
|
|
assert(
|
|
utf8.decode(alicePlaintext, allowMalformed: true) == originalMessage);
|
|
|
|
await runInteraction(aliceStore, bobStore);
|
|
|
|
aliceStore = TestInMemorySignalProtocolStore();
|
|
aliceSessionBuilder =
|
|
SessionBuilder.fromSignalStore(aliceStore, bobAddress);
|
|
aliceSessionCipher = SessionCipher.fromStore(aliceStore, bobAddress);
|
|
|
|
bobPreKeyPair = Curve.generateKeyPair();
|
|
bobSignedPreKeyPair = Curve.generateKeyPair();
|
|
bobSignedPreKeySignature = Curve.calculateSignature(
|
|
await bobStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPrivateKey()),
|
|
bobSignedPreKeyPair.publicKey.serialize());
|
|
bobPreKey = PreKeyBundle(
|
|
await bobStore.getLocalRegistrationId(),
|
|
1,
|
|
31338,
|
|
bobPreKeyPair.publicKey,
|
|
23,
|
|
bobSignedPreKeyPair.publicKey,
|
|
bobSignedPreKeySignature,
|
|
await bobStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
|
|
await bobStore.storePreKey(
|
|
31338, PreKeyRecord(bobPreKey.getPreKeyId()!, bobPreKeyPair));
|
|
await bobStore.storeSignedPreKey(
|
|
23,
|
|
SignedPreKeyRecord(23, Int64(DateTime.now().millisecondsSinceEpoch),
|
|
bobSignedPreKeyPair, bobSignedPreKeySignature));
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
|
|
outgoingMessage = await aliceSessionCipher
|
|
.encrypt(Uint8List.fromList(utf8.encode(originalMessage)));
|
|
|
|
try {
|
|
plaintext = await bobSessionCipher
|
|
.decrypt(PreKeySignalMessage(outgoingMessage.serialize()));
|
|
throw AssertionError("shouldn't be trusted!");
|
|
} on UntrustedIdentityException {
|
|
await bobStore.saveIdentity(aliceAddress,
|
|
PreKeySignalMessage(outgoingMessage.serialize()).getIdentityKey());
|
|
}
|
|
|
|
plaintext = await bobSessionCipher
|
|
.decrypt(PreKeySignalMessage(outgoingMessage.serialize()));
|
|
assert(utf8.decode(plaintext, allowMalformed: true) == originalMessage);
|
|
|
|
bobPreKey = PreKeyBundle(
|
|
await bobStore.getLocalRegistrationId(),
|
|
1,
|
|
31337,
|
|
Curve.generateKeyPair().publicKey,
|
|
23,
|
|
bobSignedPreKeyPair.publicKey,
|
|
bobSignedPreKeySignature,
|
|
await aliceStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
|
|
try {
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
throw AssertionError("shoulnd't be trusted!");
|
|
} on UntrustedIdentityException {
|
|
// good
|
|
}
|
|
});
|
|
|
|
test('testBadSignedPreKeySignature', () async {
|
|
final aliceStore = TestInMemorySignalProtocolStore();
|
|
final aliceSessionBuilder =
|
|
SessionBuilder.fromSignalStore(aliceStore, bobAddress);
|
|
|
|
final bobIdentityKeyStore = TestInMemoryIdentityKeyStore();
|
|
|
|
final bobPreKeyPair = Curve.generateKeyPair();
|
|
final bobSignedPreKeyPair = Curve.generateKeyPair();
|
|
final bobSignedPreKeySignature = Curve.calculateSignature(
|
|
await bobIdentityKeyStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPrivateKey()),
|
|
bobSignedPreKeyPair.publicKey.serialize());
|
|
|
|
for (var i = 0; i < bobSignedPreKeySignature.length * 8; i++) {
|
|
final modifiedSignature = Uint8List(bobSignedPreKeySignature.length);
|
|
Curve.arraycopy(bobSignedPreKeySignature, 0, modifiedSignature, 0,
|
|
modifiedSignature.length);
|
|
|
|
modifiedSignature[i ~/ 8] ^= 0x01 << (i % 8);
|
|
|
|
final bobPreKey = PreKeyBundle(
|
|
await bobIdentityKeyStore.getLocalRegistrationId(),
|
|
1,
|
|
31337,
|
|
bobPreKeyPair.publicKey,
|
|
22,
|
|
bobSignedPreKeyPair.publicKey,
|
|
modifiedSignature,
|
|
await bobIdentityKeyStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
|
|
try {
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
throw AssertionError('Accepted modified device key signature!');
|
|
} on InvalidKeyException {
|
|
// good
|
|
}
|
|
}
|
|
|
|
final bobPreKey = PreKeyBundle(
|
|
await bobIdentityKeyStore.getLocalRegistrationId(),
|
|
1,
|
|
31337,
|
|
bobPreKeyPair.publicKey,
|
|
22,
|
|
bobSignedPreKeyPair.publicKey,
|
|
bobSignedPreKeySignature,
|
|
await bobIdentityKeyStore
|
|
.getIdentityKeyPair()
|
|
.then((value) => value.getPublicKey()));
|
|
|
|
await aliceSessionBuilder.processPreKeyBundle(bobPreKey);
|
|
});
|
|
}
|