95 lines
3 KiB
GDScript3
95 lines
3 KiB
GDScript3
|
|
@icon("./security-icon.svg")
|
||
|
|
@tool
|
||
|
|
extends Resource
|
||
|
|
|
||
|
|
## Provides a key to encrypt secrets in the application.
|
||
|
|
## Please don't store the key in the project,
|
||
|
|
## otherwise your secrets may revealed easily!
|
||
|
|
class_name CryptoKeyProvider
|
||
|
|
|
||
|
|
## Identify oOuch library specifics without collisions
|
||
|
|
const _CONFIG_PACKAGE_KEY: String = "dev.kani.oouch"
|
||
|
|
const _CONFIG_SECRET_KEY: String = "encryption"
|
||
|
|
const _AES_BLOCK_SIZE : int = 16
|
||
|
|
|
||
|
|
## Location of the encryption secrets
|
||
|
|
@export_global_file var encrpytion_secret_location: String = "user://encryption_key.cfg"
|
||
|
|
|
||
|
|
static var aes: AESContext = AESContext.new()
|
||
|
|
|
||
|
|
## To prevent accidental spoiler in the debugger
|
||
|
|
class KeyData extends RefCounted:
|
||
|
|
var key: String
|
||
|
|
|
||
|
|
var current_key_data: KeyData
|
||
|
|
|
||
|
|
func _init() -> void:
|
||
|
|
# Call defered cause the setter of encrpytion_secret_location isn't set otherwise
|
||
|
|
_get_encryption_secret.call_deferred()
|
||
|
|
|
||
|
|
|
||
|
|
## Don't cache it in a variable so that you accidently leak your secret when you debug
|
||
|
|
func _get_encryption_secret() -> String:
|
||
|
|
if is_instance_valid(current_key_data):
|
||
|
|
return current_key_data.key
|
||
|
|
|
||
|
|
var config = ConfigFile.new()
|
||
|
|
var error = config.load(encrpytion_secret_location)
|
||
|
|
if error == ERR_FILE_NOT_FOUND:
|
||
|
|
_create_secret(config)
|
||
|
|
elif error != OK:
|
||
|
|
printerr("Can't open %s cause of %s" % [encrpytion_secret_location, error_string(error)])
|
||
|
|
return ""
|
||
|
|
|
||
|
|
var key: String = config.get_value(_CONFIG_PACKAGE_KEY, _CONFIG_SECRET_KEY, "")
|
||
|
|
if key == "":
|
||
|
|
key = _create_secret(config)
|
||
|
|
|
||
|
|
current_key_data = KeyData.new()
|
||
|
|
current_key_data.key = key
|
||
|
|
return key
|
||
|
|
|
||
|
|
|
||
|
|
func _create_secret(config: ConfigFile) -> String:
|
||
|
|
print("Creating a new secret for encryption you can find it %s" % encrpytion_secret_location)
|
||
|
|
var crypto : Crypto = Crypto.new()
|
||
|
|
|
||
|
|
var secret_data : PackedByteArray = crypto.generate_random_bytes(16)
|
||
|
|
var secret : String = secret_data.hex_encode()
|
||
|
|
config.set_value(_CONFIG_PACKAGE_KEY, _CONFIG_SECRET_KEY, secret)
|
||
|
|
var err = config.save(encrpytion_secret_location)
|
||
|
|
if err != OK: push_error("Couldn't save encryption key cause of ", error_string(err))
|
||
|
|
return secret
|
||
|
|
|
||
|
|
|
||
|
|
func _pad(value: PackedByteArray) -> PackedByteArray:
|
||
|
|
var pad_len : int = _AES_BLOCK_SIZE - (value.size() % _AES_BLOCK_SIZE)
|
||
|
|
for i in range(pad_len):
|
||
|
|
value.append(pad_len)
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
func _unpad(value: PackedByteArray) -> PackedByteArray:
|
||
|
|
if value.is_empty():
|
||
|
|
return value
|
||
|
|
var pad_len : int = value[-1]
|
||
|
|
if pad_len <= 0 or pad_len > _AES_BLOCK_SIZE or value.size() < pad_len:
|
||
|
|
push_error("Invalid padding detected (%s)" % pad_len)
|
||
|
|
return PackedByteArray()
|
||
|
|
return value.slice(0, -pad_len)
|
||
|
|
|
||
|
|
|
||
|
|
func encrypt(value: PackedByteArray) -> PackedByteArray:
|
||
|
|
var padded_value = _pad(value)
|
||
|
|
aes.start(AESContext.MODE_ECB_ENCRYPT, _get_encryption_secret().to_utf8_buffer())
|
||
|
|
var encrypted_value: PackedByteArray = aes.update(padded_value)
|
||
|
|
aes.finish()
|
||
|
|
return encrypted_value
|
||
|
|
|
||
|
|
|
||
|
|
func decrypt(value: PackedByteArray) -> PackedByteArray:
|
||
|
|
aes.start(AESContext.MODE_ECB_DECRYPT, _get_encryption_secret().to_utf8_buffer())
|
||
|
|
var decrypted_value: PackedByteArray = aes.update(value)
|
||
|
|
aes.finish()
|
||
|
|
return _unpad(decrypted_value)
|