MixinNetwork / libsignal_protocol_dart

Signal Protocol library for Dart/Flutter
https://pub.dev/packages/libsignal_protocol_dart
GNU General Public License v3.0
159 stars 42 forks source link

RangeError - Group session with multiple participants #47

Closed McCrafterIV closed 1 year ago

McCrafterIV commented 3 years ago

Can you provide and example with 3 group participants all sending a message? I can't get it to work for me. This is my code right now:

  final groupSenderAddress = SignalProtocolAddress('+00000000001', 1);
  final groupSender = SenderKeyName('Private group', groupSenderAddress);
  final aliceStore = InMemorySenderKeyStore();
  final bobStore = InMemorySenderKeyStore();
  final deanStore = InMemorySenderKeyStore();

  final aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
  final bobGroupSessionBuilder = GroupSessionBuilder(bobStore);
  final deanGroupSessionBuilder = GroupSessionBuilder(deanStore);

  final aliceGroupCipher = GroupCipher(aliceStore, groupSender);
  final bobGroupCipher = GroupCipher(bobStore, groupSender);
  final deanGroupCipher = GroupCipher(deanStore, groupSender);

  await bobGroupSessionBuilder.process(
      groupSender, await aliceGroupSessionBuilder.create(groupSender));
  await deanGroupSessionBuilder.process(
      groupSender, await bobGroupSessionBuilder.create(groupSender));

  final ciphertextFromAlice = await aliceGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Alice')));

  final ciphertextFromBob = await bobGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Bob')));

  final ciphertextFromDean = await deanGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Dean')));

  print(utf8.decode(await bobGroupCipher.decrypt(ciphertextFromAlice)));
  print(utf8.decode(await deanGroupCipher.decrypt(ciphertextFromBob)));
  print(utf8.decode(await aliceGroupCipher.decrypt(ciphertextFromDean)));

Which throws the following exception:

RangeError (index): Index out of range: no indices are valid: 0
#0      Uint8List.[] (dart:typed_data-patch/typed_data_patch.dart:2223:7)
#1      load3 (package:ed25519_edwards/src/edwards25519.dart:95:12)
#2      ScMulAdd (package:ed25519_edwards/src/edwards25519.dart:1273:22)
#3      sign (package:libsignal_protocol_dart/src/ecc/ed25519.dart:81:3)
#4      Curve.calculateSignature (package:libsignal_protocol_dart/src/ecc/curve.dart:125:14)
#5      SenderKeyMessage._getSignature (package:libsignal_protocol_dart/src/protocol/sender_key_message.dart:79:20)
#6      new SenderKeyMessage (package:libsignal_protocol_dart/src/protocol/sender_key_message.dart:27:9)
#7      GroupCipher.encrypt (package:libsignal_protocol_dart/src/groups/group_cipher.dart:29:32)
<asynchronous suspension>
#8      main (package:project/group.dart:34:29)
<asynchronous suspension>
McCrafterIV commented 3 years ago

After stumbling on the issue #45 I changed my code a little. I think this approach is much more correct, but still not working. I now get the following exception:

InvalidMessageException - No key for: 1446805559
#0      GroupCipher.decryptWithCallback (package:libsignal_protocol_dart/src/groups/group_cipher.dart:69:7)
<asynchronous suspension>
#1      main (package:project/group.dart:57:21)
<asynchronous suspension>

Process finished with exit code 255

This is my code now:

  final bobGroupSenderAddress = SignalProtocolAddress('+00000000001', 1);
  final aliceGroupSenderAddress = SignalProtocolAddress('+00000000002', 1);
  final deanGroupSenderAddress = SignalProtocolAddress('+00000000003', 1);
  final bobGroupSender = SenderKeyName('Private group', bobGroupSenderAddress);
  final aliceGroupSender =
      SenderKeyName('Private group', aliceGroupSenderAddress);
  final deanGroupSender =
      SenderKeyName('Private group', deanGroupSenderAddress);
  final aliceStore = InMemorySenderKeyStore();
  final bobStore = InMemorySenderKeyStore();
  final deanStore = InMemorySenderKeyStore();

  final aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
  final bobGroupSessionBuilder = GroupSessionBuilder(bobStore);
  final deanGroupSessionBuilder = GroupSessionBuilder(deanStore);

  final aliceGroupCipher = GroupCipher(aliceStore, aliceGroupSender);
  final bobGroupCipher = GroupCipher(bobStore, bobGroupSender);
  final deanGroupCipher = GroupCipher(deanStore, deanGroupSender);

  final aliceDistributionMessage =
      await aliceGroupSessionBuilder.create(aliceGroupSender);
  final bobDistributionMessage =
      await bobGroupSessionBuilder.create(bobGroupSender);
  final deanDistributionMessage =
      await deanGroupSessionBuilder.create(deanGroupSender);

  await bobGroupSessionBuilder.process(
      aliceGroupSender, aliceDistributionMessage);
  await bobGroupSessionBuilder.process(
      deanGroupSender, deanDistributionMessage);

  await deanGroupSessionBuilder.process(bobGroupSender, bobDistributionMessage);
  await deanGroupSessionBuilder.process(
      aliceGroupSender, aliceDistributionMessage);

  await aliceGroupSessionBuilder.process(
      bobGroupSender, bobDistributionMessage);
  await aliceGroupSessionBuilder.process(
      deanGroupSender, deanDistributionMessage);

  final ciphertextFromAlice = await aliceGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Alice')));

  final ciphertextFromBob = await bobGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Bob')));

  final ciphertextFromDean = await deanGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Dean')));

  print(utf8.decode(await bobGroupCipher.decrypt(ciphertextFromAlice)));
  print(utf8.decode(await deanGroupCipher.decrypt(ciphertextFromBob)));
  print(utf8.decode(await aliceGroupCipher.decrypt(ciphertextFromDean)));
crossle commented 2 years ago

You must use same sender for encrypt and decrypt.

 final bobGroupSenderAddress = SignalProtocolAddress('+00000000001', 1);
  final aliceGroupSenderAddress = SignalProtocolAddress('+00000000002', 1);
  final deanGroupSenderAddress = SignalProtocolAddress('+00000000003', 1);
  final bobGroupSender = SenderKeyName('Private group', bobGroupSenderAddress);
  final aliceGroupSender =
      SenderKeyName('Private group', aliceGroupSenderAddress);
  final deanGroupSender =
      SenderKeyName('Private group', deanGroupSenderAddress);
  final aliceStore = InMemorySenderKeyStore();
  final bobStore = InMemorySenderKeyStore();
  final deanStore = InMemorySenderKeyStore();

  final aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
  final bobGroupSessionBuilder = GroupSessionBuilder(bobStore);
  final deanGroupSessionBuilder = GroupSessionBuilder(deanStore);

  final aliceGroupCipher = GroupCipher(aliceStore, aliceGroupSender);
  final bobGroupCipher = GroupCipher(bobStore, aliceGroupSender); // use alice as sender for decrypt
  final deanGroupCipher = GroupCipher(deanStore, deanGroupSender);

  final aliceDistributionMessage =
      await aliceGroupSessionBuilder.create(aliceGroupSender);
  final bobDistributionMessage =
      await bobGroupSessionBuilder.create(bobGroupSender);
  final deanDistributionMessage =
      await deanGroupSessionBuilder.create(deanGroupSender);

  await bobGroupSessionBuilder.process(
      aliceGroupSender, aliceDistributionMessage);
  await bobGroupSessionBuilder.process(
      deanGroupSender, deanDistributionMessage);

  await deanGroupSessionBuilder.process(bobGroupSender, bobDistributionMessage);
  await deanGroupSessionBuilder.process(
      aliceGroupSender, aliceDistributionMessage);

  await aliceGroupSessionBuilder.process(
      bobGroupSender, bobDistributionMessage);
  await aliceGroupSessionBuilder.process(
      deanGroupSender, deanDistributionMessage);

  final ciphertextFromAlice = await aliceGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Alice')));

  // final ciphertextFromBob = await bobGroupCipher
  //     .encrypt(Uint8List.fromList(utf8.encode('Hello from Bob')));

  final ciphertextFromDean = await deanGroupCipher
      .encrypt(Uint8List.fromList(utf8.encode('Hello from Dean')));

  print(utf8.decode(await bobGroupCipher.decrypt(ciphertextFromAlice)));
  // print(utf8.decode(await deanGroupCipher.decrypt(ciphertextFromBob)));
  // print(utf8.decode(await aliceGroupCipher.decrypt(ciphertextFromDean)));
McCrafterIV commented 2 years ago

You must use same sender for encrypt and decrypt.

Thanks for the example. I now understood how to use it. This is my now working code from above with 3 group members all able to chat with one another:

Click to expand (to keep this post a little bit shorter) ```dart final bobGroupSenderAddress = SignalProtocolAddress('+00000000001', 1); final aliceGroupSenderAddress = SignalProtocolAddress('+00000000002', 1); final deanGroupSenderAddress = SignalProtocolAddress('+00000000003', 1); final bobGroupSender = SenderKeyName('Private group', bobGroupSenderAddress); final aliceGroupSender = SenderKeyName('Private group', aliceGroupSenderAddress); final deanGroupSender = SenderKeyName('Private group', deanGroupSenderAddress); final aliceStore = InMemorySenderKeyStore(); final bobStore = InMemorySenderKeyStore(); final deanStore = InMemorySenderKeyStore(); final aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore); final bobGroupSessionBuilder = GroupSessionBuilder(bobStore); final deanGroupSessionBuilder = GroupSessionBuilder(deanStore); final aliceDistributionMessage = await aliceGroupSessionBuilder.create(aliceGroupSender); final bobDistributionMessage = await bobGroupSessionBuilder.create(bobGroupSender); final deanDistributionMessage = await deanGroupSessionBuilder.create(deanGroupSender); await bobGroupSessionBuilder.process( aliceGroupSender, aliceDistributionMessage); await bobGroupSessionBuilder.process( deanGroupSender, deanDistributionMessage); await deanGroupSessionBuilder.process(bobGroupSender, bobDistributionMessage); await deanGroupSessionBuilder.process( aliceGroupSender, aliceDistributionMessage); await aliceGroupSessionBuilder.process( bobGroupSender, bobDistributionMessage); await aliceGroupSessionBuilder.process( deanGroupSender, deanDistributionMessage); final ciphertextFromAlice = await GroupCipher(aliceStore, aliceGroupSender) .encrypt(Uint8List.fromList(utf8.encode('Hello from Alice'))); final ciphertextFromBob = await GroupCipher(bobStore, bobGroupSender) .encrypt(Uint8List.fromList(utf8.encode('Hello from Bob'))); final ciphertextFromDean = await GroupCipher(deanStore, deanGroupSender) .encrypt(Uint8List.fromList(utf8.encode('Hello from Dean'))); print(utf8.decode(await GroupCipher(bobStore, aliceGroupSender) .decrypt(ciphertextFromAlice))); print(utf8.decode( await GroupCipher(deanStore, bobGroupSender).decrypt(ciphertextFromBob))); print(utf8.decode(await GroupCipher(aliceStore, deanGroupSender) .decrypt(ciphertextFromDean))); ```

But I've another question. When you take e.g. Signal Messenger, this GroupSession, GroupCipher and all would resemble the legacy groups in Signal Messenger, am I right? But what is the advantage from this system over just using a "regular" SessionCipher and SessionBuilder? I also haven't found a way yet to "kick someone (e.g. Dean) out" of a GroupSession. I tried to just create a new KeyDistributionMessages, but Dean was still able to decrypt the message (probably because it uses the old session instead). How would I achieve that functionallity? This is what I tried already:

  // to kick Dean out
  final sentAliceDistributionMessage2 =
      await aliceSessionBuilder.create(groupSenderAlice);
  bobSessionBuilder.process(groupSenderAlice, sentAliceDistributionMessage2);

  final sentBobDistributionMessage2 =
      await bobSessionBuilder.create(groupSenderBob);
  aliceSessionBuilder.process(groupSenderBob, sentBobDistributionMessage2);

  var ciphertextFromAlice2 = await GroupCipher(aliceStore, groupSenderAlice)
      .encrypt(Uint8List.fromList(utf8.encode('Hello Mixin')));
  var plaintextFromAliceDecryptedByBob2 =
      await GroupCipher(bobStore, groupSenderAlice)
          .decrypt(ciphertextFromAlice2);
  var plaintextFromAliceDecryptedByDean2 =
      await GroupCipher(deanStore, groupSenderAlice)
          .decrypt(ciphertextFromAlice2); // should fail

  print("Alice msg to Bob : ${plaintextFromAliceDecryptedByBob2}");
  print("Alice msg to Dean : ${plaintextFromAliceDecryptedByDean2}");
McCrafterIV commented 2 years ago

@crossle are you able to help me?

Tougee commented 2 years ago

final sentAliceDistributionMessage2 = await aliceSessionBuilder.create(groupSenderAlice);

This will just use the cache instead of creating a new KeyDistributionMessages.

For more info about group messaging you can read this paper A Security Analysis of the Signal Protocol’s Group Messaging Capabilities in Comparison to Direct Messaging

McCrafterIV commented 2 years ago

This will just use the cache instead of creating a new KeyDistributionMessages.

I thought of something like that, so my problem remains. How can I start a new session or continue the old one without Dean in this example?

Thanks for the document, I'll look into it. I han't had the time to read the full paper yet but as far as I scanned it, I couldn't find an answer to my question. If you've a deeper understanding of the implementation @Tougee maybe you could explain to me in a few sentences where the difference between the group implementation and just using a "normal" session for every user lies?

Tougee commented 2 years ago

In this demo you can just create new SenderKeyStore for remaining members, and Dean can not decrypt later messages.

 // to kick Dean out
    aliceStore = InMemorySenderKeyStore();
    bobStore = InMemorySenderKeyStore();
    aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
    bobGroupSessionBuilder = GroupSessionBuilder(bobStore);
    final sentAliceDistributionMessage2 = await aliceGroupSessionBuilder.create(aliceGroupSender);
    await bobGroupSessionBuilder.process(aliceGroupSender, sentAliceDistributionMessage2);

    final sentBobDistributionMessage2 = await bobGroupSessionBuilder.create(bobGroupSender);
    await aliceGroupSessionBuilder.process(bobGroupSender, sentBobDistributionMessage2);
Tougee commented 2 years ago

Each user in a group being tied to a unique group-ID, and using the direct messaging protocol to communicate with each host under the hood.

Group messaging provider what

McCrafterIV commented 2 years ago

In this demo you can just create new SenderKeyStore for remaining members, and Dean can not decrypt later messages.

 // to kick Dean out
    aliceStore = InMemorySenderKeyStore();
    bobStore = InMemorySenderKeyStore();
    aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
    bobGroupSessionBuilder = GroupSessionBuilder(bobStore);
    final sentAliceDistributionMessage2 = await aliceGroupSessionBuilder.create(aliceGroupSender);
    await bobGroupSessionBuilder.process(aliceGroupSender, sentAliceDistributionMessage2);

    final sentBobDistributionMessage2 = await bobGroupSessionBuilder.create(bobGroupSender);
    await aliceGroupSessionBuilder.process(bobGroupSender, sentBobDistributionMessage2);

Ok thank you. But this would mean that I create a new Store for every group, am I right?

McCrafterIV commented 2 years ago

You wrote about "contractible membership" but when as you described above, a new group session needs to be created if one leaves the group, isn't that the opposite @Tougee?

Tougee commented 2 years ago

Ok thank you. But this would mean that I create a new Store for every group, am I right?

Create a new _store item inside InMemorySenderKeyStore is OK.

add a new function like below for InMemorySenderKeyStore,

Future<void> deleteSenderKey(SenderKeyName senderKeyName) async {
    _store.remove(senderKeyName);
  }

then call this delete method when someone exit group,

// to kick Dean out
await aliceStore.deleteSenderKey(aliceGroupSender);
await bobStore.deleteSenderKey(bobGroupSender);
aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
bobGroupSessionBuilder = GroupSessionBuilder(bobStore);

and when next time call loadSenderKey it can create a new SenderKeyRecord for this group.

Tougee commented 2 years ago

You wrote about "contractible membership" but when as you described above, a new group session needs to be created if one leaves the group, isn't that the opposite @Tougee?

What I wrote about the concept of group chat was pasted from that paper, just a perspective , and I think the design of this is not associated with this project, you need to do research by yourself.

McCrafterIV commented 2 years ago

Ok thank you. But this would mean that I create a new Store for every group, am I right?

Create a new _store item inside InMemorySenderKeyStore is OK.

add a new function like below for InMemorySenderKeyStore,

Future<void> deleteSenderKey(SenderKeyName senderKeyName) async {
    _store.remove(senderKeyName);
  }

then call this delete method when someone exit group,

// to kick Dean out
await aliceStore.deleteSenderKey(aliceGroupSender);
await bobStore.deleteSenderKey(bobGroupSender);
aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
bobGroupSessionBuilder = GroupSessionBuilder(bobStore);

and when next time call loadSenderKey it can create a new SenderKeyRecord for this group.

great thank you, I'll try this.

Ahmadre commented 2 years ago

Ok thank you. But this would mean that I create a new Store for every group, am I right?

Create a new _store item inside InMemorySenderKeyStore is OK. add a new function like below for InMemorySenderKeyStore,

Future<void> deleteSenderKey(SenderKeyName senderKeyName) async {
    _store.remove(senderKeyName);
  }

then call this delete method when someone exit group,

// to kick Dean out
await aliceStore.deleteSenderKey(aliceGroupSender);
await bobStore.deleteSenderKey(bobGroupSender);
aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
bobGroupSessionBuilder = GroupSessionBuilder(bobStore);

and when next time call loadSenderKey it can create a new SenderKeyRecord for this group.

great thank you, I'll try this.

how did you solve this at the end? Would be interested in this also.

McCrafterIV commented 1 year ago

Ok thank you. But this would mean that I create a new Store for every group, am I right?

Create a new _store item inside InMemorySenderKeyStore is OK. add a new function like below for InMemorySenderKeyStore,

Future<void> deleteSenderKey(SenderKeyName senderKeyName) async {
    _store.remove(senderKeyName);
  }

then call this delete method when someone exit group,

// to kick Dean out
await aliceStore.deleteSenderKey(aliceGroupSender);
await bobStore.deleteSenderKey(bobGroupSender);
aliceGroupSessionBuilder = GroupSessionBuilder(aliceStore);
bobGroupSessionBuilder = GroupSessionBuilder(bobStore);

and when next time call loadSenderKey it can create a new SenderKeyRecord for this group.

great thank you, I'll try this.

how did you solve this at the end? Would be interested in this also.

I haven't implemented the solution. But if I would try to build a group chat using this system, the explained steps would be my starting point.