A way to load encrypted .tscn

Problem

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.

Why not just save it temporarily in user::// and delete it once you load it into a PackedScene?

1 Like

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.

1 Like

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)
2 Likes

This seems to work great, however only for the scene (not the script within it) - _bundled doesn’t seem to include it.

Edit: Works with the script too, complete solution provided by @mrcdk below.

Seems to be working fine in my test:

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)

You can change the file stuff with var_to_bytes_with_objects() and bytes_to_var_with_objects() and then encrypt that PackedByteArray

2 Likes

Works! Thanks so much, I think I must have missed something in my previous attempt. You’re a star, @mrcdk !!

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 guess it’s saving also the Resources (meshes, sounds, textures,…)

At this point you may find easier to write your own serializer/deserializer and use your own format.

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.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.