265 lines
11 KiB
Go
265 lines
11 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
|
||
"github.com/iamxvbaba/td/proto"
|
||
"github.com/iamxvbaba/td/tg"
|
||
"go.uber.org/zap"
|
||
|
||
appsecret "telesrv/internal/app/secretchat"
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// 私聊端对端加密(Secret Chat / encrypted chat)握手 RPC handler。状态机与 DH 校验
|
||
// 归 app/secretchat;本文件只做鉴权、入参校验、TL 转换与在线推送编排。
|
||
//
|
||
// requestEncryption / acceptEncryption / discardEncryption 的握手闭环 +
|
||
// updateEncryption 在线设备定向、durable 离线 getDifference 补偿与输家设备收敛;
|
||
// qts 消息投递(sendEncrypted 等)见 encrypted_messages.go,
|
||
// 设计 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.ErrSecretChatRandomIDDuplicate):
|
||
return secretChatRandomIDDuplicateErr()
|
||
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 把 updateEncryption 投给目标账号或精确绑定设备。targetAuthKeyID=0
|
||
// 仅允许用于 accept 前邀请/撤回;accept 后必须非零,缺少定向 binder 时 fail-closed。
|
||
func (r *Router) pushUpdateEncryption(ctx context.Context, targetUserID, targetAuthKeyID 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,
|
||
}
|
||
if targetAuthKeyID == 0 {
|
||
r.pushUserMessage(ctx, targetUserID, logMessage, upd)
|
||
return
|
||
}
|
||
if targeted, ok := r.deps.Sessions.(AuthKeyTargetedSessionBinder); ok {
|
||
_, _ = targeted.PushToUserAuthKey(ctx, targetUserID, deviceAuthKeyBytes(targetAuthKeyID), proto.MessageFromServer, upd)
|
||
return
|
||
}
|
||
r.log.Error("secret chat targeted session binder unavailable",
|
||
zap.String("update", logMessage),
|
||
zap.Int64("target_user_id", targetUserID),
|
||
zap.Int64("target_auth_key_id", targetAuthKeyID))
|
||
}
|
||
|
||
// pushAcceptedLoserDiscarded 让 participant 账号中除获胜 business auth key 外的在线设备
|
||
// 删除 requested 幽灵。离线/未就绪设备由 accept 后新增的账号级 state event 收敛。
|
||
func (r *Router) pushAcceptedLoserDiscarded(ctx context.Context, chat domain.SecretChat, date int) {
|
||
if chat.ParticipantUserID == 0 || chat.ParticipantAuthKeyID == 0 {
|
||
return
|
||
}
|
||
upd := &tg.Updates{
|
||
Updates: []tg.UpdateClass{&tg.UpdateEncryption{
|
||
Chat: &tg.EncryptedChatDiscarded{ID: chat.ID, HistoryDeleted: true},
|
||
Date: date,
|
||
}},
|
||
Users: r.tgUsersForIDs(ctx, chat.ParticipantUserID, []int64{chat.AdminUserID, chat.ParticipantUserID}),
|
||
Chats: []tg.ChatClass{},
|
||
Date: date,
|
||
Seq: 0,
|
||
}
|
||
if targeted, ok := r.deps.Sessions.(AuthKeyTargetedSessionBinder); ok {
|
||
_, _ = targeted.PushToUserExceptBusinessAuthKey(ctx, chat.ParticipantUserID,
|
||
deviceAuthKeyBytes(chat.ParticipantAuthKeyID), proto.MessageFromServer, upd, r.cfg.OutboundPushTimeout)
|
||
return
|
||
}
|
||
r.log.Error("secret chat targeted session binder unavailable",
|
||
zap.String("update", "secret chat accept loser discarded"),
|
||
zap.Int64("target_user_id", chat.ParticipantUserID),
|
||
zap.Int64("exclude_auth_key_id", chat.ParticipantAuthKeyID))
|
||
}
|
||
|
||
// recordEncryptionEventBestEffort 写入 durable updateEncryption 状态事件供离线设备
|
||
// getDifference 补偿。best-effort:失败仅记日志不阻断 RPC(chat 本身已 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.PeerAuthKeyOf(ownerUserID), 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 req.RandomID == 0 || int(int32(req.RandomID)) != req.RandomID {
|
||
return nil, secretChatRandomIDDuplicateErr()
|
||
}
|
||
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_INVALID(bot 不支持密聊)。
|
||
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=0):participant 所有设备(含离线)可见。
|
||
r.recordEncryptionEventBestEffort(ctx, chat.ID, chat.ParticipantUserID, 0, chat.Date)
|
||
// 推接受方全部在线设备 encryptedChatRequested(携 g_a)。离线设备经 getDifference 补回。
|
||
r.pushUpdateEncryption(ctx, chat.ParticipantUserID, 0, 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)
|
||
}
|
||
now := int(r.clock.Now().Unix())
|
||
// 建链完成定向发起方绑定设备(device-level):离线发起方经 getDifference 补回成型态。
|
||
r.recordEncryptionEventBestEffort(ctx, chat.ID, chat.AdminUserID, chat.AdminAuthKeyID, now)
|
||
// 给 participant 账号新增一次收敛事件:获胜 auth key 跳过,其它已见/未见邀请的设备
|
||
// 均投影为 discarded,避免 requested 幽灵与 future-device normal 泄漏。
|
||
r.recordEncryptionEventBestEffort(ctx, chat.ID, chat.ParticipantUserID, 0, now)
|
||
// 仅推发起方绑定设备 encryptedChat(GAOrB=g_b, key_fingerprint)。
|
||
r.pushUpdateEncryption(ctx, chat.AdminUserID, chat.AdminAuthKeyID, chat, "secret chat accepted")
|
||
r.pushAcceptedLoserDiscarded(ctx, chat, now)
|
||
// 接受方同步收 encryptedChat(GAOrB=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
|
||
}
|
||
deviceAuthKeyID, ok := businessAuthKeyIDFrom(ctx)
|
||
if !ok {
|
||
return false, internalErr()
|
||
}
|
||
chat, already, err := r.deps.SecretChats.DiscardEncryption(ctx, req.ChatID, userID, deviceAuthKeyID, req.DeleteHistory)
|
||
if err != nil {
|
||
return false, secretChatErr(err)
|
||
}
|
||
if !already {
|
||
// 推对端 encryptedChatDiscarded(history_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.PeerAuthKeyOf(userID), chat, "secret chat discarded")
|
||
}
|
||
}
|
||
return true, nil
|
||
}
|