Skip to content

fix: if user's keys are unavailable, attempt to relog to recover them#207

Merged
highesttt merged 2 commits into
mainfrom
highest/droid-78851
Jul 6, 2026
Merged

fix: if user's keys are unavailable, attempt to relog to recover them#207
highesttt merged 2 commits into
mainfrom
highest/droid-78851

Conversation

@highesttt

Copy link
Copy Markdown
Collaborator

No description provided.

@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

DROID-78851

@indent-zero

indent-zero Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes a class of LINE E2EE decryption failures where the bridge held some current key material but not the specific private key a message was encrypted to (e.g. after key rotation). Instead of silently logging warnings forever, the bridge now surfaces the condition via StateBadCredentials (line-e2ee-key-missing) so Beeper prompts the user to reconnect, and persists a ForceFullE2EELogin flag so the follow-up reconnect actually drops the cached certificate and stale key map. Follow-up commit hardens the notification path against concurrent decrypt goroutines and threads the recovery flag through the background token-recovery path.

  • pkg/e2ee/manager.go: channelFromKeyIDs now returns ErrMissingOwnPrivateKey (wrapping the missing raw key ID) when the peer's key is registered but neither side matches a local private key.
  • pkg/connector/client.go: adds markMissingE2EEKey guarded by a new missingE2EEKeyMu/missingE2EEKeyMarked pair so it fires exactly once per client until reset; sets ForceFullE2EELogin=true, clears Certificate, saves the metadata, and sends StateBadCredentials with UserActionRelogin. refreshLoginE2EEKeys now loads keys first, then calls the new applyRefreshedLoginE2EEKeys helper to update metadata and reset the marker.
  • pkg/connector/connector.go: adds UserLoginMetadata.ForceFullE2EELogin plus a shared applyExportedLoginE2EEKeys helper (used by both login and background recovery) that always clears the flag on a successful export. StartWithOverride honours the flag to force a full reconnect and shouldPreserveExistingE2EEKeys refuses to reuse the stale key map when it's set.
  • pkg/connector/handle_message.go: fixes the direct-DM retry to derive the peer's key ID from the correct chunk based on message direction; consolidates the group/direct log-context helpers into a single messageDecryptLogContext; invokes markMissingE2EEKey both when the retry still fails and when MyKeyIDs() reports no local key at all.
  • Tests: adds coverage for the new channelFromKeyIDs branch, for StartWithOverride forcing a full reconnect when the flag is set, for shouldPreserveExistingE2EEKeys, and for the new applyExportedLoginE2EEKeys helper clearing the recovery flag.

Issues

All clear! No issues remaining. 🎉

3 issues already resolved
  • refreshLoginE2EEKeys (background tryLogin recovery path) refreshes the exported key map but never clears meta.ForceFullE2EELogin, unlike its login-time counterpart fetchLoginKeys — so after a successful background recovery the stale flag persists and the next user-triggered StartWithOverride will still force a full reconnect (dropping the cached certificate) even though the keys are already valid. (fixed by commit 65601dd)
  • directDecryptLogContext duplicates groupDecryptLogContext almost verbatim; only the label of the last decoded key ID differs (receiver_key_id vs group_key_id). Consider parameterizing a single helper to avoid drift as either evolves. (fixed by commit 65601dd)
  • markMissingE2EEKey mutates UserLoginMetadata.ForceFullE2EELogin/Certificate and calls UserLogin.Save/BridgeState.Send without synchronization, but decryptMessageBody runs from up to 4 concurrent prefetch workers plus the poll loop — a persistent own-key gap will race on the metadata writes and stream duplicate line-e2ee-key-missing bridge-state events (one per failing message). (fixed by commit 65601dd)

CI Checks

Waiting for CI checks...

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Enhanced encrypted chat recovery on login/reconnect, triggering a full E2EE refresh when required and clearing outdated credentials.
  • Bug Fixes
    • Improved handling of missing LINE E2EE keys: users are prompted to reauthenticate with a clearer “key unavailable” message, and repeat prompts are avoided after a successful refresh.
    • Improved direct-message decryption by matching the exact sender/receiver key IDs and retrying with better fallback logic.

Walkthrough

This PR updates E2EE key resolution and login state handling so missing own private keys trigger a forced full reconnect, direct-message decrypt retries use direction-aware key selection, and refreshed login keys clear the reconnect marker.

Changes

E2EE missing key detection and forced reconnect

Layer / File(s) Summary
Detect missing own private key
pkg/e2ee/manager.go, pkg/e2ee/manager_test.go
channelFromKeyIDs returns ErrMissingOwnPrivateKey when a peer key is known but the own private key is missing; a test verifies the error and message content.
Force full reconnect in login metadata
pkg/connector/connector.go, pkg/connector/login_keys_test.go
UserLoginMetadata gains ForceFullE2EELogin; reconnect and key-preservation flow now honor it, and exported-key refresh clears it; tests cover the new behavior.
Mark missing E2EE key and clear marker
pkg/connector/client.go
LineClient adds deduplication state, markMissingE2EEKey emits a bad-credentials bridge state and forces reconnect, and refreshed keys clear the marker.
Direct decrypt retry and exact key selection
pkg/connector/handle_message.go
Shared decrypt logging is added for direct/group flows, and 1-1 decrypt retry now selects the exact key by direction before retrying and marking missing keys on failure.

Estimated code review effort: 4 (Complex) | ~55 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Handler as decryptMessageBody
  participant Client as LineClient
  participant E2EE as e2ee.Manager
  participant Login as StartWithOverride
  participant Meta as UserLoginMetadata

  Handler->>E2EE: DecryptMessageV2(message)
  E2EE-->>Handler: ErrMissingOwnPrivateKey
  Handler->>Handler: decode sender/receiver key IDs
  Handler->>E2EE: ensurePeerKeyByID / ensurePeerKey
  Handler->>Client: markMissingE2EEKey(err)
  Client->>Meta: set ForceFullE2EELogin
  Client->>Client: emit StateBadCredentials

  Login->>Meta: read ForceFullE2EELogin
  Login->>Login: clear Certificate if forced
  Login->>Meta: applyExportedLoginE2EEKeys(...)
  Meta->>Meta: reset ForceFullE2EELogin = false
Loading

Possibly related PRs

  • beeper/line#203: Both PRs change the login E2EE key refresh path in pkg/connector and touch the same reconnect/key-preservation flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No meaningful pull request description was provided, so its relation to the changes cannot be assessed. Add a short description of the functional changes and why the relogin/E2EE key recovery behavior was introduced.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: handling missing LINE E2EE keys by forcing a relogin to recover them.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch highest/droid-78851

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="Running error: context loading failed: failed to load packages: failed to load packages: failed to load with go/packages: context deadline exceeded"
level=error msg="Timeout exceeded: try increasing it by passing --timeout option"


Comment @coderabbitai help to get the list of available commands.

Comment thread pkg/connector/client.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/connector/client.go`:
- Around line 320-334: The early return in the `UserLoginMetadata` handling
block of `client.go` is preventing the bad-credentials bridge state from being
emitted when `needsSave` is false. Update the logic around `needsSave`,
`lc.missingE2EEKeyMu`, and `lc.UserLogin.Save(ctx)` so the flag only controls
whether the save is attempted, while the later `StateBadCredentials`
notification still runs for the current client lifetime. Keep the existing
metadata update to `ForceFullE2EELogin` and `Certificate`, but avoid returning
before the reconnect prompt path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59960cfd-fd52-4997-b8da-0720503d8221

📥 Commits

Reviewing files that changed from the base of the PR and between c1a370a and 65601dd.

📒 Files selected for processing (4)
  • pkg/connector/client.go
  • pkg/connector/connector.go
  • pkg/connector/handle_message.go
  • pkg/connector/login_keys_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/connector/handle_message.go
  • pkg/connector/connector.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use go fmt for code formatting across all Go files
Use goimports with -local "github.com/highesttt/matrix-line-messenger" flag to group project-local imports correctly
Use zerolog for logging throughout the codebase
Do not use Msgf in logging; use Msg with structured fields instead
Use Stringer interface where applicable in Go code

Files:

  • pkg/connector/login_keys_test.go
  • pkg/connector/client.go
**/!(ltsm)/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/!(ltsm)/**/*.go: Run staticcheck on all Go files excluding pkg/ltsm package (transpiled WASM code)
Run go vet on all Go files excluding pkg/ltsm package (transpiled WASM code)

Files:

  • pkg/connector/login_keys_test.go
  • pkg/connector/client.go
🔇 Additional comments (4)
pkg/connector/login_keys_test.go (1)

36-65: LGTM!

pkg/connector/client.go (3)

49-53: LGTM!


346-351: LGTM!


616-626: LGTM!

Comment thread pkg/connector/client.go
Comment on lines +320 to +334
if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok {
needsSave := !meta.ForceFullE2EELogin || meta.Certificate != ""
meta.ForceFullE2EELogin = true
meta.Certificate = ""
if !needsSave {
lc.missingE2EEKeyMu.Unlock()
return
}
lc.missingE2EEKeyMu.Unlock()
if errSave := lc.UserLogin.Save(ctx); errSave != nil && lc.UserLogin.Bridge != nil {
lc.UserLogin.Bridge.Log.Warn().Err(errSave).Msg("Failed to save LINE E2EE reconnect requirement")
}
} else {
lc.missingE2EEKeyMu.Unlock()
}

Copy link
Copy Markdown

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

Early return on !needsSave skips the bad-credentials bridge state.

When metadata already carries ForceFullE2EELogin=true and an empty Certificate (e.g., after a restart), needsSave is false and the function returns at Line 324–326. But missingE2EEKeyMarked was just set to true, so the StateBadCredentials emission at Line 338 never runs for this client lifetime and the user is never prompted to reconnect. The needsSave check should gate only the Save call, not the notification.

🐛 Proposed fix: skip only the Save, keep the notification
 	if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok {
 		needsSave := !meta.ForceFullE2EELogin || meta.Certificate != ""
 		meta.ForceFullE2EELogin = true
 		meta.Certificate = ""
-		if !needsSave {
-			lc.missingE2EEKeyMu.Unlock()
-			return
-		}
 		lc.missingE2EEKeyMu.Unlock()
-		if errSave := lc.UserLogin.Save(ctx); errSave != nil && lc.UserLogin.Bridge != nil {
-			lc.UserLogin.Bridge.Log.Warn().Err(errSave).Msg("Failed to save LINE E2EE reconnect requirement")
+		if needsSave {
+			if errSave := lc.UserLogin.Save(ctx); errSave != nil && lc.UserLogin.Bridge != nil {
+				lc.UserLogin.Bridge.Log.Warn().Err(errSave).Msg("Failed to save LINE E2EE reconnect requirement")
+			}
 		}
 	} else {
 		lc.missingE2EEKeyMu.Unlock()
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok {
needsSave := !meta.ForceFullE2EELogin || meta.Certificate != ""
meta.ForceFullE2EELogin = true
meta.Certificate = ""
if !needsSave {
lc.missingE2EEKeyMu.Unlock()
return
}
lc.missingE2EEKeyMu.Unlock()
if errSave := lc.UserLogin.Save(ctx); errSave != nil && lc.UserLogin.Bridge != nil {
lc.UserLogin.Bridge.Log.Warn().Err(errSave).Msg("Failed to save LINE E2EE reconnect requirement")
}
} else {
lc.missingE2EEKeyMu.Unlock()
}
if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok {
needsSave := !meta.ForceFullE2EELogin || meta.Certificate != ""
meta.ForceFullE2EELogin = true
meta.Certificate = ""
lc.missingE2EEKeyMu.Unlock()
if needsSave {
if errSave := lc.UserLogin.Save(ctx); errSave != nil && lc.UserLogin.Bridge != nil {
lc.UserLogin.Bridge.Log.Warn().Err(errSave).Msg("Failed to save LINE E2EE reconnect requirement")
}
}
} else {
lc.missingE2EEKeyMu.Unlock()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/client.go` around lines 320 - 334, The early return in the
`UserLoginMetadata` handling block of `client.go` is preventing the
bad-credentials bridge state from being emitted when `needsSave` is false.
Update the logic around `needsSave`, `lc.missingE2EEKeyMu`, and
`lc.UserLogin.Save(ctx)` so the flag only controls whether the save is
attempted, while the later `StateBadCredentials` notification still runs for the
current client lifetime. Keep the existing metadata update to
`ForceFullE2EELogin` and `Certificate`, but avoid returning before the reconnect
prompt path.

@highesttt highesttt merged commit 2772f67 into main Jul 6, 2026
10 checks passed
@highesttt highesttt deleted the highest/droid-78851 branch July 6, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant