-
Notifications
You must be signed in to change notification settings - Fork 2
fix: e2ee keys not exported correctly #204
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 |
|---|---|---|
|
|
@@ -157,6 +157,8 @@ type LineEmailLogin struct { | |
| AwaitingPIN bool | ||
| NoE2EE bool // True when login fell back to non-E2EE (LSOFF account) | ||
|
|
||
| ExistingMetadata *UserLoginMetadata | ||
|
|
||
| pollResult chan *line.LoginResult | ||
| pollErr chan error | ||
| polling bool | ||
|
|
@@ -197,10 +199,17 @@ func (ll *LineEmailLogin) StartWithOverride(ctx context.Context, override *bridg | |
| ll.Email = meta.Email | ||
| ll.Password = meta.Password | ||
| ll.Certificate = meta.Certificate | ||
| ll.ExistingMetadata = meta | ||
|
|
||
| if ll.Email == "" || ll.Password == "" { | ||
| return ll.loginErrorStep("No stored LINE credentials are available. Please enter your LINE email and password to reconnect."), nil | ||
| } | ||
| if len(meta.ExportedKeyMap) == 0 { | ||
| ll.Certificate = "" | ||
| if override.Bridge != nil { | ||
| override.Bridge.Log.Info().Msg("No stored LINE E2EE keys, forcing full reconnect") | ||
| } | ||
| } | ||
|
|
||
| override.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) | ||
|
|
||
|
|
@@ -435,7 +444,14 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult | |
|
|
||
| meta := &UserLoginMetadata{AccessToken: token, RefreshToken: refreshToken, Email: ll.Email, Password: ll.Password, Certificate: certificate, Mid: mid} | ||
|
|
||
| ll.fetchLoginKeys(res, meta, client) | ||
| exportedKeys := ll.fetchLoginKeys(res, meta, client) | ||
| if !exportedKeys && ll.ExistingMetadata != nil && len(ll.ExistingMetadata.ExportedKeyMap) > 0 { | ||
| copyLoginE2EEKeyMetadata(meta, ll.ExistingMetadata) | ||
| 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") | ||
|
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: every other user-facing failure in this file returns via |
||
| } | ||
|
|
||
| detectedLineID := networkid.UserLoginID(mid) | ||
|
|
||
|
|
@@ -499,28 +515,42 @@ func saveLoginE2EEKeyMetadata(meta *UserLoginMetadata, res *line.LoginResult) { | |
| meta.E2EEKeyID = res.E2EEKeyID | ||
| } | ||
|
|
||
| func copyLoginE2EEKeyMetadata(dst, src *UserLoginMetadata) { | ||
| dst.EncryptedKeyChain = src.EncryptedKeyChain | ||
| dst.E2EEPublicKey = src.E2EEPublicKey | ||
| dst.E2EEVersion = src.E2EEVersion | ||
| dst.E2EEKeyID = src.E2EEKeyID | ||
| if len(src.ExportedKeyMap) > 0 { | ||
| dst.ExportedKeyMap = make(map[string]string, len(src.ExportedKeyMap)) | ||
| for keyID, exported := range src.ExportedKeyMap { | ||
| dst.ExportedKeyMap[keyID] = exported | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func loginSecureDataID(meta *UserLoginMetadata, fallback string) string { | ||
| if meta.Mid != "" { | ||
| return meta.Mid | ||
| } | ||
| return fallback | ||
| } | ||
|
|
||
| func (ll *LineEmailLogin) fetchLoginKeys(res *line.LoginResult, meta *UserLoginMetadata, client *line.Client) { | ||
| func (ll *LineEmailLogin) fetchLoginKeys(res *line.LoginResult, meta *UserLoginMetadata, client *line.Client) bool { | ||
| if res.EncryptedKeyChain == "" || res.E2EEPublicKey == "" { | ||
| return | ||
| return false | ||
| } | ||
| mgr, exported, err := exportLoginE2EEKeys(res, client) | ||
| if err != nil { | ||
| ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to export E2EE keys") | ||
| return | ||
| return false | ||
| } | ||
| saveLoginE2EEKeyMetadata(meta, res) | ||
| meta.ExportedKeyMap = exported | ||
| if err := mgr.SaveSecureDataToFile(loginSecureDataID(meta, string(ll.User.MXID)), map[string]any{"exportedKeyMap": exported}); err != nil { | ||
| ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to save E2EE secure data") | ||
| } | ||
| ll.User.Bridge.Log.Info().Int("keys", len(exported)).Msg("Login: E2EE keys exported successfully") | ||
| return true | ||
| } | ||
|
|
||
| func (ll *LineEmailLogin) Cancel() {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,16 @@ func lineGroupE2EEReconnectRequiredError(err error) error { | |
| WithErrorReason(event.MessageStatusGenericError) | ||
| } | ||
|
|
||
| func lineGroupE2EEFetchFailureError(err error) error { | ||
| if err == nil || line.IsNoUsableE2EEGroupKey(err) { | ||
| return nil | ||
| } | ||
| if errStatus := lineGroupE2EEReconnectRequiredError(err); errStatus != nil { | ||
| return errStatus | ||
| } | ||
| return err | ||
|
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. Behavior change worth confirming: Previously the two group-encrypt branches only aborted on With
If the intent is only to bubble reconnect-required errors, consider returning |
||
| } | ||
|
|
||
| func (lc *LineClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { | ||
| client := lc.newClient() | ||
| callLineErr := func(call func(*line.Client) error) error { | ||
|
|
@@ -572,8 +582,8 @@ 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 errFetch = lineGroupE2EEFetchFailureError(errFetch); errFetch != nil { | ||
| return nil, errFetch | ||
| } | ||
| } | ||
| if contentType != int(ContentText) { | ||
|
|
@@ -588,8 +598,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 | ||
| } else if errFetch = lineGroupE2EEFetchFailureError(errFetch); errFetch != nil { | ||
| return nil, errFetch | ||
| } | ||
| if err != nil { | ||
| // E2EE setup failed — fall back to plain text | ||
|
|
||
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.
Latent: this copy has no
!res.NoE2EEguard, so if the newLoginResultis LSOFF (res.NoE2EE == true) with an empty keychain but the previous metadata was LSON,metaends 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.NoE2EEtoo so downstream code doesn't get contradictory metadata.