telegabber/e2ee/omemo/envelope_test.go
2026-07-29 06:06:59 -04:00

179 lines
4.9 KiB
Go

package omemo
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"reflect"
"testing"
)
// TestDecryptPayloadOmemo0LegacyKeyLayout covers the real-world interop
// fallback in decryptPayloadOmemo0: some senders (confirmed via
// Conversations' own git history and dino#303 - see that function's doc
// comment) put only the raw 16-byte AES key in <key> and let the GCM tag
// ride along appended to <payload> instead, rather than the current
// key||tag (32-byte) layout this package itself produces. This constructs
// that legacy layout directly (bypassing encryptPayloadOmemo0, which never
// produces it) to prove decryptPayloadOmemo0 still accepts it.
func TestDecryptPayloadOmemo0LegacyKeyLayout(t *testing.T) {
plaintext := []byte("hello from an older client")
key := make([]byte, omemo0KeyLen)
if _, err := rand.Read(key); err != nil {
t.Fatalf("rand.Read key: %v", err)
}
iv := make([]byte, omemo0IVLen)
if _, err := rand.Read(iv); err != nil {
t.Fatalf("rand.Read iv: %v", err)
}
block, err := aes.NewCipher(key)
if err != nil {
t.Fatalf("aes.NewCipher: %v", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
t.Fatalf("cipher.NewGCM: %v", err)
}
// The legacy layout: <payload> = ciphertext||tag (GCM's Seal output,
// kept whole), <key> = the raw 16-byte key (no tag appended).
sealed := gcm.Seal(nil, iv, plaintext, nil)
got, err := decryptPayloadOmemo0(sealed, iv, key)
if err != nil {
t.Fatalf("decryptPayloadOmemo0: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Fatalf("got %q, want %q", got, plaintext)
}
}
func TestEncryptOuterRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
version Version
}{
{"omemo0", Omemo0},
{"omemo1", Omemo1},
{"omemo2", Omemo2},
} {
t.Run(tc.name, func(t *testing.T) {
plaintext := []byte("hello from telegram")
payload, err := EncryptOuter(tc.version, plaintext, "gateway@example.com")
if err != nil {
t.Fatalf("EncryptOuter: %v", err)
}
if len(payload.Ciphertext) == 0 {
t.Fatalf("expected non-empty ciphertext")
}
if tc.version == Omemo0 && len(payload.IV) != omemo0IVLen {
t.Fatalf("expected a %d-byte IV for omemo0, got %d", omemo0IVLen, len(payload.IV))
}
if tc.version != Omemo0 && payload.IV != nil {
t.Fatalf("expected no IV for %s (receiver re-derives it), got %d bytes", tc.name, len(payload.IV))
}
got, err := DecryptOuter(tc.version, payload)
if err != nil {
t.Fatalf("DecryptOuter: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Fatalf("round trip mismatch: got %q, want %q", got, plaintext)
}
})
}
}
func TestEncryptOuterTamperDetection(t *testing.T) {
for _, tc := range []struct {
name string
version Version
}{
{"omemo0", Omemo0},
{"omemo1", Omemo1},
{"omemo2", Omemo2},
} {
t.Run(tc.name, func(t *testing.T) {
payload, err := EncryptOuter(tc.version, []byte("secret"), "gateway@example.com")
if err != nil {
t.Fatalf("EncryptOuter: %v", err)
}
tampered := *payload
tampered.Ciphertext = append([]byte{}, payload.Ciphertext...)
tampered.Ciphertext[0] ^= 0xFF
if _, err := DecryptOuter(tc.version, &tampered); err == nil {
t.Fatalf("expected DecryptOuter to reject tampered ciphertext")
}
})
}
}
func TestSCERoundTrip(t *testing.T) {
plaintext := []byte("hello sce")
t.Run("v0", func(t *testing.T) {
xmlBytes, err := sceEncodeV0(plaintext, "alice@example.com")
if err != nil {
t.Fatalf("sceEncodeV0: %v", err)
}
got, err := sceDecodeV0(xmlBytes)
if err != nil {
t.Fatalf("sceDecodeV0: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Fatalf("got %q, want %q", got, plaintext)
}
})
t.Run("v1", func(t *testing.T) {
xmlBytes, err := sceEncodeV1(plaintext, "alice@example.com")
if err != nil {
t.Fatalf("sceEncodeV1: %v", err)
}
got, err := sceDecodeV1(xmlBytes)
if err != nil {
t.Fatalf("sceDecodeV1: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Fatalf("got %q, want %q", got, plaintext)
}
})
}
func TestWireEnvelopeEncodeDecode(t *testing.T) {
env := &WireEnvelope{
Version: Omemo2,
SenderSID: 12345,
Payload: []byte("ciphertext-bytes"),
Keys: []WireKey{
{RecipientJID: "bob@example.com", DeviceID: 1, IsPreKey: true, Ciphertext: []byte("key-ciphertext-1")},
{RecipientJID: "bob@example.com", DeviceID: 2, IsPreKey: false, Ciphertext: []byte("key-ciphertext-2")},
},
}
raw, err := env.Encode()
if err != nil {
t.Fatalf("Encode: %v", err)
}
got, err := DecodeWireEnvelope(raw)
if err != nil {
t.Fatalf("DecodeWireEnvelope: %v", err)
}
if got.Version != env.Version || got.SenderSID != env.SenderSID || !bytes.Equal(got.Payload, env.Payload) {
t.Fatalf("round trip mismatch: got %+v", got)
}
if len(got.Keys) != len(env.Keys) {
t.Fatalf("expected %d keys, got %d", len(env.Keys), len(got.Keys))
}
for i := range env.Keys {
if !reflect.DeepEqual(got.Keys[i], env.Keys[i]) {
t.Fatalf("key %d mismatch: got %+v, want %+v", i, got.Keys[i], env.Keys[i])
}
}
}