145 lines
4.1 KiB
GDScript3
145 lines
4.1 KiB
GDScript3
|
|
class_name NetworkIdentityCrypto
|
||
|
|
extends RefCounted
|
||
|
|
|
||
|
|
const ALGORITHM: String = "RSA-3072-SHA256"
|
||
|
|
const FINGERPRINT_LENGTH: int = 64
|
||
|
|
const MAX_PUBLIC_KEY_BYTES: int = 8192
|
||
|
|
const MAX_SIGNATURE_BYTES: int = 1024
|
||
|
|
const DOMAIN_PREFIX: String = "NETFISHING"
|
||
|
|
const IDENTITY_VERSION: String = "identity_v1"
|
||
|
|
|
||
|
|
|
||
|
|
static func normalize_public_pem(value: String) -> String:
|
||
|
|
var normalized := value.replace("\r\n", "\n").replace("\r", "\n").strip_edges()
|
||
|
|
return normalized + "\n" if not normalized.is_empty() else ""
|
||
|
|
|
||
|
|
|
||
|
|
static func fingerprint_public_pem(value: String) -> String:
|
||
|
|
var normalized := normalize_public_pem(value)
|
||
|
|
return normalized.sha256_text() if not normalized.is_empty() else ""
|
||
|
|
|
||
|
|
|
||
|
|
static func valid_fingerprint(value: Variant) -> bool:
|
||
|
|
if typeof(value) != TYPE_STRING:
|
||
|
|
return false
|
||
|
|
var text := str(value)
|
||
|
|
if text.length() != FINGERPRINT_LENGTH:
|
||
|
|
return false
|
||
|
|
for character: String in text:
|
||
|
|
if character not in "0123456789abcdef":
|
||
|
|
return false
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
static func format_fingerprint(value: String, groups: int = 5) -> String:
|
||
|
|
if not valid_fingerprint(value):
|
||
|
|
return "Unavailable"
|
||
|
|
var visible := value.left(clampi(groups, 1, 8) * 4).to_upper()
|
||
|
|
var parts := PackedStringArray()
|
||
|
|
for offset: int in range(0, visible.length(), 4):
|
||
|
|
parts.append(visible.substr(offset, 4))
|
||
|
|
return "-".join(parts)
|
||
|
|
|
||
|
|
|
||
|
|
static func compact_suffix(value: String) -> String:
|
||
|
|
return value.right(6).to_upper() if valid_fingerprint(value) else "??????"
|
||
|
|
|
||
|
|
|
||
|
|
static func secure_id(byte_count: int = 16) -> String:
|
||
|
|
return Crypto.new().generate_random_bytes(byte_count).hex_encode()
|
||
|
|
|
||
|
|
|
||
|
|
static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray:
|
||
|
|
var output := PackedByteArray()
|
||
|
|
_append_string(output, DOMAIN_PREFIX)
|
||
|
|
_append_string(output, IDENTITY_VERSION)
|
||
|
|
_append_string(output, domain)
|
||
|
|
for value: Variant in fields:
|
||
|
|
match typeof(value):
|
||
|
|
TYPE_STRING, TYPE_STRING_NAME:
|
||
|
|
output.append(1)
|
||
|
|
_append_string(output, str(value))
|
||
|
|
TYPE_INT:
|
||
|
|
output.append(2)
|
||
|
|
_append_i64(output, int(value))
|
||
|
|
TYPE_BOOL:
|
||
|
|
output.append(3)
|
||
|
|
output.append(1 if bool(value) else 0)
|
||
|
|
TYPE_PACKED_BYTE_ARRAY:
|
||
|
|
output.append(4)
|
||
|
|
_append_bytes(output, value)
|
||
|
|
_:
|
||
|
|
return PackedByteArray()
|
||
|
|
return output
|
||
|
|
|
||
|
|
|
||
|
|
static func digest(domain: String, fields: Array) -> PackedByteArray:
|
||
|
|
var bytes := canonical_bytes(domain, fields)
|
||
|
|
if bytes.is_empty():
|
||
|
|
return PackedByteArray()
|
||
|
|
var context := HashingContext.new()
|
||
|
|
if context.start(HashingContext.HASH_SHA256) != OK:
|
||
|
|
return PackedByteArray()
|
||
|
|
context.update(bytes)
|
||
|
|
return context.finish()
|
||
|
|
|
||
|
|
|
||
|
|
static func sign_fields(
|
||
|
|
private_key: CryptoKey, domain: String, fields: Array
|
||
|
|
) -> PackedByteArray:
|
||
|
|
if private_key == null:
|
||
|
|
return PackedByteArray()
|
||
|
|
var value := digest(domain, fields)
|
||
|
|
return (
|
||
|
|
Crypto.new().sign(HashingContext.HASH_SHA256, value, private_key)
|
||
|
|
if not value.is_empty() else PackedByteArray()
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
static func verify_fields(
|
||
|
|
public_key: CryptoKey,
|
||
|
|
domain: String,
|
||
|
|
fields: Array,
|
||
|
|
signature: PackedByteArray,
|
||
|
|
) -> bool:
|
||
|
|
if (
|
||
|
|
public_key == null
|
||
|
|
or signature.is_empty()
|
||
|
|
or signature.size() > MAX_SIGNATURE_BYTES
|
||
|
|
):
|
||
|
|
return false
|
||
|
|
var value := digest(domain, fields)
|
||
|
|
return (
|
||
|
|
not value.is_empty()
|
||
|
|
and Crypto.new().verify(
|
||
|
|
HashingContext.HASH_SHA256, value, signature, public_key
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
static func load_public_key(public_pem: String) -> CryptoKey:
|
||
|
|
var normalized := normalize_public_pem(public_pem)
|
||
|
|
if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:
|
||
|
|
return null
|
||
|
|
var key := CryptoKey.new()
|
||
|
|
return key if key.load_from_string(normalized, true) == OK else null
|
||
|
|
|
||
|
|
|
||
|
|
static func _append_string(output: PackedByteArray, value: String) -> void:
|
||
|
|
_append_bytes(output, value.to_utf8_buffer())
|
||
|
|
|
||
|
|
|
||
|
|
static func _append_bytes(output: PackedByteArray, value: PackedByteArray) -> void:
|
||
|
|
_append_u32(output, value.size())
|
||
|
|
output.append_array(value)
|
||
|
|
|
||
|
|
|
||
|
|
static func _append_u32(output: PackedByteArray, value: int) -> void:
|
||
|
|
for shift: int in [24, 16, 8, 0]:
|
||
|
|
output.append((value >> shift) & 0xff)
|
||
|
|
|
||
|
|
|
||
|
|
static func _append_i64(output: PackedByteArray, value: int) -> void:
|
||
|
|
for shift: int in [56, 48, 40, 32, 24, 16, 8, 0]:
|
||
|
|
output.append((value >> shift) & 0xff)
|