115 lines
4.4 KiB
Go
115 lines
4.4 KiB
Go
package store
|
||
|
||
import (
|
||
"bytes"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/tinode/chat/server/store/types"
|
||
)
|
||
|
||
func TestMessageCryptoRoundTrip(t *testing.T) {
|
||
tenantID := types.TenantID(9001)
|
||
key := bytes.Repeat([]byte{7}, 32)
|
||
messageKeyCache.Lock()
|
||
expires := time.Now().Add(time.Minute)
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 0)] = cachedMessageKey{enabled: true, version: 3, key: key, expires: expires}
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 3)] = cachedMessageKey{enabled: true, version: 3, key: key, expires: expires}
|
||
messageKeyCache.Unlock()
|
||
defer InvalidateMessageEncryptionKeys(tenantID)
|
||
|
||
original := &types.Message{TenantID: tenantID, Topic: "p2p-test", SeqId: 42, Content: map[string]any{"txt": "hello"}}
|
||
stored, err := encryptMessageContent(original)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if stored.KeyVersion != 3 || len(stored.ContentNonce) != 12 || stored.Content == original.Content {
|
||
t.Fatalf("message was not encrypted: %#v", stored)
|
||
}
|
||
if err = decryptMessageContent(stored); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
content, ok := stored.Content.(map[string]any)
|
||
if !ok || content["txt"] != "hello" {
|
||
t.Fatalf("unexpected plaintext: %#v", stored.Content)
|
||
}
|
||
}
|
||
|
||
func TestMessageCryptoPlaintextWhenDisabled(t *testing.T) {
|
||
tenantID := types.TenantID(9002)
|
||
messageKeyCache.Lock()
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 0)] = cachedMessageKey{expires: time.Now().Add(time.Minute)}
|
||
messageKeyCache.Unlock()
|
||
defer InvalidateMessageEncryptionKeys(tenantID)
|
||
|
||
original := &types.Message{TenantID: tenantID, Topic: "grp-test", SeqId: 7, Content: "plain"}
|
||
stored, err := encryptMessageContent(original)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if stored.KeyVersion != 0 || stored.Content != "plain" || len(stored.ContentNonce) != 0 {
|
||
t.Fatalf("unexpected encrypted message: %#v", stored)
|
||
}
|
||
}
|
||
|
||
func TestMessageCryptoRejectsTampering(t *testing.T) {
|
||
tenantID := types.TenantID(9003)
|
||
key := bytes.Repeat([]byte{9}, 32)
|
||
expires := time.Now().Add(time.Minute)
|
||
messageKeyCache.Lock()
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 0)] = cachedMessageKey{enabled: true, version: 4, key: key, expires: expires}
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 4)] = cachedMessageKey{enabled: true, version: 4, key: key, expires: expires}
|
||
messageKeyCache.Unlock()
|
||
defer InvalidateMessageEncryptionKeys(tenantID)
|
||
|
||
original := &types.Message{TenantID: tenantID, Topic: "p2p-secure", SeqId: 8, Content: map[string]any{"txt": "secret"}}
|
||
stored, err := encryptMessageContent(original)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
stored.Topic = "p2p-tampered"
|
||
if err = decryptMessageContent(stored); err == nil {
|
||
t.Fatal("expected authenticated decryption to reject changed topic")
|
||
}
|
||
|
||
stored, err = encryptMessageContent(original)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
stored.ContentNonce = stored.ContentNonce[:len(stored.ContentNonce)-1]
|
||
if err = decryptMessageContent(stored); err == nil {
|
||
t.Fatal("expected invalid nonce length to be rejected")
|
||
}
|
||
}
|
||
|
||
// 撤回消息 content 已被清 NULL,但 recall UPDATE 保留了 content_nonce/key_version;
|
||
// 读路径解密必须跳过此类行,否则单条撤回消息会把整个 get data 窗口毒化成 500(2026-10-08 测试环境实案)。
|
||
func TestMessageCryptoSkipsRecalledContent(t *testing.T) {
|
||
tenantID := types.TenantID(9004)
|
||
key := bytes.Repeat([]byte{5}, 32)
|
||
expires := time.Now().Add(time.Minute)
|
||
messageKeyCache.Lock()
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 0)] = cachedMessageKey{enabled: true, version: 2, key: key, expires: expires}
|
||
messageKeyCache.items[messageKeyCacheID(tenantID, 2)] = cachedMessageKey{enabled: true, version: 2, key: key, expires: expires}
|
||
messageKeyCache.Unlock()
|
||
defer InvalidateMessageEncryptionKeys(tenantID)
|
||
|
||
recalled := &types.Message{TenantID: tenantID, Topic: "p2p-recalled", SeqId: 64, KeyVersion: 2, ContentNonce: bytes.Repeat([]byte{1}, 12), Content: nil}
|
||
if err := decryptMessageContent(recalled); err != nil {
|
||
t.Fatalf("expected recalled message with nil content to be skipped, got: %v", err)
|
||
}
|
||
if recalled.Content != nil {
|
||
t.Fatalf("nil content should stay nil: %#v", recalled.Content)
|
||
}
|
||
}
|
||
|
||
func TestMessageCryptoSearchMatchesDecryptedContent(t *testing.T) {
|
||
content := map[string]any{"txt": "项目 验收通过", "nested": map[string]any{"title": "日报"}}
|
||
if !messageContentMatchesSearch(content, "验收") {
|
||
t.Fatal("expected plaintext keyword to match")
|
||
}
|
||
if messageContentMatchesSearch(content, "不存在") {
|
||
t.Fatal("unexpected keyword match")
|
||
}
|
||
}
|