[Bugfix] Updating scorehash on every password changes/Explicitly save config.xml into shift_jis - #6
Conversation
| updateScoreCmd.Parameters.Add("@hash", SqliteType.Text); | ||
| updateScoreCmd.Prepare(); | ||
|
|
||
| using (var selectScoresCmd = new SqliteCommand(SelectScoresQuery, connection, transaction)) |
There was a problem hiding this comment.
I'd make sure current 'scorehash' values are valid, otherwise 'scorehash' as a concept loses value
You can even do it in the same loop since you have a transaction, just don't forget to ROLLBACK on bad scores.
To check scorehash validity, you'd have to add some fallbacks that manipulate 'rank'.
Use OpenLR2 as a reference: https://github.com/GOMazk/OpenLR2/blob/15b2fc066a8bdbfcc28d3d2a88e6467048f7bd7f/LR2/LR2_statlong.cpp#L190
There was a problem hiding this comment.
I’ve added the validation logic as you suggested. Any invalid scorehash is now automatically corrected to the valid one within the transaction loop. By ensuring only valid data is written, we prevent bad scores from being persisted, which keeps the data integrity solid without needing extra rollback steps.
Regarding the ROLLBACK strategy: I've ensured that any unexpected exception during the hash correction will trigger a rollback.
|
|
||
| public class DBPlayerRow(SqliteDataReader reader) | ||
| { | ||
| public string? Id { get; set; } = DatabaseUtils.GetValue<string>(reader, "id"); |
There was a problem hiding this comment.
Here and everywhere below, is it necessary to have strings as optionals? I'm not familiar with C#
There was a problem hiding this comment.
The goal is to explicitly distinguish between an intentional null assignment and an implicit default null state. While the type distinction might seem like extra overhead at first, it's far more cost-effective to handle these cases as compile-time warnings than to deal with runtime errors.
That's why C# strictly flags potential nullability for all variables at compile time. It acts as a contract: it either warns you that a variable might be null and requires handling, or gives you a strong guarantee that it won't be null, allowing you to use it safely. I'm leveraging these features to make the code's intent clearer and more robust.
There was a problem hiding this comment.
In short, cleaning up all those compiler warnings by using nullable types gives me a sense of psychological safety. And honestly, that kind of peace of mind is invaluable XD
There was a problem hiding this comment.
I know SQL tables don't have 'NOT NULL' in them, but they will never be NULL in practice.
If we detect a NULL, we should reject such database early.
If I understand you correctly, this nullability flag forces you to check that the string is not null every time you use it.
But it should be done just once when we read from the database.
Also, why are only string fields nullable, but not the int ones?
There was a problem hiding this comment.
I know SQL tables don't have 'NOT NULL' in them, but they will never be NULL in practice.
If we detect a NULL, we should reject such database early.
If I understand you correctly, this nullability flag forces you to check that the string is not null every time you use it.
But it should be done just once when we read from the database.
SqliteDataReader.GetValue returns object.
If the returned value is casted to ref type, it is potentially nullable so we should declare it nullable.
Also, why are only string fields nullable, but not the int ones?
Cause string is originally ref type and int is value type. It is kinda hard to explain....
| { | ||
| public static class MD5Util | ||
| { | ||
| public static string CalculateMD5(object[] dataRaw) |
There was a problem hiding this comment.
IMO, it would be more clear, if you concatenated all fields on the outside and then used 'CalculateMD5(string input)'.
Performance wise, this method already does 'string.Concat' anyway, so I have doubts it is any faster than 'CalculateMD5(string input)'.
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| Console.WriteLine("Password update cancelled."); |
| if (!DBScoreRow.IsScorehashValid(score, targetPlayerPasswordHashMD5, targetScorehashMD5)) | ||
| { | ||
| updateScoreCmd.Parameters["@scorehash"].Value = targetScorehashMD5.Body; | ||
| updateScoreCmd.Parameters["@hash"].Value = score.Hash; | ||
| updateScoreCmd.ExecuteNonQuery(); | ||
| Console.WriteLine($"Updating scorehash of a score.\n[Update] Hash : {score.Hash} / Scorehash : {targetScorehashMD5}"); | ||
| } |
There was a problem hiding this comment.
If an invalid scorehash is found, the database should be rejected
(pseudo-code, not sure if this compiles)
| if (!DBScoreRow.IsScorehashValid(score, targetPlayerPasswordHashMD5, targetScorehashMD5)) | |
| { | |
| updateScoreCmd.Parameters["@scorehash"].Value = targetScorehashMD5.Body; | |
| updateScoreCmd.Parameters["@hash"].Value = score.Hash; | |
| updateScoreCmd.ExecuteNonQuery(); | |
| Console.WriteLine($"Updating scorehash of a score.\n[Update] Hash : {score.Hash} / Scorehash : {targetScorehashMD5}"); | |
| } | |
| if (!DBScoreRow.IsScorehashValid(score, targetPlayerPasswordHashMD5, targetScorehashMD5)) | |
| throw new Exception("Invalid score found in the database"); | |
| updateScoreCmd.Parameters["@scorehash"].Value = targetScorehashMD5.Body; | |
| updateScoreCmd.Parameters["@hash"].Value = score.Hash; | |
| updateScoreCmd.ExecuteNonQuery(); | |
| Console.WriteLine($"Updating scorehash of a score.\n[Update] Hash : {score.Hash} / Scorehash : {targetScorehashMD5}"); |
There was a problem hiding this comment.
Anyone can theoretically generate a valid scorehash by reverse-engineering the algorithm, so scorehash's value as a security measure is definitely limited. I agree that it’s not a perfect defense against malicious users.
However, my intent is to ensure that the system remains consistent for all users. By force-updating the scorehash whenever it's invalid, I'm making sure that legitimate player data is always in a stable state and doesn't suffer from 'stale' or inconsistent records. I see this as maintaining data integrity for the game's overall reliability, rather than trying to block sophisticated attackers.
There was a problem hiding this comment.
by reverse-engineering the algorithm
Exactly, and that's something a bit deeper than what a script kiddie could achive. But modifying a score with DB Browser for SQLite is very simple and would take just a few minutes even for someone who's never heard of SQL before.
There was a problem hiding this comment.
doesn't suffer from 'stale' or inconsistent records
In LR2 itself, OpenLR2 included, there is no possiblity of that happening
There was a problem hiding this comment.
In LR2 itself, OpenLR2 included, there is no possiblity of that happening
Yes, I definitely trust it!
But.... actually I don't trust LR2Nexus. Password can be changed with that, and it could be risky. And I want to handle that.
There was a problem hiding this comment.
In other words, LR2Nexus might be a 'sophisticated attacker' with that.
There was a problem hiding this comment.
If you don't trust LR2Nexus, how about making a backup of score DB file if the password is being changed?
There was a problem hiding this comment.
Alright, I guaranteed the backup. If creating backup failed, I made it immediately throw exception to prevent further operations progressed.
I really appreciate your dedication and time for the LR2 community. Thanks for the help!
Changes
scorestable in<player>.dbupdated when the password is changed.Related Issues
#4, #5