AnemoneLabs / unmessage

Privacy enhanced instant messenger
GNU General Public License v3.0
42 stars 7 forks source link

Add an initialization step to conversation requests #87

Open felipedau opened 6 years ago

felipedau commented 6 years ago

One of the assumptions we made for unMessage's adversary is:

7. An adversary is unable to compromise a peer's private identity key
   to decrypt any of the requests they received/accepted.

The reason for this assumption is the following attack:

- All the peer information required to send/receive conversation
  requests is encrypted with a shared secret derived from a
  Diffie-Hellman key exchange using Bob's request key and Alice's
  identity key. The request key is ephemeral and is disposed once the
  conversation is established, but the identity key is not. For that
  reason, the attack mentioned in item `7` is possible to be performed
  but not prevented by unMessage.

As identity keys are supposed to be long-term and used to establish conversations, we cannot prevent an attacker from using them to access request information, but we can reduce such information. The following is a solution @rxcomm and I have been discussing.

(In the following examples I omitted both the IV hash and the keyed hash for simplicity.)

Roles:

Currently, request packets provide all information Alice needs to establish a conversation (name, address, identity/handshake/ratchet keys), encrypted with dh(req_b, id_a). Example:

# b_id_address and b_id keypair are generated when Bob creates his peer
# b_req, b_hs and b_ratch keypairs are generated for this request
# a_id_pub_key is acquired via another channel

shared_req_key = dh(b_req_priv_key, a_id_pub_key)

enc_b_hs_pkt = symm_enc(shared_req_key,
   b_id_address +
   b_id_pub_key +
   b_hs_pub_key +
   b_ratch_pub_key)

req_pkt = b_req_pub_key + enc_b_hs_pkt

If an attacker decrypts that (by compromising Alice's identity private key), they have access to all that info and become aware that Bob sent such request.

Reply packets provide the remaining information Bob needs to establish a conversation. That key is encrypted with the same shared_req_key used by the request packet. Example:

# a_id_address and a_id_pub_key are generated when Alice creates her peer
# the a_hs keypair is generated for this request

shared_req_key = dh(a_id_priv_key, b_req_pub_key)

enc_a_hs_pub_key = symm_enc(shared_req_key, a_hs_pub_key)

reply_pkt = enc_a_hs_pub_key + pyaxo_pkt

That means attackers can also identify which requests Alice accepted.

(It would not be possible to say for sure if that conversation really existed because anyone is able to create a shared secret with an identity key and make fake packets using it. Anyway, an attacker in possession of Alice's identity private key could just assume all conversations existed.)

What we can do is adding another step by sending a request for a request key - say, an "initialization" step. That way, the request packet would not contain that contact info right away, but just Bob's request public key. Example:

shared_init_key = dh(b_init_priv_key, a_id_pub_key)

enc_b_init_pkt = symm_enc(shared_init_key, b_req_pub_key)

init_pkt = b_init_pub_key + enc_b_init_pkt

(We can include more information in the initialization packet if needed. We just need to make sure that Alice can derive the shared secret that decrypts it. Would it be possible to require proof-of-work (PoW) in this process?)

When Alice receives such packet and is able to generate a shared key that decrypts that inner initialization packet, then it was meant for her and she is aware that someone wants to establish a conversation. She would reply with a request key so that Bob is able provide his real info and a handshake key for the 3DH. Example:

shared_init_key = dh(a_id_priv_key, b_init_pub_key)

shared_req_key = dh(a_req_priv_key, b_req_pub_key)

enc_a_req_key = symm_enc(shared_init_key, a_req_pub_key)

enc_a_hs_packet = symm_enc(shared_req_key, a_hs_pub_key)

a_req_packet = enc_a_req_key + enc_a_hs_packet

(I think it is also important to make Alice to do the least work as possible, because this step can be done by anyone and can also be target for attacks. In this example, her handshake packet would only contain the encrypted handshake key, but we could also include information for the PoW that Bob uses for the actual request.)

Bob would receive Alice's request packet and would finally be able to compose his inner handshake packet. Example:

shared_req_key = dh(b_req_priv_key, a_req_pub_key)

enc_b_hs_packet = symm_enc(shared_req_key,
   b_id_address +
   b_id_pub_key +
   b_hs_pub_key +
   b_ratchet_pub_key)

b_req_packet = enc_b_hs_packet

(At this point, Bob has enough information to create a Double Ratchet conversation with Alice. He could even send messages already and Alice would read once she accepts the request.)

Once Alice receives Bob's request packet, she is able to finally see who Bob really is and decide whether to accept or deny the request. With that packet she would have enough info to create a Double Ratchet conversation as well and can send/receive messages.

We would still need to decide if we are going to initialize the states before Alice's decision on accepting/denying, but that is just a detail.

Unfortunately we would have an additional step in the request process, but I think that if we want to have future secrecy when/wherever is possible then it seems like a good improvement.

With these changes, all the identifying information is encrypted with keys that are deleted once the request process is done and if in the future an identity key is compromised, all an attacker obtains are public request keys and the additional information in the initialization packet.