We would like to give users the ability to send anonymous messages:
This is captured by the following user stories:
As a user
I want to able to send anonymous messages in public chats
So that I can communicate without disclosing my identity
And another that is not so obvious is:
As a user
I want to see anonymous messages sent by me on a different device as originating from myself
So that I can easily understand the interactions when using a different device
Basically anonymous messages should be anonymous for everyone, but for the sender (the owner of a given key pair).
Here I describe two potential solutions to this issue.
Solution 1
The first solution that came to mind was to generate a key pair using a subpath of the chat key of the user (authorKeyPair1
), for example:
Our current path for the chat key is m / 43' / 60' / 1581' / 0' / 0
.
When sending an anonymous message we derive the path from m / 43' / 60' / 1581' / 0' / 0' / x
and sign the message using that key (randomKeyPair1
).
x
is the added to the message, in the payload, to indicate this is an anonymous message
https://github.com/status-im/specs/blob/master/docs/stable/6-payloads.md#payload
message ChatMessage {
...
uint anonymous_derivation_path = 20;`
...
}
Upon receiving the message, each client will understand that this is meant to be an anoymous message by the presence of anonymous_derivation_path
.
It will then extract the public key of the sender from the signature (as with any other message), which is just a randomly generated key.
To check that this message is not originating from a different device, it will then take the key specified at the derivation path:
m / 43' / 60' / 1581' / 0' / 0' / x
and check if it matches the random pk generated by the sender. If it matches, we can tell that this is an anonymous message originating from one of our devices, otherwise is just an anonymous message originating from someone else.
The important things we are validating here are:
- The content of the message has been authored by
randomKeyPair1
and not tempered with (this is given by the already existing protocol, thanks to the signature of the payload) - Through checking the derivation, we can tell that the sender of the message has access to
authorKeyPair1
As a further protection:
uint anonymous_derivation_path = 20;
Can actually be encrypted using authorKeyPair1.PublicKey
, so that the actual path is not disclosed to everyone, and it would be matched if it can be decrypted.
Attack vectors
One potential attack vector that comes to mind is:
What happens if an attacker take an existing anonymous_derivation_path
and associate it with a different message (sort of a replay attack)?
The public key won’t match so it will be considered an anonymous message by everyone, the message ID will be different.
Because the new field is inside the payload, which is signed, we can rest assured that any modification will result in either the signature not being valid or a different public key and messageID would be derived.
It’s important that this new field is put inside the payload
and not on the wrapper
, otherwise we would be exposed to ID clashes.
Thanks to @gravityblast for the help and suggestions.
Backward compatibility
Current clients will see a random name instead of Anonymous
in the UI, but will be able to read and receive those messages.
Solution 2
Simplifying this further, instead of using the derivation path, why not instead put a piece of information that we can use to reveal the original author (authorKeyPair1
)?
Here, we would generate any random key pair, and sign the message with it. In the payload we include a new field:
message ChatMessage {
...
bytes anonymous_signature = 20;`
...
}
This is generated:
signedPublicKey = Signature(authorKeyPair1.PrivateKey, []byte(randomKeyPair1.PublicKey))
chatMessage.anonymous_signature = Encryption(authorKeyPair1.PublicKey, signedPublicKey)
On receiving the message:
- a client will derive the public key of the sender (
randomKeyPair1.PublicKey
) - It will try to decrypt
anonmous_signature
. If not able to do so, it will be marking as an anonymous message not sent by us. otherwise it will continue. - It will then extract the public key from the inner signature
signedPublicKey
and check if it matches its own private key. If not, the message will be discarded, otherwise is an anonymous message sent by us.
At this point we know that:
- The message was signed by
randomKeyPair1
and it wasn’t modified (that given from the existing protocol) authorKeyPair1
has signedrandomKeyPair1
, which means that it showed the intention (does not prove ownership though), to send a message using that key pair.
Attack vectors
What happens if we take an existing anonymous_signature
and reply it in a different message?
The public key extracted from the payload won’t match the inner signature in step 3
, and the message will be discarded. This is because we already know was intended for us, as we can decrypt the field, but if the key does not match, it means it had been tampered with.
Any other tampering of the content will give similar scenarios.
Backward compatibility
Current clients will see a random name instead of Anonymous
in the UI, but will be able to read and receive those messages.
Other approaches considered
A more naive approach is to just mark the message as anonymous with a boolean flag and send a separate message to your device disclosing the fact that you sent the message. This is operationally harder and might lead to inconsistencies (have to send two messages, what happens if you don’t receive one).
Another suggested by @ricardo3
- Allow users to send messages in public chats as “anonymous”: use a keyless method of signature (use the hash of the message as r,s of signature) - if r and s are a exactly the same, and/or are hash of the message, the message can be considered “signed by anonymous”
This is very neat and the most elegant method (no extra piece of data is needed to tell is an anonymous message), but requires more fundamental changes to the protocol and is too generic for our use case (we only want user to be able to send anonymous messages in public chats, narrowing down the options limits the potential for attacks/issues), and we still need to solve the multiple devices problem, so more info will have to be propagated, likely in the payload following a method as described above.
Of all the 2 solutions proposed above, I think 1 looks more elegant, but it’s a bit more complex and not sure it’s justified, 2 seems to serve our purpose just fine, although the geek inside of me would love to code 1, 2 seems to be the safest choice.
Opinions are welcome.
Thanks