Add end-to-end encryption for chat messages (X25519 / NaCl box)
Messages between matched users are now encrypted client-side with each user's own key pair before they ever reach the server; the backend only ever stores/relays opaque ciphertext + nonce and cannot read message content, including for the last-message preview on the matches list. - User gains a PublicKey field, registered/rotated via PUT /profile/public-key - Message gains CipherText/Nonce; legacy plaintext Text is kept as-is for messages sent before this change - MatchView exposes the partner's public key and the last message's ciphertext/nonce so the client can decrypt previews itself - Removed the now-dead plaintext SanitizeMessageText utility — the server can no longer sanitize message content since it's opaque ciphertext Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,9 @@ func (h *ChatHandler) ListMessages(c *gin.Context) {
|
||||
utils.Success(c, http.StatusOK, "", messages)
|
||||
}
|
||||
|
||||
// SendMessage POST /matches/:id/messages body: {text}
|
||||
// SendMessage POST /matches/:id/messages body: {cipher_text, nonce}
|
||||
// متن خام پیام هیچوقت اینجا نمیرسه؛ کلاینت قبل از ارسال با کلید عمومی
|
||||
// طرف مقابل (NaCl box / X25519) رمزنگاریش کرده.
|
||||
func (h *ChatHandler) SendMessage(c *gin.Context) {
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
@@ -66,10 +68,10 @@ func (h *ChatHandler) SendMessage(c *gin.Context) {
|
||||
}
|
||||
var req dto.SendMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, http.StatusBadRequest, "متن پیام نمیتونه خالی باشه", err.Error())
|
||||
utils.Fail(c, http.StatusBadRequest, "پیام رمزنگاریشده نامعتبر است", err.Error())
|
||||
return
|
||||
}
|
||||
msg, err := h.chatService.SendMessage(user.ID, uint(matchID), req.Text)
|
||||
msg, err := h.chatService.SendMessage(user.ID, uint(matchID), req.CipherText, req.Nonce)
|
||||
if err != nil {
|
||||
h.handleError(c, err)
|
||||
return
|
||||
@@ -81,9 +83,7 @@ func (h *ChatHandler) handleError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrNotMatchMember):
|
||||
utils.Fail(c, http.StatusForbidden, err.Error(), nil)
|
||||
case errors.Is(err, utils.ErrInvalidMessageEncoding),
|
||||
errors.Is(err, utils.ErrMessageEmpty),
|
||||
errors.Is(err, utils.ErrMessageTooLong):
|
||||
case errors.Is(err, service.ErrInvalidMessageEncoding):
|
||||
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
||||
default:
|
||||
utils.Fail(c, http.StatusNotFound, "گفتگو یافت نشد", nil)
|
||||
|
||||
@@ -201,3 +201,27 @@ func (h *ProfileHandler) DeleteAccount(c *gin.Context) {
|
||||
}
|
||||
utils.Success(c, http.StatusOK, "حساب کاربری غیرفعال شد", nil)
|
||||
}
|
||||
|
||||
// UpdatePublicKey PUT /profile/public-key — ثبت/جایگزینی کلید عمومی X25519
|
||||
// برای رمزنگاری سرتاسری پیامها. کلاینت این کلید رو خودش تولید میکنه؛ کلید
|
||||
// خصوصی متناظرش هیچوقت به این endpoint (یا هیچ endpoint دیگهای) فرستاده نمیشه.
|
||||
func (h *ProfileHandler) UpdatePublicKey(c *gin.Context) {
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req dto.UpdatePublicKeyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, http.StatusBadRequest, "کلید عمومی نامعتبر است", err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.profileService.UpdatePublicKey(user, req.PublicKey); err != nil {
|
||||
if errors.Is(err, service.ErrInvalidPublicKey) {
|
||||
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیرهی کلید عمومی", nil)
|
||||
return
|
||||
}
|
||||
utils.Success(c, http.StatusOK, "کلید عمومی ذخیره شد", nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user