Last active
January 22, 2020 20:14
-
-
Save divinity76/d40690d817cb6e44846edc81621f8a21 to your computer and use it in GitHub Desktop.
encrypt/decrypt debug messages/backtraces
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
class CryptedDebug | |
{ | |
private const CRYPT_KEY = 'CHANGE_ME_!!!'; | |
private const FORMAT_VERSION = 1; | |
private const CRYPT_ALGO = 'aes-256-ctr'; | |
// (i would use blake2/blake3 if it was easily available, but it isn't) | |
private const CSUM_ALGO = 'sha256'; | |
private const CSUM_LEN = 10; | |
/** | |
* encrypt a debug message | |
* | |
* @param string $message | |
* @param bool $add_timestamp_header | |
* @param bool $add_backtrace | |
* @return string encrypted | |
* looks like: crypted_debug:1:124:blabla | |
*/ | |
public static function encrypt(string $message, bool $add_timestamp_header = true, bool $add_backtrace = true): string | |
{ | |
if ($add_backtrace) { | |
ob_start(); | |
debug_print_backtrace(); | |
$bt = ob_get_clean(); | |
$message = "backtrace: " . $bt . "\n\n" . $message; | |
} | |
if ($add_timestamp_header) { | |
$message = "message encrypted at " . date(\DateTime::ATOM) . "\n\n" . $message; | |
} | |
// basically: | |
// crypted_debug:1:b64_len:base64(iv+encrypt(csum+gzip($message))) | |
// crypted_debug:format_version:b64_len:base64(iv+encrypt(csum+gzip($message))) | |
// this is "Hello World" with both timestamp and backtrace: | |
// crypted_debug:1:124:b16gHUJw94nzihg6huFDRQrCdAiZh4eGy4iNgGICJ8UIkN31oBQeaQciBLXk5PjHBz5pr62MWke9dAaEU8XhDXsFn0ah5aCzafwaMb4Yv4jfC/DeFwFCMrPOBf3X | |
// . | |
// compress it | |
$ret = gzcompress($message, 9); | |
// checksum to protect against tampering | |
$csum = self::csum($ret); | |
$ret = $csum . $ret; | |
// (as for authenticity, we're just using the key for that | |
// anyone with the encryption key is considered authentic, | |
// and anyone without the key won't be able to tamper and update csum) | |
// generate IV | |
$iv = random_bytes(openssl_cipher_iv_length(self::CRYPT_ALGO)); | |
$ret = openssl_encrypt($ret, self::CRYPT_ALGO, self::CRYPT_KEY, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); | |
// add the iv | |
$ret = $iv . $ret; | |
// base64 encode everything | |
// (it's all binary data.. also b64 is not the optimal format, | |
// but it's best one easily available. base91 mumblemumble) | |
$ret = rtrim(base64_encode($ret), '='); | |
$format_header = "crypted_debug:" . self::FORMAT_VERSION . ":" . strlen($ret) . ":"; | |
return $format_header . $ret; | |
} | |
/** | |
* decrypt an encrypted message | |
* | |
* @param string $message | |
* encrypted message | |
* @param string $fail_reason | |
* ByRef error string explaining why decryption failed (if it fails) | |
* @return string|NULL decrypted message, or NULL if decryption failed. | |
*/ | |
public static function decrypt(string $message, string &$fail_reason = null): ?string | |
{ | |
$fail_reason = ""; | |
// // crypted_debug:format_version:b64_len:base64(iv+encrypt(csum+gzip($message))) | |
$start_needle = 'crypted_debug:' . self::FORMAT_VERSION . ":"; | |
$startpos = strpos($message, $start_needle); | |
if (false === $startpos) { | |
$fail_reason = "cannot find start_needle {$start_needle}"; | |
return null; | |
} | |
// garbage crypted_debug:1:250:B64 garbage | |
$message = substr($message, $startpos + strlen($start_needle)); | |
// 250:B64 garbage | |
$b64_bytes = (int) $message; // << 250 | |
$message = substr($message, strlen((string) $b64_bytes) + 1); | |
// B64 garbage | |
// (now remove the garbage, if any) | |
$message = substr($message, 0, $b64_bytes); | |
// B64 | |
if (strlen($message) !== $b64_bytes) { | |
$fail_reason = "header said {$b64_bytes} base64 characters, but could only find " . strlen($message) . " b64 characters."; | |
return null; | |
} | |
$message = base64_decode($message); | |
if (false === $message) { | |
$fail_reason = "base64_decode failed, not valid base64 string.."; | |
return null; | |
} | |
$iv_len = openssl_cipher_iv_length(self::CRYPT_ALGO); | |
$iv = substr($message, 0, $iv_len); | |
if (strlen($iv) !== $iv_len) { | |
$fail_reason = "iv is missing!"; | |
return null; | |
} | |
$message = substr($message, $iv_len); | |
$message = openssl_decrypt($message, self::CRYPT_ALGO, self::CRYPT_KEY, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); | |
if (false === $message) { | |
$fail_reason = "openssl_decrypt failed - possibly encrypted with a different key?"; | |
return null; | |
} | |
$supplied_csum = substr($message, 0, self::CSUM_LEN); | |
if (strlen($supplied_csum) !== self::CSUM_LEN) { | |
$fail_reason = "csum is missing!"; | |
return null; | |
} | |
$message = substr($message, self::CSUM_LEN); | |
$calculated_csum = self::csum($message); | |
if (! hash_equals($supplied_csum, $calculated_csum)) { | |
$fail_reason = "csum mismatch - data corrupt (possibly tampered?)"; | |
return null; | |
} | |
$message = gzuncompress($message); | |
if (false === $message) { | |
$fail_reason = "gzuncompress() failed! .. should never happen"; | |
return null; | |
} | |
// finally! | |
return $message; | |
} | |
private static function csum(string $data): string | |
{ | |
return substr(hash(self::CSUM_ALGO, $data, true), 0, self::CSUM_LEN); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment