Re-adding an existing contact currently throws a ContactExistsException. But re-adding a contact may be necessary if transport properties get out of sync, making it impossible to connect via BTP. If we re-add a contact we should keep any existing private messages and group subscriptions.
If one contact uses the same identity as last time and the other doesn't, they'll disagree about whether it's a re-add, so we can't re-derive the same transport keys. Can we reuse the contact ID (if any), import the new transport properties, delete the old transport keys (if any), and create new transport keys?
Replacing the transport properties and transport keys should be easy enough, but different clients may want to handle re-added contacts in different ways. For example the private messaging client might want to keep any messages exchanged before the contact was re-added, whereas the forum sharing client might want to reset the visibility of forums, reset sessions to the start state, and mark pending invitations unavailable.
Perhaps we should create a new ContactManager hook for re-adding a contact?
Why is this? The contact exists already, right? What has changed that would justify resetting things in the background?
We don't know whether the contact removed us before re-adding us, so the contact may be starting from a blank slate. So the idea is that we reset any state that might be inconsistent with the contact's state.
We might also want to consider how this ticket relates to verifying contacts (#513).
We don't know whether the contact removed us before re-adding us, so the contact may be starting from a blank slate. So the idea is that we reset any state that might be inconsistent with the contact's state.
Ah thanks. That had escaped me, but does make total sense!
Can't the key exchange protocol know or find out whether both peers are still contacts and only run the re-added hook if one of them is not?
But how does that work for introductions?
I guess the peer that had deleted its contact, will then get the entire private message history synced back, right?
Can't the key exchange protocol know or find out whether both peers are still contacts and only run the re-added hook if one of them is not?
Yup, we could add an extra step to the contact exchange protocol and avoid resetting client state if both peers already have each other as contacts.
But how does that work for introductions?
That's a tricky one. We don't want the introducer to know whether the introducees are already contacts, so if we add an extra step where the introducees decide whether to reset client state, the new step should be encrypted end-to-end between the introducees. But that would make an already complex protocol even more complex. I'll give this some more thought tonight.
I guess the peer that had deleted its contact, will then get the entire private message history synced back, right?
Not at the moment, no. The peer that didn't delete her contact (let's call her Alice) still has a record of which messages the other peer (Bob) has seen, so Alice won't try to sync any messages back to Bob. We could easily reset that record when Alice re-adds Bob, but the messages aren't signed, so Bob would have to trust Alice to send him an unmodified copy of the conversation! Again, this is fairly easy to solve by adding signatures and dependencies between messages, so the worst Alice can do is to send a subset of the conversation. But then there's another hurdle: Bob has to validate and deliver the messages received from Alice, some of which were originally local messages. That would mean substantial changes to the delivery hook, to validate either a local or remote signature, update the message metadata and group count, and fire events accordingly. Quite a big task.
These problems are similar to the ones that come up when we think about multi-device support. Maybe we should take a step back and see if there are some concepts that could be useful in both cases. For example, should we think differently about how local messages are added to the DB, so that they share more of the code path with remote messages?
Regarding introducees letting each other know whether they're already contacts without letting the introducer know: rather than exchanging encrypted messages, I guess we could modify the function that derives nonces from the shared secret, which the introducees sign to prove ownership of their identity keys. Each introducee would derive four nonces instead of two:
To be signed by Alice if she doesn't have Bob as a contact
To be signed by Alice if she does have Bob as a contact
To be signed by Bob if he doesn't have Alice as a contact
To be signed by Bob if he does have Alice as a contact
However, this is really mixing two concerns: can the other introducee prove that they own the claimed identity key, and do they claim that we're already contacts?
We'll need to add a round-trip to the introduction protocol to allow the introducees to signal to each other whether they're already contacts without letting the introducer know. The approach I sketched above, where the introducees pick different nonces depending on whether they're already contacts, isn't secure because the introducees haven't verified ownership of the received identity keys at the point where the nonces are chosen. So the introducer could present an identity she doesn't own in order to learn whether an introducee has that identity as a contact.
If we combine this with the improved key binding described in #902 (closed), the new protocol looks like this:
Introducer sends request message to introducee 1 containing introducee 2's nickname N2 and identity public key IK2
Introducee 1 sends accept message to introducee 2 via introducer containing introducee 1's ephemeral public key EK1 and timestamp T1
Introducee 1 receives introducee 2's accept message, derives initial shared secret S from DH(EK1, EK2) and MIN(T1, T2)
Introducee 1 derives MAC key MK1 and cipher key CK1 from S and starts key rotation
Introducee 1 sends verify message to introducee 2 via introducer containing MAC(MK1, [N1, IK1, EK1, T1, N2, IK2, EK2, T2]) and SIGN(IK1, [EK1, T1, EK2, T2])
Introducee 1 receives introducee 2's verify message, sends confidential message to introducee 2 via introducer containing introducee 1's transport properties and existing contact flag: AUTH_ENC(CK1, [TP1, F1])
Introducee 1 receives introducee 2's confidential message, adds introducee 2 to contact list
Similarly, we'll need to add a round-trip to the contact exchange protocol so the parties can verify each other's identities before revealing whether they're already contacts. This prevents either of the parties from presenting an identity she doesn't own in order to learn whether the other party has that identity as a contact.
Combined with the improved key binding described in #901, the new protocol looks like this:
Face-to-face key exchange protocol exchanges ephemeral public keys and timestamps: EK1, T1, EK2, T2
Face-to-face key exchange protocol proves to each party that the received ephemeral public key and timestamp were sent by the other party
Party 1 derives initial shared secret S from DH(EK1, EK2) and MIN(T1, T2)
All subsequent messages are encrypted and authenticated with symmetric keys derived from S
Party 1 sends identity message to party 2 containing party 1's nickname N1 and identity public key IK1
Party 1 receives party 2's identity message, sends verify message containing N2, IK2, SIGN(IK1, [EK1, T1, EK2, T2])
Party 1 receives party 2's verify message, sends confidential message containing party 1's transport properties TP1 and existing contact flag F1
Party 1 receives party 2's confidential message, adds party 2 to contact list
Unlike the introduction protocol, we don't need to calculate a separate MAC over [N1, IK1, EK1, T1, N2, IK2, EK2, T2], or separately encrypt and authenticate [TP1, F1], because the messages containing those values are already encrypted and authenticated with symmetric keys derived from the ephemeral shared secret.
Another idea discussed recently in the architecture channel is to introduce "sync epochs" to the sync protocol.
Let's say we use a random unique id to identify the sync epoch.
Alice resets her state and generates a new epoch id that bob's never seen before.
So bob immediately knows that alice has reset her state.
Now if bob receives a stream that alice sent in the old epoch,
he needs to know that it should be discarded.
So he needs to keep every old epoch id that alice has used, for at least one rotation period.
To cover the possibility of alice bombarding bob with an endless series of epoch records,
we can put some limit on the number of epoch ids he'll try to remember.
Bob's epoch record contains his own epoch id and the latest epoch id received from alice.
If alice has reset, and bob doesn't know that yet, alice will see that bob hasn't received her new epoch id yet,
so she can ignore the rest of the session.
When bob eventually receives alice's new epoch id, he'll know that she's reset,
so he'll reset and start echoing her new id, at which point she'll know that he's also reset
and she can start processing his messages/acks/etc.