mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
179 lines
4.9 KiB
Go
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])
|
|
}
|
|
}
|
|
}
|