0 votes

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
Godot version 3.5.stable
in Engine by (34 points)

2 Answers

+1 vote
Best answer

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

by (518 points)
selected by

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()

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

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

Looks good to me. Robust file tampering detection

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!

+1 vote

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

by (3,144 points)

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.

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.