|
|
|
|
Reply From: |
godot_dev_ |
Ultimately, I think you must assume a really tech savy user will always be able to edit your save file, since your storing the file on the user’s machine. So relaxing the requirements and assuming the user won’t reverse engineer your code, you could detect file changes using hashing by doing the following:
- Hash (using MD5 or SHA256, for example) the datastructure
d
you use to store data in your file into a value h
- Store
d
and h
in your file
- When your read your file, hash
d
into h'
. If h != h'
, the file was edited.
Of course, a tech savy user could generate his own hash after changing the file, but this method will prevent virtually any tampering to your file by allowing you to detect any changes, because d'
, the new changed datastructure won’t hash (with very very high probability) to h
I’m more worried about the simple user who opening the file on like the notepad.
Adds a letter and saves it, and makes the file unusable (forcing himself to manually delete the file to make it works again).
Im looking to automate this process (detect changes,delete,recreate) on startup.
mrfatalo | 2022-08-15 14:49
Okay. Well the solution I proposed above should solve that. Any user that opens it and edits it will make the program detect the change on startup, and then you can handle the situation however you want (deleting the file, creating a new one, reverting to the last save, etc.). I think making the assumption that averages users won’t edit a settings file should be fair, because such files can be saved in directories like AppData on Windows, which I beleive is a hidden directory. If a user still edits and saves a file, he shouldn’t be surprised when the game crashes because of that file edit.
godot_dev_ | 2022-08-15 15:15
The post here on StackOverflow has a similar topic talking about taking file hashes to detect content change
godot_dev_ | 2022-08-15 15:19
Thank you for the infos.
I tried to follow the instructions but I am not able to do it, im a very newbie.
I only accumulated errors and unclear results.
I followed this guide:
HashingContext — Godot Engine (stable) documentation in English
I don’t think I have the programming skills to do something like this…
I was hoping for a simpler solution related to the identification and reaction with a kind of function for the error ERR_INVALID_DATA
.
Or maybe nothing, I think that, yes, it is an excessive measure for files that should not be touched by users.
mrfatalo | 2022-08-15 18:52
Here are some resources that might help you:
godot_dev_ | 2022-08-15 19:19
I’m trying, at the moment h
and h'
are always different, even if I haven’t touched the file outside the editor, as if the process does not allow them to be the same.
(I miss understanding something about the instruction you gave me).
This is my actual code:
extends Control
const CHUCK_SIZE = 1024
const SETTINGS_FILE_PATH = "user://settings.ini"
var result_main_hash: PoolByteArray
var game_version = 0.1
var screen_resolution = OS.get_window_size()
var max_resolution = 0
var settings_file = File.new()
func save_settings() -> void:
settings_file.open(SETTINGS_FILE_PATH, File.WRITE)
settings_file.store_var(game_version)
settings_file.store_var(screen_resolution)
settings_file.store_var(max_resolution)
#HASH D in H
if settings_file.file_exists(SETTINGS_FILE_PATH):
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
if not settings_file.file_exists(SETTINGS_FILE_PATH):
return
settings_file.open(SETTINGS_FILE_PATH, File.READ_WRITE)
while not settings_file.eof_reached():
ctx.update(settings_file.get_buffer(CHUCK_SIZE))
result_main_hash = ctx.finish()
print(result_main_hash.hex_encode(), Array(result_main_hash))
settings_file.store_var(result_main_hash)
settings_file.close()
func load_settings() -> void:
if settings_file.file_exists(SETTINGS_FILE_PATH):
settings_file.open(SETTINGS_FILE_PATH, File.READ)
game_version = settings_file.get_var()
screen_resolution = settings_file.get_var()
max_resolution = settings_file.get_var()
#HASH D into H'
var check_hash: PoolByteArray
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
if not settings_file.file_exists(SETTINGS_FILE_PATH):
return
while not settings_file.eof_reached():
ctx.update(settings_file.get_buffer(CHUCK_SIZE))
check_hash = ctx.finish()
print(check_hash.hex_encode(), Array(check_hash))
settings_file.close()
else:
game_version = game_version
screen_resolution = screen_resolution
max_resolution = max_resolution
#SaveTest
func _on_Button_pressed() -> void:
save_settings()
mrfatalo | 2022-08-15 21:28
I tried running your code, and ended up changing it abit. Below, I got what you wanted to work. If the user changes the settings file, the program will detect it , and if it wasn’t changed, the program is also aware not change was made since it was last saved. The change I made was seperating the hash into a seperate file, since I beleive you were including the hash value itself in the hash computation when loading the game:
extends Control
const CHUCK_SIZE = 1024
const SETTINGS_FILE_PATH = "user://settings.ini"
const HASH_FILE_PATH = "user://hash.ini"
var result_main_hash: PoolByteArray
var game_version = 0.1
var screen_resolution = OS.get_window_size()
var max_resolution = 0
var settings_file = File.new()
var hash_file = File.new()
func save_settings() -> void:
#save the settings
settings_file.open(SETTINGS_FILE_PATH, File.WRITE)
settings_file.store_var(game_version)
settings_file.store_var(screen_resolution)
settings_file.store_var(max_resolution)
settings_file.close()
#compute the hash
#read the setting's files bytes
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#hash the bytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
result_main_hash = ctx.finish()
#STORE the hash
hash_file.open(HASH_FILE_PATH, File.WRITE)
hash_file.store_var(result_main_hash)
hash_file.close()
func load_settings() -> void:
#read the settings file bytes
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#compute the hash of file bytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
var actualHash = ctx.finish()
#open and read the expected hash
hash_file.open(HASH_FILE_PATH, File.READ)
var expectedHash = hash_file.get_var()
hash_file.close()
#print the hash we expect (stored in file)
print(expectedHash.hex_encode(), Array(expectedHash))
#print hash that actually occured
print(actualHash.hex_encode(), Array(actualHash))
if compareHashes(actualHash,expectedHash):
print("The file hasn't been altered")
else:
print("The file has changed since we last saved it")
func compareHashes(h1,h2):
if h1 == null and h2 != null:
return false
if h2 == null and h1 != null:
return false
#hashes must be same size
if h2.size() != h1.size():
return false
#make sure all bytes match
for i in h2.size():
var b1 = h1[i]
var b2 = h2[i]
if b1 != b2:
return false
return true
godot_dev_ | 2022-08-15 23:35
It seems to work, but there is a problem (sorry, still a fantastic work).
Now ERR_INVALID_DATA
i think is replaced by an 3 to 5sec freezing/crash of the game.
If i adding the “CAKE” inside the binary at hash.ini
freezing/crash happening.
It dosnt happen when i do the same on setting.ini
and “The file has changed since we last saved it” pop.
mrfatalo | 2022-08-16 09:17
It’s likely due to the hash prints. When reading the variable from the hash file, it’s assume to be a byte stream, but adding ‘CAKE’ will break that rule. Try removing print(expectedHash.hex_encode(), Array(expectedHash))
and print(actualHash.hex_encode(), Array(actualHash))
.
If it still crashes, it’s likely because of if h2.size() != h1.size():
, that assumes the variable stored in hash.ini
is an array of bytes. You will need to add a type check. If it’s not an array, then it has been altered.
In any case, I think it’s safe to assume the hash file won’t be change (and if it does, the user is trying to create trouble for themselves). You could store hash.ini
in a hidden folder while the settings file is in an easy to find area, and don’t need test the case where the hash file is changed (although if you wish to do so, that’s even better!) since your requirement is to detect changes in settings.ini
(hash.ini
is only used to accomplish this)
godot_dev_ | 2022-08-16 14:25
Now that I think about it, var expectedHash = hash_file.get_var()
may be crashing the program, since it assumes the variable is properly encoded in the file, but if you added ‘CAKE’, then it’s illformed. If this is the line causing the issue, just assume the hash file remains unchanged and ignore it, since it will be trouble to also detect changes in the hash file .
That said, if this is the line crashing your code, then if a user also adds CAKE to the settings file, your logic has to avoid reading variables in the settings file into memory or you risk crashing your program
godot_dev_ | 2022-08-16 14:28
Deleting the prints solved the crashes.
But the problem remains in the hash_file_get.var()
, it is precisely this that causes excessive slowdowns and freezing to the loading.
Editing hash.ini
externally (adding or deleting random numbers and words) makes the process very painful within the code.
(The computation has a bit overloaded my pc).
I think I will take a step back and re-evaluate the system. Thank you very much for the support, I rate your answer as the best.
I would like more this process with the ability to set hidden system files.
When I think of the user I imagine a 6 year old son who manages randomly to find and corrupt these files to his father’s pc.
At this point it becomes easier to go back to the dictionary functions.
mrfatalo | 2022-08-16 17:00
I covered the freezing\slow loading with this.
Im not very happy about it and unsure how will work actually in final project.
if compareHashes(actualHash,expectedHash):
print("The file hasn't been altered")
else:
print("The file has changed since we last saved it")
OS.alert("File Corrupted, Reload to Fix", "File Corrupted hash.ini")
var dir := Directory.new()
dir.remove(HASH_FILE_PATH)
get_tree().quit()
mrfatalo | 2022-08-16 19:49
You could just create a default settings file whenever you detect the file got corrupted. So worst case scenario your working with default settings
godot_dev_ | 2022-08-16 19:53
You might be able to detect if the hash file got corrupted as follows:
#open and read the expected hash
hash_file.open(HASH_FILE_PATH, File.READ)
var expectedHash = hash_file.get_var()
hash_file.close()
if not expectedHash is PoolByteArray:
print("Hash file corrupted")
return
#you can now safely proceed to compare the hashes since we confirmed the hash is indeed in the file and has properly been read
if compareHashes(actualHash,expectedHash):
#....
godot_dev_ | 2022-08-16 20:00
This example didn’t reach the if not expectedHash is PoolByteArray:
But Jumped on compareHases
and make pop the OS.alert
Where the code have encounter an error:
var expectedHash = hash_file.get_var()
mrfatalo | 2022-08-16 20:16
Sometimes even no-errors at all even with the strangest hash.ini
manual edit by notepad, but still the catch on compare.
mrfatalo | 2022-08-16 20:41
This is the code-recap.
Made some changes in hashs compare names and add some extra IF/ELSE
.
Works soso, not as best i wished, the detection don’t looks stable.
extends Control
const CHUCK_SIZE = 1024
const SETTINGS_FILE_PATH := "user://settings.ini"
const HASH_FILE_PATH := "user://hash.ini"
var result_main_hash: PoolByteArray
var game_version := 0.1
var screen_resolution := OS.get_window_size()
var max_resolution := 0
var settings_file = File.new()
var hash_file = File.new()
func _ready() -> void:
load_settings()
OS.set_window_size(screen_resolution)
print(screen_resolution)
func save_settings() -> void:
settings_file.open(SETTINGS_FILE_PATH, File.WRITE)
settings_file.store_var(game_version)
settings_file.store_var(screen_resolution)
settings_file.store_var(max_resolution)
settings_file.close()
#ComputeHash
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#HashBytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
result_main_hash = ctx.finish()
hash_file.open(HASH_FILE_PATH, File.WRITE)
hash_file.store_var(result_main_hash)
hash_file.close()
func load_settings() -> void:
#SettingsBytes
if settings_file.file_exists(SETTINGS_FILE_PATH) and hash_file.file_exists(HASH_FILE_PATH):
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#ComputeHashBytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
var actual_hash = ctx.finish()
#ReadExpectedHash
hash_file.open(HASH_FILE_PATH, File.READ)
var expected_hash = hash_file.get_var()
hash_file.close()
#ExpectingAndCompareHashes
if not expected_hash is PoolByteArray:
print("Hash file corrupted")
OS.alert("File Corrupted, Reload to Fix", "Error File hash.ini")
var dir := Directory.new()
# warning-ignore:return_value_discarded
dir.remove(HASH_FILE_PATH)
# warning-ignore:return_value_discarded
dir.remove(SETTINGS_FILE_PATH)
return
if compare_hashes(actual_hash,expected_hash):
print("The file hasn't been altered")
if settings_file.file_exists(SETTINGS_FILE_PATH):
settings_file.open(SETTINGS_FILE_PATH, File.READ)
game_version = settings_file.get_var()
screen_resolution = settings_file.get_var()
max_resolution = settings_file.get_var()
settings_file.close()
else: #DefaultSettings
game_version = game_version
screen_resolution = screen_resolution
max_resolution = max_resolution
else:
print("The file has changed since we last saved it")
OS.alert("Settings File Corrupted, Reload to Fix", "Error File settings.ini")
var dir := Directory.new()
# warning-ignore:return_value_discarded
dir.remove(HASH_FILE_PATH)
# warning-ignore:return_value_discarded
dir.remove(SETTINGS_FILE_PATH)
return
func compare_hashes(h1,h2) -> bool:
if h1 == null and h2 != null:
return false
if h2 == null and h1 != null:
return false
#hashes must be same size
if h2.size() != h1.size():
return false
#make sure all bytes match
for i in h2.size():
var b1 = h1[i]
var b2 = h2[i]
if b1 != b2:
return false
return true
#Testing Button
func _on_Button_pressed() -> void:
screen_resolution = Vector2(640,320)
save_settings()
mrfatalo | 2022-08-16 21:28
I fixed your issue. The problem is that var expected_hash = hash_file.get_var()
assumes the file contains an encoded byte array (which is the case unless someone changes the file). So instead, I used the File
get_buffer
and store_buffer
API. These functions read and write raw byte arrays directly without assuming any encoding. This way you can change the settings file or hash (or even delete the hash file) and the integrity check will pickup changes in the files without crashing. Hope this helps
extends Control
const SETTINGS_FILE_PATH = "user://settings10.ini"
const HASH_FILE_PATH = "user://hash10.ini"
var game_version = 0.1
var screen_resolution = OS.get_window_size()
var max_resolution = 0
var settings_file = File.new()
var hash_file = File.new()
func save_settings() -> void:
#save the settings
settings_file.open(SETTINGS_FILE_PATH, File.WRITE)
settings_file.store_var(game_version)
settings_file.store_var(screen_resolution)
settings_file.store_var(max_resolution)
settings_file.close()
#compute the hash
#read the setting's files bytes
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#hash the bytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
var actual_hash = ctx.finish()
#STORE the hash
hash_file.open(HASH_FILE_PATH, File.WRITE)
hash_file.store_buffer(actual_hash)
hash_file.close()
func load_settings() -> void:
#read the settings file bytes
settings_file.open(SETTINGS_FILE_PATH, File.READ)
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#compute the hash of file bytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
var actualHash = ctx.finish()
if hash_file.file_exists(HASH_FILE_PATH):
#open and read the expected hash
hash_file.open(HASH_FILE_PATH, File.READ)
var expectedHash = hash_file.get_buffer(hash_file.get_len())
hash_file.close()
if compareHashes(actualHash,expectedHash):
print("The file hasn't been altered")
else:
print("The file has changed since we last saved it")
else:
print("The file has changed since we last saved it (missing hash file)") #hash file is missing, so consider this a change to seetings
func compareHashes(h1,h2):
if h1 == null and h2 != null:
return false
if h2 == null and h1 != null:
return false
if not h1 is PoolByteArray:
return false
if not h2 is PoolByteArray:
return false
#hashes must be same size
if h2.size() != h1.size():
return false
#make sure all bytes match
for i in h2.size():
var b1 = h1[i]
var b2 = h2[i]
if b1 != b2:
return false
return true
godot_dev_ | 2022-08-17 13:43
I would say brilliant, you are a kind of genius.
There were still a couple of errors:
If setting.ini
deleted:
if setting.ini
blanked:
I fixed these errors by adding two nests:
if settings_file.file_exists(SETTINGS_FILE_PATH):
This that detect the file settings.ini
at beginning of load_settings
, and if not exist return else
an .write
auto-save settings.in
i newfile with default valutes and return
again up.
if settings_file.get_len() != 0:
And this for detecting a blank setting.ini
with else
that do save_settings
(File exist, so can save it default values).
func load_settings() -> void:
#read the settings file bytes
if settings_file.file_exists(SETTINGS_FILE_PATH):
settings_file.open(SETTINGS_FILE_PATH, File.READ)
if settings_file.get_len() != 0:
var bytes = settings_file.get_buffer (settings_file.get_len())
settings_file.close()
#compute the hash of file bytes
var ctx = HashingContext.new()
ctx.start(HashingContext.HASH_SHA256)
ctx.update(bytes)
var actualHash = ctx.finish()
if hash_file.file_exists(HASH_FILE_PATH):
#open and read the expected hash
hash_file.open(HASH_FILE_PATH, File.READ)
var expectedHash = hash_file.get_buffer(hash_file.get_len())
hash_file.close()
if compareHashes(actualHash,expectedHash):
print("The file hasn't been altered")
settings_file.open(SETTINGS_FILE_PATH, File.READ)
game_version = settings_file.get_var()
screen_resolution = settings_file.get_var()
max_resolution = settings_file.get_var()
settings_file.close()
else:
print("The file has changed since we last saved it")
#Hash.ini changed
#Settings.ini changed/deleted
else:
print("The file has changed since we last saved it (missing hash file)")
#hash file is missing, so consider this a change to seetings
else:
#Setting.ini blank
save_settings()
else:
settings_file.open(SETTINGS_FILE_PATH, File.WRITE)
settings_file.store_var(game_version)
settings_file.store_var(screen_resolution)
settings_file.store_var(max_resolution)
settings_file.close()
return
mrfatalo | 2022-08-17 15:46
Looks good to me. Robust file tampering detection
godot_dev_ | 2022-08-17 15:56
Yea! Looks good, now I’m feeling much better to give a try!
Not sure about how much is safe, the hash.ini
.
(Always print the same line without encoding bytes).
But i wasnt looking to encoding or anything, just storing some basic settings (screen size, audio bus, ect) without worries about user actions.
Again thank you so much!
mrfatalo | 2022-08-17 16:05