Skip to content

Commit 4369879

Browse files
authored
Load and export database as JSON-formatted buffer to enable database encryption (#216)
* Add `import_from_buffer()`- and `export_to_buffer()`-methods to export/import a database from/to a PackedByteArray. * Updated relevant documentation * Added example of AES database encryption to demo
1 parent 3a7d25e commit 4369879

File tree

5 files changed

+145
-27
lines changed

5 files changed

+145
-27
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ Additionally, a video tutorial by [Mitch McCollum (finepointcgi)](https://github
215215
216216
Exports the database structure and content to `export_path.json` as a backup or for ease of editing.
217217
218+
- Boolean success = **import_from_buffer(** PackedByteArray input_buffer **)**
219+
220+
Drops all database tables and imports the database structure and content encoded in JSON-formatted input_buffer.
221+
222+
Can be used together with `export_to_buffer()` to implement database encryption.
223+
224+
- PackedByteArray output_buffer = **export_to_buffer()**
225+
226+
Returns the database structure and content as JSON-formatted buffer.
227+
228+
Can be used together with `import_from_buffer()` to implement database encryption.
229+
218230
- Boolean success = **create_function(** String function_name, FuncRef function_reference, int number_of_arguments **)**
219231
220232
Bind a [scalar SQL function](https://www.sqlite.org/appfunc.html) to the database that can then be used in subsequent queries.

demo/database.gd

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func _ready():
4040
example_of_read_only_database()
4141
example_of_database_persistency()
4242
example_of_fts5_usage()
43+
example_of_encrypted_database()
4344

4445
func cprint(text : String) -> void:
4546
print(text)
@@ -174,7 +175,7 @@ func example_of_basic_database_querying():
174175

175176
# Close the current database
176177
db.close_db()
177-
178+
178179
# Import (and, consequently, open) a database from an old backup json-file
179180
cprint("Overwriting database content with old backup...")
180181
db.import_from_json(json_name + "_old")
@@ -494,3 +495,71 @@ func example_of_fts5_usage():
494495

495496
# Close the current database
496497
db.close_db()
498+
499+
500+
# Example of "secure" database storage using AES encryption
501+
# Mind that this is still vulnerable to various attacks like memory dumps during runtime
502+
func example_of_encrypted_database():
503+
# Unlike in this example, the key should never be stored persistently inside the project
504+
var key = "secret_key123456" # Key must be either 16 or 32 bytes
505+
506+
# Create a table containing "sensitive" data
507+
var table_dict : Dictionary = Dictionary()
508+
table_dict["name"] = {"data_type":"text", "not_null": true}
509+
510+
db = SQLite.new()
511+
# Use in-memory shared database to avoid storing database on disk
512+
db.path = "file::memory:?cache=shared"
513+
db.verbosity_level = verbosity_level
514+
db.open_db()
515+
db.create_table("agents", table_dict)
516+
517+
var row_array : Array = []
518+
var row_dict : Dictionary = Dictionary()
519+
for i in range(0,names.size()):
520+
row_dict["name"] = names[i]
521+
row_array.append(row_dict.duplicate())
522+
523+
db.insert_row("agents", row_dict)
524+
row_dict.clear()
525+
526+
# Output original database
527+
var agents_pre: Array = db.select_rows("agents", "", ["name"])
528+
var names_pre = agents_pre.map(func(agent): return agent["name"])
529+
cprint("Agent names pre encryption: " + str(names_pre))
530+
531+
# Export database as JSON-formatted buffer
532+
var unencrypted: PackedByteArray = db.export_to_buffer()
533+
db.drop_table("agents")
534+
535+
# Add padding
536+
var buffer_size = unencrypted.size()
537+
var padding_size = 16-posmod(buffer_size, 16)
538+
var padding := PackedByteArray()
539+
padding.resize(padding_size)
540+
padding.fill(padding_size)
541+
unencrypted.append_array(padding)
542+
543+
var aes = AESContext.new()
544+
aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer())
545+
var encrypted: PackedByteArray = aes.update(unencrypted)
546+
aes.finish()
547+
# encrypted now contains the padded AES-encrypted database and can be stored using FileAccess
548+
549+
aes.start(AESContext.MODE_ECB_DECRYPT, key.to_utf8_buffer())
550+
var decrypted: PackedByteArray = aes.update(encrypted)
551+
aes.finish()
552+
# decrypted now contains the padded AES-encrypted database
553+
554+
# Remove padding
555+
buffer_size = decrypted.size()
556+
padding_size = decrypted.get(buffer_size-1)
557+
decrypted = decrypted.slice(0, buffer_size-padding_size)
558+
559+
# Import database from JSON-formatted buffer
560+
db.import_from_buffer(decrypted)
561+
562+
# Output database after encryption and decryption
563+
var agents_post: Array = db.select_rows("agents", "", ["name"])
564+
var names_post = agents_post.map(func(agent): return agent["name"])
565+
cprint("Agent names post decryption: " + str(names_post))

doc_classes/SQLite.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@
162162
Exports the database structure and content to [code]export_path.json[/code] as a backup or for ease of editing.
163163
</description>
164164
</method>
165+
<method name="import_from_buffer">
166+
<return type="bool" />
167+
<description>
168+
Drops all database tables and imports the database structure and content encoded in JSON-formatted input buffer.
169+
Can be used together with [method SQLite.export_to_buffer] to implement database encryption.
170+
</description>
171+
</method>
172+
<method name="export_to_buffer">
173+
<return type="PackedByteArray" />
174+
<description>
175+
Returns the database structure and content as JSON-formatted buffer.
176+
Can be used together with [method SQLite.import_from_buffer] to implement database encryption.
177+
</description>
178+
</method>
165179
<method name="create_function">
166180
<return type="bool" />
167181
<description>

src/gdsqlite.cpp

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ void SQLite::_bind_methods() {
2626

2727
ClassDB::bind_method(D_METHOD("import_from_json", "import_path"), &SQLite::import_from_json);
2828
ClassDB::bind_method(D_METHOD("export_to_json", "export_path"), &SQLite::export_to_json);
29+
ClassDB::bind_method(D_METHOD("import_from_buffer", "json_buffer"), &SQLite::import_from_buffer);
30+
ClassDB::bind_method(D_METHOD("export_to_buffer"), &SQLite::export_to_buffer);
2931

3032
ClassDB::bind_method(D_METHOD("get_autocommit"), &SQLite::get_autocommit);
3133
ClassDB::bind_method(D_METHOD("compileoption_used", "option_name"), &SQLite::compileoption_used);
@@ -819,15 +821,53 @@ bool SQLite::import_from_json(String import_path) {
819821
ERR_PRINT("GDSQLite Error: " + String(std::strerror(errno)) + " (" + import_path + ")");
820822
return false;
821823
}
824+
822825
std::stringstream buffer;
823826
buffer << ifs.rdbuf();
824827
std::string str = buffer.str();
825-
String json_string = String::utf8(str.c_str());
826828
ifs.close();
827829

828-
/* Attempt to parse the result and, if unsuccessful, throw a parse error specifying the erroneous line */
830+
String json_string = String::utf8(str.c_str());
831+
PackedByteArray json_buffer = json_string.to_utf8_buffer();
832+
833+
return import_from_buffer(json_buffer);
834+
}
835+
836+
bool SQLite::export_to_json(String export_path) {
837+
PackedByteArray json_buffer = export_to_buffer();
838+
String json_string = json_buffer.get_string_from_utf8();
839+
840+
/* Add .json to the import_path String if not present */
841+
String ending = String(".json");
842+
if (!export_path.ends_with(ending)) {
843+
export_path += ending;
844+
}
845+
/* Find the real path */
846+
export_path = ProjectSettings::get_singleton()->globalize_path(export_path.strip_edges());
847+
CharString dummy_path = export_path.utf8();
848+
const char *char_path = dummy_path.get_data();
849+
//const char *char_path = export_path.alloc_c_string();
850+
851+
std::ofstream ofs(char_path, std::ios::trunc);
852+
if (ofs.fail()) {
853+
ERR_PRINT("GDSQLite Error: " + String(std::strerror(errno)) + " (" + export_path + ")");
854+
return false;
855+
}
856+
857+
CharString dummy_string = json_string.utf8();
858+
ofs << dummy_string.get_data();
859+
//ofs << json_string.alloc_c_string();
860+
ofs.close();
861+
862+
return true;
863+
}
864+
865+
bool SQLite::import_from_buffer(PackedByteArray json_buffer) {
866+
/* Attempt to parse the input json_string and, if unsuccessful, throw a parse error specifying the erroneous line */
867+
829868
Ref<JSON> json;
830869
json.instantiate();
870+
String json_string = json_buffer.get_string_from_utf8();
831871
Error error = json->parse(json_string);
832872
if (error != Error::OK) {
833873
/* Throw a parsing error */
@@ -934,7 +974,7 @@ bool SQLite::import_from_json(String import_path) {
934974
return true;
935975
}
936976

937-
bool SQLite::export_to_json(String export_path) {
977+
PackedByteArray SQLite::export_to_buffer() {
938978
/* Get all names and sql templates for all tables present in the database */
939979
query(String("SELECT name,sql,type FROM sqlite_master;"));
940980
TypedArray<Dictionary> database_array = query_result.duplicate(true);
@@ -943,7 +983,7 @@ bool SQLite::export_to_json(String export_path) {
943983
remove_shadow_tables(database_array);
944984
#endif
945985
int64_t number_of_objects = database_array.size();
946-
/* Construct a Dictionary for each table, convert it to JSON and write it to file */
986+
/* Construct a Dictionary for each table, convert it to JSON and write it to String */
947987
for (int64_t i = 0; i <= number_of_objects - 1; i++) {
948988
Dictionary object_dict = database_array[i];
949989

@@ -989,31 +1029,11 @@ bool SQLite::export_to_json(String export_path) {
9891029
}
9901030
}
9911031

992-
/* Add .json to the import_path String if not present */
993-
String ending = String(".json");
994-
if (!export_path.ends_with(ending)) {
995-
export_path += ending;
996-
}
997-
/* Find the real path */
998-
export_path = ProjectSettings::get_singleton()->globalize_path(export_path.strip_edges());
999-
CharString dummy_path = export_path.utf8();
1000-
const char *char_path = dummy_path.get_data();
1001-
//const char *char_path = export_path.alloc_c_string();
1002-
1003-
std::ofstream ofs(char_path, std::ios::trunc);
1004-
if (ofs.fail()) {
1005-
ERR_PRINT("GDSQLite Error: " + String(std::strerror(errno)) + " (" + export_path + ")");
1006-
return false;
1007-
}
10081032
Ref<JSON> json;
10091033
json.instantiate();
10101034
String json_string = json->stringify(database_array, "\t");
1011-
CharString dummy_string = json_string.utf8();
1012-
ofs << dummy_string.get_data();
1013-
//ofs << json_string.alloc_c_string();
1014-
ofs.close();
1015-
1016-
return true;
1035+
PackedByteArray json_buffer = json_string.to_utf8_buffer();
1036+
return json_buffer;
10171037
}
10181038

10191039
bool SQLite::validate_json(const Array &database_array, std::vector<object_struct> &objects_to_import) {

src/gdsqlite.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class SQLite : public RefCounted {
9191
bool import_from_json(String import_path);
9292
bool export_to_json(String export_path);
9393

94+
bool import_from_buffer(PackedByteArray json_buffer);
95+
PackedByteArray export_to_buffer();
96+
9497
int get_autocommit() const;
9598
int compileoption_used(const String &option_name) const;
9699

0 commit comments

Comments
 (0)