class_name LocalSigningIdentityStore extends Node const FORMAT_VERSION: int = 1 var fingerprint: String = "" var public_pem: String = "" var error_message: String = "" var _private_key: CryptoKey var _prefix: String = "" var _allow_generation: bool = true func configure(prefix: String, allow_generation: bool = true) -> void: _prefix = prefix _allow_generation = allow_generation func load_or_create() -> bool: if _prefix.is_empty(): error_message = "Identity storage is not configured." return false var key_path := _path(".key") var public_path := _path(".pub") var metadata_path := _path(".json") var any_exists := ( FileAccess.file_exists(key_path) or FileAccess.file_exists(public_path) or FileAccess.file_exists(metadata_path) ) if any_exists: return _load_existing() if not _allow_generation: return false return _generate_new() func is_ready() -> bool: return _private_key != null and NetworkIdentityCrypto.valid_fingerprint(fingerprint) func sign(domain: String, fields: Array) -> PackedByteArray: return NetworkIdentityCrypto.sign_fields(_private_key, domain, fields) func verify(domain: String, fields: Array, signature: PackedByteArray) -> bool: return NetworkIdentityCrypto.verify_fields( NetworkIdentityCrypto.load_public_key(public_pem), domain, fields, signature, ) func _generate_new() -> bool: var key := Crypto.new().generate_rsa(3072) if key == null: error_message = "A signing identity could not be generated." return false var private_text := key.save_to_string() var public_text := NetworkIdentityCrypto.normalize_public_pem( key.save_to_string(true) ) var derived := NetworkIdentityCrypto.fingerprint_public_pem(public_text) var probe := NetworkIdentityCrypto.sign_fields(key, "identity_self_test", [derived]) var public_key := NetworkIdentityCrypto.load_public_key(public_text) if ( not NetworkIdentityCrypto.valid_fingerprint(derived) or not NetworkIdentityCrypto.verify_fields( public_key, "identity_self_test", [derived], probe ) ): error_message = "The generated signing identity failed verification." return false var metadata := { "format_version": FORMAT_VERSION, "algorithm": NetworkIdentityCrypto.ALGORITHM, "fingerprint": derived, "created_at_unix": int(Time.get_unix_time_from_system()), } if ( not _write_atomic(_path(".key"), private_text) or not _write_atomic(_path(".pub"), public_text) or not _write_atomic(_path(".json"), JSON.stringify(metadata, "\t")) ): error_message = "The signing identity could not be stored." return false FileAccess.set_unix_permissions(_path(".key"), 384) return _load_existing() func _load_existing() -> bool: var required := [_path(".key"), _path(".pub"), _path(".json")] for path: String in required: if not FileAccess.file_exists(path): error_message = "Identity recovery is required. An identity file is missing." return false var private_file := FileAccess.open(_path(".key"), FileAccess.READ) var public_file := FileAccess.open(_path(".pub"), FileAccess.READ) var metadata_file := FileAccess.open(_path(".json"), FileAccess.READ) if private_file == null or public_file == null or metadata_file == null: error_message = "Identity recovery is required. Identity files could not be read." return false var private_text := private_file.get_as_text() var loaded_public := NetworkIdentityCrypto.normalize_public_pem(public_file.get_as_text()) var metadata_text := metadata_file.get_as_text() var json := JSON.new() if json.parse(metadata_text) != OK or typeof(json.data) != TYPE_DICTIONARY: error_message = "Identity recovery is required. Identity metadata is damaged." return false var metadata: Dictionary = json.data var derived := NetworkIdentityCrypto.fingerprint_public_pem(loaded_public) if ( metadata.get("format_version") != FORMAT_VERSION or metadata.get("algorithm") != NetworkIdentityCrypto.ALGORITHM or metadata.get("fingerprint") != derived ): error_message = "Identity recovery is required. Identity files do not match." return false var key := CryptoKey.new() if key.load_from_string(private_text) != OK: error_message = "Identity recovery is required. The private key is damaged." return false var probe := NetworkIdentityCrypto.sign_fields(key, "identity_self_test", [derived]) if not NetworkIdentityCrypto.verify_fields( NetworkIdentityCrypto.load_public_key(loaded_public), "identity_self_test", [derived], probe, ): error_message = "Identity recovery is required. The key pair does not match." return false _private_key = key public_pem = loaded_public fingerprint = derived error_message = "" return true func _write_atomic(path: String, content: String) -> bool: var temporary := path + ".tmp" var file := FileAccess.open(temporary, FileAccess.WRITE) if file == null: return false file.store_string(content) file.flush() var write_error := file.get_error() file.close() if write_error != OK: return false if FileAccess.file_exists(path): var backup := path + ".backup" _remove_if_present(backup) if not _rename(path, backup): return false if not _rename(temporary, path): _rename(backup, path) return false _remove_if_present(backup) return true return _rename(temporary, path) func _path(extension: String) -> String: return "user://%s%s" % [_prefix, extension] func _rename(from_path: String, to_path: String) -> bool: return DirAccess.rename_absolute( ProjectSettings.globalize_path(from_path), ProjectSettings.globalize_path(to_path), ) == OK func _remove_if_present(path: String) -> void: if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path))