merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package secretchat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
|
|
@ -11,39 +12,44 @@ import (
|
|||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// idAllocRetries 是 chat_id 撞键自愈的有界重试次数。
|
||||
const idAllocRetries = 4
|
||||
|
||||
// Service 实现密聊握手状态机 + qts 消息投递。所有返回的 domain.SecretChat 都是当时快照。
|
||||
// 访问校验(self/bot/拉黑/隐私)在 rpc 层先行;本层做 DH 校验、id/access_hash 分配、
|
||||
// 访问校验(self/bot/拉黑/隐私)在 rpc 层先行;本层做 DH 校验、chat_id wire 不变量、access_hash 分配、
|
||||
// 状态机迁移与 qts 队列写入。绑定维度是设备级 perm auth_key(int64)。
|
||||
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}
|
||||
func NewService(st store.SecretChatStore, queue store.EncryptedQueueStore) *Service {
|
||||
return &Service{store: st, queue: queue}
|
||||
}
|
||||
|
||||
// RequestEncryption 受理 requestEncryption:校验 g_a → 幂等去重 → 分配 chat_id + 双
|
||||
// RequestEncryption 受理 requestEncryption:校验 g_a → 校验 random_id/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
|
||||
}
|
||||
if req.RandomID == 0 {
|
||||
return domain.SecretChat{}, domain.ErrSecretChatRandomIDDuplicate
|
||||
}
|
||||
ga, err := validateDHParam(req.GA)
|
||||
if err != nil {
|
||||
return domain.SecretChat{}, err
|
||||
}
|
||||
// 幂等:同发起设备 + random_id 重发返回既有 chat(DISCARDED 视为新请求)。
|
||||
if existing, ok, err := s.store.GetByAdminRandom(ctx, req.AdminAuthKeyID, req.RandomID); err != nil {
|
||||
// Telegram wire 契约:requestEncryption.random_id 同时就是 chat_id。TDLib 会先以
|
||||
// random_id 创建本地 SecretChatActor,并在消费响应时强校验 response.id 相等;禁止
|
||||
// 用服务端序列替换。全局主键碰撞只允许相同意图的网络重放,其余显式 duplicate。
|
||||
chatID := int(req.RandomID)
|
||||
if existing, ok, err := s.store.GetSecretChat(ctx, chatID); err != nil {
|
||||
return domain.SecretChat{}, err
|
||||
} else if ok && !existing.Terminal() {
|
||||
return existing, nil
|
||||
} else if ok {
|
||||
if sameSecretChatRequest(existing, req, ga) && !existing.Terminal() {
|
||||
return existing, nil
|
||||
}
|
||||
return domain.SecretChat{}, domain.ErrSecretChatRandomIDDuplicate
|
||||
}
|
||||
adminAH, err := randomAccessHash()
|
||||
if err != nil {
|
||||
|
|
@ -54,6 +60,7 @@ func (s *Service) RequestEncryption(ctx context.Context, req domain.SecretChatRe
|
|||
return domain.SecretChat{}, err
|
||||
}
|
||||
chat := domain.SecretChat{
|
||||
ID: chatID,
|
||||
AdminAccessHash: adminAH,
|
||||
ParticipantAccessHash: participantAH,
|
||||
AdminUserID: req.AdminUserID,
|
||||
|
|
@ -64,46 +71,27 @@ func (s *Service) RequestEncryption(ctx context.Context, req domain.SecretChatRe
|
|||
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
|
||||
}
|
||||
if err := s.store.CreateSecretChat(ctx, chat); err == nil {
|
||||
return chat, nil
|
||||
} else if !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
return domain.SecretChat{}, err
|
||||
}
|
||||
// 并发相同请求可能在预查后由另一 goroutine 插入;只在重新读取后仍证明
|
||||
// 是完全相同意图时收敛为幂等成功。
|
||||
existing, ok, getErr := s.store.GetSecretChat(ctx, chatID)
|
||||
if getErr != nil {
|
||||
return domain.SecretChat{}, getErr
|
||||
}
|
||||
if ok && sameSecretChatRequest(existing, req, ga) && !existing.Terminal() {
|
||||
return existing, nil
|
||||
}
|
||||
return domain.SecretChat{}, domain.ErrSecretChatRandomIDDuplicate
|
||||
}
|
||||
|
||||
// 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
|
||||
func sameSecretChatRequest(chat domain.SecretChat, req domain.SecretChatRequest, normalizedGA []byte) bool {
|
||||
return chat.ID == int(req.RandomID) && chat.RandomID == req.RandomID &&
|
||||
chat.AdminUserID == req.AdminUserID && chat.AdminAuthKeyID == req.AdminAuthKeyID &&
|
||||
chat.ParticipantUserID == req.ParticipantUserID && bytes.Equal(chat.GA, normalizedGA)
|
||||
}
|
||||
|
||||
// AcceptEncryption 受理 acceptEncryption:定位 + participant 视角 access_hash 校验 →
|
||||
|
|
@ -132,15 +120,24 @@ func (s *Service) AcceptEncryption(ctx context.Context, chatID int, viewerUserID
|
|||
return s.store.AcceptSecretChat(ctx, chatID, participantAuthKeyID, gbPadded, keyFingerprint)
|
||||
}
|
||||
|
||||
// DiscardEncryption 受理 discardEncryption:定位 + 参与者校验 → 迁移到 discarded。
|
||||
// DiscardEncryption 受理 discardEncryption:定位 + 参与者/绑定设备校验 → 迁移到 discarded。
|
||||
// already=true 表示已是终态(幂等成功)。返回的密聊由 rpc 层投影为对端
|
||||
// encryptedChatDiscarded 推送。
|
||||
func (s *Service) DiscardEncryption(ctx context.Context, chatID int, viewerUserID int64, deleteHistory bool) (domain.SecretChat, bool, error) {
|
||||
func (s *Service) DiscardEncryption(ctx context.Context, chatID int, viewerUserID, viewerAuthKeyID 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) {
|
||||
if !ok || !chat.HasParticipant(viewerUserID) || viewerAuthKeyID == 0 {
|
||||
return domain.SecretChat{}, false, domain.ErrSecretChatNotFound
|
||||
}
|
||||
// Admin 从 request 起即绑定;participant 在 accept 前尚无绑定,任一收到账号级邀请的
|
||||
// participant 设备都可拒绝。accept 一旦完成,双方所有操作都必须来自各自绑定设备。
|
||||
boundAuthKeyID := chat.AuthKeyOf(viewerUserID)
|
||||
if boundAuthKeyID != 0 && boundAuthKeyID != viewerAuthKeyID {
|
||||
return domain.SecretChat{}, false, domain.ErrSecretChatNotFound
|
||||
}
|
||||
if boundAuthKeyID == 0 && viewerUserID != chat.ParticipantUserID {
|
||||
return domain.SecretChat{}, false, domain.ErrSecretChatNotFound
|
||||
}
|
||||
return s.store.DiscardSecretChat(ctx, chatID, deleteHistory)
|
||||
|
|
@ -180,16 +177,17 @@ func (s *Service) DiscardForAuthKey(ctx context.Context, authKeyID int64) ([]dom
|
|||
return discarded, nil
|
||||
}
|
||||
|
||||
// SendEncrypted 受理 sendEncrypted*:定位 + 发送方视角 access_hash 校验 + 态须 normal →
|
||||
// SendEncrypted 受理 sendEncrypted*:定位 + 发送方绑定设备/access_hash 校验 + 态须 normal →
|
||||
// 给【对端绑定设备】分配 qts 并把不透明 bytes 写入投递队列(幂等:同 chat+random_id 返既有
|
||||
// qts/date)。返回密聊快照 + 已落库消息(携 qts/date,rpc 层据此推 updateNewEncryptedMessage
|
||||
// 并回 SentEncryptedMessage{date})。盲中継:不解密 bytes。
|
||||
func (s *Service) SendEncrypted(ctx context.Context, chatID int, viewerUserID, accessHash int64, delivery domain.SecretMessageDelivery) (domain.SecretChat, domain.SecretChatMessage, error) {
|
||||
func (s *Service) SendEncrypted(ctx context.Context, chatID int, viewerUserID, viewerAuthKeyID, 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 {
|
||||
if !ok || !chat.HasParticipant(viewerUserID) || viewerAuthKeyID == 0 ||
|
||||
chat.AuthKeyOf(viewerUserID) != viewerAuthKeyID || chat.AccessHashFor(viewerUserID) != accessHash {
|
||||
return domain.SecretChat{}, domain.SecretChatMessage{}, domain.ErrSecretChatNotFound
|
||||
}
|
||||
if chat.State != domain.SecretChatStateNormal {
|
||||
|
|
|
|||
|
|
@ -3,30 +3,13 @@ package secretchat
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
// fakeChatIDAllocator 是单调自增的测试分配器(无 Redis)。
|
||||
type fakeChatIDAllocator struct{ n int }
|
||||
|
||||
func (a *fakeChatIDAllocator) NextSecretChatID(context.Context) (int, error) {
|
||||
a.n++
|
||||
return a.n, nil
|
||||
}
|
||||
|
||||
func (a *fakeChatIDAllocator) NextSecretChatIDAtLeast(_ context.Context, floor int) (int, error) {
|
||||
if a.n < floor {
|
||||
a.n = floor
|
||||
}
|
||||
a.n++
|
||||
return a.n, nil
|
||||
}
|
||||
|
||||
func (a *fakeChatIDAllocator) CurrentSecretChatID(context.Context) (int, error) { return a.n, nil }
|
||||
|
||||
// validGA 返回一个落在合法 DH 区间的 256 字节 g_a(首字节 0x55 ≈ 2^2046,
|
||||
// 既 > 2^1984 又 < p≈0xc7..)。
|
||||
func validGA() []byte {
|
||||
|
|
@ -40,7 +23,7 @@ func validGA() []byte {
|
|||
|
||||
func newTestService() (*Service, *memory.SecretChatStore) {
|
||||
st := memory.NewSecretChatStore()
|
||||
return NewService(st, memory.NewEncryptedQueueStore(), &fakeChatIDAllocator{}), st
|
||||
return NewService(st, memory.NewEncryptedQueueStore()), st
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -48,6 +31,7 @@ const (
|
|||
partUser = int64(2002)
|
||||
adminAuthKey = int64(0x1111)
|
||||
partAuthKey = int64(0x2222)
|
||||
otherAuthKey = int64(0x3333)
|
||||
keyFP = int64(0x0123456789abcdef)
|
||||
)
|
||||
|
||||
|
|
@ -69,8 +53,8 @@ func TestRequestEncryption(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("RequestEncryption: %v", err)
|
||||
}
|
||||
if chat.ID <= 0 || chat.ID > 0x7fffffff {
|
||||
t.Fatalf("chat id out of int32 range: %d", chat.ID)
|
||||
if chat.ID != int(requestFixture().RandomID) {
|
||||
t.Fatalf("chat id = %d, want request random_id %d", chat.ID, requestFixture().RandomID)
|
||||
}
|
||||
if chat.State != domain.SecretChatStateRequested {
|
||||
t.Fatalf("state = %q, want requested", chat.State)
|
||||
|
|
@ -105,6 +89,94 @@ func TestRequestEncryptionIdempotent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRequestEncryptionConcurrentExactRetry(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
results := make([]domain.SecretChat, 2)
|
||||
errs := make([]error, 2)
|
||||
var wg sync.WaitGroup
|
||||
for i := range results {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
results[i], errs[i] = svc.RequestEncryption(ctx, requestFixture())
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("concurrent request %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if results[0].ID != int(requestFixture().RandomID) || results[1].ID != results[0].ID ||
|
||||
results[1].AdminAccessHash != results[0].AdminAccessHash ||
|
||||
results[1].ParticipantAccessHash != results[0].ParticipantAccessHash {
|
||||
t.Fatalf("concurrent exact retry diverged: first=%+v second=%+v", results[0], results[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestEncryptionPreservesNegativeRandomID(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
req := requestFixture()
|
||||
req.RandomID = -12345
|
||||
chat, err := svc.RequestEncryption(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("request negative random_id: %v", err)
|
||||
}
|
||||
if chat.ID != int(req.RandomID) || chat.RandomID != req.RandomID {
|
||||
t.Fatalf("chat id/random_id = %d/%d, want %d", chat.ID, chat.RandomID, req.RandomID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestEncryptionRejectsChangedIntentAndGlobalCollision(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
if _, err := svc.RequestEncryption(ctx, requestFixture()); err != nil {
|
||||
t.Fatalf("first request: %v", err)
|
||||
}
|
||||
|
||||
changedPeer := requestFixture()
|
||||
changedPeer.ParticipantUserID++
|
||||
if _, err := svc.RequestEncryption(ctx, changedPeer); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
t.Fatalf("changed peer err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||||
}
|
||||
|
||||
changedGA := requestFixture()
|
||||
changedGA.GA = validGA()
|
||||
changedGA.GA[1] ^= 0x01
|
||||
if _, err := svc.RequestEncryption(ctx, changedGA); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
t.Fatalf("changed g_a err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||||
}
|
||||
|
||||
otherAuthKey := requestFixture()
|
||||
otherAuthKey.AdminUserID++
|
||||
otherAuthKey.AdminAuthKeyID++
|
||||
if _, err := svc.RequestEncryption(ctx, otherAuthKey); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
t.Fatalf("global collision err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestEncryptionRejectsZeroAndDiscardedReuse(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
zero := requestFixture()
|
||||
zero.RandomID = 0
|
||||
if _, err := svc.RequestEncryption(ctx, zero); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
t.Fatalf("zero random_id err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||||
}
|
||||
|
||||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, true); err != nil {
|
||||
t.Fatalf("discard: %v", err)
|
||||
}
|
||||
if _, err := svc.RequestEncryption(ctx, requestFixture()); !errors.Is(err, domain.ErrSecretChatRandomIDDuplicate) {
|
||||
t.Fatalf("discarded reuse err = %v, want ErrSecretChatRandomIDDuplicate", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestEncryptionInvalidGA(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
req := requestFixture()
|
||||
|
|
@ -174,6 +246,63 @@ func TestAcceptEncryptionDoubleAccept(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAcceptEncryptionConcurrentDevicesSingleWinner(t *testing.T) {
|
||||
svc, st := newTestService()
|
||||
ctx := context.Background()
|
||||
chat, err := svc.RequestEncryption(ctx, requestFixture())
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
|
||||
authKeys := []int64{partAuthKey, otherAuthKey}
|
||||
errs := make([]error, len(authKeys))
|
||||
var wg sync.WaitGroup
|
||||
for i, authKeyID := range authKeys {
|
||||
wg.Add(1)
|
||||
go func(i int, authKeyID int64) {
|
||||
defer wg.Done()
|
||||
_, errs[i] = svc.AcceptEncryption(ctx, chat.ID, partUser, authKeyID, chat.ParticipantAccessHash, validGA(), keyFP)
|
||||
}(i, authKeyID)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
winners := 0
|
||||
losers := 0
|
||||
for _, err := range errs {
|
||||
switch {
|
||||
case err == nil:
|
||||
winners++
|
||||
case errors.Is(err, domain.ErrSecretChatAlreadyAccepted):
|
||||
losers++
|
||||
default:
|
||||
t.Fatalf("concurrent accept err = %v", err)
|
||||
}
|
||||
}
|
||||
if winners != 1 || losers != 1 {
|
||||
t.Fatalf("concurrent accepts winners=%d losers=%d, want 1/1", winners, losers)
|
||||
}
|
||||
|
||||
stored, ok, err := st.GetSecretChat(ctx, chat.ID)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("get accepted chat: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if stored.State != domain.SecretChatStateNormal ||
|
||||
(stored.ParticipantAuthKeyID != partAuthKey && stored.ParticipantAuthKeyID != otherAuthKey) {
|
||||
t.Fatalf("accepted chat = %+v, want normal bound to one participant device", stored)
|
||||
}
|
||||
loserAuthKeyID := partAuthKey
|
||||
if stored.ParticipantAuthKeyID == partAuthKey {
|
||||
loserAuthKeyID = otherAuthKey
|
||||
}
|
||||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, partUser, loserAuthKeyID, true); !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("loser discard err = %v, want ErrSecretChatNotFound", err)
|
||||
}
|
||||
stored, ok, err = st.GetSecretChat(ctx, chat.ID)
|
||||
if err != nil || !ok || stored.State != domain.SecretChatStateNormal {
|
||||
t.Fatalf("chat after loser discard = %+v ok=%v err=%v, want normal", stored, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptEncryptionInvalidGB(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
|
|
@ -188,7 +317,7 @@ func TestDiscardEncryption(t *testing.T) {
|
|||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||||
got, already, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, true)
|
||||
got, already, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, true)
|
||||
if err != nil {
|
||||
t.Fatalf("discard: %v", err)
|
||||
}
|
||||
|
|
@ -199,7 +328,7 @@ func TestDiscardEncryption(t *testing.T) {
|
|||
t.Fatalf("discarded chat = %+v", got)
|
||||
}
|
||||
// 幂等:再 discard 返回 already=true。
|
||||
_, already, err = svc.DiscardEncryption(ctx, chat.ID, partUser, false)
|
||||
_, already, err = svc.DiscardEncryption(ctx, chat.ID, partUser, partAuthKey, false)
|
||||
if err != nil || !already {
|
||||
t.Fatalf("idempotent discard: already=%v err=%v", already, err)
|
||||
}
|
||||
|
|
@ -209,7 +338,7 @@ func TestDiscardEncryptionNonParticipant(t *testing.T) {
|
|||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||||
_, _, err := svc.DiscardEncryption(ctx, chat.ID, int64(9999), false)
|
||||
_, _, err := svc.DiscardEncryption(ctx, chat.ID, int64(9999), int64(9999), false)
|
||||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||||
}
|
||||
|
|
@ -245,20 +374,20 @@ func TestSendEncryptedQtsAllocation(t *testing.T) {
|
|||
chat := acceptedChat(t, svc)
|
||||
|
||||
// admin 发 → 投给 participant 设备(partAuthKey),qts 从 1 起。
|
||||
_, m1, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 2000})
|
||||
_, m1, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 2000})
|
||||
if err != nil {
|
||||
t.Fatalf("send 1: %v", err)
|
||||
}
|
||||
if m1.Qts != 1 || m1.ReceiverAuthKeyID != partAuthKey || m1.ReceiverUserID != partUser {
|
||||
t.Fatalf("msg1 = %+v (want qts=1, receiver=participant device)", m1)
|
||||
}
|
||||
_, m2, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 222, Bytes: []byte{4}, Date: 2001})
|
||||
_, m2, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 222, Bytes: []byte{4}, Date: 2001})
|
||||
if err != nil || m2.Qts != 2 {
|
||||
t.Fatalf("msg2 qts = %d err=%v, want 2", m2.Qts, err)
|
||||
}
|
||||
|
||||
// 幂等重发同 random_id → 返回首次 qts/date,不分配新 qts。
|
||||
_, dup, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 9999})
|
||||
_, dup, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 111, Bytes: []byte{1, 2, 3}, Date: 9999})
|
||||
if err != nil {
|
||||
t.Fatalf("dup send: %v", err)
|
||||
}
|
||||
|
|
@ -267,7 +396,7 @@ func TestSendEncryptedQtsAllocation(t *testing.T) {
|
|||
}
|
||||
|
||||
// participant 发 → 投给 admin 设备(adminAuthKey),独立 qts 序列从 1 起。
|
||||
_, pm, err := svc.SendEncrypted(ctx, chat.ID, partUser, chat.ParticipantAccessHash, domain.SecretMessageDelivery{RandomID: 333, Bytes: []byte{9}, Date: 2002})
|
||||
_, pm, err := svc.SendEncrypted(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, domain.SecretMessageDelivery{RandomID: 333, Bytes: []byte{9}, Date: 2002})
|
||||
if err != nil {
|
||||
t.Fatalf("participant send: %v", err)
|
||||
}
|
||||
|
|
@ -280,17 +409,55 @@ func TestSendEncryptedWrongAccessHash(t *testing.T) {
|
|||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat := acceptedChat(t, svc)
|
||||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash+1, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash+1, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEncryptedRejectsUnboundAccountDevice(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat := acceptedChat(t, svc)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
userID int64
|
||||
accessHash int64
|
||||
}{
|
||||
{name: "admin", userID: adminUser, accessHash: chat.AdminAccessHash},
|
||||
{name: "participant", userID: partUser, accessHash: chat.ParticipantAccessHash},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, err := svc.SendEncrypted(ctx, chat.ID, tc.userID, otherAuthKey, tc.accessHash, domain.SecretMessageDelivery{
|
||||
RandomID: 991, Bytes: []byte{1}, Date: 2000,
|
||||
})
|
||||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("err = %v, want ErrSecretChatNotFound", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscardEncryptionRejectsUnboundAccountDeviceAfterAccept(t *testing.T) {
|
||||
svc, st := newTestService()
|
||||
ctx := context.Background()
|
||||
chat := acceptedChat(t, svc)
|
||||
|
||||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, partUser, otherAuthKey, true); !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("unbound discard err = %v, want ErrSecretChatNotFound", err)
|
||||
}
|
||||
stored, ok, err := st.GetSecretChat(ctx, chat.ID)
|
||||
if err != nil || !ok || stored.State != domain.SecretChatStateNormal {
|
||||
t.Fatalf("chat after rejected discard = %+v ok=%v err=%v", stored, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEncryptedNonNormal(t *testing.T) {
|
||||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat, _ := svc.RequestEncryption(ctx, requestFixture()) // requested, 未 accept
|
||||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||||
_, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: 1, Bytes: []byte{1}, Date: 2000})
|
||||
if !errors.Is(err, domain.ErrSecretChatNotFound) {
|
||||
t.Fatalf("err = %v, want ErrSecretChatNotFound (未成型不能发)", err)
|
||||
}
|
||||
|
|
@ -301,7 +468,7 @@ func TestListNewMessagesAndAck(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
chat := acceptedChat(t, svc)
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: int64(1000 + i), Bytes: []byte{byte(i)}, Date: 2000 + i}); err != nil {
|
||||
if _, _, err := svc.SendEncrypted(ctx, chat.ID, adminUser, adminAuthKey, chat.AdminAccessHash, domain.SecretMessageDelivery{RandomID: int64(1000 + i), Bytes: []byte{byte(i)}, Date: 2000 + i}); err != nil {
|
||||
t.Fatalf("send %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -336,7 +503,7 @@ func TestAcceptAfterDiscard(t *testing.T) {
|
|||
svc, _ := newTestService()
|
||||
ctx := context.Background()
|
||||
chat, _ := svc.RequestEncryption(ctx, requestFixture())
|
||||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, false); err != nil {
|
||||
if _, _, err := svc.DiscardEncryption(ctx, chat.ID, adminUser, adminAuthKey, false); err != nil {
|
||||
t.Fatalf("discard: %v", err)
|
||||
}
|
||||
_, err := svc.AcceptEncryption(ctx, chat.ID, partUser, partAuthKey, chat.ParticipantAccessHash, validGA(), keyFP)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue