Merge pull request #4867 from coo1white/fix-backup-s3-secret-reencrypt

fix(backup): keep the S3 secret encrypted when a save inherits it
This commit is contained in:
Wesley Liddick
2026-09-23 11:17:55 +08:00
committed by GitHub
2 changed files with 56 additions and 3 deletions
+9 -3
View File
@@ -357,19 +357,25 @@ func (s *BackupService) GetS3Config(ctx context.Context) (*BackupS3Config, error
}
func (s *BackupService) UpdateS3Config(ctx context.Context, cfg BackupS3Config) (*BackupS3Config, error) {
// 如果没提供 secret,保留原有值
// 如果没提供 secret,保留原有值。loadS3Config 会解密,所以这里拿到的是明文,
// 和调用方新填的 secret 一样,都必须走下面的加密再落库。
if cfg.SecretAccessKey == "" {
old, _ := s.loadS3Config(ctx)
if old != nil {
cfg.SecretAccessKey = old.SecretAccessKey
}
} else {
}
// 加密 SecretAccessKey。这一步不能只在“调用方填了新 secret”时做:表单保存成功
// 后本函数会清空返回值里的 secret,所以第二次保存(改端点、存定时配置)送上来的
// secret 是空的,会走上面的分支继承旧值。若那条路径跳过加密,明文就会覆盖掉库里
// 的密文,而读取侧“兼容未加密旧数据”的回退会把这件事永久掩盖成一条日志。
if cfg.SecretAccessKey != "" {
// 拒绝用自动生成的临时密钥加密:该密钥每次重启都会变化,落库的密文在
// 重启/升级后无法解密(#4524)。与支付、TOTP 的处理保持一致。
if !s.encryptionKeyConfigured {
return nil, ErrSecretEncryptionKeyNotConfigured
}
// 加密 SecretAccessKey
encrypted, err := s.encryptor.Encrypt(cfg.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("encrypt secret: %w", err)
@@ -372,6 +372,53 @@ func TestBackupService_S3ConfigKeepExistingSecret(t *testing.T) {
require.Equal(t, "AKID-NEW", internal.AccessKeyID)
}
// 一次不带 secret 的保存(表单第二次提交、改端点、存定时配置)继承的是 loadS3Config
// 解密后的明文。若那条路径跳过加密,明文就会覆盖库里的密文,而读取侧“兼容未加密旧
// 数据”的回退会把它掩盖成一条日志,功能照常,密钥却是明文落库的。
func TestBackupService_S3ConfigStaysEncryptedAfterSecondSave(t *testing.T) {
repo := newMockSettingRepo()
svc := newTestBackupService(repo, &mockDumper{}, newMockObjectStore())
_, err := svc.UpdateS3Config(context.Background(), BackupS3Config{
Bucket: "my-bucket",
AccessKeyID: "AKID",
SecretAccessKey: "original-secret",
})
require.NoError(t, err)
storedSecret := func() string {
raw, _ := repo.GetValue(context.Background(), settingKeyBackupS3Config)
var stored BackupS3Config
require.NoError(t, json.Unmarshal([]byte(raw), &stored))
return stored.SecretAccessKey
}
require.Equal(t, "ENC:original-secret", storedSecret())
// 第二次保存不带 secret,只改别的字段。
_, err = svc.UpdateS3Config(context.Background(), BackupS3Config{
Bucket: "my-bucket",
AccessKeyID: "AKID-NEW",
})
require.NoError(t, err)
require.Equal(t, "ENC:original-secret", storedSecret(),
"secret must stay encrypted at rest after a save that inherits it")
require.NotEqual(t, "original-secret", storedSecret(), "secret must never be stored as plaintext")
// 第三次保存,确认不会反复套壳加密。
_, err = svc.UpdateS3Config(context.Background(), BackupS3Config{
Bucket: "my-bucket",
AccessKeyID: "AKID-THIRD",
})
require.NoError(t, err)
require.Equal(t, "ENC:original-secret", storedSecret(), "secret must not be double-encrypted")
internal, err := svc.loadS3Config(context.Background())
require.NoError(t, err)
require.Equal(t, "original-secret", internal.SecretAccessKey)
require.Equal(t, "AKID-THIRD", internal.AccessKeyID)
}
func TestBackupService_UpdateS3Config_RejectsEphemeralKey(t *testing.T) {
repo := newMockSettingRepo()
svc := newTestBackupServiceEphemeralKey(repo)