publicKey, $crypto); self::validateKeypair($loaded, $crypto); self::validateAddress($loaded->address, $loaded->publicKey, $crypto); return $loaded; } private static function decrypt(string $encodedCiphertext, string $walletKey): string { $encrypted = base64_decode($encodedCiphertext, true); if ($encrypted === false || strlen($encrypted) < 49) { throw new RuntimeException('The encrypted wallet payload is invalid.'); } $key = substr(str_pad($walletKey, 16, "\0"), 0, 16); $iv = substr($encrypted, 0, 16); $expectedHmac = substr($encrypted, 16, 32); $ciphertext = substr($encrypted, 48); $actualHmac = hash_hmac('sha256', $ciphertext, $key, true); if (!hash_equals($expectedHmac, $actualHmac)) { throw new RuntimeException('The wallet decryption key is incorrect.'); } $privateKey = openssl_decrypt( $ciphertext, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $iv, ); if ($privateKey === false || strlen($privateKey) !== 2562 || !ctype_xdigit($privateKey)) { throw new RuntimeException('The decrypted Falcon private key is invalid.'); } return strtolower($privateKey); } private static function normalizePublicKey(string $publicKeyHex, string $network): string { if ($publicKeyHex === '' || !ctype_xdigit($publicKeyHex)) { throw new RuntimeException('The wallet contains an invalid Falcon public key.'); } $publicKey = hex2bin($publicKeyHex); if ($publicKey === false) { throw new RuntimeException('The wallet public key could not be decoded.'); } $networkByte = $network === 'clc' ? 1 : 2; if (strlen($publicKey) === 898 && ord($publicKey[0]) === $networkByte) { $publicKey = substr($publicKey, 1); } if (strlen($publicKey) !== 897) { throw new RuntimeException('The wallet Falcon public key has an invalid length.'); } return $publicKey; } private static function validatePublicKey(string $publicKey, CryptoInterface $crypto): void { if (ord($publicKey[0]) !== 9 || ord($crypto->skein256($publicKey)[0]) !== 239) { throw new RuntimeException( 'The wallet public key does not satisfy the Contractless key rule.', ); } } private static function validateKeypair(FaucetWallet $wallet, CryptoInterface $crypto): void { $challenge = $crypto->skein256('contractless-wallet-keypair-check'); $signature = (new FalconSigner($wallet->publicKey, $wallet->privateKey))->sign($challenge); if (!$crypto->verify($challenge, $signature, $wallet->publicKey)) { throw new RuntimeException( 'The wallet public key does not match the decrypted private key.', ); } } private static function validateAddress( string $address, string $publicKey, CryptoInterface $crypto, ): void { $suffix = str_ends_with($address, '.clc') ? 'clc' : 'cltc'; $payload = hash('ripemd160', $crypto->skein256($publicKey)); if (!hash_equals($address, $payload . '.' . $suffix)) { throw new RuntimeException( 'The wallet short address does not match its Falcon public key.', ); } } }