Skip to content

Commit 1bff055

Browse files
authored
fix: LTSM aborts caused encryption notices and a failed image send (#233)
1 parent 62ecc27 commit 1bff055

15 files changed

Lines changed: 448 additions & 138 deletions

pkg/connector/handle_message.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package connector
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"html"
89
"sort"
@@ -20,6 +21,7 @@ import (
2021
"github.com/highesttt/matrix-line-messenger/pkg/connector/handlers"
2122
"github.com/highesttt/matrix-line-messenger/pkg/e2ee"
2223
"github.com/highesttt/matrix-line-messenger/pkg/line"
24+
"github.com/highesttt/matrix-line-messenger/pkg/ltsm"
2325
)
2426

2527
const (
@@ -265,8 +267,8 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string,
265267
decryptionFailed = false
266268
} else {
267269
groupDecryptLogContext(lc.UserLogin.Bridge.Log.Debug().Err(err), msg, portalIDStr, opType).
268-
Msg("DecryptGroupMessage failed, trying to fetch key")
269-
if keyID != 0 {
270+
Msg("DecryptGroupMessage failed")
271+
if !errors.Is(err, ltsm.ErrAbort) && keyID != 0 {
270272
if errFetch := lc.fetchAndUnwrapGroupKey(context.Background(), portalIDStr, keyID); errFetch != nil {
271273
groupDecryptLogContext(lc.UserLogin.Bridge.Log.Warn().Err(errFetch), msg, portalIDStr, opType).
272274
Msg("Failed to fetch/unwrap group key")
@@ -287,7 +289,10 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string,
287289
} else {
288290
directDecryptLogContext(lc.UserLogin.Bridge.Log.Debug().Err(err), msg, portalIDStr, opType).
289291
Msg("DecryptMessageV2 failed on first attempt")
290-
if _, _, errKey := lc.E2EE.MyKeyIDs(); errKey != nil {
292+
if errors.Is(err, ltsm.ErrAbort) {
293+
directDecryptLogContext(lc.UserLogin.Bridge.Log.Warn().Err(err), msg, portalIDStr, opType).
294+
Msg("LTSM runtime aborted; skipping key refresh")
295+
} else if _, _, errKey := lc.E2EE.MyKeyIDs(); errKey != nil {
291296
directDecryptLogContext(lc.UserLogin.Bridge.Log.Error().Err(errKey), msg, portalIDStr, opType).
292297
Msg("E2EE own key not loaded; cannot decrypt any messages. Re-login required")
293298
lc.markMissingE2EEKey(context.Background(), fmt.Errorf("%w: %v", e2ee.ErrMissingOwnPrivateKey, errKey))

pkg/connector/handlers/audio.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,35 +68,10 @@ func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, int
6868
return mediaDownloadFailure("Audio", err, relatesTo)
6969
}
7070

71-
// Decrypt audio if it has keyMaterial (E2EE)
72-
decrypted := false
73-
if decryptedBody != "" && strings.Contains(decryptedBody, "keyMaterial") {
74-
var decryptInfo struct {
75-
KeyMaterial string `json:"keyMaterial"`
76-
}
77-
if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err == nil && decryptInfo.KeyMaterial != "" {
78-
decryptedAudio, err := h.DecryptMedia(audioData, decryptInfo.KeyMaterial)
79-
if err != nil {
80-
h.Log.Error().Err(err).Msg("Failed to decrypt audio data")
81-
return nil, fmt.Errorf("failed to decrypt audio data: %w", err)
82-
}
83-
audioData = decryptedAudio
84-
decrypted = true
85-
}
86-
}
87-
88-
// ENC_KM is a fallback when the in-body keyMaterial path didn't decrypt
89-
// (e.g. E2EE chunk decryption failed). Running it unconditionally would
90-
// double-decrypt for bridge-sent LSON audio and corrupt the bytes.
91-
if !decrypted {
92-
if encKM := data.ContentMetadata["ENC_KM"]; encKM != "" && len(audioData) > 32 {
93-
decryptedAudio, err := h.DecryptMedia(audioData, encKM)
94-
if err != nil {
95-
h.Log.Warn().Err(err).Msg("ENC_KM fallback decrypt failed, sending raw audio")
96-
} else {
97-
audioData = decryptedAudio
98-
}
99-
}
71+
audioData, err = h.decryptDownloadedMedia(audioData, decryptedBody, data.ContentMetadata, "audio")
72+
if err != nil {
73+
h.Log.Error().Err(err).Msg("Failed to decrypt audio data")
74+
return nil, err
10075
}
10176

10277
if oversized := h.oversizedMediaNotice(int64(len(audioData)), "downloaded", relatesTo); oversized != nil {

pkg/connector/handlers/file.go

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,39 +58,22 @@ func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, inte
5858
return mediaDownloadFailure("File", err, relatesTo)
5959
}
6060

61-
// Try to decrypt using keyMaterial from encrypted payload
6261
var fileName string
63-
if decryptedBody != "" && strings.Contains(decryptedBody, "keyMaterial") {
64-
var decryptInfo struct {
65-
KeyMaterial string `json:"keyMaterial"`
66-
FileName string `json:"fileName"`
62+
if strings.Contains(decryptedBody, "fileName") {
63+
var fileInfo struct {
64+
FileName string `json:"fileName"`
6765
}
68-
if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err != nil {
66+
if err := json.Unmarshal([]byte(decryptedBody), &fileInfo); err != nil {
6967
h.Log.Error().Err(err).Msg("Failed to parse file payload JSON")
7068
return nil, fmt.Errorf("failed to parse file payload: %w", err)
7169
}
70+
fileName = fileInfo.FileName
71+
}
7272

73-
if decryptInfo.KeyMaterial != "" {
74-
keyPreview := decryptInfo.KeyMaterial
75-
if len(keyPreview) > 20 {
76-
keyPreview = keyPreview[:20] + "..."
77-
}
78-
h.Log.Debug().
79-
Str("key_material_preview", keyPreview).
80-
Msg("Decrypting file using keyMaterial from payload")
81-
82-
decryptedFile, err := h.DecryptMedia(fileData, decryptInfo.KeyMaterial)
83-
if err != nil {
84-
h.Log.Error().Err(err).Msg("Failed to decrypt file data")
85-
return nil, fmt.Errorf("failed to decrypt file data: %w", err)
86-
}
87-
fileData = decryptedFile
88-
h.Log.Info().Int("decrypted_size", len(fileData)).Msg("Successfully decrypted file")
89-
}
90-
91-
if decryptInfo.FileName != "" {
92-
fileName = decryptInfo.FileName
93-
}
73+
fileData, err = h.decryptDownloadedMedia(fileData, decryptedBody, data.ContentMetadata, "file")
74+
if err != nil {
75+
h.Log.Error().Err(err).Msg("Failed to decrypt file data")
76+
return nil, err
9477
}
9578

9679
if oversized := h.oversizedMediaNotice(int64(len(fileData)), "downloaded", relatesTo); oversized != nil {

pkg/connector/handlers/handler.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"net/http"
@@ -36,6 +37,51 @@ type Handler struct {
3637
DecryptMedia func(data []byte, keyMaterial string) ([]byte, error)
3738
}
3839

40+
func (h *Handler) decryptDownloadedMedia(data []byte, decryptedBody string, metadata map[string]string, kind string) ([]byte, error) {
41+
var bodyKey string
42+
var bodyKeyDeclared bool
43+
if strings.Contains(decryptedBody, "keyMaterial") {
44+
var decryptInfo map[string]json.RawMessage
45+
if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err != nil {
46+
return nil, fmt.Errorf("%w: failed to parse encrypted %s payload: %w", bridgev2.ErrIgnoringRemoteEvent, strings.ToLower(kind), err)
47+
}
48+
if rawKey, ok := decryptInfo["keyMaterial"]; ok {
49+
bodyKeyDeclared = true
50+
if err := json.Unmarshal(rawKey, &bodyKey); err != nil {
51+
return nil, fmt.Errorf("%w: failed to parse encrypted %s key material: %w", bridgev2.ErrIgnoringRemoteEvent, strings.ToLower(kind), err)
52+
}
53+
}
54+
}
55+
56+
keys := make([]string, 0, 2)
57+
if bodyKey != "" {
58+
keys = append(keys, bodyKey)
59+
}
60+
encKM, metadataKeyDeclared := metadata["ENC_KM"]
61+
if encKM != "" && encKM != bodyKey {
62+
keys = append(keys, encKM)
63+
}
64+
if len(keys) == 0 {
65+
if bodyKeyDeclared || metadataKeyDeclared {
66+
return nil, fmt.Errorf("%w: encrypted %s has no usable media key", bridgev2.ErrIgnoringRemoteEvent, strings.ToLower(kind))
67+
}
68+
return data, nil
69+
}
70+
if h.DecryptMedia == nil {
71+
return nil, fmt.Errorf("%w: encrypted %s has no media decryptor", bridgev2.ErrIgnoringRemoteEvent, strings.ToLower(kind))
72+
}
73+
74+
decryptErrors := make([]error, 0, len(keys))
75+
for _, key := range keys {
76+
decrypted, err := h.DecryptMedia(data, key)
77+
if err == nil {
78+
return decrypted, nil
79+
}
80+
decryptErrors = append(decryptErrors, err)
81+
}
82+
return nil, fmt.Errorf("%w: failed to decrypt %s data: %w", bridgev2.ErrIgnoringRemoteEvent, strings.ToLower(kind), errors.Join(decryptErrors...))
83+
}
84+
3985
func (h *Handler) downloadAlbumPreview(ctx context.Context, client *line.Client, oid, chatID, albumID string) ([]byte, error) {
4086
if h.DownloadAlbumPreview != nil {
4187
return h.DownloadAlbumPreview(ctx, client, oid, chatID, albumID)

pkg/connector/handlers/handler_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
7+
"fmt"
68
"testing"
79

810
"maunium.net/go/mautrix/bridgev2"
@@ -11,6 +13,98 @@ import (
1113
"github.com/highesttt/matrix-line-messenger/pkg/line"
1214
)
1315

16+
func TestDecryptDownloadedMediaUsesBodyKey(t *testing.T) {
17+
ciphertext := []byte("ciphertext")
18+
plaintext := []byte("plaintext")
19+
var keys []string
20+
h := &Handler{DecryptMedia: func(data []byte, key string) ([]byte, error) {
21+
if !bytes.Equal(data, ciphertext) {
22+
t.Fatalf("decrypt input = %q, want ciphertext", data)
23+
}
24+
keys = append(keys, key)
25+
return plaintext, nil
26+
}}
27+
28+
got, err := h.decryptDownloadedMedia(ciphertext, `{"keyMaterial":"body-key"}`, map[string]string{"ENC_KM": "metadata-key"}, "image")
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
if !bytes.Equal(got, plaintext) {
33+
t.Fatalf("decrypted data = %q, want %q", got, plaintext)
34+
}
35+
if len(keys) != 1 || keys[0] != "body-key" {
36+
t.Fatalf("keys = %v, want body key only", keys)
37+
}
38+
}
39+
40+
func TestDecryptDownloadedMediaFallsBackToENCKM(t *testing.T) {
41+
ciphertext := []byte("ciphertext")
42+
var keys []string
43+
h := &Handler{DecryptMedia: func(data []byte, key string) ([]byte, error) {
44+
if !bytes.Equal(data, ciphertext) {
45+
t.Fatalf("decrypt input = %q, want original ciphertext", data)
46+
}
47+
keys = append(keys, key)
48+
if key == "body-key" {
49+
return nil, errors.New("body key failed")
50+
}
51+
return []byte("metadata plaintext"), nil
52+
}}
53+
54+
got, err := h.decryptDownloadedMedia(ciphertext, `{"keyMaterial":"body-key"}`, map[string]string{"ENC_KM": "metadata-key"}, "file")
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
if string(got) != "metadata plaintext" {
59+
t.Fatalf("decrypted data = %q", got)
60+
}
61+
if fmt.Sprint(keys) != "[body-key metadata-key]" {
62+
t.Fatalf("keys = %v, want body then metadata", keys)
63+
}
64+
}
65+
66+
func TestDecryptDownloadedMediaPassesThroughPlainMedia(t *testing.T) {
67+
data := []byte("plain media")
68+
got, err := new(Handler).decryptDownloadedMedia(data, "", nil, "audio")
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
if !bytes.Equal(got, data) {
73+
t.Fatalf("data = %q, want unchanged media", got)
74+
}
75+
}
76+
77+
func TestDecryptDownloadedMediaRejectsEmptyDeclaredKey(t *testing.T) {
78+
got, err := new(Handler).decryptDownloadedMedia([]byte("ciphertext"), `{"keyMaterial":""}`, nil, "image")
79+
if got != nil {
80+
t.Fatalf("data = %q, want no ciphertext returned", got)
81+
}
82+
if !errors.Is(err, bridgev2.ErrIgnoringRemoteEvent) {
83+
t.Fatalf("err = %v, want ErrIgnoringRemoteEvent", err)
84+
}
85+
}
86+
87+
func TestDecryptDownloadedMediaFailsClosed(t *testing.T) {
88+
ciphertext := []byte("ciphertext")
89+
decryptErr := errors.New("invalid media key")
90+
var calls int
91+
h := &Handler{DecryptMedia: func([]byte, string) ([]byte, error) {
92+
calls++
93+
return nil, decryptErr
94+
}}
95+
96+
got, err := h.decryptDownloadedMedia(ciphertext, `{"keyMaterial":"body-key"}`, map[string]string{"ENC_KM": "metadata-key"}, "video")
97+
if got != nil {
98+
t.Fatalf("data = %q, want no ciphertext returned", got)
99+
}
100+
if calls != 2 {
101+
t.Fatalf("decrypt calls = %d, want both declared keys attempted", calls)
102+
}
103+
if !errors.Is(err, decryptErr) || !errors.Is(err, bridgev2.ErrIgnoringRemoteEvent) {
104+
t.Fatalf("err = %v, want decrypt error and ErrIgnoringRemoteEvent", err)
105+
}
106+
}
107+
14108
func TestTryRecoverClientPassesOriginatingClient(t *testing.T) {
15109
errAuth := errors.New("SSE error: 401")
16110
var recoverCalled bool

pkg/connector/handlers/image.go

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"strings"
87
"time"
98

109
"maunium.net/go/mautrix/bridgev2"
@@ -75,27 +74,17 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int
7574
return mediaDownloadFailure("Image", err, relatesTo)
7675
}
7776

78-
// Decrypt image if it has keyMaterial (E2EE)
79-
var decryptDuration time.Duration
80-
if decryptedBody != "" && strings.Contains(decryptedBody, "keyMaterial") {
81-
var decryptInfo struct {
82-
KeyMaterial string `json:"keyMaterial"`
83-
FileName string `json:"fileName"`
84-
}
85-
if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err == nil && decryptInfo.KeyMaterial != "" {
86-
decryptStart := time.Now()
87-
decryptedImg, err := h.DecryptMedia(imgData, decryptInfo.KeyMaterial)
88-
decryptDuration = time.Since(decryptStart)
89-
if err != nil {
90-
h.Log.Error().
91-
Err(err).
92-
Dur("download_duration", downloadDuration).
93-
Dur("decrypt_duration", decryptDuration).
94-
Msg("Failed to decrypt image data")
95-
return nil, fmt.Errorf("failed to decrypt image data: %w", err)
96-
}
97-
imgData = decryptedImg
98-
}
77+
// Decrypt encrypted media before it can reach Matrix.
78+
decryptStart := time.Now()
79+
imgData, err = h.decryptDownloadedMedia(imgData, decryptedBody, data.ContentMetadata, "image")
80+
decryptDuration := time.Since(decryptStart)
81+
if err != nil {
82+
h.Log.Error().
83+
Err(err).
84+
Dur("download_duration", downloadDuration).
85+
Dur("decrypt_duration", decryptDuration).
86+
Msg("Failed to decrypt image data")
87+
return nil, err
9988
}
10089

10190
if oversized := h.oversizedMediaNotice(int64(len(imgData)), "downloaded", relatesTo); oversized != nil {

pkg/connector/handlers/video.go

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -71,46 +71,10 @@ func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, int
7171
return mediaDownloadFailure("Video", err, relatesTo)
7272
}
7373

74-
decrypted := false
75-
if decryptedBody != "" && strings.Contains(decryptedBody, "keyMaterial") {
76-
var decryptInfo struct {
77-
KeyMaterial string `json:"keyMaterial"`
78-
FileName string `json:"fileName"`
79-
}
80-
if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err == nil && decryptInfo.KeyMaterial != "" {
81-
h.Log.Debug().
82-
Str("key_material_len", fmt.Sprintf("%d", len(decryptInfo.KeyMaterial))).
83-
Str("file_name", decryptInfo.FileName).
84-
Msg("Decrypting E2EE video")
85-
86-
decryptedVideo, err := h.DecryptMedia(videoData, decryptInfo.KeyMaterial)
87-
if err != nil {
88-
h.Log.Error().Err(err).Msg("Failed to decrypt video data")
89-
return nil, fmt.Errorf("failed to decrypt video data: %w", err)
90-
}
91-
videoData = decryptedVideo
92-
decrypted = true
93-
h.Log.Info().Int("decrypted_size", len(videoData)).Msg("Successfully decrypted video")
94-
}
95-
}
96-
97-
// ENC_KM is a fallback when the in-body keyMaterial path didn't decrypt
98-
// (e.g. E2EE chunk decryption failed). Running it unconditionally would
99-
// double-decrypt for bridge-sent LSON videos and corrupt the bytes.
100-
if !decrypted {
101-
if encKM := data.ContentMetadata["ENC_KM"]; encKM != "" && len(videoData) > 32 {
102-
h.Log.Debug().
103-
Str("enc_km_preview", encKM[:min(20, len(encKM))]+"...").
104-
Msg("Decrypting video using ENC_KM from metadata (fallback)")
105-
106-
decryptedVideo, err := h.DecryptMedia(videoData, encKM)
107-
if err != nil {
108-
h.Log.Warn().Err(err).Msg("ENC_KM fallback decrypt failed, sending raw video")
109-
} else {
110-
videoData = decryptedVideo
111-
h.Log.Info().Int("decrypted_size", len(videoData)).Msg("Successfully decrypted video from ENC_KM")
112-
}
113-
}
74+
videoData, err = h.decryptDownloadedMedia(videoData, decryptedBody, data.ContentMetadata, "video")
75+
if err != nil {
76+
h.Log.Error().Err(err).Msg("Failed to decrypt video data")
77+
return nil, err
11478
}
11579

11680
if oversized := h.oversizedMediaNotice(int64(len(videoData)), "downloaded", relatesTo); oversized != nil {

0 commit comments

Comments
 (0)