Skip to content

fix: e2ee keys not exported correctly#204

Merged
highesttt merged 1 commit into
mainfrom
highest/plat-37742
Jul 1, 2026
Merged

fix: e2ee keys not exported correctly#204
highesttt merged 1 commit into
mainfrom
highest/plat-37742

Conversation

@highesttt

Copy link
Copy Markdown
Collaborator

No description provided.

@linear-code

linear-code Bot commented Jul 1, 2026

Copy link
Copy Markdown

PLAT-37742

@indent-zero

indent-zero Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes an E2EE-key export/persistence bug on re-login and adds group-key self-healing when the latest group key targets a private key we no longer hold. Prevents silent E2EE downgrades after certificate-based re-authentication and lets group sends recover automatically instead of always requiring a full user reconnect.

  • StartWithOverride now blanks the stored certificate when no ExportedKeyMap is present, forcing LINE back through the PIN flow so a fresh keychain is issued; the prior metadata is captured on the login struct.
  • finishLogin preserves the previous E2EE metadata (via new copyLoginE2EEKeyMetadata) when the new login result carries no keychain, and errors out loudly if an E2EE-eligible login still has no exported keys.
  • fetchLoginKeys now returns a bool so callers can distinguish "no keys exported" from "keys stored".
  • fetchAndUnwrapGroupKey retries with a freshly-registered group key when the latest key can't be unwrapped due to ErrMissingOwnPrivateKey (only for the groupKeyID == 0 path).
  • New lineGroupE2EEFetchFailureError helper centralizes group-key fetch error handling in HandleMatrixMessage, ignoring benign NoUsableE2EEGroupKey, surfacing the reconnect status for ErrMissingOwnPrivateKey, and passing other errors through.
  • Adds unit tests covering the certificate-clearing branch, ExportedKeyMap cloning, and the new fetch-failure classification.

Issues

3 potential issues found:

  • copyLoginE2EEKeyMetadata runs even when res.NoE2EE == true, so a re-login that reports LSOFF still inherits the old EncryptedKeyChain/E2EEPublicKey/ExportedKeyMap from ExistingMetadata, leaving E2EE fields populated on a non-E2EE login. → Autofix
  • The new "please reconnect again and complete the LINE verification prompt" failure in finishLogin returns a bare fmt.Errorf instead of a loginErrorStep, so unlike every other user-facing failure in this file the user won't get a re-prompted login step — just a raw bridge error string. → Autofix
  • Group-send behavior change: lineGroupE2EEFetchFailureError now surfaces any non-nil, non-NoUsableE2EEGroupKey fetch error as a raw error to Matrix, so transient failures (network blips, getChats failed for …, autoRegisterGroupKey failures) that were previously logged-and-ignored — allowing encrypt with cached state or plaintext fallback — now hard-fail the send without a wrapped MessageStatus. → Autofix

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch highest/plat-37742

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

if errStatus := lineGroupE2EEReconnectRequiredError(err); errStatus != nil {
return errStatus
}
return err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Behavior change worth confirming: Previously the two group-encrypt branches only aborted on ErrMissingOwnPrivateKey and logged/ignored every other fetchAndUnwrapGroupKey error, so a transient LINE API failure (e.g. getChats failed for X: connection refused, autoRegisterGroupKey failure, token-recovery failure) would fall through to EncryptGroupMessage with cached state or eventually the plaintext fallback at line 605.

With lineGroupE2EEFetchFailureError, any non-NoUsableE2EEGroupKey error is now returned as-is to Matrix (unwrapped — no MessageStatus, no reconnect notice), which:

  1. Turns transient errors into hard user-visible send failures instead of best-effort delivery.
  2. Skips the plaintext-fallback path in the retry branch (line ~605) entirely for these errors.
  3. Surfaces raw fmt.Errorf strings like "getChats failed for X: %w" to end users instead of the actionable reconnect notice.

If the intent is only to bubble reconnect-required errors, consider returning nil (instead of err) from the default branch of lineGroupE2EEFetchFailureError to preserve the previous "log and continue" semantics.


ll.fetchLoginKeys(res, meta, client)
exportedKeys := ll.fetchLoginKeys(res, meta, client)
if !exportedKeys && ll.ExistingMetadata != nil && len(ll.ExistingMetadata.ExportedKeyMap) > 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Latent: this copy has no !res.NoE2EE guard, so if the new LoginResult is LSOFF (res.NoE2EE == true) with an empty keychain but the previous metadata was LSON, meta ends up with a full E2EE block on a NoE2EE login. In practice accounts rarely downgrade LSON→LSOFF, but the guard on line 452 (!res.NoE2EE && len(meta.ExportedKeyMap) == 0) implies you already treat those two states as mutually exclusive — consider gating the copy on !res.NoE2EE too so downstream code doesn't get contradictory metadata.

ll.User.Bridge.Log.Info().Int("keys", len(meta.ExportedKeyMap)).Msg("Preserved existing E2EE keys after re-login")
}
if !res.NoE2EE && len(meta.ExportedKeyMap) == 0 {
return nil, fmt.Errorf("LINE login completed without E2EE keychain; please reconnect again and complete the LINE verification prompt")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: every other user-facing failure in this file returns via ll.loginErrorStep(...) (see lines 205, 222, 248) so the user gets a re-prompt with the error message inline. This branch instead returns a raw fmt.Errorf, which bubbles up to the bridge framework as a plain error string — less actionable UX than the surrounding paths.

@highesttt highesttt merged commit db69682 into main Jul 1, 2026
9 of 10 checks passed
@highesttt highesttt deleted the highest/plat-37742 branch July 1, 2026 16:00
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