-
Notifications
You must be signed in to change notification settings - Fork 2
fix: chat keys not updating when refreshing #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package connector | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/highesttt/matrix-line-messenger/pkg/e2ee" | ||
| "github.com/highesttt/matrix-line-messenger/pkg/line" | ||
| ) | ||
|
|
||
| func TestSaveLoginE2EEKeyMetadata(t *testing.T) { | ||
| meta := &UserLoginMetadata{} | ||
| res := &line.LoginResult{ | ||
| EncryptedKeyChain: "encrypted-keychain", | ||
| E2EEPublicKey: "public-key", | ||
| E2EEVersion: "2", | ||
| E2EEKeyID: "5625926", | ||
| } | ||
|
|
||
| saveLoginE2EEKeyMetadata(meta, res) | ||
|
|
||
| if meta.EncryptedKeyChain != res.EncryptedKeyChain || | ||
| meta.E2EEPublicKey != res.E2EEPublicKey || | ||
| meta.E2EEVersion != res.E2EEVersion || | ||
| meta.E2EEKeyID != res.E2EEKeyID { | ||
| t.Fatalf("metadata = %#v, want login E2EE fields copied", meta) | ||
| } | ||
| } | ||
|
|
||
| func TestLoginSecureDataIDPrefersLineMID(t *testing.T) { | ||
| meta := &UserLoginMetadata{Mid: "u-line-mid"} | ||
| if got := loginSecureDataID(meta, "@user:example.com"); got != "u-line-mid" { | ||
| t.Fatalf("loginSecureDataID = %q, want LINE MID", got) | ||
| } | ||
|
|
||
| meta.Mid = "" | ||
| if got := loginSecureDataID(meta, "@user:example.com"); got != "@user:example.com" { | ||
| t.Fatalf("loginSecureDataID fallback = %q, want Matrix fallback", got) | ||
| } | ||
| } | ||
|
|
||
| func TestRefreshLoginE2EEKeysKeepsMetadataOnExportFailure(t *testing.T) { | ||
| oldManager := newE2EEManager | ||
| exportErr := errors.New("manager unavailable") | ||
| newE2EEManager = func() (*e2ee.Manager, error) { | ||
| return nil, exportErr | ||
| } | ||
| t.Cleanup(func() { | ||
| newE2EEManager = oldManager | ||
| }) | ||
|
|
||
| meta := &UserLoginMetadata{ | ||
| EncryptedKeyChain: "old-keychain", | ||
| E2EEPublicKey: "old-public-key", | ||
| E2EEVersion: "1", | ||
| E2EEKeyID: "old-key-id", | ||
| ExportedKeyMap: map[string]string{"old-key-id": "old-export"}, | ||
| } | ||
| res := &line.LoginResult{ | ||
| EncryptedKeyChain: "new-keychain", | ||
| E2EEPublicKey: "new-public-key", | ||
| E2EEVersion: "2", | ||
| E2EEKeyID: "new-key-id", | ||
| } | ||
|
|
||
| err := (&LineClient{}).refreshLoginE2EEKeys(res, meta, nil) | ||
| if !errors.Is(err, exportErr) { | ||
| t.Fatalf("err = %v, want %v", err, exportErr) | ||
| } | ||
| if meta.EncryptedKeyChain != "old-keychain" || | ||
| meta.E2EEPublicKey != "old-public-key" || | ||
| meta.E2EEVersion != "1" || | ||
| meta.E2EEKeyID != "old-key-id" || | ||
| meta.ExportedKeyMap["old-key-id"] != "old-export" { | ||
| t.Fatalf("metadata was clobbered on export failure: %#v", meta) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "image" | ||
| "image/jpeg" | ||
|
|
@@ -18,6 +19,7 @@ import ( | |
| "maunium.net/go/mautrix/bridgev2/networkid" | ||
| "maunium.net/go/mautrix/event" | ||
|
|
||
| "github.com/highesttt/matrix-line-messenger/pkg/e2ee" | ||
| "github.com/highesttt/matrix-line-messenger/pkg/line" | ||
| ) | ||
|
|
||
|
|
@@ -30,6 +32,20 @@ type mentionEntry struct { | |
|
|
||
| var mentionLinkRegex = regexp.MustCompile(`<a\s+[^>]*href="https://matrix\.to/#/([^"]+)"[^>]*>([^<]+)</a>`) | ||
|
|
||
| const lineGroupE2EEReconnectNotice = "LINE encryption keys for this group are unavailable. Reconnect LINE in Beeper, then try sending again." | ||
|
|
||
| func lineGroupE2EEReconnectRequiredError(err error) error { | ||
| if !errors.Is(err, e2ee.ErrMissingOwnPrivateKey) { | ||
| return nil | ||
| } | ||
| return bridgev2.WrapErrorInStatus(fmt.Errorf("%s: %w", lineGroupE2EEReconnectNotice, err)). | ||
| WithStatus(event.MessageStatusFail). | ||
| WithIsCertain(true). | ||
| WithMessage(lineGroupE2EEReconnectNotice). | ||
| WithSendNotice(true). | ||
| WithErrorReason(event.MessageStatusGenericError) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit — reconnect notice can loop. |
||
| } | ||
|
|
||
| func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { | ||
| client := lc.newClient() | ||
| callLineErr := func(call func(*line.Client) error) error { | ||
|
|
@@ -556,6 +572,9 @@ func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Mat | |
| if isGroup { | ||
| if errFetch := lc.fetchAndUnwrapGroupKey(ctx, portalMid, 0); errFetch != nil { | ||
| lc.UserLogin.Bridge.Log.Debug().Err(errFetch).Str("chat_mid", portalMid).Msg("fetchAndUnwrapGroupKey before encrypt failed") | ||
| if errStatus := lineGroupE2EEReconnectRequiredError(errFetch); errStatus != nil { | ||
| return nil, errStatus | ||
| } | ||
| } | ||
| if contentType != int(ContentText) { | ||
| chunks, err = lc.E2EE.EncryptGroupMessageRaw(portalMid, fromMid, contentType, payload) | ||
|
|
@@ -569,6 +588,8 @@ func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Mat | |
| } else { | ||
| chunks, err = lc.E2EE.EncryptGroupMessage(portalMid, fromMid, msg.Content.Body) | ||
| } | ||
| } else if errStatus := lineGroupE2EEReconnectRequiredError(errFetch); errStatus != nil { | ||
| return nil, errStatus | ||
| } | ||
| if err != nil { | ||
| // E2EE setup failed — fall back to plain text | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package connector | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "maunium.net/go/mautrix/bridgev2" | ||
| "maunium.net/go/mautrix/event" | ||
|
|
||
| "github.com/highesttt/matrix-line-messenger/pkg/e2ee" | ||
| "github.com/highesttt/matrix-line-messenger/pkg/line" | ||
| ) | ||
|
|
||
| func TestLineGroupE2EEReconnectRequiredError(t *testing.T) { | ||
| err := lineGroupE2EEReconnectRequiredError(fmt.Errorf("failed to unwrap group key: %w for 5625926", e2ee.ErrMissingOwnPrivateKey)) | ||
| if err == nil { | ||
| t.Fatal("expected missing private key to produce a message status error") | ||
| } | ||
|
|
||
| var status bridgev2.MessageStatus | ||
| if !errors.As(err, &status) { | ||
| t.Fatalf("error %T does not wrap bridgev2.MessageStatus", err) | ||
| } | ||
| if status.Status != event.MessageStatusFail { | ||
| t.Fatalf("Status = %q, want %q", status.Status, event.MessageStatusFail) | ||
| } | ||
| if !status.IsCertain || !status.SendNotice { | ||
| t.Fatalf("status certainty/notice flags = %v/%v, want true/true", status.IsCertain, status.SendNotice) | ||
| } | ||
| if status.Message != lineGroupE2EEReconnectNotice { | ||
| t.Fatalf("Message = %q, want %q", status.Message, lineGroupE2EEReconnectNotice) | ||
| } | ||
| if !errors.Is(err, e2ee.ErrMissingOwnPrivateKey) { | ||
| t.Fatalf("err = %v, want to wrap ErrMissingOwnPrivateKey", err) | ||
| } | ||
| } | ||
|
|
||
| func TestLineGroupE2EEReconnectRequiredErrorIgnoresNoUsableGroupKey(t *testing.T) { | ||
| err := lineGroupE2EEReconnectRequiredError(line.ErrNoUsableE2EEGroupKey) | ||
| if err != nil { | ||
| t.Fatalf("err = %v, want nil", err) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package e2ee | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/highesttt/matrix-line-messenger/pkg/line" | ||
| ) | ||
|
|
||
| func TestUnwrapGroupSharedKeyReturnsMissingOwnPrivateKey(t *testing.T) { | ||
| manager := &Manager{ | ||
| peerPublic: map[int]string{1234: "creator-public-key"}, | ||
| keyByRawID: map[int]int{}, | ||
| } | ||
|
|
||
| _, err := manager.UnwrapGroupSharedKey("c-group", &line.E2EEGroupSharedKey{ | ||
| CreatorKeyID: 1234, | ||
| ReceiverKeyID: 5625926, | ||
| }) | ||
| if !errors.Is(err, ErrMissingOwnPrivateKey) { | ||
| t.Fatalf("err = %v, want ErrMissingOwnPrivateKey", err) | ||
| } | ||
| } | ||
|
|
||
| func TestEncryptGroupMessageRawReturnsGroupKeyNotLoaded(t *testing.T) { | ||
| manager := &Manager{ | ||
| sequence: map[string]int{}, | ||
| groupKeys: map[string]map[int]int{}, | ||
| latestGroupKey: map[string]int{}, | ||
| } | ||
|
|
||
| _, err := manager.EncryptGroupMessageRaw("c-group", "u-sender", 0, []byte(`{"text":"hello"}`)) | ||
| if !errors.Is(err, ErrGroupKeyNotLoaded) { | ||
| t.Fatalf("err = %v, want ErrGroupKeyNotLoaded", err) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Install the freshly initialized manager when
lc.E2EEis nil.exportLoginE2EEKeysreturns an initialized manager, but this branch drops it when no active manager exists. That leaves the re-logged-in client without in-memory E2EE keys even after a successful refresh.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents