Skip to content

client: P2P Encrypted Messaging#18

Open
martonp wants to merge 2 commits intobisoncraft:masterfrom
martonp:encryptedClientMessaging
Open

client: P2P Encrypted Messaging#18
martonp wants to merge 2 commits intobisoncraft:masterfrom
martonp:encryptedClientMessaging

Conversation

@martonp
Copy link
Copy Markdown
Contributor

@martonp martonp commented Mar 4, 2026

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.

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.
Comment thread client/client.go
Comment on lines +152 to +154
unsignedReq := proto.Clone(hsResp).(*clientpb.HandshakeResponse)
unsignedReq.Signature = nil
unsignedReqPayload, err := proto.Marshal(unsignedReq)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, that makes sense.

Comment thread client/client.go
Comment on lines +300 to +302
unsignedReq := proto.Clone(hsReq).(*clientpb.HandshakeRequest)
unsignedReq.Signature = nil
unsignedReqPayload, err := proto.Marshal(unsignedReq)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same cloning issue here, refer to the comment above.

Comment thread client/client.go Outdated
return fmt.Errorf("client host not initialized")
}

c.host.SetStreamHandler(protocols.TatankaRelayMessageProtocol, func(s network.Stream) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the inlined handler here can be refactored into handleTatankaRelayMessage

Comment thread client/p2p_crypto.go Outdated
Comment on lines +251 to +253
return []byte("tatanka-p2p-v1|" + string(a) + "|" + string(b))
}
return []byte("tatanka-p2p-v1|" + string(b) + "|" + string(a))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make the protocol version prefix tatanka-p2p-v1 and separator | constants.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants