diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7455c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +Zephir/aes-loader/hydraterbootloader/ext +Zephir/aes-loader/hydraterbootloader/.zephir +Zephir/aes-license/hydraterlicense/ext +Zephir/aes-license/hydraterlicense/.zephir +*.log +*.swp diff --git a/Zephir/aes-license/hydraterlicense/config.json b/Zephir/aes-license/hydraterlicense/config.json new file mode 100644 index 0000000..2dbe5a4 --- /dev/null +++ b/Zephir/aes-license/hydraterlicense/config.json @@ -0,0 +1,69 @@ +{ + "stubs": { + "path": "ide\/%version%\/%namespace%\/", + "stubs-run-after-generate": false, + "banner": "" + }, + "api": { + "path": "doc\/%version%", + "theme": { + "name": "zephir", + "options": { + "github": null, + "analytics": null, + "main_color": "#3E6496", + "link_color": "#3E6496", + "link_hover_color": "#5F9AE7" + } + } + }, + "warnings": { + "unused-variable": true, + "unused-variable-external": false, + "possible-wrong-parameter": true, + "possible-wrong-parameter-undefined": false, + "nonexistent-function": true, + "nonexistent-class": true, + "non-valid-isset": true, + "non-array-update": true, + "non-valid-objectupdate": true, + "non-valid-fetch": true, + "invalid-array-index": true, + "non-array-append": true, + "invalid-return-type": true, + "unreachable-code": true, + "nonexistent-constant": true, + "not-supported-magic-constant": true, + "non-valid-decrement": true, + "non-valid-increment": true, + "non-valid-clone": true, + "non-valid-new": true, + "non-array-access": true, + "invalid-reference": true, + "invalid-typeof-comparison": true, + "conditional-initialization": true + }, + "optimizations": { + "static-type-inference": true, + "static-type-inference-second-pass": true, + "local-context-pass": true, + "constant-folding": true, + "static-constant-class-folding": true, + "call-gatherer-pass": true, + "check-invalid-reads": false, + "internal-call-transformation": false + }, + "extra": { + "indent": "spaces", + "export-classes": false + }, + "namespace": "hydraterlicense", + "name": "hydraterlicense", + "description": "", + "author": "Phalcon Team", + "version": "0.0.1", + "verbose": false, + "requires": { + "extensions": ["openssl"] + } +} \ No newline at end of file diff --git a/Zephir/aes-license/hydraterlicense/hydraterlicense/keygenerator.zep b/Zephir/aes-license/hydraterlicense/hydraterlicense/keygenerator.zep new file mode 100644 index 0000000..d6d29ec --- /dev/null +++ b/Zephir/aes-license/hydraterlicense/hydraterlicense/keygenerator.zep @@ -0,0 +1,71 @@ +namespace HydraterLicense; + +class KeyGenerator +{ + /** + * Generates an RSA private key and saves it to a file. + * + * @param string path Output path for private key (e.g., "private.pem") + * @param int bits RSA key size (default: 2048) + * @return bool + */ + public function generatePrivateKey(string path, int bits = 2048) -> bool + { + + if unlikely !extension_loaded("openssl") { + echo "OpenSSL extension not loaded"; + return false; + } + + var config, privateKey, exported, result; + + let config = [ + "private_key_type": OPENSSL_KEYTYPE_RSA, + "private_key_bits": bits, + "digest_alg": "sha256" + ]; + + let privateKey = openssl_pkey_new(config); + + let exported = ""; + if unlikely !openssl_pkey_export(privateKey, exported) { + echo "Failed to export pkey"; + return false; + } + + let result = file_put_contents(path, exported); + if !result { + echo "Unable to save"; + } + return result !== false; + } + + /** + * Extracts public key from private key and saves it to file. + * + * @param string privatePath Path to private.pem + * @param string publicPath Output path for public.pem + * @return bool + */ + public function generatePublicKey(string privatePath, string publicPath) -> bool + { + var privatePem, privateKey, pubKeyDetails, pubKey, written; + + let privatePem = file_get_contents(privatePath); + if privatePem === false { + return false; + } + + let privateKey = openssl_pkey_get_private(privatePem); + + let pubKeyDetails = openssl_pkey_get_details(privateKey); + if typeof pubKeyDetails !== "array" || !isset pubKeyDetails["key"] { + return false; + } + + let pubKey = pubKeyDetails["key"]; + let written = file_put_contents(publicPath, pubKey); + return written !== false; + } +} + diff --git a/Zephir/aes-license/hydraterlicense/hydraterlicense/licensewriter.zep b/Zephir/aes-license/hydraterlicense/hydraterlicense/licensewriter.zep new file mode 100644 index 0000000..7f64474 --- /dev/null +++ b/Zephir/aes-license/hydraterlicense/hydraterlicense/licensewriter.zep @@ -0,0 +1,95 @@ +namespace HydraterLicense; + +class LicenseWriter +{ + /** + * Create a digitally signed license.json file + * + * @param array fileSettings Array of [filename => ["enabled": bool, "expires": string ISO 8601 expiration date, "password": string]] + * @param array domains Allowed domain names + * @param string privateKeyPem Private key in PEM format + * @param string aesKey 32-byte encryption key + * @param string aesIV 16-byte initialization vector + */ + public function createLicenseJson(array fileSettings, array domains, string privateKeyPem, string aesKey, string aesIV, string licenseFile) + { + var features = [], filename, setting, feature, plainPassword; + var license, licenseJson, signature, finalPayload; + var encrypted, encryptedB64, finalJson, enabled, expires; + var fileHandle, myfeature; + + // Build feature list + for filename, setting in fileSettings { + if typeof setting !== "array" { + continue; + } + if !isset setting["feature"] || !isset setting["enabled"] || !isset setting["password"] { + continue; + } + let myfeature = setting["feature"]; + + if !isset setting["expires"] { + let expires = "*"; // Never Expires + } else { + let expires = (string) setting["expires"]; + } + + if ends_with(filename, ".aes") { + let plainPassword = (string) setting["password"]; + + // Encrypt password with AES-256-CBC + let encrypted = openssl_encrypt( + plainPassword, + "aes-256-cbc", + aesKey, + 1, // OPENSSL_RAW_DATA + aesIV + ); + + // Base64 encode encrypted output + let encryptedB64 = base64_encode(encrypted); + if setting["enabled"] == true || setting["enabled"] == 1 { + let enabled = true; + } else { + let enabled = false; + } + + let feature = [ + "file": filename, + "feature": myfeature, + "enabled": enabled, + "expires": expires, + "password": encryptedB64 + ]; + let features[] = feature; + } + } + + let license = [ + "features": features, + "domains": domains + ]; + + // JSON encode license (pretty format) + let licenseJson = json_encode(license, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + // Sign using openssl_sign via PHP + let signature = ""; + openssl_sign(licenseJson, signature, privateKeyPem, "sha256"); + + // Wrap license + signature into final JSON + let finalPayload = [ + "license": json_decode(licenseJson), + "signature": base64_encode(signature) + ]; + + let finalJson = json_encode(finalPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + // Save to license.json + let fileHandle = fopen(licenseFile, "w"); + if fileHandle !== false { + fwrite(fileHandle, finalJson); + fclose(fileHandle); + } + } +} diff --git a/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep b/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep new file mode 100644 index 0000000..36c6af0 --- /dev/null +++ b/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep @@ -0,0 +1,148 @@ +use HydraterLicense\KeyGenerator; +use HydraterLicense\LicenseWriter; + +namespace HydraterLicense; + +class MakeLicense +{ + + public function makePassword(int size = 16)->string { + var r; + let r = this->generateCryptoKey(size); + return bin2hex(r); + } + + public function generateLicense( + array Array_For_Files, + array AllowedDomains, + string PrivatePEM, + string PublicPEM, + string AESKeysFile, + string LicenseFile + ) { + var Key, KeyHex, iv, ivHex, aes, privateKey, password; + var secret_php_file, writer, file; + var aesKey, aesIV, gen, v, e, r; + try { + if !file_exists(AESKeysFile) { + // Generate a 32-byte (256-bit) AES key + let Key = this->generateCryptoKey(32); + let KeyHex = bin2hex(Key); + + // Generate a 16-byte (128-bit) IV + let iv = this->generateCryptoKey(16); + let ivHex = bin2hex(iv); + + // Output the results + let aes = "generatePrivateKey(PrivatePEM) { + echo "Private key generated.\n"; + } else { + echo "Private key generation failed.\n"; + exit(1); + } + + if gen->generatePublicKey(PrivatePEM, PublicPEM) { + echo "Public key extracted.\n"; + } else { + echo "Public key extraction failed.\n"; + exit(1); + } + let privateKey = file_get_contents(PrivatePEM); + } + + // Go ahead and make encypted AES files from PHP files + for file, v in Array_For_Files { + let password = v["password"]; + let secret_php_file = str_replace(".aes", ".php", file); + let secret_php_file = str_replace("/aes/", "/secret_php_files/", secret_php_file); + this->encryptPhpFile(secret_php_file, file, password); + } + + // Make new License File with new AES files + let writer = new LicenseWriter(); + writer->createLicenseJson( + Array_For_Files, + AllowedDomains, + privateKey, + aesKey, + aesIV, + LicenseFile + ); + echo "Encrypted and signed license.json created.\n"; + return true; + } catch RuntimeException, e { + echo "Runtime error occurred: ", e->getMessage(), "\n"; + } catch Exception, e { + echo "Generic error: ", e->getMessage(), "\n"; + } + } + + protected function generateCryptoKey(int length) { + if (function_exists("random_bytes")) { + return random_bytes(length); + } elseif (function_exists("openssl_random_pseudo_bytes")) { + return openssl_random_pseudo_bytes(length); + } else { + throw new \RuntimeException("No cryptographically secure random function available"); + } + } + + protected function evpBytesToKey(string password, string salt, int keyLen = 32, int ivLen = 16) { + var dtot, d, key, iv; + let dtot = ""; + let d = ""; + while (strlen(dtot) < (keyLen + ivLen)) { + let d = md5(d . password . salt, true); + let dtot .= d; + } + let key = substr(dtot, 0, keyLen); + let iv = substr(dtot, keyLen, ivLen); + return [key, iv]; + } + + protected function encryptPhpFile(string inFile, string outFile, string password) { + var data, salt, key, iv, ciphertext, prefix, r; + let data = file_get_contents(inFile); + if data === false { + die("Cannot read input file.\n"); + } + + let salt = random_bytes(8); + let r = this->evpBytesToKey(password, salt); + let key = r[0]; + let iv = r[1]; + + let ciphertext = openssl_encrypt(data, "AES-256-CBC", key, OPENSSL_RAW_DATA, iv); + if ciphertext === false { + die("Encryption failed.\n"); + } + + let prefix = "Salted__" . salt; + file_put_contents(outFile, prefix . ciphertext); + echo "Encrypted file written to: " . outFile ."\n"; + } +} diff --git a/Zephir/aes-loader/hydraterbootloader/config.json b/Zephir/aes-loader/hydraterbootloader/config.json new file mode 100644 index 0000000..c478e47 --- /dev/null +++ b/Zephir/aes-loader/hydraterbootloader/config.json @@ -0,0 +1,69 @@ +{ + "stubs": { + "path": "ide\/%version%\/%namespace%\/", + "stubs-run-after-generate": false, + "banner": "" + }, + "api": { + "path": "doc\/%version%", + "theme": { + "name": "zephir", + "options": { + "github": null, + "analytics": null, + "main_color": "#3E6496", + "link_color": "#3E6496", + "link_hover_color": "#5F9AE7" + } + } + }, + "warnings": { + "unused-variable": true, + "unused-variable-external": false, + "possible-wrong-parameter": true, + "possible-wrong-parameter-undefined": false, + "nonexistent-function": true, + "nonexistent-class": true, + "non-valid-isset": true, + "non-array-update": true, + "non-valid-objectupdate": true, + "non-valid-fetch": true, + "invalid-array-index": true, + "non-array-append": true, + "invalid-return-type": true, + "unreachable-code": true, + "nonexistent-constant": true, + "not-supported-magic-constant": true, + "non-valid-decrement": true, + "non-valid-increment": true, + "non-valid-clone": true, + "non-valid-new": true, + "non-array-access": true, + "invalid-reference": true, + "invalid-typeof-comparison": true, + "conditional-initialization": true + }, + "optimizations": { + "static-type-inference": true, + "static-type-inference-second-pass": true, + "local-context-pass": true, + "constant-folding": true, + "static-constant-class-folding": true, + "call-gatherer-pass": true, + "check-invalid-reads": false, + "internal-call-transformation": false + }, + "extra": { + "indent": "spaces", + "export-classes": false + }, + "namespace": "hydraterbootloader", + "name": "hydraterbootloader", + "description": "Hydrater bootloader for encrypted PHP files", + "author": "Robert Strutts", + "version": "0.0.1", + "verbose": false, + "requires": { + "extensions": ["openssl"] + } +} diff --git a/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/licenseverifier.zep b/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/licenseverifier.zep new file mode 100644 index 0000000..81642cc --- /dev/null +++ b/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/licenseverifier.zep @@ -0,0 +1,204 @@ +namespace HydraterBootloader; + +use HydraterBootloader\Loader; + +class LicenseVerifier +{ + public function tryEnabledItem(string licenseFile, string keyFile, string featureName, string publicKeyPem, bool useInclude = true, bool forceRefresh = false) { + var l, f, found, password, filePath, r, aesKey, aesIV, data; + if featureName == "" || licenseFile == "" || publicKeyPem == "" { + return false; + } + if ! this->verifySignature(licenseFile, publicKeyPem) { + return false; + } + + let l = new Loader(); + + if ! file_exists(keyFile) { + return false; + } + require keyFile; + + let r = getAES_master_keys(); + let aesKey = r[0]; + let aesIV = r[1]; + + let data = this->getEnabledFeatures(licenseFile, aesKey, aesIV); + let found = false; + for f in data { + if ! isset f["feature"] || f["feature"] != featureName { + continue; + } + let filePath = f["file"]; + if isset f["enabled"] && f["enabled"] == true { + if isset f["password"] { + let password = f["password"]; + let found = true; + break; + } + } + } + if found { + return l->run(filePath, password, useInclude, forceRefresh); + } + } + + /** + * Verify digital signature using public key + */ + protected function verifySignature(string licensePath, string publicKeyPem) -> bool + { + var contents, parsed, licensePart, licenseJson, signatureB64, signature, result, PEMContent; + + if licensePath == "" || publicKeyPem == "" { + return false; + } + let PEMContent = file_get_contents(publicKeyPem); + if PEMContent === false || PEMContent == "" { + return false; + } + + let contents = file_get_contents(licensePath); + if contents === false { + throw new \Exception("No License Data!"); + } + + let parsed = json_decode(contents, true); + if typeof parsed !== "array" || !isset parsed["license"] || !isset parsed["signature"] { + throw new \Exception("No Signature!"); + } + + let licensePart = parsed["license"]; + let licenseJson = json_encode(licensePart, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + let signatureB64 = parsed["signature"]; + let signature = base64_decode(signatureB64); + + let result = openssl_verify(licenseJson, signature, PEMContent, "sha256"); + return result === 1; + } + + /** + * Return enabled features with decrypted passwords + */ + protected function getEnabledFeatures(string licensePath, string aesKey, string aesIV) -> array + { + var contents, parsed, features, feature, result = [], encryptedB64, encryptedRaw, decrypted, expires; + var i, total; + + let contents = file_get_contents(licensePath); + if contents === false { + return []; + } + + let parsed = json_decode(contents, true); + if typeof parsed !== "array" || !isset parsed["license"]["features"] { + return []; + } + + let features = parsed["license"]["features"]; + let total = count(features); + + for i in range(0, total - 1) { + let feature = features[i]; + if isset feature["enabled"] && feature["enabled"] == true { + if isset feature["file"] && isset feature["feature"] && isset feature["password"] { + + if ! isset feature["expires"] { + let expires = "*"; + } else { + let expires = feature["expires"]; + } + + if this->isExpired(expires) { + + let result[] = [ + "file": feature["file"], + "feature": feature["feature"], + "enabled": false, + "status": "⏳ License expired" + ]; + + continue; // Skip granting Feature as its expired! + } + + let encryptedB64 = feature["password"]; + let encryptedRaw = base64_decode(encryptedB64); + let decrypted = openssl_decrypt(encryptedRaw, "aes-256-cbc", aesKey, 1, aesIV); + + let result[] = [ + "file": feature["file"], + "feature": feature["feature"], + "password": decrypted, + "enabled": true, + "status": "License valid" + ]; + } + } + } + + return result; + } + + /** + * Check if license is expired + */ + protected function isExpired(string expires) -> bool + { + var now, licenseTime; + + // Grant unlimited access time on a star! + if expires == "*" { + return false; + } + + // Compare current timestamp with expiration + let now = time(); + let licenseTime = strtotime(expires); + + if unlikely licenseTime === false { + throw new \Exception("Invalid date format: " . expires); + } + + return now > licenseTime; + } + + /** + * Check if a domain is allowed (exact or wildcard match) + */ + public function isDomainAllowed(string licensePath, string domain) -> bool + { + var contents, parsed, allowed, i, rule, suffix; + + let contents = file_get_contents(licensePath); + if contents === false { + return false; + } + + let parsed = json_decode(contents, true); + if typeof parsed !== "array" || !isset parsed["license"]["domains"] { + return false; + } + + let allowed = parsed["license"]["domains"]; + for i in range(0, count(allowed) - 1) { + let rule = allowed[i]; + + // Wildcard match: *.example.com + if starts_with(rule, "*.") { + let suffix = substr(rule, 1); // remove * + if ends_with(domain, suffix) { + return true; + } + } else { + // Exact match + if domain == rule { + return true; + } + } + } + + return false; + } +} + diff --git a/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/loader.zep b/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/loader.zep new file mode 100644 index 0000000..2755602 --- /dev/null +++ b/Zephir/aes-loader/hydraterbootloader/hydraterbootloader/loader.zep @@ -0,0 +1,125 @@ +namespace HydraterBootloader; + +class Loader +{ + protected function evpBytesToKey(string password, string salt) -> array + { + var key, iv, d, d_i, result; + + let d = "", + result = ""; + + while strlen(result) < 48 { + let d_i = md5(d . password . salt, true); + let d = d_i; + let result .= d_i; + } + + let key = substr(result, 0, 32); + let iv = substr(result, 32, 16); + + return [key, iv]; + } + + protected function getCacheDir() -> string + { + var path; + + if is_writable("/dev/shm") { + let path = "/dev/shm/tank"; + } else { + let path = "/tmp/tank"; // fallback + } + + if !is_dir(path) { + mkdir(path, 0700, true); + } + + return path; + } + + protected function isExpired(string path, int maxAgeSeconds) -> bool + { + var mtime, now; + + let mtime = filemtime(path); + if mtime === false { + return true; + } + + let now = time(); + return (now - mtime) > maxAgeSeconds; + } + + protected function decrypt(string ciphertext, string password) -> string + { + var salt, key, iv, body, decrypted, args, result; + + if substr(ciphertext, 0, 8) != "Salted__" { + throw new \Exception("Missing OpenSSL salt header."); + } + + let salt = substr(ciphertext, 8, 8); + let body = substr(ciphertext, 16); + + let result = this->evpBytesToKey(password, salt); + let key = result[0]; + let iv = result[1]; + + let args = [ + body, + "AES-256-CBC", + key, + 1, + iv + ]; + + let decrypted = call_user_func_array("openssl_decrypt", args); + + if typeof decrypted != "string" { + throw new \Exception("Decryption failed."); + } + + return decrypted; + } + + public function run(string filePath, string password, bool useInclude = true, bool forceRefresh = false) + { + var ciphertext, plaintext, tmpPath, hash, cacheDir, expired; + + if !file_exists(filePath) { + throw new \Exception("File does not exist: " . filePath); + } + + let cacheDir = this->getCacheDir(); + let hash = hash("sha256", filePath); + let tmpPath = cacheDir . "/aesphp_" . hash . ".php"; + + if useInclude && file_exists(tmpPath) && !forceRefresh { + let expired = this->isExpired(tmpPath, 28800); // 8 hours = 28800 seconds + if !expired { + require tmpPath; + return; + } else { + unlink(tmpPath); // Remove expired cache + } + } + + let ciphertext = file_get_contents(filePath); + if !ciphertext { + throw new \Exception("Unable to read file: " . filePath); + } + + let plaintext = this->decrypt(ciphertext, password); + + if useInclude { + file_put_contents(tmpPath, plaintext); + if forceRefresh { + register_shutdown_function("unlink", tmpPath); + } + return require tmpPath; + } else { + return eval(plaintext); + } + } +} \ No newline at end of file