Hey guys, I’m trying to implement a way to load encrypted .tscn files (with embedded gdscript) in my Android app, which can be (optionally) fetched by a client over the internet.
So the question is really about being able to load a scene which does not exist in file system, but only in memory. Simplest way, I would imagine for it to work, would be like this:
func _ready():
var data = FileAccess.get_file_as_bytes("user://downloaded.tscn.encrypted")
#<-- perform some decryption, using custom C++ library made with GDExtension
#<-- convert [decrypted_data: String] into [scene: PackedScene]
scene.instantiate()
Question
Is there any way of doing this:
var scene: PackedScene = convert(decrypted_data: String)
Notes
Why not use PCK/APK Expansion to encrypt the whole .obb? Because it’s pain in the ass for prototyping/sharing it with third parties/having to use Android Public Key, and it’s gonna be easier to crack than my own custom decryption class (unless I’m mistaken?). Bear in mind I don’t want to save the decrypted file on disk, for the ResourceLoader.load() to be able to load it, kind of defies the purpose.
And it looks like no, the engine class ResourceLoaderText that loads resources only uses a VariantParser::StreamFile, there’s no way for it to use a VariantParser::StreamString or make a fake file as far as I can tell.
It would probably require editing engine code or building with a custom module, unless you want to recreate the resource format loader entirely.
Don’t save the scene itself. Save and encrypt the PackedScene._bundled property.
Then you can load it with:
func _ready():
var data = FileAccess.get_file_as_bytes("user://downloaded.tscn.encrypted")
var decrypted_data = decrypt(data)
var scene = PackedScene.new()
scene._bundled = decrypted_data
var node = scene.instantiate()
add_child(node)
extends Node
func _ready() -> void:
var crypto = Crypto.new()
var key = crypto.generate_random_bytes(32)
save_scene(key)
await get_tree().create_timer(2).timeout
load_scene(key)
func save_scene(key:PackedByteArray) -> void:
var scene = load("res://test_2dhdr.tscn") as PackedScene
var data = scene._bundled.duplicate()
var file = FileAccess.open_encrypted('user://test.data', FileAccess.WRITE, key)
file.store_var(data, true)
file.close()
func load_scene(key:PackedByteArray) -> void:
var file = FileAccess.open_encrypted('user://test.data', FileAccess.READ, key)
var data = file.get_var(true)
file.close()
var scene = PackedScene.new()
scene._bundled = data
var instance = scene.instantiate()
add_child(instance)
Unfortunately this seems to only work for tiny scenes with a few objects. For some reason my 12kb .tscn turns into 300mb encrypted file, which also refuses to load. Can you think of the reason why that might happen?
I think you’re right. The funny thing is that no way that scene could take 300 megs of memory in fully uncompressed format (I’d say 20x less). That being said, your solution works great on simple nodes with scripts, which I think can be used to hide some core aspects of game logic (in conjunction with GDExtension). Cheers.