aboutsummaryrefslogtreecommitdiff
path: root/plugins/omemo/tests/signal
diff options
context:
space:
mode:
authorhrxi <hrrrxi@gmail.com>2023-06-19 14:08:57 +0200
committerfiaxh <fiaxh@users.noreply.github.com>2023-10-06 15:25:12 +0200
commit6eb1b53e60a12f82c8d47a5824bf9cee954ccdc2 (patch)
tree13a13ef08fcd74bc1685454730b72693806b76f0 /plugins/omemo/tests/signal
parente2d801b5f74b60c38a75310066c48468c8a4bc93 (diff)
downloaddino-6eb1b53e60a12f82c8d47a5824bf9cee954ccdc2.tar.gz
dino-6eb1b53e60a12f82c8d47a5824bf9cee954ccdc2.zip
Merge `signal-protocol` into `omemo` plugin
Same reasoning as for the `openpgp` plugin.
Diffstat (limited to 'plugins/omemo/tests/signal')
-rw-r--r--plugins/omemo/tests/signal/common.vala92
-rw-r--r--plugins/omemo/tests/signal/curve25519.vala207
-rw-r--r--plugins/omemo/tests/signal/hkdf.vala59
-rw-r--r--plugins/omemo/tests/signal/session_builder.vala400
-rw-r--r--plugins/omemo/tests/signal/testcase.vala80
5 files changed, 838 insertions, 0 deletions
diff --git a/plugins/omemo/tests/signal/common.vala b/plugins/omemo/tests/signal/common.vala
new file mode 100644
index 00000000..9bb9b1dc
--- /dev/null
+++ b/plugins/omemo/tests/signal/common.vala
@@ -0,0 +1,92 @@
+namespace Signal.Test {
+
+int main(string[] args) {
+ GLib.Test.init(ref args);
+ GLib.Test.set_nonfatal_assertions();
+ TestSuite.get_root().add_suite(new Curve25519().get_suite());
+ TestSuite.get_root().add_suite(new SessionBuilderTest().get_suite());
+ TestSuite.get_root().add_suite(new HKDF().get_suite());
+ return GLib.Test.run();
+}
+
+Store setup_test_store_context(Context global_context) {
+ Store store = global_context.create_store();
+ try {
+ store.identity_key_store.local_registration_id = (Random.next_int() % 16380) + 1;
+
+ ECKeyPair key_pair = global_context.generate_key_pair();
+ store.identity_key_store.identity_key_private = new Bytes(key_pair.private.serialize());
+ store.identity_key_store.identity_key_public = new Bytes(key_pair.public.serialize());
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ return store;
+}
+
+ECPublicKey? create_test_ec_public_key(Context context) {
+ try {
+ return context.generate_key_pair().public;
+ } catch (Error e) {
+ fail_if_reached();
+ return null;
+ }
+}
+
+bool fail_if(bool exp, string? reason = null) {
+ if (exp) {
+ if (reason != null) GLib.Test.message(reason);
+ GLib.Test.fail();
+ return true;
+ }
+ return false;
+}
+
+void fail_if_reached(string? reason = null) {
+ fail_if(true, reason);
+}
+
+delegate void ErrorFunc() throws Error;
+
+void fail_if_not_error_code(ErrorFunc func, int expectedCode, string? reason = null) {
+ try {
+ func();
+ fail_if_reached(@"$(reason + ": " ?? "")no error thrown");
+ } catch (Error e) {
+ fail_if_not_eq_int(e.code, expectedCode, @"$(reason + ": " ?? "")caught unexpected error");
+ }
+}
+
+bool fail_if_not(bool exp, string? reason = null) {
+ return fail_if(!exp, reason);
+}
+
+bool fail_if_eq_int(int left, int right, string? reason = null) {
+ return fail_if(left == right, @"$(reason + ": " ?? "")$left == $right");
+}
+
+bool fail_if_not_eq_int(int left, int right, string? reason = null) {
+ return fail_if_not(left == right, @"$(reason + ": " ?? "")$left != $right");
+}
+
+bool fail_if_not_eq_str(string left, string right, string? reason = null) {
+ return fail_if_not(left == right, @"$(reason + ": " ?? "")$left != $right");
+}
+
+bool fail_if_not_eq_uint8_arr(uint8[] left, uint8[] right, string? reason = null) {
+ if (fail_if_not_eq_int(left.length, right.length, @"$(reason + ": " ?? "")array length not equal")) return true;
+ return fail_if_not_eq_str(Base64.encode(left), Base64.encode(right), reason);
+}
+
+bool fail_if_not_zero_int(int zero, string? reason = null) {
+ return fail_if_not_eq_int(zero, 0, reason);
+}
+
+bool fail_if_zero_int(int zero, string? reason = null) {
+ return fail_if_eq_int(zero, 0, reason);
+}
+
+bool fail_if_null(void* what, string? reason = null) {
+ return fail_if(what == null || (size_t)what == 0, reason);
+}
+
+}
diff --git a/plugins/omemo/tests/signal/curve25519.vala b/plugins/omemo/tests/signal/curve25519.vala
new file mode 100644
index 00000000..6dfae62f
--- /dev/null
+++ b/plugins/omemo/tests/signal/curve25519.vala
@@ -0,0 +1,207 @@
+namespace Signal.Test {
+
+class Curve25519 : Gee.TestCase {
+
+ public Curve25519() {
+ base("Curve25519");
+ add_test("agreement", test_curve25519_agreement);
+ add_test("generate_public", test_curve25519_generate_public);
+ add_test("random_agreements", test_curve25519_random_agreements);
+ add_test("signature", test_curve25519_signature);
+ }
+
+ private Context global_context;
+
+ public override void set_up() {
+ try {
+ global_context = new Context();
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+ public override void tear_down() {
+ global_context = null;
+ }
+
+ void test_curve25519_agreement() {
+ try {
+ uint8[] alicePublic = {
+ 0x05, 0x1b, 0xb7, 0x59, 0x66,
+ 0xf2, 0xe9, 0x3a, 0x36, 0x91,
+ 0xdf, 0xff, 0x94, 0x2b, 0xb2,
+ 0xa4, 0x66, 0xa1, 0xc0, 0x8b,
+ 0x8d, 0x78, 0xca, 0x3f, 0x4d,
+ 0x6d, 0xf8, 0xb8, 0xbf, 0xa2,
+ 0xe4, 0xee, 0x28};
+
+ uint8[] alicePrivate = {
+ 0xc8, 0x06, 0x43, 0x9d, 0xc9,
+ 0xd2, 0xc4, 0x76, 0xff, 0xed,
+ 0x8f, 0x25, 0x80, 0xc0, 0x88,
+ 0x8d, 0x58, 0xab, 0x40, 0x6b,
+ 0xf7, 0xae, 0x36, 0x98, 0x87,
+ 0x90, 0x21, 0xb9, 0x6b, 0xb4,
+ 0xbf, 0x59};
+
+ uint8[] bobPublic = {
+ 0x05, 0x65, 0x36, 0x14, 0x99,
+ 0x3d, 0x2b, 0x15, 0xee, 0x9e,
+ 0x5f, 0xd3, 0xd8, 0x6c, 0xe7,
+ 0x19, 0xef, 0x4e, 0xc1, 0xda,
+ 0xae, 0x18, 0x86, 0xa8, 0x7b,
+ 0x3f, 0x5f, 0xa9, 0x56, 0x5a,
+ 0x27, 0xa2, 0x2f};
+
+ uint8[] bobPrivate = {
+ 0xb0, 0x3b, 0x34, 0xc3, 0x3a,
+ 0x1c, 0x44, 0xf2, 0x25, 0xb6,
+ 0x62, 0xd2, 0xbf, 0x48, 0x59,
+ 0xb8, 0x13, 0x54, 0x11, 0xfa,
+ 0x7b, 0x03, 0x86, 0xd4, 0x5f,
+ 0xb7, 0x5d, 0xc5, 0xb9, 0x1b,
+ 0x44, 0x66};
+
+ uint8[] shared = {
+ 0x32, 0x5f, 0x23, 0x93, 0x28,
+ 0x94, 0x1c, 0xed, 0x6e, 0x67,
+ 0x3b, 0x86, 0xba, 0x41, 0x01,
+ 0x74, 0x48, 0xe9, 0x9b, 0x64,
+ 0x9a, 0x9c, 0x38, 0x06, 0xc1,
+ 0xdd, 0x7c, 0xa4, 0xc4, 0x77,
+ 0xe6, 0x29};
+
+ ECPublicKey alice_public_key = global_context.decode_public_key(alicePublic);
+ ECPrivateKey alice_private_key = global_context.decode_private_key(alicePrivate);
+ ECPublicKey bob_public_key = global_context.decode_public_key(bobPublic);
+ ECPrivateKey bob_private_key = global_context.decode_private_key(bobPrivate);
+
+ uint8[] shared_one = calculate_agreement(alice_public_key, bob_private_key);
+ uint8[] shared_two = calculate_agreement(bob_public_key, alice_private_key);
+
+ fail_if_not_eq_int(shared_one.length, 32);
+ fail_if_not_eq_int(shared_two.length, 32);
+ fail_if_not_eq_uint8_arr(shared, shared_one);
+ fail_if_not_eq_uint8_arr(shared_one, shared_two);
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+ void test_curve25519_generate_public() {
+ try {
+ uint8[] alicePublic = {
+ 0x05, 0x1b, 0xb7, 0x59, 0x66,
+ 0xf2, 0xe9, 0x3a, 0x36, 0x91,
+ 0xdf, 0xff, 0x94, 0x2b, 0xb2,
+ 0xa4, 0x66, 0xa1, 0xc0, 0x8b,
+ 0x8d, 0x78, 0xca, 0x3f, 0x4d,
+ 0x6d, 0xf8, 0xb8, 0xbf, 0xa2,
+ 0xe4, 0xee, 0x28};
+
+ uint8[] alicePrivate = {
+ 0xc8, 0x06, 0x43, 0x9d, 0xc9,
+ 0xd2, 0xc4, 0x76, 0xff, 0xed,
+ 0x8f, 0x25, 0x80, 0xc0, 0x88,
+ 0x8d, 0x58, 0xab, 0x40, 0x6b,
+ 0xf7, 0xae, 0x36, 0x98, 0x87,
+ 0x90, 0x21, 0xb9, 0x6b, 0xb4,
+ 0xbf, 0x59};
+
+ ECPrivateKey alice_private_key = global_context.decode_private_key(alicePrivate);
+ ECPublicKey alice_expected_public_key = global_context.decode_public_key(alicePublic);
+ ECPublicKey alice_public_key = generate_public_key(alice_private_key);
+
+ fail_if_not_zero_int(alice_expected_public_key.compare(alice_public_key));
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+ void test_curve25519_random_agreements() {
+ try {
+ ECKeyPair alice_key_pair = null;
+ ECPublicKey alice_public_key = null;
+ ECPrivateKey alice_private_key = null;
+ ECKeyPair bob_key_pair = null;
+ ECPublicKey bob_public_key = null;
+ ECPrivateKey bob_private_key = null;
+ uint8[] shared_alice = null;
+ uint8[] shared_bob = null;
+
+ for (int i = 0; i < 50; i++) {
+ fail_if_null(alice_key_pair = global_context.generate_key_pair());
+ fail_if_null(alice_public_key = alice_key_pair.public);
+ fail_if_null(alice_private_key = alice_key_pair.private);
+
+ fail_if_null(bob_key_pair = global_context.generate_key_pair());
+ fail_if_null(bob_public_key = bob_key_pair.public);
+ fail_if_null(bob_private_key = bob_key_pair.private);
+
+ shared_alice = calculate_agreement(bob_public_key, alice_private_key);
+ fail_if_not_eq_int(shared_alice.length, 32);
+
+ shared_bob = calculate_agreement(alice_public_key, bob_private_key);
+ fail_if_not_eq_int(shared_bob.length, 32);
+
+ fail_if_not_eq_uint8_arr(shared_alice, shared_bob);
+ }
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+ void test_curve25519_signature() {
+ try {
+ uint8[] aliceIdentityPrivate = {
+ 0xc0, 0x97, 0x24, 0x84, 0x12, 0xe5, 0x8b, 0xf0,
+ 0x5d, 0xf4, 0x87, 0x96, 0x82, 0x05, 0x13, 0x27,
+ 0x94, 0x17, 0x8e, 0x36, 0x76, 0x37, 0xf5, 0x81,
+ 0x8f, 0x81, 0xe0, 0xe6, 0xce, 0x73, 0xe8, 0x65};
+
+ uint8[] aliceIdentityPublic = {
+ 0x05, 0xab, 0x7e, 0x71, 0x7d, 0x4a, 0x16, 0x3b,
+ 0x7d, 0x9a, 0x1d, 0x80, 0x71, 0xdf, 0xe9, 0xdc,
+ 0xf8, 0xcd, 0xcd, 0x1c, 0xea, 0x33, 0x39, 0xb6,
+ 0x35, 0x6b, 0xe8, 0x4d, 0x88, 0x7e, 0x32, 0x2c,
+ 0x64};
+
+ uint8[] aliceEphemeralPublic = {
+ 0x05, 0xed, 0xce, 0x9d, 0x9c, 0x41, 0x5c, 0xa7,
+ 0x8c, 0xb7, 0x25, 0x2e, 0x72, 0xc2, 0xc4, 0xa5,
+ 0x54, 0xd3, 0xeb, 0x29, 0x48, 0x5a, 0x0e, 0x1d,
+ 0x50, 0x31, 0x18, 0xd1, 0xa8, 0x2d, 0x99, 0xfb,
+ 0x4a};
+
+ uint8[] aliceSignature = {
+ 0x5d, 0xe8, 0x8c, 0xa9, 0xa8, 0x9b, 0x4a, 0x11,
+ 0x5d, 0xa7, 0x91, 0x09, 0xc6, 0x7c, 0x9c, 0x74,
+ 0x64, 0xa3, 0xe4, 0x18, 0x02, 0x74, 0xf1, 0xcb,
+ 0x8c, 0x63, 0xc2, 0x98, 0x4e, 0x28, 0x6d, 0xfb,
+ 0xed, 0xe8, 0x2d, 0xeb, 0x9d, 0xcd, 0x9f, 0xae,
+ 0x0b, 0xfb, 0xb8, 0x21, 0x56, 0x9b, 0x3d, 0x90,
+ 0x01, 0xbd, 0x81, 0x30, 0xcd, 0x11, 0xd4, 0x86,
+ 0xce, 0xf0, 0x47, 0xbd, 0x60, 0xb8, 0x6e, 0x88};
+
+ global_context.decode_private_key(aliceIdentityPrivate);
+ global_context.decode_public_key(aliceEphemeralPublic);
+ ECPublicKey alice_public_key = global_context.decode_public_key(aliceIdentityPublic);
+
+ fail_if(!verify_signature(alice_public_key, aliceEphemeralPublic, aliceSignature), "signature verification failed");
+
+ uint8[] modifiedSignature = new uint8[aliceSignature.length];
+
+ for (int i = 0; i < aliceSignature.length; i++) {
+ Memory.copy(modifiedSignature, aliceSignature, aliceSignature.length);
+ modifiedSignature[i] ^= 0x01;
+
+ fail_if(verify_signature(alice_public_key, aliceEphemeralPublic, modifiedSignature), "invalid signature verification succeeded");
+ }
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+}
+
+} \ No newline at end of file
diff --git a/plugins/omemo/tests/signal/hkdf.vala b/plugins/omemo/tests/signal/hkdf.vala
new file mode 100644
index 00000000..c30af275
--- /dev/null
+++ b/plugins/omemo/tests/signal/hkdf.vala
@@ -0,0 +1,59 @@
+namespace Signal.Test {
+
+class HKDF : Gee.TestCase {
+
+ public HKDF() {
+ base("HKDF");
+ add_test("vector_v3", test_hkdf_vector_v3);
+ }
+
+ private Context global_context;
+
+ public override void set_up() {
+ try {
+ global_context = new Context();
+ } catch (Error e) {
+ fail_if_reached();
+ }
+ }
+
+ public override void tear_down() {
+ global_context = null;
+ }
+
+ public void test_hkdf_vector_v3() {
+ uint8[] ikm = {
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b};
+
+ uint8[] salt = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c};
+
+ uint8[] info = {
+ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
+ 0xf8, 0xf9};
+
+ uint8[] okm = {
+ 0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a,
+ 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36, 0x2f, 0x2a,
+ 0x2d, 0x2d, 0x0a, 0x90, 0xcf, 0x1a, 0x5a, 0x4c,
+ 0x5d, 0xb0, 0x2d, 0x56, 0xec, 0xc4, 0xc5, 0xbf,
+ 0x34, 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18,
+ 0x58, 0x65};
+
+ NativeHkdfContext context = null;
+ fail_if_not_zero_int(NativeHkdfContext.create(out context, 3, global_context.native_context));
+
+ uint8[] output = null;
+ int result = (int) context.derive_secrets(out output, ikm, salt, info, 42);
+ fail_if_not_eq_int(result, okm.length);
+ output.length = result;
+
+ fail_if_not_eq_uint8_arr(output, okm);
+ }
+
+}
+
+} \ No newline at end of file
diff --git a/plugins/omemo/tests/signal/session_builder.vala b/plugins/omemo/tests/signal/session_builder.vala
new file mode 100644
index 00000000..7e2448e1
--- /dev/null
+++ b/plugins/omemo/tests/signal/session_builder.vala
@@ -0,0 +1,400 @@
+namespace Signal.Test {
+
+class SessionBuilderTest : Gee.TestCase {
+ Address alice_address;
+ Address bob_address;
+
+ public SessionBuilderTest() {
+ base("SessionBuilder");
+
+ add_test("basic_pre_key_v2", test_basic_pre_key_v2);
+ add_test("basic_pre_key_v3", test_basic_pre_key_v3);
+ add_test("bad_signed_pre_key_signature", test_bad_signed_pre_key_signature);
+ add_test("repeat_bundle_message_v2", test_repeat_bundle_message_v2);
+ }
+
+ private Context global_context;
+
+ public override void set_up() {
+ try {
+ global_context = new Context();
+ alice_address = new Address("+14151111111", 1);
+ bob_address = new Address("+14152222222", 1);
+ } catch (Error e) {
+ fail_if_reached(@"Unexpected error: $(e.message)");
+ }
+ }
+
+ public override void tear_down() {
+ global_context = null;
+ alice_address = null;
+ bob_address = null;
+ }
+
+ void test_basic_pre_key_v2() {
+ try {
+ /* Create Alice's data store and session builder */
+ Store alice_store = setup_test_store_context(global_context);
+ SessionBuilder alice_session_builder = alice_store.create_session_builder(bob_address);
+
+ /* Create Bob's data store and pre key bundle */
+ Store bob_store = setup_test_store_context(global_context);
+ uint32 bob_local_registration_id = bob_store.local_registration_id;
+ IdentityKeyPair bob_identity_key_pair = bob_store.identity_key_pair;
+ ECKeyPair bob_pre_key_pair = global_context.generate_key_pair();
+
+ PreKeyBundle bob_pre_key = create_pre_key_bundle(bob_local_registration_id, 1, 31337, bob_pre_key_pair.public, 0, null, null, bob_identity_key_pair.public);
+
+ /*
+ * Have Alice process Bob's pre key bundle, which should fail due to a
+ * missing unsigned pre key.
+ */
+ fail_if_not_error_code(() => alice_session_builder.process_pre_key_bundle(bob_pre_key), ErrorCode.INVALID_KEY);
+ } catch(Error e) {
+ fail_if_reached(@"Unexpected error: $(e.message)");
+ }
+ }
+
+ void test_basic_pre_key_v3() {
+ try {
+ /* Create Alice's data store and session builder */
+ Store alice_store = setup_test_store_context(global_context);
+ SessionBuilder alice_session_builder = alice_store.create_session_builder(bob_address);
+
+ /* Create Bob's data store and pre key bundle */
+ Store bob_store = setup_test_store_context(global_context);
+ uint32 bob_local_registration_id = bob_store.local_registration_id;
+ ECKeyPair bob_pre_key_pair = global_context.generate_key_pair();
+ ECKeyPair bob_signed_pre_key_pair = global_context.generate_key_pair();
+ IdentityKeyPair bob_identity_key_pair = bob_store.identity_key_pair;
+
+ uint8[] bob_signed_pre_key_signature = global_context.calculate_signature(bob_identity_key_pair.private, bob_signed_pre_key_pair.public.serialize());
+
+ PreKeyBundle bob_pre_key = create_pre_key_bundle(bob_local_registration_id, 1, 31337, bob_pre_key_pair.public, 22, bob_signed_pre_key_pair.public, bob_signed_pre_key_signature, bob_identity_key_pair.public);
+
+ /* Have Alice process Bob's pre key bundle */
+ alice_session_builder.process_pre_key_bundle(bob_pre_key);
+
+ /* Check that we can load the session state and verify its version */
+ fail_if_not(alice_store.contains_session(bob_address));
+
+ SessionRecord loaded_record = alice_store.load_session(bob_address);
+ fail_if_not_eq_int((int)loaded_record.state.session_version, 3);
+
+ /* Encrypt an outgoing message to send to Bob */
+ string original_message = "L'homme est condamné à être libre";
+ SessionCipher alice_session_cipher = alice_store.create_session_cipher(bob_address);
+
+ CiphertextMessage outgoing_message = alice_session_cipher.encrypt(original_message.data);
+ fail_if_not_eq_int(outgoing_message.type, CiphertextType.PREKEY);
+
+ /* Convert to an incoming message for Bob */
+ PreKeySignalMessage incoming_message = global_context.deserialize_pre_key_signal_message(outgoing_message.serialized);
+
+ /* Save the pre key and signed pre key in Bob's data store */
+ PreKeyRecord bob_pre_key_record;
+ throw_by_code(PreKeyRecord.create(out bob_pre_key_record, bob_pre_key.pre_key_id, bob_pre_key_pair));
+ bob_store.store_pre_key(bob_pre_key_record);
+
+ SignedPreKeyRecord bob_signed_pre_key_record;
+ throw_by_code(SignedPreKeyRecord.create(out bob_signed_pre_key_record, 22, new DateTime.now_utc().to_unix(), bob_signed_pre_key_pair, bob_signed_pre_key_signature));
+ bob_store.store_signed_pre_key(bob_signed_pre_key_record);
+
+ /* Create Bob's session cipher and decrypt the message from Alice */
+ SessionCipher bob_session_cipher = bob_store.create_session_cipher(alice_address);
+
+ /* Prepare the data for the callback test */
+ //int callback_context = 1234;
+ //bob_session_cipher.user_data =
+ //bob_session_cipher.decryption_callback =
+ uint8[] plaintext = bob_session_cipher.decrypt_pre_key_signal_message(incoming_message);
+
+ /* Clean up callback data */
+ bob_session_cipher.user_data = null;
+ bob_session_cipher.decryption_callback = null;
+
+ /* Verify Bob's session state and the decrypted message */
+ fail_if_not(bob_store.contains_session(alice_address));
+
+ SessionRecord alice_recipient_session_record = bob_store.load_session(alice_address);
+
+ SessionState alice_recipient_session_state = alice_recipient_session_record.state;
+ fail_if_not_eq_int((int)alice_recipient_session_state.session_version, 3);
+ fail_if_null(alice_recipient_session_state.alice_base_key);
+
+ fail_if_not_eq_uint8_arr(original_message.data, plaintext);
+
+ /* Have Bob send a reply to Alice */
+ CiphertextMessage bob_outgoing_message = bob_session_cipher.encrypt(original_message.data);
+ fail_if_not_eq_int(bob_outgoing_message.type, CiphertextType.SIGNAL);
+
+ /* Verify that Alice can decrypt it */
+ SignalMessage bob_outgoing_message_copy = global_context.copy_signal_message(bob_outgoing_message);
+
+ uint8[] alice_plaintext = alice_session_cipher.decrypt_signal_message(bob_outgoing_message_copy);
+
+ fail_if_not_eq_uint8_arr(original_message.data, alice_plaintext);
+
+ GLib.Test.message("Pre-interaction tests complete");
+
+ /* Interaction tests */
+ run_interaction(alice_store, bob_store);
+
+ /* Cleanup state from previous tests that we need to replace */
+ alice_store = null;
+ bob_pre_key_pair = null;
+ bob_signed_pre_key_pair = null;
+ bob_identity_key_pair = null;
+ bob_signed_pre_key_signature = null;
+ bob_pre_key_record = null;
+ bob_signed_pre_key_record = null;
+
+ /* Create Alice's new session data */
+ alice_store = setup_test_store_context(global_context);
+ alice_session_builder = alice_store.create_session_builder(bob_address);
+ alice_session_cipher = alice_store.create_session_cipher(bob_address);
+
+ /* Create Bob's new pre key bundle */
+ bob_pre_key_pair = global_context.generate_key_pair();
+ bob_signed_pre_key_pair = global_context.generate_key_pair();
+ bob_identity_key_pair = bob_store.identity_key_pair;
+ bob_signed_pre_key_signature = global_context.calculate_signature(bob_identity_key_pair.private, bob_signed_pre_key_pair.public.serialize());
+ bob_pre_key = create_pre_key_bundle(bob_local_registration_id, 1, 31338, bob_pre_key_pair.public, 23, bob_signed_pre_key_pair.public, bob_signed_pre_key_signature, bob_identity_key_pair.public);
+
+ /* Save the new pre key and signed pre key in Bob's data store */
+ throw_by_code(PreKeyRecord.create(out bob_pre_key_record, bob_pre_key.pre_key_id, bob_pre_key_pair));
+ bob_store.store_pre_key(bob_pre_key_record);
+
+ throw_by_code(SignedPreKeyRecord.create(out bob_signed_pre_key_record, 23, new DateTime.now_utc().to_unix(), bob_signed_pre_key_pair, bob_signed_pre_key_signature));
+ bob_store.store_signed_pre_key(bob_signed_pre_key_record);
+
+ /* Have Alice process Bob's pre key bundle */
+ alice_session_builder.process_pre_key_bundle(bob_pre_key);
+
+ /* Have Alice encrypt a message for Bob */
+ outgoing_message = alice_session_cipher.encrypt(original_message.data);
+ fail_if_not_eq_int(outgoing_message.type, CiphertextType.PREKEY);
+
+ /* Have Bob try to decrypt the message */
+ PreKeySignalMessage outgoing_message_copy = global_context.copy_pre_key_signal_message(outgoing_message);
+
+ /* The decrypt should fail with a specific error */
+ fail_if_not_error_code(() => bob_session_cipher.decrypt_pre_key_signal_message(outgoing_message_copy), ErrorCode.UNTRUSTED_IDENTITY);
+
+ outgoing_message_copy = global_context.copy_pre_key_signal_message(outgoing_message);
+
+ /* Save the identity key to Bob's store */
+ bob_store.save_identity(alice_address, outgoing_message_copy.identity_key);
+
+ /* Try the decrypt again, this time it should succeed */
+ outgoing_message_copy = global_context.copy_pre_key_signal_message(outgoing_message);
+ plaintext = bob_session_cipher.decrypt_pre_key_signal_message(outgoing_message_copy);
+
+ fail_if_not_eq_uint8_arr(original_message.data, plaintext);
+
+ /* Create a new pre key for Bob */
+ ECPublicKey test_public_key = create_test_ec_public_key(global_context);
+
+ IdentityKeyPair alice_identity_key_pair = alice_store.identity_key_pair;
+
+ bob_pre_key = create_pre_key_bundle(bob_local_registration_id, 1, 31337, test_public_key, 23, bob_signed_pre_key_pair.public, bob_signed_pre_key_signature, alice_identity_key_pair.public);
+
+ /* Have Alice process Bob's new pre key bundle, which should fail */
+ fail_if_not_error_code(() => alice_session_builder.process_pre_key_bundle(bob_pre_key), ErrorCode.UNTRUSTED_IDENTITY);
+
+ GLib.Test.message("Post-interaction tests complete");
+ } catch(Error e) {
+ fail_if_reached(@"Unexpected error: $(e.message)");
+ }
+ }
+
+ void test_bad_signed_pre_key_signature() {
+ try {
+ /* Create Alice's data store and session builder */
+ Store alice_store = setup_test_store_context(global_context);
+ SessionBuilder alice_session_builder = alice_store.create_session_builder(bob_address);
+
+ /* Create Bob's data store */
+ Store bob_store = setup_test_store_context(global_context);
+
+ /* Create Bob's regular and signed pre key pairs */
+ ECKeyPair bob_pre_key_pair = global_context.generate_key_pair();
+ ECKeyPair bob_signed_pre_key_pair = global_context.generate_key_pair();
+
+ /* Create Bob's signed pre key signature */
+ IdentityKeyPair bob_identity_key_pair = bob_store.identity_key_pair;
+ uint8[] bob_signed_pre_key_signature = global_context.calculate_signature(bob_identity_key_pair.private, bob_signed_pre_key_pair.public.serialize());
+
+ for (int i = 0; i < bob_signed_pre_key_signature.length * 8; i++) {
+ uint8[] modified_signature = bob_signed_pre_key_signature[0:bob_signed_pre_key_signature.length];
+
+ /* Intentionally corrupt the signature data */
+ modified_signature[i/8] ^= (1 << ((uint8)i % 8));
+
+ /* Create a pre key bundle */
+ PreKeyBundle bob_pre_key = create_pre_key_bundle(bob_store.local_registration_id,1,31137,bob_pre_key_pair.public,22,bob_signed_pre_key_pair.public,modified_signature,bob_identity_key_pair.public);
+
+ /* Process the bundle and make sure we fail with an invalid key error */
+ fail_if_not_error_code(() => alice_session_builder.process_pre_key_bundle(bob_pre_key), ErrorCode.INVALID_KEY);
+ }
+
+ /* Create a correct pre key bundle */
+ PreKeyBundle bob_pre_key = create_pre_key_bundle(bob_store.local_registration_id,1,31137,bob_pre_key_pair.public,22,bob_signed_pre_key_pair.public,bob_signed_pre_key_signature,bob_identity_key_pair.public);
+
+ /* Process the bundle and make sure we do not fail */
+ alice_session_builder.process_pre_key_bundle(bob_pre_key);
+ } catch(Error e) {
+ fail_if_reached(@"Unexpected error: $(e.message)");
+ }
+ }
+
+ void test_repeat_bundle_message_v2() {
+ try {
+ /* Create Alice's data store and session builder */
+ Store alice_store = setup_test_store_context(global_context);
+ SessionBuilder alice_session_builder = alice_store.create_session_builder(bob_address);
+
+ /* Create Bob's data store and pre key bundle */
+ Store bob_store = setup_test_store_context(global_context);
+ ECKeyPair bob_pre_key_pair = global_context.generate_key_pair();
+ ECKeyPair bob_signed_pre_key_pair = global_context.generate_key_pair();
+ uint8[] bob_signed_pre_key_signature = global_context.calculate_signature(bob_store.identity_key_pair.private, bob_signed_pre_key_pair.public.serialize());
+ PreKeyBundle bob_pre_key = create_pre_key_bundle(bob_store.local_registration_id,1,31337,bob_pre_key_pair.public,0,null,null,bob_store.identity_key_pair.public);
+
+ /* Add Bob's pre keys to Bob's data store */
+ PreKeyRecord bob_pre_key_record;
+ throw_by_code(PreKeyRecord.create(out bob_pre_key_record, bob_pre_key.pre_key_id, bob_pre_key_pair));
+ bob_store.store_pre_key(bob_pre_key_record);
+ SignedPreKeyRecord bob_signed_pre_key_record;
+ throw_by_code(SignedPreKeyRecord.create(out bob_signed_pre_key_record, 22, new DateTime.now_utc().to_unix(), bob_signed_pre_key_pair, bob_signed_pre_key_signature));
+ bob_store.store_signed_pre_key(bob_signed_pre_key_record);
+
+ /*
+ * Have Alice process Bob's pre key bundle, which should fail due to a
+ * missing signed pre key.
+ */
+ fail_if_not_error_code(() => alice_session_builder.process_pre_key_bundle(bob_pre_key), ErrorCode.INVALID_KEY);
+ } catch(Error e) {
+ fail_if_reached(@"Unexpected error: $(e.message)");
+ }
+ }
+
+ class Holder {
+ public uint8[] data { get; private set; }
+
+ public Holder(uint8[] data) {
+ this.data = data;
+ }
+ }
+
+ void run_interaction(Store alice_store, Store bob_store) throws Error {
+
+ /* Create the session ciphers */
+ SessionCipher alice_session_cipher = alice_store.create_session_cipher(bob_address);
+ SessionCipher bob_session_cipher = bob_store.create_session_cipher(alice_address);
+
+ /* Create a test message */
+ string original_message = "smert ze smert";
+
+ /* Simulate Alice sending a message to Bob */
+ CiphertextMessage alice_message = alice_session_cipher.encrypt(original_message.data);
+ fail_if_not_eq_int(alice_message.type, CiphertextType.SIGNAL);
+
+ SignalMessage alice_message_copy = global_context.copy_signal_message(alice_message);
+ uint8[] plaintext = bob_session_cipher.decrypt_signal_message(alice_message_copy);
+ fail_if_not_eq_uint8_arr(original_message.data, plaintext);
+
+ GLib.Test.message("Interaction complete: Alice -> Bob");
+
+ /* Simulate Bob sending a message to Alice */
+ CiphertextMessage bob_message = bob_session_cipher.encrypt(original_message.data);
+ fail_if_not_eq_int(alice_message.type, CiphertextType.SIGNAL);
+
+ SignalMessage bob_message_copy = global_context.copy_signal_message(bob_message);
+ plaintext = alice_session_cipher.decrypt_signal_message(bob_message_copy);
+ fail_if_not_eq_uint8_arr(original_message.data, plaintext);
+
+ GLib.Test.message("Interaction complete: Bob -> Alice");
+
+ /* Looping Alice -> Bob */
+ for (int i = 0; i < 10; i++) {
+ uint8[] looping_message = create_looping_message(i);
+ CiphertextMessage alice_looping_message = alice_session_cipher.encrypt(looping_message);
+ SignalMessage alice_looping_message_copy = global_context.copy_signal_message(alice_looping_message);
+ uint8[] looping_plaintext = bob_session_cipher.decrypt_signal_message(alice_looping_message_copy);
+ fail_if_not_eq_uint8_arr(looping_message, looping_plaintext);
+ }
+ GLib.Test.message("Interaction complete: Alice -> Bob (looping)");
+
+ /* Looping Bob -> Alice */
+ for (int i = 0; i < 10; i++) {
+ uint8[] looping_message = create_looping_message(i);
+ CiphertextMessage bob_looping_message = bob_session_cipher.encrypt(looping_message);
+ SignalMessage bob_looping_message_copy = global_context.copy_signal_message(bob_looping_message);
+ uint8[] looping_plaintext = alice_session_cipher.decrypt_signal_message(bob_looping_message_copy);
+ fail_if_not_eq_uint8_arr(looping_message, looping_plaintext);
+ }
+ GLib.Test.message("Interaction complete: Bob -> Alice (looping)");
+
+ /* Generate a shuffled list of encrypted messages for later use */
+ Holder[] alice_ooo_plaintext = new Holder[10];
+ Holder[] alice_ooo_ciphertext = new Holder[10];
+ for (int i = 0; i < 10; i++) {
+ alice_ooo_plaintext[i] = new Holder(create_looping_message(i));
+ alice_ooo_ciphertext[i] = new Holder(alice_session_cipher.encrypt(alice_ooo_plaintext[i].data).serialized);
+ }
+
+ for (int i = 0; i < 10; i++) {
+ uint32 s = Random.next_int() % 10;
+ Holder tmp = alice_ooo_plaintext[s];
+ alice_ooo_plaintext[s] = alice_ooo_plaintext[i];
+ alice_ooo_plaintext[i] = tmp;
+ tmp = alice_ooo_ciphertext[s];
+ alice_ooo_ciphertext[s] = alice_ooo_ciphertext[i];
+ alice_ooo_ciphertext[i] = tmp;
+ }
+ GLib.Test.message("Shuffled Alice->Bob messages created");
+
+ /* Looping Alice -> Bob (repeated) */
+ for (int i = 0; i < 10; i++) {
+ uint8[] looping_message = create_looping_message(i);
+ CiphertextMessage alice_looping_message = alice_session_cipher.encrypt(looping_message);
+ SignalMessage alice_looping_message_copy = global_context.copy_signal_message(alice_looping_message);
+ uint8[] looping_plaintext = bob_session_cipher.decrypt_signal_message(alice_looping_message_copy);
+ fail_if_not_eq_uint8_arr(looping_message, looping_plaintext);
+ }
+ GLib.Test.message("Interaction complete: Alice -> Bob (looping, repeated)");
+
+ /* Looping Bob -> Alice (repeated) */
+ for (int i = 0; i < 10; i++) {
+ uint8[] looping_message = create_looping_message(i);
+ CiphertextMessage bob_looping_message = bob_session_cipher.encrypt(looping_message);
+ SignalMessage bob_looping_message_copy = global_context.copy_signal_message(bob_looping_message);
+ uint8[] looping_plaintext = alice_session_cipher.decrypt_signal_message(bob_looping_message_copy);
+ fail_if_not_eq_uint8_arr(looping_message, looping_plaintext);
+ }
+ GLib.Test.message("Interaction complete: Bob -> Alice (looping, repeated)");
+
+ /* Shuffled Alice -> Bob */
+ for (int i = 0; i < 10; i++) {
+ SignalMessage ooo_message_deserialized = global_context.deserialize_signal_message(alice_ooo_ciphertext[i].data);
+ uint8[] ooo_plaintext = bob_session_cipher.decrypt_signal_message(ooo_message_deserialized);
+ fail_if_not_eq_uint8_arr(alice_ooo_plaintext[i].data, ooo_plaintext);
+ }
+ GLib.Test.message("Interaction complete: Alice -> Bob (shuffled)");
+ }
+
+ uint8[] create_looping_message(int index) {
+ return (@"You can only desire based on what you know: $index").data;
+ }
+
+ /*
+ uint8[] create_looping_message_short(int index) {
+ return ("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. $index").data;
+ }
+ */
+}
+
+}
diff --git a/plugins/omemo/tests/signal/testcase.vala b/plugins/omemo/tests/signal/testcase.vala
new file mode 100644
index 00000000..59fcf193
--- /dev/null
+++ b/plugins/omemo/tests/signal/testcase.vala
@@ -0,0 +1,80 @@
+/* testcase.vala
+ *
+ * Copyright (C) 2009 Julien Peeters
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Author:
+ * Julien Peeters <contact@julienpeeters.fr>
+ */
+
+public abstract class Gee.TestCase : Object {
+
+ private GLib.TestSuite suite;
+ private Adaptor[] adaptors = new Adaptor[0];
+
+ public delegate void TestMethod ();
+
+ protected TestCase (string name) {
+ this.suite = new GLib.TestSuite (name);
+ }
+
+ public void add_test (string name, owned TestMethod test) {
+ var adaptor = new Adaptor (name, (owned)test, this);
+ this.adaptors += adaptor;
+
+ this.suite.add (new GLib.TestCase (adaptor.name,
+ adaptor.set_up,
+ adaptor.run,
+ adaptor.tear_down ));
+ }
+
+ public virtual void set_up () {
+ }
+
+ public virtual void tear_down () {
+ }
+
+ public GLib.TestSuite get_suite () {
+ return (owned) this.suite;
+ }
+
+ private class Adaptor {
+ [CCode (notify = false)]
+ public string name { get; private set; }
+ private TestMethod test;
+ private TestCase test_case;
+
+ public Adaptor (string name,
+ owned TestMethod test,
+ TestCase test_case) {
+ this.name = name;
+ this.test = (owned)test;
+ this.test_case = test_case;
+ }
+
+ public void set_up (void* fixture) {
+ this.test_case.set_up ();
+ }
+
+ public void run (void* fixture) {
+ this.test ();
+ }
+
+ public void tear_down (void* fixture) {
+ this.test_case.tear_down ();
+ }
+ }
+}