Skip to content

Instantly share code, notes, and snippets.

@divinity76
Last active January 22, 2020 20:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save divinity76/d40690d817cb6e44846edc81621f8a21 to your computer and use it in GitHub Desktop.
Save divinity76/d40690d817cb6e44846edc81621f8a21 to your computer and use it in GitHub Desktop.
encrypt/decrypt debug messages/backtraces
<?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