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 and let the GCM tag // ride along appended to 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: = ciphertext||tag (GCM's Seal output, // kept whole), = 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]) } } }