Asset - Extension SafeSave - INI and Data Structure Encryption

zbox

Member
GMC Elder
Safe Save

Itch.io: https://taylordale.itch.io/safesave-ini
Marketplace: https://marketplace.yoyogames.com/assets/4685/safesave-ini-more
Category: Extensions
Price: $5.99 $3.99
Modules: All

Description:
Securely save data on your user's devices!

Whether you are used to using INI files or are looking for a more complicated solution to encrypt data structures this extension will cater to your needs.

Features

  • Fully encrypted and unrecognizable INI files or regular data (see modes below)
  • Tamper proof mechanism ensures data cannot be changed
  • Prevents users from sharing save files & data
  • Can be used to store IAP data and much more
  • More compressed than an INI (smaller filesize)
  • Compatible with all exports
  • Choice of RC4 (default) or 128-bit AES (sold separately) for encryption!
INI MODE
To fully encrypt and make your INI files tamper-proof, all you have to do is prefix all "ini_*" functions in your project with "safe_", and change ini_open(filename) to safe_ini_open(filename, key) where key is a random string of letters that is used to encrypt your INI file. This is all that is required.

ADVANCED MODE
If you need more flexibility when you are saving data, safe_save will take a ds_map and a filename and write it to the disk fully encrypted with built in tamper-proofing. It will fully save and reconstitute all embedded data structures as well.

There are examples of both modes in use included.

Screenshots


 
Last edited:
E

Ethanicus

Guest
Very cool! Does the developer have any control over encryption keys or anything?
 

zbox

Member
GMC Elder
Absolutely :) You supply a key for both modes, an arbitrarily long string you can decide on.

INI Mode
safe_ini_open(fname,key);

Or for advanced mode
safe_save(filename,data,key)
safe_load(filename,key)
 

zbox

Member
GMC Elder
New update
  • Optional: Support for 128-bit AES to strengthen the encryption of your save files for storage and transmission (available separately)
  • Filesize reduction of up to 33%
  • Typos fixed and Readme updated
  • General bug fixes
 
L

Lycanphoenix

Guest
I assume there's an option to just compress the save files, without encrypting them?
 

zbox

Member
GMC Elder
I assume there's an option to just compress the save files, without encrypting them?
The main point of the extension is to encrypt the save file, without it the encryption functionality most of the functions already exist in GM, however I guess I can add no encryption on request??

There is no compression involved, the filesize reduction I mention is relative to old versions of SafeSave in which I've optimized the format :)
 
L

Lycanphoenix

Guest
Oh. Right >.<
How about File Integrity Verification? I do know that some encryption schemes are often used to hash files, and check to see of the files are damaged or incomplete.
 

zbox

Member
GMC Elder
Absolutely - the save files have a built in integrity check and there is functionality to report whether the file has been tampered with or not. By default if the file is tampered with then it will fail to load. The save files are also device-specific as to not allow transfer between devices.
 

Ednei

Member
I'm having this strange error:

Code:
############################################################################################
FATAL ERROR in
action number 1
of Key Press Event for <Space> Key
for object obSYS_Save_Game:

Safe Save INI: This character is not allowed in Section or Key names: _
 at gml_Script_safe_ini_write_real (line 6) -     show_error("Safe Save INI: This character is not allowed in Section or Key names: " + string(global._safe_ini_sep), true);
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_safe_ini_write_real (line 6)
called from - gml_Script_scSYS_Save (line 34) -  safe_ini_write_real("OVER_Data","A_"+string(i),data) //levels
called from - gml_Object_obSYS_Save_Game_KeyPress_32 (line 62) -   scSYS_Save(menu+1)
My save script:

Code:
// Inicia argumentos
var i, arg;
for (i = 0; i < 5; i += 1;)
   {
   if argument_count > i
      {
      arg[i] = argument[i]
      }
   else
      {
      arg[i] = -1;
      }
   };
var file, data, a, b, c, size;

file = working_directory+"/OVER_Data/OVER_Save0"+string(arg[0])+".ini"

size = ds_list_size(global.Player_Party)

if (file_exists(file) = true)
{
 file_delete(file)
}

safe_ini_open(file, global.key_pass)

a = floor(random(150))+75
b = floor(random(300))+100
c = floor(random(50))+50

for (i=0; i<size; i+=1)
{
 data = (global.Character_Level[ds_list_find_value(global.Player_Party,i)])*(c*i+1)-b
 safe_ini_write_real("OVER_Data","A_"+string(i),data) //levels
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_Energy[ds_list_find_value(global.Player_Party,i)])*(i*10+1)+150-a
 safe_ini_write_real("OVER_Data","B_"+string(i),data) //energy
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_Health[ds_list_find_value(global.Player_Party,i)])*(i+(i*25)+1)-c
 safe_ini_write_real("OVER_Data","C_"+string(i),data) //health
}

safe_ini_write_real("OVER_Data","D_0",c+42) //random c
data = (global.last_room)*b-c
safe_ini_write_real("OVER_Data","D_1",data) //last room
safe_ini_write_real("OVER_Data","D_2",a+17) // random a
data = (global.Player_x)*(c+5)-b
safe_ini_write_real("OVER_Data","D_3",data) //player x pos

data = (global.Player_y)*(b+2)-a
safe_ini_write_real("OVER_Data","E_0",data) //player y pos
safe_ini_write_real("OVER_Data","E_1",b+24) //random b
data = (global.Minute)*c-a
safe_ini_write_real("OVER_Data","E_2",data) //timer: minute
data = (global.Hour)*b+(a*2)
safe_ini_write_real("OVER_Data","E_3",data) //timber: hour

data = (global.Second)*(a+3)-c
safe_ini_write_real("OVER_Data","F_0",data) //timer: second
data = (global.Area_Name)*c+100
safe_ini_write_real("OVER_Data","F_1",data) //current area
data = (global.Currency[1])*b-(a+c)
safe_ini_write_real("OVER_Data","F_2",data) //magic currency
data = (global.Currency[0])*c+b
safe_ini_write_real("OVER_Data","F_3",data) //money

for (i=0; i<size; i+=1)
{
 data = (global.Character_Speed[ds_list_find_value(global.Player_Party,i)])*(i+c)-(i*b)
 safe_ini_write_real("OVER_Data","G_"+string(i),data) //character speed
}

for (i=0; i<size; i+=1)
{
 data = ((global.Character_Class[ds_list_find_value(global.Player_Party,i)])-b-(i*a))*c
 safe_ini_write_real("OVER_Data","H_"+string(i),data) //character current class
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_EXP_Current[ds_list_find_value(global.Player_Party,i)])*(i*b-a)
 safe_ini_write_real("OVER_Data","I_"+string(i),data) //character current EXP
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_EXP_Max[ds_list_find_value(global.Player_Party,i)])*(b+(i*5))
 safe_ini_write_real("OVER_Data","J_"+string(i),data) //character EXP max
}

for (i=0; i<size; i+=1)
{
 data = (global.Item_Slot[ds_list_find_value(global.Player_Party,i),0])*(i*b+c-50)
 safe_ini_write_real("OVER_Data","K_"+string(i),data) //Helm
}

for (i=0; i<size; i+=1)
{
 data = (global.Item_Slot[ds_list_find_value(global.Player_Party,i),1])*b+(c*i)
 safe_ini_write_real("OVER_Data","L_"+string(i),data) //Chest
}

for (i=0; i<size; i+=1)
{
 data = (global.Item_Slot[ds_list_find_value(global.Player_Party,i),2])*(c+25*i)
 safe_ini_write_real("OVER_Data","M_"+string(i),data) //Weapon
}

for (i=0; i<size; i+=1)
{
 data = (global.Item_Slot[ds_list_find_value(global.Player_Party,i),3])+c*(i*b-a)
 safe_ini_write_real("OVER_Data","N_"+string(i),data) //Accessory
}

for (i=0; i<size; i+=1)
{
 data = (global.Item_Slot[ds_list_find_value(global.Player_Party,i),4])-c*(i*2+10)
 safe_ini_write_real("OVER_Data","O_"+string(i),data) //Special
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_Rune_Slot[ds_list_find_value(global.Player_Party,i),0])*((i*b)+17)
 safe_ini_write_real("OVER_Data","P_"+string(i),data) //Rune 1
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_Rune_Slot[ds_list_find_value(global.Player_Party,i),1])-b*(i*7+1)
 safe_ini_write_real("OVER_Data","Q_"+string(i),data) //Rune 2
}

for (i=0; i<size; i+=1)
{
 data = (global.Character_Rune_Slot[ds_list_find_value(global.Player_Party,i),2])+43*(b*i+23)
 safe_ini_write_real("OVER_Data","R_"+string(i),data) //Rune 3
}

for (i=0; i<ds_list_size(global.Player_Iventory); i+=1)
{
 data = (ds_list_find_value(global.Player_Iventory,i))*(i*b-c)
 safe_ini_write_real("OVER_Data","S_"+string(i),data) //ITEMS - for inventory order
}

data = (global.Total_Items)*c+b
safe_ini_write_real("OVER_Data","T_0",data) //total ITEMS in game as of this version
data = (global.Total_Creatures)*b-(c*21)
safe_ini_write_real("OVER_Data","T_1",data) //total CREATURES in game as of this version
data = (global.Total_Classes)*a-c
safe_ini_write_real("OVER_Data","T_2",data) //total CLASSES in game as of this version
data = (global.total_switches)*17+c
safe_ini_write_real("OVER_Data","T_3",data) //total SWITCHES in game as of this version
data = (ds_list_size(global.Player_Party))*c-b
safe_ini_write_real("OVER_Data","T_4",data) //total PARTY MEMBERS with the player
data = (ds_list_size(global.Player_Iventory))*a+b
safe_ini_write_real("OVER_Data","T_5",data) //size of Player's inventory
data = (global.Save_Disabled)*b+c
safe_ini_write_real("OVER_Data","T_6",data) //player can save the game or not in the current room
data = string_length(global.Character_Name[0])-(a-b)
safe_ini_write_real("OVER_Data","T_7",data) //player name length


for (i=0; i<ds_list_size(global.Player_Iventory); i+=1)
{
 data = (global.Item_Quan[ds_list_find_value(global.Player_Iventory,i)])*(i+c)-b
 safe_ini_write_real("OVER_Data","U_"+string(i),data) //ITEM quantities
}

for (i=0; i<global.Total_Creatures; i+=1)
{
 data = (global.Creature_Seen[i])*c-(b*i+3)
 safe_ini_write_real("OVER_Data","V_"+string(i),data) //CREATURES SEEN LIST
}

for (i=0; i<size; i+=1)
{
 data = (ds_list_find_value(global.Player_Party,i))*b-c+(i*5)
 safe_ini_write_real("OVER_Data","W_"+string(i),data) //CHARACTER PARTY POSES YO
}

if (ds_list_size(global.Player_Party) > 0)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_Level[ds_list_find_value(global.Player_Party,0),i])*(i+c)
  safe_ini_write_real("OVER_Data","X_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 1)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_Level[ds_list_find_value(global.Player_Party,1),i])*((i+1)-b)
  safe_ini_write_real("OVER_Data","Y_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 2)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_Level[ds_list_find_value(global.Player_Party,2),i])*(i+b)
  safe_ini_write_real("OVER_Data","Z_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 3)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_Level[ds_list_find_value(global.Player_Party,3),i])*(b+(i*3))
  safe_ini_write_real("OVER_Data","AA_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 0)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_EXP[ds_list_find_value(global.Player_Party,0),i])*(10+(i*b))
  safe_ini_write_real("OVER_Data","BB_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 1)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_EXP[ds_list_find_value(global.Player_Party,1),i])*(b+(i*c)-a)
  safe_ini_write_real("OVER_Data","CC_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 2)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_EXP[ds_list_find_value(global.Player_Party,2),i])*b-(i*3)
  safe_ini_write_real("OVER_Data","DD_"+string(i),data) //CLASS LEVELS
 }
}

if (ds_list_size(global.Player_Party) > 3)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Character_Class_EXP[ds_list_find_value(global.Player_Party,3),i])*(i+b-a)
  safe_ini_write_real("OVER_Data","OVER_"+string(i),data) //CLASS LEVELS
 }
}


//SKILL INICIO
for (i=0; i<size; i+=1)
{
 data = (ds_list_size(global.Character_Skills[ds_list_find_value(global.Player_Party,i)]))*(i+(i*b)+1)
 safe_ini_write_real("OVER_Data","FF_"+string(i),data) //CHARACTER SKILL LIST SIZES
}

if (ds_list_size(global.Player_Party) > 0)
{
 for (i=0; i<ds_list_size(global.Character_Skills[ds_list_find_value(global.Player_Party,0)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Skills[ds_list_find_value(global.Player_Party,0)],i))*((i+c)*b-31)
  safe_ini_write_real("OVER_Data","GG_"+string(i),data) //CHARACTER SKILL LIST
 }
}

if (ds_list_size(global.Player_Party) > 1)
{
 for (i=0; i<ds_list_size(global.Character_Skills[ds_list_find_value(global.Player_Party,1)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Skills[ds_list_find_value(global.Player_Party,1)],i))*(b+(c+i))
  safe_ini_write_real("OVER_Data","HH_"+string(i),data) //CHARACTER SKILL LIST
 }
}

if (ds_list_size(global.Player_Party) > 2)
{
 for (i=0; i<ds_list_size(global.Character_Skills[ds_list_find_value(global.Player_Party,2)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Skills[ds_list_find_value(global.Player_Party,2)],i))*b+((i+1)*c)
  safe_ini_write_real("OVER_Data","II_"+string(i),data) //CHARACTER SKILL LIST
 }
}

if (ds_list_size(global.Player_Party) > 3)
{
 for (i=0; i<ds_list_size(global.Character_Skills[ds_list_find_value(global.Player_Party,3)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Skills[ds_list_find_value(global.Player_Party,3)],i))*((i*a)-b)
  safe_ini_write_real("OVER_Data","JJ_"+string(i),data) //CHARACTER SKILL LIST
 }
}

// SKILL FIM
//SITUATION INICIO
for (i=0; i<size; i+=1)
{
 data = (ds_list_size(global.Character_Situation[ds_list_find_value(global.Player_Party,i)]))*(i+(i*b)+1)
 safe_ini_write_real("OVER_Data","FFS_"+string(i),data) //CHARACTER SITUATION LIST SIZES
}

if (ds_list_size(global.Player_Party) > 0)
{
 for (i=0; i<ds_list_size(global.Character_Situation[ds_list_find_value(global.Player_Party,0)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Situation[ds_list_find_value(global.Player_Party,0)],i))*((i+c)*b-31)
  safe_ini_write_real("OVER_Data","GGS_"+string(i),data) //CHARACTER SITUATION LIST
 }
}

if (ds_list_size(global.Player_Party) > 1)
{
 for (i=0; i<ds_list_size(global.Character_Situation[ds_list_find_value(global.Player_Party,1)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Situation[ds_list_find_value(global.Player_Party,1)],i))*(b+(c+i))
  safe_ini_write_real("OVER_Data","HHS_"+string(i),data) //CHARACTER SITUATION LIST
 }
}

if (ds_list_size(global.Player_Party) > 2)
{
 for (i=0; i<ds_list_size(global.Character_Situation[ds_list_find_value(global.Player_Party,2)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Situation[ds_list_find_value(global.Player_Party,2)],i))*b+((i+1)*c)
  safe_ini_write_real("OVER_Data","IIS_"+string(i),data) //CHARACTER SITUATION LIST
 }
}

if (ds_list_size(global.Player_Party) > 3)
{
 for (i=0; i<ds_list_size(global.Character_Situation[ds_list_find_value(global.Player_Party,3)]); i+=1)
 {
  data = (ds_list_find_value(global.Character_Situation[ds_list_find_value(global.Player_Party,3)],i))*((i*a)-b)
  safe_ini_write_real("OVER_Data","JJS_"+string(i),data) //CHARACTER SITUATION LIST
 }
}

// SITUATION FIM




for (i=0; i<global.total_switches; i+=1)
{
 data = (global.Switch[i])*(i*c-a)
 safe_ini_write_real("OVER_Data","KK_"+string(i),data) //SWITCH STATUS
}

for (i=0; i<string_length(global.Character_Name[0]); i+=1)
{
 temp = string_char_at(global.Character_Name[0],i+1)
 data = ord(temp)+(i+1*a)
 safe_ini_write_real("OVER_Data","LL_"+string(i),data) //Player Name
}

if (ds_list_size(global.Player_Party) > 0)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Class_Unlocked[i,ds_list_find_value(global.Player_Party,0)])*(10+(i*b))
  safe_ini_write_real("OVER_Data","MM_"+string(i),data) //CLASS UNLOCKS
 }
}

if (ds_list_size(global.Player_Party) > 1)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Class_Unlocked[i,ds_list_find_value(global.Player_Party,1)])*(b+(i*c)-a)
  safe_ini_write_real("OVER_Data","NN_"+string(i),data) //CLASS UNLOCKS
 }
}

if (ds_list_size(global.Player_Party) > 2)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Class_Unlocked[i,ds_list_find_value(global.Player_Party,2)])*b-(i*3)
  safe_ini_write_real("OVER_Data","OO_"+string(i),data) //CLASS UNLOCKS
 }
}

if (ds_list_size(global.Player_Party) > 3)
{
 for (i=0; i<global.Total_Classes; i+=1)
 {
  data = (global.Class_Unlocked[i,ds_list_find_value(global.Player_Party,3)])*(i+b-a)
  safe_ini_write_real("OVER_Data","PP_"+string(i),data) //CLASS UNLOCKS
 }
}
// INCLUIDOS INICIO

safe_ini_write_real( "SAVE", "combat_animation_save", global.combat_animation );// true
safe_ini_write_real( "SAVE", "textbox_style_save", global.textbox_style  );//0
safe_ini_write_real( "SAVE", "textbox_dialog_save", global.textbox_dialog );//0
safe_ini_write_string( "SAVE", "language_save", global.language );// string
safe_ini_write_real( "SAVE", "curWeekday_save", global.curWeekday );// 5
safe_ini_write_real( "SAVE", "curMonth_save", global.curMonth );// 7
safe_ini_write_real( "SAVE", "curDay_save", global.curDay );//23
safe_ini_write_real( "SAVE", "curYear_save", global.curYear );//2997
safe_ini_write_real( "SAVE", "secondsx_save", global.secondsx );//99999
safe_ini_write_real( "SAVE", "showcalendar_save", global.showcalendar );//false
safe_ini_write_real( "SAVE", "clockspeed_save", global.clockspeed );//99999

safe_ini_write_real( "SAVE", "caterpillar_save", global.caterpillar );// true
safe_ini_write_real( "SAVE", "lightsystem_save", global.lightsystem );// true
safe_ini_write_real( "SAVE", "custom_battle_save", global.custom_battle );// true
safe_ini_write_real( "SAVE", "input_number_save", global.input_number );//0
safe_ini_write_real( "SAVE", "Player_Disabled_save", global.Player_Disabled );// false
safe_ini_write_real( "SAVE", "Player_Movement_Speed_save", global.Player_Movement_Speed );// 4
safe_ini_write_real( "SAVE", "current_account_save", global.current_account );// 0
safe_ini_write_real( "SAVE", "savings_account_save", global.savings_account );// 0
safe_ini_write_real( "SAVE", "CDB_account_save", global.CDB_account );// 0
safe_ini_write_real( "SAVE", "RDB_account_save", global.RDB_account );// 0
safe_ini_write_real( "SAVE", "stock_exchange_actions_save", global.stock_exchange_actions );// 0




// INCLUIDOS FIM

with (obSYS_Save_Game)
{
 section = 3
 alarm[7]  = 30
}

safe_ini_close()

My script was working perfectly, I just made small minor changes to "ini_open" and changed the prefixes "ini_" to "safe_ini".

Can someone help me ?

Ednei
 

zbox

Member
GMC Elder
Hi there, the error is as describes if you have a look at the middle line: "Safe Save INI: This character is not allowed in Section or Key names: _"

Just change the underscore to a hyphen "-" or some other symbol and you'll be be fine. :)
 

Ednei

Member
Hi there, the error is as describes if you have a look at the middle line: "Safe Save INI: This character is not allowed in Section or Key names: _"

Just change the underscore to a hyphen "-" or some other symbol and you'll be be fine. :)
Is it serious?
Would not it be possible to make a change in its extension so that it can use "_" in the variables?
The original functions (ini_write_real) allow "_" to be used.
I'll have to practically rewrite my game to be able to use the safe save extension.
 

zbox

Member
GMC Elder
You could probably set
global._safe_ini_sep
to "." or something but I wouldn't recommend it and this behavior is not supported. If you've got any more queries feel free to send me an email though the marketplace and that way I'll be able to get back quicker :)
 

Ednei

Member
You could probably set
global._safe_ini_sep
to "." or something but I wouldn't recommend it and this behavior is not supported. If you've got any more queries feel free to send me an email though the marketplace and that way I'll be able to get back quicker :)
It's working correctly. Thank you for your support.
 
D

djrain

Guest
Installing this extension causes my game to immediately crash on iOS when compiled with YYC (works with VM though). Any reason why that would be happening? I really need it to work with YYC...
 

zbox

Member
GMC Elder
Hi there - This extension is written in pure GML so I am skeptical as to whether it is the sole cause of the crash - Can you email me using the contact publisher button on the Marketplace and send me a copy of your logs when you run the game??
 
D

djrain

Guest
Hi there - This extension is written in pure GML so I am skeptical as to whether it is the sole cause of the crash - Can you email me using the contact publisher button on the Marketplace and send me a copy of your logs when you run the game??
Sure. Just to be clear, you want everything that Gamemaker generates (under "output") when I build it for iOS?
 

zbox

Member
GMC Elder
Sure. Just to be clear, you want everything that Gamemaker generates (under "output") when I build it for iOS?
Indeed - email it to me, in a separate text file.

Edit: was a problem with another extension, not Safesave :)
 
Last edited:

rIKmAN

Member
Absolutely - the save files have a built in integrity check and there is functionality to report whether the file has been tampered with or not. By default if the file is tampered with then it will fail to load. The save files are also device-specific as to not allow transfer between devices.
Are we able to choose whether the saves are device specific?
 

zbox

Member
GMC Elder
Are we able to choose whether the saves are device specific?
Currently not - however a quick hack you could do would be to change the "device_get_id()" function to always return the same value - then saves would be transferrable :)
 

Tornado

Member
@zbox
Hi!
Your asset "AES GML" looks very nice. Looks like probably the best out there for encryption.
I'm trying to go deeper into this topic these days, but it is a hard topic for a layman. I saw some extensions which operates on Strings and there it is said it exists this problem with null string termination. So people keep saying that using buffers is the way to go. I saw in your documentation that you use arrays. So I guess arrays like buffers also do not have this null string problem?
What is then the difference between implementation with buffers vs implementation with arrays? Just speed? Or more? Or actually nothing really?
Thank you!
 

zbox

Member
GMC Elder
Hi! The arrays are there to solve exactly the problem you mention, null string termination. (Which however once you're done decrypting you can turn it back into a string, given that your strings don't contain nulls (which they probably don't and if they do, you're most likely dealing with binary data blobs which would be better handled with the write to file of base64 encoding options))

I find with this many operations on each char of the string the speed improvement buffers would give is not important, whilst they probably take a bit less memory, at this scale, the difference is negligible to modern computers. So I went with the portability of arrays instead - The outcome is the same.

The two assets perform different functions - SafeSave is to store and encrypt data easily (With basic RC4 encryption), and the AES extension is a tool to help with encryption. SafeSave offers the functionality to switch out the normal RC4 encryption with AES encryption if it suits your purposes :)
 

Tornado

Member
I'm still not sure if I understand. SafeSave contains big functionality with all the features listed on the marketplace. But if I want it to use AES encryption within it, I have to have AES GML extension too? AES GML can be seen either as a smaller standalone product and as well as a "plugin" for SafeSave?
So if I want to encrypt ini files plus checksum checking, etc...but with strong AES encryption, then I have to have both assets?
Is that understanding correct?

Thank you very much for your kind support!
 

zbox

Member
GMC Elder
AES GML can be seen either as a smaller standalone product and as well as a "plugin" for SafeSave. So if I want to encrypt ini files plus checksum checking, etc...but with strong AES encryption, then I have to have both assets?
Correct. If for your purposes RC4 is fine, then there is no need to buy the AES extension (however if you want to encrypt something else unrelated you can still use the AES extension!). Adding the AES integration just changes the type of encryption used in SafeSave, the rest of the functionality remains the same. Google is your friend in deciding which one is right for you :)
 

Tornado

Member
The extensions are cheap for the value and it is only one time buy. I can buy both, this will be no problem. I aim for the maximal security of the ini file.

I will be glad If you can recommend the algorithm.

This will save me hours of layman surfing and digging for info which I might not even understand. Encryption is not an easy topic. I did quick googling it seems that AES is stronger!
THX
 
Last edited:

RyanC

Member
This asset looks very nice!
I'm a little concerned about the device_id function though. It looks likes it's a gml function? in which case may just return -1 for an unknown device or "android_tablet", which would still allows many users to share the file.
 

zbox

Member
GMC Elder
Where do you get that impression :)?

It would be literally impossible for the function to return either of those values, given it actually returns a checksum of the unique features of each device concatenated to eachother. You're also more than free to replace the function with your own device ID getter, if it suits you more.
 

Tornado

Member
@zbox
I bought both "SafeSave INI" and "AES GML". I can run both separately, both demos are working. I also ran SafeSave using AES.

1) How can I check that the produced ini file is really AES encrypted?

2)
Why when importing those extensions some scripts are doubled and the duplicates get the suffix "_new". Then I have to go and delete all _new files.
I'm using GMS v1.4.1772

3) after importing into GMSG, can I remove everything from extensions folder? Or some stuff is needed from there too?

Thx
 
Last edited:

zbox

Member
GMC Elder
Great :)

1) Take your key you are using to encrypt your data, and an example of the saved data file, decrypt it using the key. You'll see your data structure you saved
2) There may be some overlap in scripts I use, delete the _new ones.
3) The extensions are needed

Cheers
 

Zuljaras

Member
Great :)

1) Take your key you are using to encrypt your data, and an example of the saved data file, decrypt it using the key. You'll see your data structure you saved
2) There may be some overlap in scripts I use, delete the _new ones.
3) The extensions are needed

Cheers
Hello.
I have the following errors after I try to compile my game:
"
Error : gml_Script_safe_save(17) : unknown function or script safe_device_id
Error : gml_Script_safe_save(27) : unknown function or script safe_save_use_aes
Error : gml_Script_safe_save(27) : unknown function or script safe_save_use_aes
Error : gml_Script_safe_save(41) : unknown function or script array_to_list
Error : gml_Script_safe_load(22) : unknown function or script safe_save_use_aes
Error : gml_Script_safe_load(22) : unknown function or script safe_save_use_aes
Error : gml_Script_safe_load(31) : unknown function or script list_to_array
Error : gml_Script_safe_load(32) : unknown function or script array_to_list
Error : gml_Script_safe_load(43) : unknown function or script string_trim_trail
Error : gml_Script_safe_load(61) : unknown function or script safe_device_id
"

I did not even used the encryption yet. I just imported it into my project and I cant start my game. This is GMS1.4.
 

zbox

Member
GMC Elder
Hi - looks like you have not imported the scripts that come with the extension.
 

Zuljaras

Member
I managed to start it but the saving is too slow.
Any other users get the same problems?

I must save 2 arrays with 1000 positions. When I save them I get a freeze that lasts around 20-30 seconds. Every time.
 
E

ei8htbit

Guest
I purchased both the Safe Save and AES extension - great product, very useful - thanks for making this. I have a few question regarding best practices for storing Keys (and/or IVs for AES) is it advisable to:
A) keep the Key and/or IV values stored as a variable within the script that gets compiled with the game package (ie. is there an easy way for anyone to search through compiled game code to sniff out a key string variable stored in game script?)
B) obfuscate and save the key variable within a separate text file or elsewhere and then pull it into code in order to decrypt as needed during runtime?
C) Bonus question - do you recommend generating individual Keys/IVs for every single value being encrypted with AES or is that overkill (essentially triples the amount of variables required for each encrypted data asset)?
D) Last question: I checked the readme and looked through all script comments in the package for Safe Save and was not able to find an answer - how do you activate AES as the default encryption method for Safe Save (as opposed to RC4). It seems to mention on the marketplace page but the readme does not include that info - just thought I'd ask..

Thanks again for your time and efforts, much appreciated.
 
Last edited by a moderator:
H

Hikati Games

Guest
I purchased both the Safe Save and AES extension - great product, very useful - thanks for making this. I have a few question regarding best practices for storing Keys (and/or IVs for AES) is it advisable to:
A) keep the Key and/or IV values stored as a variable within the script that gets compiled with the game package (ie. is there an easy way for anyone to search through compiled game code to sniff out a key string variable stored in game script?)
B) obfuscate and save the key variable within a separate text file or elsewhere and then pull it into code in order to decrypt as needed during runtime?
C) Bonus question - do you recommend generating individual Keys/IVs for every single value being encrypted with AES or is that overkill (essentially triples the amount of variables required for each encrypted data asset)?
D) Last question: I checked the readme and looked through all script comments in the package for Safe Save and was not able to find an answer - how do you activate AES as the default encryption method for Safe Save (as opposed to RC4). It seems to mention on the marketplace page but the readme does not include that info - just thought I'd ask..

Thanks again for your time and efforts, much appreciated.
@zbox

I am also looking for the same answers. The readme file no longer contains any mention of how to switch to aes and the file: save_save.gml looks like it only references rc4? I also checked the readme on the AES extension just in case but saw no mention of safesave in it.

I do want to say: SafeSave and AES are great extensions, I have not had any problems using either one by itself.
 

zbox

Member
GMC Elder
Hi all - please contact me through marketplace so I can follow up and be notified!
 

chirpy

Member
Hi, I'm trying to use this as well. It seems that the gms2 package contains version 1.0.2 of the extension, and only the gms1.4 package contains 1.0.3 with aes capability. Reported via marketplace as well.
 

Andrey

Member
Hello!
Great extension.

I encountered a minor memory leak when:
safe_ini_open

safe_ini_close


I guess this could be related to creating local maps, for example:
var def = ds_map_create()…

and the lack of deletion for them:
ds_map_destroy(def);

Please check this out.
 

zbox

Member
GMC Elder
Hello!
Great extension.

I encountered a minor memory leak when:
safe_ini_open

safe_ini_close


I guess this could be related to creating local maps, for example:
var def = ds_map_create()…

and the lack of deletion for them:
ds_map_destroy(def);

Please check this out.
Hi, please call safe_ini_close() when you're done with the file. This cleans up the memory use.
 

Andrey

Member
Hi, please call safe_ini_close() when you're done with the file. This cleans up the memory use.
Thanks for the answer!

But the problem exists for me.
If you copy the code from the example into the Step event, you can clearly see the leak:

In event Step object "objDemo":
GML:
safe_ini_open(fn, key);

    safe_ini_write_real("player-1", "health", 100);
    safe_ini_write_real("player-1", "lives", 4);
    safe_ini_write_string("player-1", "name", "Player 1");
    test_variable = 12345;
    safe_ini_write_string("player-1", "testvar", test_variable);
    
    safe_ini_write_real("player-1", "health", 70);
    safe_ini_write_real("player-1", "lives", 1);
    safe_ini_write_string("player-1", "name", "Player 2");
    
    safe_ini_write_real("IAP", "weapon-1", true);
    safe_ini_write_real("IAP", "weapon-2", false);
    
safe_ini_close();
Result:

leak.png
 

zbox

Member
GMC Elder
Cheers Andrey! On second look, I believe I've found it.

Add the following:

ds_list_destroy(decrypted_data_list);


To line 85 in the file called safe_save.gml in the extension's folder. I will release an update for this soon. One pesky line.
 

Tornado

Member
Hi!
I added that line to save_save.gml.

We are also using your AES extensions. Any memeory leaks there that need to be fixed?

I use this opportunity to say it again: we are very pleased with both extensions!!!
Nice piece of work, we are using it in our game which is in production :)

Thank you for this awesome piece of software!
 

zbox

Member
GMC Elder
Awesome glad to hear it. Not that I know of! But if you notice any please report them to me.
 
Top