mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
feat(config): add DisableSaveIntegrityCheck flag for save transfers
The SHA-256 integrity check introduced in migration 0007 blocks saves when a character's savedata blob is imported from another server instance, because the stored hash in the target DB no longer matches the new blob. Adding DisableSaveIntegrityCheck (default: false) lets server operators bypass the check to unblock cross-server save transfers. A warning is logged each time the check is skipped so the flag's use is auditable. Documents the per-character SQL alternative in CHANGELOG: UPDATE characters SET savedata_hash = NULL WHERE id = <id> Closes #183.
This commit is contained in:
@@ -55,7 +55,8 @@ func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error)
|
||||
// Verify integrity checksum if one was stored with this save.
|
||||
// A nil hash means the character was saved before checksums were introduced,
|
||||
// so we skip verification (the next save will compute and store the hash).
|
||||
if storedHash != nil {
|
||||
// DisableSaveIntegrityCheck bypasses this entirely for cross-server save transfers.
|
||||
if storedHash != nil && !s.server.erupeConfig.DisableSaveIntegrityCheck {
|
||||
computedHash := sha256.Sum256(saveData.decompSave)
|
||||
if !bytes.Equal(storedHash, computedHash[:]) {
|
||||
s.logger.Error("Savedata integrity check failed: hash mismatch",
|
||||
@@ -66,6 +67,10 @@ func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error)
|
||||
// TODO: attempt recovery from savedata_backups here
|
||||
return nil, errors.New("savedata integrity check failed")
|
||||
}
|
||||
} else if storedHash != nil && s.server.erupeConfig.DisableSaveIntegrityCheck {
|
||||
s.logger.Warn("Savedata integrity check skipped (DisableSaveIntegrityCheck=true)",
|
||||
zap.Uint32("charID", charID),
|
||||
)
|
||||
}
|
||||
|
||||
saveData.updateStructWithSaveData()
|
||||
|
||||
@@ -747,3 +747,68 @@ func TestGetCharacterSaveData_ConfigMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCharacterSaveData_IntegrityCheck verifies the SHA-256 hash guard and
|
||||
// that DisableSaveIntegrityCheck bypasses it without returning an error.
|
||||
func TestGetCharacterSaveData_IntegrityCheck(t *testing.T) {
|
||||
// Build a minimal valid savedata blob and compress it.
|
||||
rawSave := make([]byte, 150000)
|
||||
copy(rawSave[88:], []byte("TestChar\x00"))
|
||||
compressed, err := nullcomp.Compress(rawSave)
|
||||
if err != nil {
|
||||
t.Fatalf("compress: %v", err)
|
||||
}
|
||||
|
||||
// A hash that deliberately does NOT match rawSave.
|
||||
wrongHash := bytes.Repeat([]byte{0xDE}, 32)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
disable bool
|
||||
hash []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil hash skips check",
|
||||
disable: false,
|
||||
hash: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mismatched hash fails when check enabled",
|
||||
disable: false,
|
||||
hash: wrongHash,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "mismatched hash passes when check disabled",
|
||||
disable: true,
|
||||
hash: wrongHash,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mock := newMockCharacterRepo()
|
||||
mock.loadSaveDataID = 1
|
||||
mock.loadSaveDataData = compressed
|
||||
mock.loadSaveDataName = "TestChar"
|
||||
mock.loadSaveDataHash = tc.hash
|
||||
|
||||
server := createMockServer()
|
||||
server.erupeConfig.RealClientMode = cfg.ZZ
|
||||
server.erupeConfig.DisableSaveIntegrityCheck = tc.disable
|
||||
server.charRepo = mock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
_, err := GetCharacterSaveData(session, 1)
|
||||
if tc.wantErr && err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ type mockCharacterRepo struct {
|
||||
loadSaveDataData []byte
|
||||
loadSaveDataNew bool
|
||||
loadSaveDataName string
|
||||
loadSaveDataHash []byte
|
||||
loadSaveDataErr error
|
||||
|
||||
// ReadEtcPoints mock fields
|
||||
@@ -245,7 +246,7 @@ func (m *mockCharacterRepo) SaveBackup(_ uint32, _ int, _ []byte) error {
|
||||
func (m *mockCharacterRepo) GetLastBackupTime(_ uint32) (time.Time, error) { return time.Time{}, nil }
|
||||
func (m *mockCharacterRepo) SaveCharacterDataAtomic(_ SaveAtomicParams) error { return nil }
|
||||
func (m *mockCharacterRepo) LoadSaveDataWithHash(_ uint32) (uint32, []byte, bool, string, []byte, error) {
|
||||
return m.loadSaveDataID, m.loadSaveDataData, m.loadSaveDataNew, m.loadSaveDataName, nil, m.loadSaveDataErr
|
||||
return m.loadSaveDataID, m.loadSaveDataData, m.loadSaveDataNew, m.loadSaveDataName, m.loadSaveDataHash, m.loadSaveDataErr
|
||||
}
|
||||
|
||||
// --- mockGoocooRepo ---
|
||||
|
||||
Reference in New Issue
Block a user