Making save file not editable

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By mrfatalo

How do I ensure that my file is protected from being opened\edited manually?
Not just an encryption but a way to avoid manually changes.

enter image description here

This is the inside, if adding random types i get back ERR_INVALID_DATA.
Only deleting the file and reboot the editor by save\load defaults make it works again.
I already tried File = OK but i failed, the loader keep load the default values.
I tried also deleting the file with OS.move_to_trash, but i failed again.
Another try was with get_property_list, but didn’t understand well.
I think the solution is somewhere in these formulas.
What is the method I am looking for?
Maybe I can hide the file via editor\script? Or set it read only?
Thanks…

This is my Saving\Loading code:

extends Node
const SETTINGS_FILE_PATH = "user://settings.ini"

var game_version := 0.1
var screen_resolution := OS.get_window_size()
var max_resolution := 0

var settings_file := File.new()
func _init() -> void:
    load_settings()
    OS.set_window_size(screen_resolution)
    if max_resolution == 1:
        OS.set_window_maximized(true)
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()
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()
		settings_file.close()
	else:
		game_version = game_version
		screen_resolution = screen_resolution
		max_resolution = max_resolution
:bust_in_silhouette: Reply From: Ertain

The File class’ method store_var() saves the data to files in binary (see this article for more details). To save the data as plain text, look into using the other methods the File class provides (for example, the store_string() method, or even using JSON).

If i want to keep the file in binary and store_var():
There isn’t a way to compare a first autosave and a possible manually modification by the user?
(To auto-delete and replace with a default new autosave and game saves).
Or there isn’t a way to hide the file in folder?
Because using store_script() or JSON make more harder the conversions i need.

mrfatalo | 2022-08-15 14:38

:bust_in_silhouette: 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:

  1. Hash (using MD5 or SHA256, for example) the datastructure d you use to store data in your file into a value h
  2. Store d and h in your file
  3. 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.

enter image description here

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

enter image description here
Where the code have encounter an error:

 var expectedHash = hash_file.get_var()

enter image description here

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.

enter image description here

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_bufferAPI. 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:

enter image description here

if setting.ini blanked:

enter image description here

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.ini 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