From 2990e09c6c5276882a0c6d80ccaaa846dffeef0f Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sat, 24 Oct 2026 18:28:03 +0000 Subject: [PATCH 1/5] fix: stop writing E2EE key files to a missing data/data folder The bridge saved an encrypted copy of the login key map to data/lcs_secure_.json, relative to its working directory. Docker and the README's native steps both start the bridge inside data/, so the write targeted data/data/ and failed on every login with "failed to save E2EE secure data". The key map is already stored in the user login metadata, which is loaded last and wins, so the file was never needed. Stop writing the file and keep reading old files as a fallback. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YLetjAVceT9hFiBGm9qiTG --- pkg/connector/client.go | 9 --------- pkg/connector/connector.go | 13 ------------- pkg/connector/login_keys_test.go | 12 ------------ pkg/e2ee/manager.go | 19 ++----------------- 4 files changed, 2 insertions(+), 51 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 4369df38..7cde4121 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -906,12 +906,6 @@ func (lc *LineClient) tryLogin(ctx context.Context) error { if mgr != nil { lc.missingE2EEKeyMarked = false } - if mgr != nil { - if err := mgr.SaveSecureDataToFile(loginSecureDataID(&staged, string(lc.UserLogin.ID)), map[string]any{"exportedKeyMap": exported}); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to save E2EE secure data after re-login") - } - } - lc.UserLogin.Bridge.Log.Info().Msg("Login successful!") return nil } @@ -965,9 +959,6 @@ func (lc *LineClient) refreshLoginE2EEKeys(res *line.LoginResult, meta *UserLogi return errLineClientSuperseded } lc.applyRefreshedLoginE2EEKeys(meta, res, exported) - if err := mgr.SaveSecureDataToFile(loginSecureDataID(meta, string(lc.UserLogin.ID)), map[string]any{"exportedKeyMap": exported}); err != nil { - return fmt.Errorf("save E2EE secure data after re-login: %w", err) - } lc.UserLogin.Bridge.Log.Info().Int("keys", len(exported)).Msg("Refreshed E2EE keys after re-login") return nil } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index ccf3e06f..3edd5319 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -828,12 +828,6 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult retireLineLogin(ll.ExistingLogin) } - if loginManager != nil { - if err := loginManager.SaveSecureDataToFile(loginSecureDataID(meta, string(ll.User.MXID)), map[string]any{"exportedKeyMap": meta.ExportedKeyMap}); err != nil { - ll.User.Bridge.Log.Warn().Err(err).Msg("Login: failed to save E2EE secure data") - } - } - go newClient.Connect(context.Background()) return &bridgev2.LoginStep{ @@ -946,13 +940,6 @@ func shouldPreserveExistingE2EEKeys(exportedKeys bool, existing *UserLoginMetada return !exportedKeys && existing != nil && len(existing.ExportedKeyMap) > 0 && !existing.ForceFullE2EELogin } -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) (*e2ee.Manager, error) { if res.EncryptedKeyChain == "" || res.E2EEPublicKey == "" { return nil, nil diff --git a/pkg/connector/login_keys_test.go b/pkg/connector/login_keys_test.go index 554f464e..3449d524 100644 --- a/pkg/connector/login_keys_test.go +++ b/pkg/connector/login_keys_test.go @@ -89,18 +89,6 @@ func TestCopyLoginE2EEKeyMetadataClonesExportedKeyMap(t *testing.T) { } } -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 TestStartWithOverrideForcesFullReconnectWithoutStoredE2EEKeys(t *testing.T) { oldLogin := loginWithCredentials var gotCertificate string diff --git a/pkg/e2ee/manager.go b/pkg/e2ee/manager.go index e146dd7e..038fde2d 100644 --- a/pkg/e2ee/manager.go +++ b/pkg/e2ee/manager.go @@ -258,14 +258,8 @@ func (m *Manager) DecryptSecureBlob(b64Cipher string) (map[string]any, error) { return out, nil } -func (m *Manager) EncryptSecureBlob(data map[string]any) (string, error) { - buf, err := json.Marshal(data) - if err != nil { - return "", err - } - return m.runner.StorageEncrypt(string(buf)) -} - +// LoadSecureDataFromFile reads key files written by older bridge versions. +// New keys are only stored in the user login metadata. func (m *Manager) LoadSecureDataFromFile(id string) (map[string]any, error) { path := filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) b, err := os.ReadFile(path) @@ -275,15 +269,6 @@ func (m *Manager) LoadSecureDataFromFile(id string) (map[string]any, error) { return m.DecryptSecureBlob(strings.TrimSpace(string(b))) } -func (m *Manager) SaveSecureDataToFile(id string, data map[string]any) error { - ct, err := m.EncryptSecureBlob(data) - if err != nil { - return err - } - path := filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) - return os.WriteFile(path, []byte(ct), 0o600) -} - func (m *Manager) InitFromLoginKeyChain(serverPubB64, encryptedKeyChainB64 string) (map[string]string, error) { keys, err := m.runner.LoginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64) if err != nil { From 9ffed97316280fb4920eae0ab4b8bb60d3cc772a Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sun, 1 Nov 2026 17:10:53 +0900 Subject: [PATCH 2/5] e2ee: add helpers to find and delete old key files Co-Authored-By: Claude Opus 5.5 --- pkg/e2ee/manager.go | 22 ++++++++++++++++++++-- pkg/e2ee/secure_file_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 pkg/e2ee/secure_file_test.go diff --git a/pkg/e2ee/manager.go b/pkg/e2ee/manager.go index 038fde2d..fa43bb0e 100644 --- a/pkg/e2ee/manager.go +++ b/pkg/e2ee/manager.go @@ -258,17 +258,35 @@ func (m *Manager) DecryptSecureBlob(b64Cipher string) (map[string]any, error) { return out, nil } +func secureDataPath(id string) string { + return filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) +} + // LoadSecureDataFromFile reads key files written by older bridge versions. // New keys are only stored in the user login metadata. func (m *Manager) LoadSecureDataFromFile(id string) (map[string]any, error) { - path := filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) - b, err := os.ReadFile(path) + b, err := os.ReadFile(secureDataPath(id)) if err != nil { return nil, err } return m.DecryptSecureBlob(strings.TrimSpace(string(b))) } +// HasSecureDataFile reports whether an older bridge version left a key file for id. +func HasSecureDataFile(id string) bool { + _, err := os.Stat(secureDataPath(id)) + return err == nil +} + +// RemoveSecureDataFile deletes a key file written by an older bridge version. +// A missing file is not an error. +func RemoveSecureDataFile(id string) error { + if err := os.Remove(secureDataPath(id)); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + func (m *Manager) InitFromLoginKeyChain(serverPubB64, encryptedKeyChainB64 string) (map[string]string, error) { keys, err := m.runner.LoginUnwrapKeyChain(serverPubB64, encryptedKeyChainB64) if err != nil { diff --git a/pkg/e2ee/secure_file_test.go b/pkg/e2ee/secure_file_test.go new file mode 100644 index 00000000..4985f166 --- /dev/null +++ b/pkg/e2ee/secure_file_test.go @@ -0,0 +1,31 @@ +package e2ee + +import ( + "os" + "testing" +) + +func TestRemoveSecureDataFile(t *testing.T) { + t.Chdir(t.TempDir()) + if err := os.Mkdir("data", 0o700); err != nil { + t.Fatal(err) + } + if HasSecureDataFile("u-mid") { + t.Fatal("HasSecureDataFile = true before the file exists") + } + if err := RemoveSecureDataFile("u-mid"); err != nil { + t.Fatalf("RemoveSecureDataFile(missing) = %v, want nil", err) + } + if err := os.WriteFile(secureDataPath("u-mid"), []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + if !HasSecureDataFile("u-mid") { + t.Fatal("HasSecureDataFile = false after writing the file") + } + if err := RemoveSecureDataFile("u-mid"); err != nil { + t.Fatalf("RemoveSecureDataFile = %v", err) + } + if HasSecureDataFile("u-mid") { + t.Fatal("file still exists after RemoveSecureDataFile") + } +} From 17e82703572ab9fbeb7137207c76494b4f041386 Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sun, 1 Nov 2026 17:10:54 +0900 Subject: [PATCH 3/5] connector: move old key files into the DB once, then delete them On connect, the bridge no longer fetches the storage key and decrypts the old lcs_secure file every time. If an old file is still on disk, its keys are merged into the login metadata once (metadata keys win) and the file is deleted. If the merge cannot be saved, the file stays for the next connect. Co-Authored-By: Claude Opus 5.5 --- pkg/connector/client.go | 121 +++++++++++++++++++++++++------ pkg/connector/login_keys_test.go | 20 +++++ 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 7cde4121..d18e4627 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -231,6 +231,103 @@ func (lc *LineClient) claimForcedLogoutState() bool { return true } +// loadConnectE2EEKeys loads the user's E2EE keys from the login metadata. +// Older bridge versions also wrote the keys to an encrypted file. If such a +// file is still on disk, its keys are merged into the metadata once and the +// file is deleted, so later connects never read or decrypt it. +func (lc *LineClient) loadConnectE2EEKeys(ctx context.Context, mgr *e2ee.Manager) { + log := lc.UserLogin.Bridge.Log + meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata) + if !ok { + return + } + lc.migrateLegacySecureDataFiles(ctx, mgr, meta) + if len(meta.ExportedKeyMap) == 0 { + return + } + if err := mgr.LoadMyKeyFromExportedMap(meta.ExportedKeyMap); err != nil { + log.Warn().Err(err).Msg("Failed to load E2EE key from DB metadata") + return + } + log.Info().Int("exported_keys", len(meta.ExportedKeyMap)).Msg("Loaded E2EE key from DB metadata") +} + +// migrateLegacySecureDataFiles merges the keys from old key files into the +// login metadata and deletes the files. Keys already in the metadata win, +// because they come from a newer login. If the merge cannot be saved, the +// files stay on disk so the next connect can try again. +func (lc *LineClient) migrateLegacySecureDataFiles(ctx context.Context, mgr *e2ee.Manager, meta *UserLoginMetadata) { + log := lc.UserLogin.Bridge.Log + var fileIDs []string + for _, id := range legacySecureDataIDs(meta.Mid, string(lc.UserLogin.ID), lc.UserLogin.UserMXID.String()) { + if e2ee.HasSecureDataFile(id) { + fileIDs = append(fileIDs, id) + } + } + if len(fileIDs) == 0 { + return + } + ei3, err := lc.newClient().GetEncryptedIdentityV3() + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch EncryptedIdentityV3 for old E2EE key file") + return + } + if err := mgr.InitStorage(ei3.WrappedNonce, ei3.KDFParameter1, ei3.KDFParameter2); err != nil { + log.Warn().Err(err).Msg("Failed to init storage key for old E2EE key file") + return + } + fromFiles := make(map[string]string) + for _, id := range fileIDs { + data, err := mgr.LoadSecureDataFromFile(id) + if err != nil { + log.Warn().Err(err).Str("id", id).Msg("Failed to read old E2EE key file") + return + } + fromFiles = mergeLoginKeyMaps(fromFiles, exportedKeyMapFromSecureData(data)) + } + merged := mergeLoginKeyMaps(fromFiles, meta.ExportedKeyMap) + if len(merged) > len(meta.ExportedKeyMap) { + previous := meta.ExportedKeyMap + meta.ExportedKeyMap = merged + if err := lc.UserLogin.Save(ctx); err != nil { + meta.ExportedKeyMap = previous + log.Warn().Err(err).Msg("Failed to save E2EE keys from old key file") + return + } + log.Info().Int("added_keys", len(merged)-len(previous)).Msg("Moved E2EE keys from old key file to DB metadata") + } + for _, id := range fileIDs { + if err := e2ee.RemoveSecureDataFile(id); err != nil { + log.Warn().Err(err).Str("id", id).Msg("Failed to delete old E2EE key file") + } + } +} + +// legacySecureDataIDs lists the IDs that older bridge versions used to name +// key files: the LINE MID, the login ID, and the Matrix user ID. +func legacySecureDataIDs(ids ...string) []string { + out := make([]string, 0, len(ids)) + seen := make(map[string]bool, len(ids)) + for _, id := range ids { + if id != "" && !seen[id] { + seen[id] = true + out = append(out, id) + } + } + return out +} + +func exportedKeyMapFromSecureData(data map[string]any) map[string]string { + raw, _ := data["exportedKeyMap"].(map[string]any) + out := make(map[string]string, len(raw)) + for k, v := range raw { + if s, ok := v.(string); ok && s != "" { + out[k] = s + } + } + return out +} + func (lc *LineClient) newClient() *line.Client { return lc.newClientWithToken(lc.getAccessToken()) } @@ -684,29 +781,7 @@ func (lc *LineClient) Connect(ctx context.Context) { lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to init E2EE manager") } else { lc.E2EE = mgr - // Storage key is optional for runtime decrypt/encrypt; try it for file support - client := lc.newClient() - ei3, err := client.GetEncryptedIdentityV3() - if err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to fetch EncryptedIdentityV3") - } else { - if err := mgr.InitStorage(ei3.WrappedNonce, ei3.KDFParameter1, ei3.KDFParameter2); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to init storage key") - } else if data, err := mgr.LoadSecureDataFromFile(string(lc.UserLogin.ID)); err == nil { - if err := mgr.LoadMyKeyFromSecureData(data); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to load E2EE key from secure data") - } - } - } - // DB metadata reflects the latest login, even if writing the secure file failed. - // Load it last so an older file cannot replace the freshly verified key. - if meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata); ok && len(meta.ExportedKeyMap) > 0 { - if err := mgr.LoadMyKeyFromExportedMap(meta.ExportedKeyMap); err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to load E2EE key from DB metadata") - } else { - lc.UserLogin.Bridge.Log.Info().Int("exported_keys", len(meta.ExportedKeyMap)).Msg("Loaded E2EE key from DB metadata") - } - } + lc.loadConnectE2EEKeys(ctx, mgr) } // Seed the last-known block list before fetching a fresh copy, so a diff --git a/pkg/connector/login_keys_test.go b/pkg/connector/login_keys_test.go index 3449d524..fe6ff0e0 100644 --- a/pkg/connector/login_keys_test.go +++ b/pkg/connector/login_keys_test.go @@ -235,3 +235,23 @@ func TestRefreshLoginE2EEKeysKeepsMetadataOnExportFailure(t *testing.T) { t.Fatalf("metadata was clobbered on export failure: %#v", meta) } } + +func TestLegacySecureDataIDsSkipsEmptyAndDuplicates(t *testing.T) { + got := legacySecureDataIDs("u-mid", "", "u-mid", "@user:example.com") + want := []string{"u-mid", "@user:example.com"} + if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("legacySecureDataIDs = %v, want %v", got, want) + } +} + +func TestExportedKeyMapFromSecureDataKeepsStringKeys(t *testing.T) { + got := exportedKeyMapFromSecureData(map[string]any{ + "exportedKeyMap": map[string]any{"1": "a", "2": "", "3": 4}, + }) + if len(got) != 1 || got["1"] != "a" { + t.Fatalf("exportedKeyMapFromSecureData = %v, want only key 1", got) + } + if got := exportedKeyMapFromSecureData(map[string]any{}); len(got) != 0 { + t.Fatalf("exportedKeyMapFromSecureData(empty) = %v, want empty", got) + } +} From ec28092dec7bcc234d3cb980d18cf4e996f77bed Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sun, 1 Nov 2026 17:10:54 +0900 Subject: [PATCH 4/5] connector: stop reading old key files and delete them on connect The old file was never written in Docker or with the README steps, and the login metadata already holds the keys. A login without keys in the metadata does a full E2EE login, so the file fallback has nothing to do. Connect now loads keys only from the metadata and deletes any old file. This replaces the one-time migration from the previous commit. Co-Authored-By: Claude Opus 5.5 --- pkg/connector/client.go | 91 ++++---------------------------- pkg/connector/login_keys_test.go | 20 ------- pkg/e2ee/manager.go | 42 +++------------ pkg/e2ee/secure_file_test.go | 21 ++++---- 4 files changed, 26 insertions(+), 148 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d18e4627..1d41f84e 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -232,16 +232,19 @@ func (lc *LineClient) claimForcedLogoutState() bool { } // loadConnectE2EEKeys loads the user's E2EE keys from the login metadata. -// Older bridge versions also wrote the keys to an encrypted file. If such a -// file is still on disk, its keys are merged into the metadata once and the -// file is deleted, so later connects never read or decrypt it. -func (lc *LineClient) loadConnectE2EEKeys(ctx context.Context, mgr *e2ee.Manager) { +// Older bridge versions also tried to save the keys to a file. Delete any file +// that is left, because the keys in the metadata replace it. +func (lc *LineClient) loadConnectE2EEKeys(mgr *e2ee.Manager) { log := lc.UserLogin.Bridge.Log meta, ok := lc.UserLogin.Metadata.(*UserLoginMetadata) if !ok { return } - lc.migrateLegacySecureDataFiles(ctx, mgr, meta) + for _, id := range []string{meta.Mid, string(lc.UserLogin.ID), lc.UserLogin.UserMXID.String()} { + if err := e2ee.RemoveSecureDataFile(id); err != nil { + log.Warn().Err(err).Msg("Failed to delete old E2EE key file") + } + } if len(meta.ExportedKeyMap) == 0 { return } @@ -252,82 +255,6 @@ func (lc *LineClient) loadConnectE2EEKeys(ctx context.Context, mgr *e2ee.Manager log.Info().Int("exported_keys", len(meta.ExportedKeyMap)).Msg("Loaded E2EE key from DB metadata") } -// migrateLegacySecureDataFiles merges the keys from old key files into the -// login metadata and deletes the files. Keys already in the metadata win, -// because they come from a newer login. If the merge cannot be saved, the -// files stay on disk so the next connect can try again. -func (lc *LineClient) migrateLegacySecureDataFiles(ctx context.Context, mgr *e2ee.Manager, meta *UserLoginMetadata) { - log := lc.UserLogin.Bridge.Log - var fileIDs []string - for _, id := range legacySecureDataIDs(meta.Mid, string(lc.UserLogin.ID), lc.UserLogin.UserMXID.String()) { - if e2ee.HasSecureDataFile(id) { - fileIDs = append(fileIDs, id) - } - } - if len(fileIDs) == 0 { - return - } - ei3, err := lc.newClient().GetEncryptedIdentityV3() - if err != nil { - log.Warn().Err(err).Msg("Failed to fetch EncryptedIdentityV3 for old E2EE key file") - return - } - if err := mgr.InitStorage(ei3.WrappedNonce, ei3.KDFParameter1, ei3.KDFParameter2); err != nil { - log.Warn().Err(err).Msg("Failed to init storage key for old E2EE key file") - return - } - fromFiles := make(map[string]string) - for _, id := range fileIDs { - data, err := mgr.LoadSecureDataFromFile(id) - if err != nil { - log.Warn().Err(err).Str("id", id).Msg("Failed to read old E2EE key file") - return - } - fromFiles = mergeLoginKeyMaps(fromFiles, exportedKeyMapFromSecureData(data)) - } - merged := mergeLoginKeyMaps(fromFiles, meta.ExportedKeyMap) - if len(merged) > len(meta.ExportedKeyMap) { - previous := meta.ExportedKeyMap - meta.ExportedKeyMap = merged - if err := lc.UserLogin.Save(ctx); err != nil { - meta.ExportedKeyMap = previous - log.Warn().Err(err).Msg("Failed to save E2EE keys from old key file") - return - } - log.Info().Int("added_keys", len(merged)-len(previous)).Msg("Moved E2EE keys from old key file to DB metadata") - } - for _, id := range fileIDs { - if err := e2ee.RemoveSecureDataFile(id); err != nil { - log.Warn().Err(err).Str("id", id).Msg("Failed to delete old E2EE key file") - } - } -} - -// legacySecureDataIDs lists the IDs that older bridge versions used to name -// key files: the LINE MID, the login ID, and the Matrix user ID. -func legacySecureDataIDs(ids ...string) []string { - out := make([]string, 0, len(ids)) - seen := make(map[string]bool, len(ids)) - for _, id := range ids { - if id != "" && !seen[id] { - seen[id] = true - out = append(out, id) - } - } - return out -} - -func exportedKeyMapFromSecureData(data map[string]any) map[string]string { - raw, _ := data["exportedKeyMap"].(map[string]any) - out := make(map[string]string, len(raw)) - for k, v := range raw { - if s, ok := v.(string); ok && s != "" { - out[k] = s - } - } - return out -} - func (lc *LineClient) newClient() *line.Client { return lc.newClientWithToken(lc.getAccessToken()) } @@ -781,7 +708,7 @@ func (lc *LineClient) Connect(ctx context.Context) { lc.UserLogin.Bridge.Log.Warn().Err(err).Msg("Failed to init E2EE manager") } else { lc.E2EE = mgr - lc.loadConnectE2EEKeys(ctx, mgr) + lc.loadConnectE2EEKeys(mgr) } // Seed the last-known block list before fetching a fresh copy, so a diff --git a/pkg/connector/login_keys_test.go b/pkg/connector/login_keys_test.go index fe6ff0e0..3449d524 100644 --- a/pkg/connector/login_keys_test.go +++ b/pkg/connector/login_keys_test.go @@ -235,23 +235,3 @@ func TestRefreshLoginE2EEKeysKeepsMetadataOnExportFailure(t *testing.T) { t.Fatalf("metadata was clobbered on export failure: %#v", meta) } } - -func TestLegacySecureDataIDsSkipsEmptyAndDuplicates(t *testing.T) { - got := legacySecureDataIDs("u-mid", "", "u-mid", "@user:example.com") - want := []string{"u-mid", "@user:example.com"} - if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { - t.Fatalf("legacySecureDataIDs = %v, want %v", got, want) - } -} - -func TestExportedKeyMapFromSecureDataKeepsStringKeys(t *testing.T) { - got := exportedKeyMapFromSecureData(map[string]any{ - "exportedKeyMap": map[string]any{"1": "a", "2": "", "3": 4}, - }) - if len(got) != 1 || got["1"] != "a" { - t.Fatalf("exportedKeyMapFromSecureData = %v, want only key 1", got) - } - if got := exportedKeyMapFromSecureData(map[string]any{}); len(got) != 0 { - t.Fatalf("exportedKeyMapFromSecureData(empty) = %v, want empty", got) - } -} diff --git a/pkg/e2ee/manager.go b/pkg/e2ee/manager.go index fa43bb0e..dd25cb16 100644 --- a/pkg/e2ee/manager.go +++ b/pkg/e2ee/manager.go @@ -246,42 +246,14 @@ func (m *Manager) LoadMyKeyFromExportedMap(exported map[string]string) error { return m.LoadMyKeyFromSecureData(map[string]any{"exportedKeyMap": raw}) } -func (m *Manager) DecryptSecureBlob(b64Cipher string) (map[string]any, error) { - pt, err := m.runner.StorageDecrypt(b64Cipher) - if err != nil { - return nil, err - } - var out map[string]any - if err := json.Unmarshal([]byte(pt), &out); err != nil { - return nil, err - } - return out, nil -} - -func secureDataPath(id string) string { - return filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) -} - -// LoadSecureDataFromFile reads key files written by older bridge versions. -// New keys are only stored in the user login metadata. -func (m *Manager) LoadSecureDataFromFile(id string) (map[string]any, error) { - b, err := os.ReadFile(secureDataPath(id)) - if err != nil { - return nil, err - } - return m.DecryptSecureBlob(strings.TrimSpace(string(b))) -} - -// HasSecureDataFile reports whether an older bridge version left a key file for id. -func HasSecureDataFile(id string) bool { - _, err := os.Stat(secureDataPath(id)) - return err == nil -} - -// RemoveSecureDataFile deletes a key file written by an older bridge version. -// A missing file is not an error. +// RemoveSecureDataFile deletes a key file that an older bridge version wrote. +// A missing file or an empty id is not an error. func RemoveSecureDataFile(id string) error { - if err := os.Remove(secureDataPath(id)); err != nil && !errors.Is(err, os.ErrNotExist) { + if id == "" { + return nil + } + path := filepath.Join("data", fmt.Sprintf("lcs_secure_%s.json", id)) + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil diff --git a/pkg/e2ee/secure_file_test.go b/pkg/e2ee/secure_file_test.go index 4985f166..e7046337 100644 --- a/pkg/e2ee/secure_file_test.go +++ b/pkg/e2ee/secure_file_test.go @@ -2,6 +2,7 @@ package e2ee import ( "os" + "path/filepath" "testing" ) @@ -10,22 +11,20 @@ func TestRemoveSecureDataFile(t *testing.T) { if err := os.Mkdir("data", 0o700); err != nil { t.Fatal(err) } - if HasSecureDataFile("u-mid") { - t.Fatal("HasSecureDataFile = true before the file exists") + path := filepath.Join("data", "lcs_secure_u-mid.json") + if err := os.WriteFile(path, []byte("x"), 0o600); err != nil { + t.Fatal(err) } if err := RemoveSecureDataFile("u-mid"); err != nil { - t.Fatalf("RemoveSecureDataFile(missing) = %v, want nil", err) - } - if err := os.WriteFile(secureDataPath("u-mid"), []byte("x"), 0o600); err != nil { - t.Fatal(err) + t.Fatalf("RemoveSecureDataFile = %v", err) } - if !HasSecureDataFile("u-mid") { - t.Fatal("HasSecureDataFile = false after writing the file") + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("file still exists after RemoveSecureDataFile: %v", err) } if err := RemoveSecureDataFile("u-mid"); err != nil { - t.Fatalf("RemoveSecureDataFile = %v", err) + t.Fatalf("RemoveSecureDataFile(missing) = %v, want nil", err) } - if HasSecureDataFile("u-mid") { - t.Fatal("file still exists after RemoveSecureDataFile") + if err := RemoveSecureDataFile(""); err != nil { + t.Fatalf("RemoveSecureDataFile(empty) = %v, want nil", err) } } From 3fd1cef8ea780df58ca05d5e4e86b3ee8409c12a Mon Sep 17 00:00:00 2001 From: Caio Lins Date: Sun, 1 Nov 2026 18:01:03 +0900 Subject: [PATCH 5/5] e2ee: drop the RemoveSecureDataFile test It only checked os.Remove and gave no coverage of the connect change. Co-Authored-By: Claude Opus 5.5 --- pkg/e2ee/secure_file_test.go | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 pkg/e2ee/secure_file_test.go diff --git a/pkg/e2ee/secure_file_test.go b/pkg/e2ee/secure_file_test.go deleted file mode 100644 index e7046337..00000000 --- a/pkg/e2ee/secure_file_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package e2ee - -import ( - "os" - "path/filepath" - "testing" -) - -func TestRemoveSecureDataFile(t *testing.T) { - t.Chdir(t.TempDir()) - if err := os.Mkdir("data", 0o700); err != nil { - t.Fatal(err) - } - path := filepath.Join("data", "lcs_secure_u-mid.json") - if err := os.WriteFile(path, []byte("x"), 0o600); err != nil { - t.Fatal(err) - } - if err := RemoveSecureDataFile("u-mid"); err != nil { - t.Fatalf("RemoveSecureDataFile = %v", err) - } - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("file still exists after RemoveSecureDataFile: %v", err) - } - if err := RemoveSecureDataFile("u-mid"); err != nil { - t.Fatalf("RemoveSecureDataFile(missing) = %v, want nil", err) - } - if err := RemoveSecureDataFile(""); err != nil { - t.Fatalf("RemoveSecureDataFile(empty) = %v, want nil", err) - } -}