owpengram-server/internal/rpc/encrypted_chats.go

213 lines
8.3 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 rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"go.uber.org/zap"
appsecret "telesrv/internal/app/secretchat"
"telesrv/internal/domain"
)
// 私聊端对端加密Secret Chat / encrypted chat握手 RPC handler。状态机与 DH 校验
// 归 app/secretchat本文件只做鉴权、入参校验、TL 转换与在线推送编排。
//
// P0 范围requestEncryption / acceptEncryption / discardEncryption 的握手闭环 +
// updateEncryption 在线推送(账号级 pushUserMessage与 phone 同套)。设备级定向、
// durable 离线 getDifference 补偿、qts 消息投递sendEncrypted 等)见 P1
// 设计 docs/secret-chat-module.md。服务端是盲中继永不接触共享密钥与明文。
// secretChatErr 把 app/secretchat + domain 业务错误映射为 RPC_ERROR。
func secretChatErr(err error) error {
switch {
case errors.Is(err, appsecret.ErrGAInvalid):
return dhGAInvalidErr()
case errors.Is(err, domain.ErrSecretChatAlreadyAccepted):
return encryptionAlreadyAcceptedErr()
case errors.Is(err, domain.ErrSecretChatAlreadyDeclined):
return encryptionAlreadyDeclinedErr()
case errors.Is(err, domain.ErrSecretChatNotFound):
return chatIDInvalidErr()
default:
return internalErr()
}
}
// secretChatRequireUser 是密聊 RPC 的统一登录闸门。
func (r *Router) secretChatRequireUser(ctx context.Context) (int64, error) {
userID, ok, err := r.currentUserID(ctx)
if err != nil {
return 0, internalErr()
}
if !ok {
return 0, authKeyUnregisteredErr()
}
return userID, nil
}
// businessAuthKeyIDFrom 返回当前连接业务视角 auth_key_id 的 int64 绑定值。
func businessAuthKeyIDFrom(ctx context.Context) (int64, bool) {
id, ok := AuthKeyIDFrom(ctx)
if !ok {
return 0, false
}
return businessAuthKeyInt64(id), true
}
// pushUpdateEncryption 把 targetUserID 视角的 updateEncryption 推给其全部在线设备。
// P0 用账号级在线推送(设备级定向 + 离线补偿见 P1
func (r *Router) pushUpdateEncryption(ctx context.Context, targetUserID int64, chat domain.SecretChat, logMessage string) {
now := int(r.clock.Now().Unix())
upd := &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateEncryption{
Chat: tgEncryptedChatForViewer(chat, targetUserID),
Date: now,
}},
Users: r.tgUsersForIDs(ctx, targetUserID, []int64{chat.AdminUserID, chat.ParticipantUserID}),
Chats: []tg.ChatClass{},
Date: now,
Seq: 0,
}
r.pushUserMessage(ctx, targetUserID, logMessage, upd)
}
// recordEncryptionEventBestEffort 写入 durable updateEncryption 状态事件供离线设备
// getDifference 补偿。best-effort失败仅记日志不阻断 RPCchat 本身已 durable
func (r *Router) recordEncryptionEventBestEffort(ctx context.Context, chatID int, targetUserID, targetAuthKeyID int64, date int) {
if r.deps.SecretChats == nil {
return
}
if err := r.deps.SecretChats.RecordEncryptionEvent(ctx, chatID, targetUserID, targetAuthKeyID, date); err != nil {
r.log.Debug("record encryption state event", zap.Error(err))
}
}
// discardSecretChatsForAuthKey 在设备登出 / 授权撤销时级联 discard 该 perm auth_key 绑定的
// 全部活跃密聊,并向对端推送 encryptedChatDiscarded在线+ 写 durable 事件(离线 getDifference
// 补偿。ownerUserID 是被销毁设备的所有者用于定位对端。best-effort失败仅记日志绝不阻断
// 登出/撤销。修复 P1此前 onAuthLogOut 等不级联 discard对端继续往死 auth_key 投递成静默死链
// (消息 acked=f / qts 永久积压,对端永看不到 discarded
func (r *Router) discardSecretChatsForAuthKey(ctx context.Context, businessAuthKeyID, ownerUserID int64) {
if r.deps.SecretChats == nil || businessAuthKeyID == 0 || ownerUserID == 0 {
return
}
discarded, err := r.deps.SecretChats.DiscardForAuthKey(ctx, businessAuthKeyID)
if err != nil {
// DiscardForAuthKey 出错也会返回已成功 discard 的部分,继续通知这部分对端。
r.log.Debug("cascade discard secret chats for auth key", zap.Error(err))
}
now := int(r.clock.Now().Unix())
for _, chat := range discarded {
peer := chat.PeerOf(ownerUserID)
if peer == 0 {
continue
}
// 对端绑定设备已知则 device-level 定向建链前未绑定0则账号级。
r.recordEncryptionEventBestEffort(ctx, chat.ID, peer, chat.PeerAuthKeyOf(ownerUserID), now)
r.pushUpdateEncryption(ctx, peer, chat, "secret chat discarded on peer logout/revoke")
}
}
func (r *Router) onMessagesRequestEncryption(ctx context.Context, req *tg.MessagesRequestEncryptionRequest) (tg.EncryptedChatClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
if r.deps.SecretChats == nil || r.deps.Users == nil {
return nil, notImplementedErr()
}
adminID, err := r.secretChatRequireUser(ctx)
if err != nil {
return nil, err
}
adminAuthKeyID, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, internalErr()
}
participant, found, err := r.userFromInput(ctx, adminID, req.UserID)
if err != nil {
return nil, internalErr()
}
// 自聊 / bot / 不存在统一 USER_ID_INVALIDbot 不支持密聊)。
if !found || participant.ID == 0 || participant.ID == adminID || participant.Bot {
return nil, userIDInvalidErr()
}
if blocked, err := r.peerBlocksUser(ctx, adminID, participant.ID); err != nil {
return nil, err
} else if blocked {
return nil, userIsBlockedErr()
}
chat, err := r.deps.SecretChats.RequestEncryption(ctx, domain.SecretChatRequest{
AdminUserID: adminID,
AdminAuthKeyID: adminAuthKeyID,
ParticipantUserID: participant.ID,
RandomID: int32(req.RandomID),
GA: req.GA,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
return nil, secretChatErr(err)
}
// 建链前邀请是账号级targetAuthKeyID=0participant 所有设备(含离线)可见。
r.recordEncryptionEventBestEffort(ctx, chat.ID, chat.ParticipantUserID, 0, chat.Date)
// 推接受方全部在线设备 encryptedChatRequested携 g_a。离线设备经 getDifference 补回。
r.pushUpdateEncryption(ctx, chat.ParticipantUserID, chat, "secret chat requested")
// 发起方同步收 encryptedChatWaiting无 g_a
return tgEncryptedChatForViewer(chat, adminID), nil
}
func (r *Router) onMessagesAcceptEncryption(ctx context.Context, req *tg.MessagesAcceptEncryptionRequest) (tg.EncryptedChatClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
if r.deps.SecretChats == nil {
return nil, notImplementedErr()
}
userID, err := r.secretChatRequireUser(ctx)
if err != nil {
return nil, err
}
participantAuthKeyID, ok := businessAuthKeyIDFrom(ctx)
if !ok {
return nil, internalErr()
}
chat, err := r.deps.SecretChats.AcceptEncryption(ctx, req.Peer.ChatID, userID,
participantAuthKeyID, req.Peer.AccessHash, req.GB, req.KeyFingerprint)
if err != nil {
return nil, secretChatErr(err)
}
// 建链完成定向发起方绑定设备device-level离线发起方经 getDifference 补回成型态。
r.recordEncryptionEventBestEffort(ctx, chat.ID, chat.AdminUserID, chat.AdminAuthKeyID, int(r.clock.Now().Unix()))
// 推发起方全部在线设备 encryptedChatGAOrB=g_b, key_fingerprint发起方据此
// 算共享密钥并比对指纹。
r.pushUpdateEncryption(ctx, chat.AdminUserID, chat, "secret chat accepted")
// 接受方同步收 encryptedChatGAOrB=g_a
return tgEncryptedChatForViewer(chat, userID), nil
}
func (r *Router) onMessagesDiscardEncryption(ctx context.Context, req *tg.MessagesDiscardEncryptionRequest) (bool, error) {
if req == nil {
return false, inputRequestInvalidErr()
}
if r.deps.SecretChats == nil {
return false, notImplementedErr()
}
userID, err := r.secretChatRequireUser(ctx)
if err != nil {
return false, err
}
chat, already, err := r.deps.SecretChats.DiscardEncryption(ctx, req.ChatID, userID, req.DeleteHistory)
if err != nil {
return false, secretChatErr(err)
}
if !already {
// 推对端 encryptedChatDiscardedhistory_deleted 决定对端是否删整个会话)。
// 对端绑定设备已知则 device-level建链前未绑定则账号级同 requested 集合)。
if peer := chat.PeerOf(userID); peer != 0 {
r.recordEncryptionEventBestEffort(ctx, chat.ID, peer, chat.PeerAuthKeyOf(userID), int(r.clock.Now().Unix()))
r.pushUpdateEncryption(ctx, peer, chat, "secret chat discarded")
}
}
return true, nil
}