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:
2026-09-26 18:57:46 +03:30
parent 2c6d4bfbd2
commit 1e5ef02bdf
11 changed files with 154 additions and 180 deletions
+21 -4
View File
@@ -1,6 +1,7 @@
package service
import (
"encoding/base64"
"errors"
"strings"
"time"
@@ -13,10 +14,11 @@ import (
const MaxPhotosPerUser = 6
var (
ErrTooManyPhotos = errors.New("حداکثر تعداد عکس مجاز رو داری")
ErrPhotoNotOwned = errors.New("این عکس متعلق به تو نیست")
ErrInvalidImage = errors.New("فرمت فایل باید jpg، png یا webp باشه")
ErrImageTooLarge = errors.New("حجم عکس نباید بیشتر از ۵ مگابایت باشه")
ErrTooManyPhotos = errors.New("حداکثر تعداد عکس مجاز رو داری")
ErrPhotoNotOwned = errors.New("این عکس متعلق به تو نیست")
ErrInvalidImage = errors.New("فرمت فایل باید jpg، png یا webp باشه")
ErrImageTooLarge = errors.New("حجم عکس نباید بیشتر از ۵ مگابایت باشه")
ErrInvalidPublicKey = errors.New("کلید عمومی نامعتبر است")
)
const MaxPhotoSizeBytes = 5 * 1024 * 1024
@@ -163,3 +165,18 @@ func (s *ProfileService) UpdateNotificationSettings(user *models.User, req dto.U
func (s *ProfileService) DeleteAccount(user *models.User) error {
return s.userRepo.UpdateFields(user, map[string]interface{}{"is_active": false})
}
// UpdatePublicKey کلید عمومی X25519 کاربر رو برای رمزنگاری سرتاسری پیام‌ها
// ثبت/جایگزین می‌کنه. کلید خصوصی متناظرش هیچ‌وقت به سرور نمی‌رسه؛ اینجا فقط
// طول کلید خام (باید دقیقاً ۳۲ بایت باشه) بعد از decode چک می‌شه، نه محتواش.
func (s *ProfileService) UpdatePublicKey(user *models.User, publicKeyB64 string) error {
raw, err := base64.StdEncoding.DecodeString(publicKeyB64)
if err != nil || len(raw) != 32 {
return ErrInvalidPublicKey
}
if err := s.userRepo.UpdateFields(user, map[string]interface{}{"public_key": publicKeyB64}); err != nil {
return err
}
user.PublicKey = &publicKeyB64
return nil
}