client: P2P Encrypted Messaging#18
Conversation
This commit implements encrypted p2p messaging. MessagePeer and HandlePeerMessage functions are added to the Client. Peers use symmetric encryption keys derived via Elliptic Curve Diffie- Hellman (ECDH) to communicate privately. When a client communicates with another Client for the first time, it sends a signed HandshakeRequest containing an ephemeral public key. The counterparty responds with its own ephemeral public key, allowing both parties to derive the shared key. To avoid handshake race conditions and allow key rotation/expiry without interrupting communication, the client stores up to 5 keys per peer. The latest key is used for outbound encryption, while previous keys remain available for decrypting inbound messages during rotation. Each encrypted message is tagged with a key ID so the receiver can select the right key. Keys expire after 1 hour.
| unsignedReq := proto.Clone(hsResp).(*clientpb.HandshakeResponse) | ||
| unsignedReq.Signature = nil | ||
| unsignedReqPayload, err := proto.Marshal(unsignedReq) |
There was a problem hiding this comment.
This is cloning the message just to unset the signature field. It's more expensive than reconstructing the unsigned response.
unsignedResp := &clientpb.HandshakeResponse{
Nonce: hsResp.GetNonce(),
PublicKey: hsResp.GetPublicKey(),
// Leave Signature unset (defaults to nil)
}There was a problem hiding this comment.
It's true, but cloning allows this to keep working even if there are unknown fields (the schema changes in a newer version but someone is still running an older version). The cost of this cloning is minor compared to signature verification anyways.
| unsignedReq := proto.Clone(hsReq).(*clientpb.HandshakeRequest) | ||
| unsignedReq.Signature = nil | ||
| unsignedReqPayload, err := proto.Marshal(unsignedReq) |
There was a problem hiding this comment.
Same cloning issue here, refer to the comment above.
| return fmt.Errorf("client host not initialized") | ||
| } | ||
|
|
||
| c.host.SetStreamHandler(protocols.TatankaRelayMessageProtocol, func(s network.Stream) { |
There was a problem hiding this comment.
I think the inlined handler here can be refactored into handleTatankaRelayMessage
| return []byte("tatanka-p2p-v1|" + string(a) + "|" + string(b)) | ||
| } | ||
| return []byte("tatanka-p2p-v1|" + string(b) + "|" + string(a)) |
There was a problem hiding this comment.
Let's make the protocol version prefix tatanka-p2p-v1 and separator | constants.
This commit implements encrypted p2p messaging. MessagePeer and HandlePeerMessage functions are added to the Client.
Peers use symmetric encryption keys derived via Elliptic Curve Diffie- Hellman (ECDH) to communicate privately. When a client communicates with another Client for the first time, it sends a signed HandshakeRequest containing an ephemeral public key. The counterparty responds with its own ephemeral public key, allowing both parties to derive the shared key. To avoid handshake race conditions and allow key rotation/expiry without interrupting communication, the client stores up to 5 keys per peer. The latest key is used for outbound encryption, while previous keys remain available for decrypting inbound messages during rotation. Each encrypted message is tagged with a key ID so the receiver can select the right key. Keys expire after 1 hour.