owpengram-server/internal/app/secretchat/service.go

320 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package secretchat
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"telesrv/internal/domain"
"telesrv/internal/store"
)
// idAllocRetries 是 chat_id 撞键自愈的有界重试次数。
const idAllocRetries = 4
// Service 实现密聊握手状态机 + qts 消息投递。所有返回的 domain.SecretChat 都是当时快照。
// 访问校验self/bot/拉黑/隐私)在 rpc 层先行;本层做 DH 校验、id/access_hash 分配、
// 状态机迁移与 qts 队列写入。绑定维度是设备级 perm auth_keyint64
type Service struct {
store store.SecretChatStore
queue store.EncryptedQueueStore
ids store.SecretChatIDAllocator
}
// NewService 创建密聊服务。
func NewService(st store.SecretChatStore, queue store.EncryptedQueueStore, ids store.SecretChatIDAllocator) *Service {
return &Service{store: st, queue: queue, ids: ids}
}
// RequestEncryption 受理 requestEncryption校验 g_a → 幂等去重 → 分配 chat_id + 双
// access_hash → 盲存 g_a → 落 requested 态。返回的密聊由 rpc 层投影为 admin 视角
// encryptedChatWaiting同步响应与 participant 视角 encryptedChatRequested推送
func (s *Service) RequestEncryption(ctx context.Context, req domain.SecretChatRequest) (domain.SecretChat, error) {
if req.AdminUserID == 0 || req.ParticipantUserID == 0 || req.AdminAuthKeyID == 0 {
return domain.SecretChat{}, ErrGAInvalid
}
ga, err := validateDHParam(req.GA)
if err != nil {
return domain.SecretChat{}, err
}
// 幂等:同发起设备 + random_id 重发返回既有 chatDISCARDED 视为新请求)。
if existing, ok, err := s.store.GetByAdminRandom(ctx, req.AdminAuthKeyID, req.RandomID); err != nil {
return domain.SecretChat{}, err
} else if ok && !existing.Terminal() {
return existing, nil
}
adminAH, err := randomAccessHash()
if err != nil {
return domain.SecretChat{}, err
}
participantAH, err := randomAccessHash()
if err != nil {
return domain.SecretChat{}, err
}
chat := domain.SecretChat{
AdminAccessHash: adminAH,
ParticipantAccessHash: participantAH,
AdminUserID: req.AdminUserID,
ParticipantUserID: req.ParticipantUserID,
AdminAuthKeyID: req.AdminAuthKeyID,
State: domain.SecretChatStateRequested,
GA: ga,
RandomID: req.RandomID,
Date: req.Date,
}
for attempt := 0; ; attempt++ {
chatID, err := s.nextChatID(ctx, attempt)
if err != nil {
return domain.SecretChat{}, err
}
chat.ID = chatID
err = s.store.CreateSecretChat(ctx, chat)
if err == nil {
return chat, nil
}
if errors.Is(err, domain.ErrSecretChatIDConflict) && attempt < idAllocRetries {
continue
}
return domain.SecretChat{}, err
}
}
// nextChatID 分配下一个 chat_id撞键后用 AtLeast(MaxSecretChatID) 顶起计数器自愈。
// 校验 int32 正区间上界EncryptedChat.ID 是 int32 量级)。
func (s *Service) nextChatID(ctx context.Context, attempt int) (int, error) {
var (
id int
err error
)
if attempt == 0 {
id, err = s.ids.NextSecretChatID(ctx)
} else {
floor, ferr := s.store.MaxSecretChatID(ctx)
if ferr != nil {
return 0, ferr
}
id, err = s.ids.NextSecretChatIDAtLeast(ctx, floor)
}
if err != nil {
return 0, err
}
if id <= 0 || id > 0x7fffffff {
return 0, fmt.Errorf("secretchat: chat id out of int32 range: %d", id)
}
return id, nil
}
// AcceptEncryption 受理 acceptEncryption定位 + participant 视角 access_hash 校验 →
// 校验 g_b → 原子 CAS 绑定接受设备并落 g_b/key_fingerprint → normal。返回的密聊由
// rpc 层投影为 participant 视角 encryptedChatGAOrB=g_a同步响应与 admin 视角
// encryptedChatGAOrB=g_b推送
func (s *Service) AcceptEncryption(ctx context.Context, chatID int, viewerUserID, participantAuthKeyID, accessHash int64, gb []byte, keyFingerprint int64) (domain.SecretChat, error) {
gbPadded, err := validateDHParam(gb)
if err != nil {
return domain.SecretChat{}, err
}
chat, ok, err := s.store.GetSecretChat(ctx, chatID)
if err != nil {
return domain.SecretChat{}, err
}
// 视角校验:调用方必须是接受方本人且 access_hash 匹配 participant 视角。
if !ok || chat.ParticipantUserID != viewerUserID || chat.ParticipantAccessHash != accessHash {
return domain.SecretChat{}, domain.ErrSecretChatNotFound
}
switch chat.State {
case domain.SecretChatStateNormal:
return domain.SecretChat{}, domain.ErrSecretChatAlreadyAccepted
case domain.SecretChatStateDiscarded:
return domain.SecretChat{}, domain.ErrSecretChatAlreadyDeclined
}
return s.store.AcceptSecretChat(ctx, chatID, participantAuthKeyID, gbPadded, keyFingerprint)
}
// DiscardEncryption 受理 discardEncryption定位 + 参与者校验 → 迁移到 discarded。
// already=true 表示已是终态(幂等成功)。返回的密聊由 rpc 层投影为对端
// encryptedChatDiscarded 推送。
func (s *Service) DiscardEncryption(ctx context.Context, chatID int, viewerUserID int64, deleteHistory bool) (domain.SecretChat, bool, error) {
chat, ok, err := s.store.GetSecretChat(ctx, chatID)
if err != nil {
return domain.SecretChat{}, false, err
}
if !ok || !chat.HasParticipant(viewerUserID) {
return domain.SecretChat{}, false, domain.ErrSecretChatNotFound
}
return s.store.DiscardSecretChat(ctx, chatID, deleteHistory)
}
// GetSecretChat 取密聊快照rpc 层访问校验用)。
func (s *Service) GetSecretChat(ctx context.Context, chatID int) (domain.SecretChat, bool, error) {
return s.store.GetSecretChat(ctx, chatID)
}
// DiscardForAuthKey 级联 discard 绑定该设备 perm auth_key作为 admin 或 participant
// 全部活跃密聊,用于设备登出 / 授权撤销。返回本次实际从非终态迁移到 discarded 的密聊快照
// (已是终态的不返回),供 rpc 层据此向对端推送 encryptedChatDiscarded。盲中继不删历史
// history_deleted=false对端自行决定本地处置。出错时返回已成功 discard 的部分 + err
// 让调用方仍能通知这部分对端(登出/撤销是 best-effort不因此回退
func (s *Service) DiscardForAuthKey(ctx context.Context, authKeyID int64) ([]domain.SecretChat, error) {
if authKeyID == 0 {
return nil, nil
}
chats, err := s.store.ListActiveSecretChatsByAuthKey(ctx, authKeyID)
if err != nil {
return nil, err
}
var discarded []domain.SecretChat
for _, c := range chats {
updated, already, derr := s.store.DiscardSecretChat(ctx, c.ID, false)
if derr != nil {
if errors.Is(derr, domain.ErrSecretChatNotFound) {
continue
}
return discarded, derr
}
if !already {
discarded = append(discarded, updated)
}
}
return discarded, nil
}
// SendEncrypted 受理 sendEncrypted*:定位 + 发送方视角 access_hash 校验 + 态须 normal →
// 给【对端绑定设备】分配 qts 并把不透明 bytes 写入投递队列(幂等:同 chat+random_id 返既有
// qts/date。返回密聊快照 + 已落库消息(携 qts/daterpc 层据此推 updateNewEncryptedMessage
// 并回 SentEncryptedMessage{date})。盲中継:不解密 bytes。
func (s *Service) SendEncrypted(ctx context.Context, chatID int, viewerUserID, accessHash int64, delivery domain.SecretMessageDelivery) (domain.SecretChat, domain.SecretChatMessage, error) {
chat, ok, err := s.store.GetSecretChat(ctx, chatID)
if err != nil {
return domain.SecretChat{}, domain.SecretChatMessage{}, err
}
if !ok || !chat.HasParticipant(viewerUserID) || chat.AccessHashFor(viewerUserID) != accessHash {
return domain.SecretChat{}, domain.SecretChatMessage{}, domain.ErrSecretChatNotFound
}
if chat.State != domain.SecretChatStateNormal {
// 未成型 / 已销毁的密聊不能收发CHAT_ID_INVALID
return domain.SecretChat{}, domain.SecretChatMessage{}, domain.ErrSecretChatNotFound
}
receiverUserID := chat.PeerOf(viewerUserID)
receiverAuthKeyID := chat.AdminAuthKeyID
if chat.IsAdmin(viewerUserID) {
receiverAuthKeyID = chat.ParticipantAuthKeyID
}
if receiverUserID == 0 || receiverAuthKeyID == 0 {
return domain.SecretChat{}, domain.SecretChatMessage{}, domain.ErrSecretChatNotFound
}
stored, _, err := s.queue.AppendEncryptedMessage(ctx, domain.SecretChatMessage{
ReceiverAuthKeyID: receiverAuthKeyID,
ReceiverUserID: receiverUserID,
ChatID: chatID,
RandomID: delivery.RandomID,
Date: delivery.Date,
IsService: delivery.IsService,
Bytes: delivery.Bytes,
File: delivery.File,
})
if err != nil {
return domain.SecretChat{}, domain.SecretChatMessage{}, err
}
return chat, stored, nil
}
// ListNewMessages 返回某设备 qts > sinceQts 的连续加密消息getDifference 补差分用)。
func (s *Service) ListNewMessages(ctx context.Context, deviceAuthKeyID int64, sinceQts, limit int) ([]domain.SecretChatMessage, error) {
if deviceAuthKeyID == 0 {
return nil, nil
}
return s.queue.ListEncryptedMessagesSince(ctx, deviceAuthKeyID, sinceQts, limit)
}
// DeviceReservedQts 返回某设备当前已分配的最高 qtsgetState 用)。
func (s *Service) DeviceReservedQts(ctx context.Context, deviceAuthKeyID int64) (int, error) {
if deviceAuthKeyID == 0 {
return 0, nil
}
return s.queue.ReservedQts(ctx, deviceAuthKeyID)
}
// AckQueue 推进某设备的 confirmed qts 并标记 ackedreceivedQueue
func (s *Service) AckQueue(ctx context.Context, deviceAuthKeyID int64, maxQts int) error {
if deviceAuthKeyID == 0 || maxQts <= 0 {
return nil
}
return s.queue.AckEncryptedMessages(ctx, deviceAuthKeyID, maxQts)
}
// RecordEncryptionEvent 写入 durable updateEncryption 状态事件(离线补偿)。
// targetAuthKeyID=0 表示账号级(建链前邀请/撤回对 target 所有设备可见),非 0 表示
// 绑定设备定向。投递时按 secret_chats 权威态重建(不固化快照)。
func (s *Service) RecordEncryptionEvent(ctx context.Context, chatID int, targetUserID, targetAuthKeyID int64, date int) error {
if targetUserID == 0 {
return nil
}
_, err := s.queue.AppendStateEvent(ctx, domain.EncryptedStateEvent{
TargetUserID: targetUserID,
TargetAuthKeyID: targetAuthKeyID,
ChatID: chatID,
Type: domain.EncryptedStateEventEncryption,
Date: date,
})
return err
}
// RecordReadEvent 写入 durable updateEncryptedMessagesRead 状态事件(离线补偿,设备定向)。
func (s *Service) RecordReadEvent(ctx context.Context, chatID int, targetUserID, targetAuthKeyID int64, maxDate, date int) error {
if targetUserID == 0 {
return nil
}
_, err := s.queue.AppendStateEvent(ctx, domain.EncryptedStateEvent{
TargetUserID: targetUserID,
TargetAuthKeyID: targetAuthKeyID,
ChatID: chatID,
Type: domain.EncryptedStateEventRead,
MaxDate: maxDate,
Date: date,
})
return err
}
// ListStateEvents 返回某设备未投递的密聊状态事件getDifference 补偿用)。
func (s *Service) ListStateEvents(ctx context.Context, userID, deviceAuthKeyID int64, limit int) ([]domain.EncryptedStateEvent, error) {
if userID == 0 || deviceAuthKeyID == 0 {
return nil, nil
}
return s.queue.ListUndeliveredStateEvents(ctx, userID, deviceAuthKeyID, limit)
}
// MarkStateEventsDelivered 登记某设备已投递这些状态事件。
func (s *Service) MarkStateEventsDelivered(ctx context.Context, deviceAuthKeyID int64, eventIDs []int64) error {
if deviceAuthKeyID == 0 || len(eventIDs) == 0 {
return nil
}
return s.queue.MarkStateEventsDelivered(ctx, deviceAuthKeyID, eventIDs)
}
// PutEncryptedFile 持久化密聊文件元数据快照(铸造后写一次)。
func (s *Service) PutEncryptedFile(ctx context.Context, ownerUserID int64, ref domain.EncryptedFileRef) error {
if ref.ID == 0 {
return nil
}
return s.queue.PutEncryptedFile(ctx, ownerUserID, ref)
}
// GetEncryptedFile 按 id + access_hash 回查文件快照inputEncryptedFile 复用路径)。
func (s *Service) GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error) {
return s.queue.GetEncryptedFile(ctx, id, accessHash)
}
// randomAccessHash 生成正 int64 access_hashrand 8B → 高位清零保正0 置 1
func randomAccessHash() (int64, error) {
var b [8]byte
if _, err := rand.Read(b[:]); err != nil {
return 0, fmt.Errorf("secretchat: access hash rand: %w", err)
}
v := int64(binary.BigEndian.Uint64(b[:]) >> 1)
if v == 0 {
v = 1
}
return v, nil
}