friendship ended with social-app. php is my new best friend
1<?php 2/** 3 * Class Crypto 4 * 5 * @created 29.10.2024 6 * @author smiley <smiley@chillerlan.net> 7 * @copyright 2024 smiley 8 * @license MIT 9 */ 10declare(strict_types=1); 11 12namespace chillerlan\Utilities; 13 14use InvalidArgumentException; 15use RuntimeException; 16use function hash; 17use function random_bytes; 18use function random_int; 19use function sodium_bin2hex; 20use function sodium_crypto_secretbox; 21use function sodium_crypto_secretbox_keygen; 22use function sodium_crypto_secretbox_open; 23use function sodium_hex2bin; 24use function sodium_memzero; 25use function strlen; 26use function substr; 27use const PHP_VERSION_ID; 28use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; 29 30/** 31 * Basic cryptographic utilities 32 */ 33final class Crypto{ 34 35 public const ENCRYPT_FORMAT_BINARY = 0b00; 36 public const ENCRYPT_FORMAT_BASE64 = 0b01; 37 public const ENCRYPT_FORMAT_HEX = 0b10; 38 39 public const NUMERIC = '0123456789'; 40 public const ASCII_LOWER = 'abcdefghijklmnopqrstuvwxyz'; 41 public const ASCII_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 42 public const ASCII_SYMBOL = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'; 43 public const HEXADECIMAL = self::NUMERIC.'abcdef'; 44 public const ASCII_ALPHANUM = self::NUMERIC.self::ASCII_LOWER.self::ASCII_UPPER; 45 public const ASCII_PRINTABLE = self::NUMERIC.self::ASCII_LOWER.self::ASCII_UPPER.self::ASCII_SYMBOL; 46 public const ASCII_COMMON_PW = self::ASCII_ALPHANUM.'!#$%&()*+,-./:;<=>?@[]~_|'; 47 48 /** 49 * Generates an SHA-256 hash for the given value 50 * 51 * @see \hash() 52 */ 53 public static function sha256(string $data, bool $binary = false):string{ 54 return hash('sha256', $data, $binary); 55 } 56 57 /** 58 * Generates an SHA-512 hash for the given value 59 * 60 * @see \hash() 61 */ 62 public static function sha512(string $data, bool $binary = false):string{ 63 return hash('sha512', $data, $binary); 64 } 65 66 /** 67 * Generates a secure random string of the given `$length`, using the characters (8-bit byte) in the given `$keyspace`. 68 * 69 * @see \random_int() - PHP <= 8.2 70 * @see \Random\Randomizer - PHP >= 8.3 71 * 72 * @noinspection PhpFullyQualifiedNameUsageInspection 73 */ 74 public static function randomString(int $length, string $keyspace = self::ASCII_COMMON_PW):string{ 75 76 // use the Randomizer if available 77 // https://github.com/phpstan/phpstan/issues/7843 78 if(PHP_VERSION_ID >= 80300){ 79 return (new \Random\Randomizer(new \Random\Engine\Secure))->getBytesFromString($keyspace, $length); 80 } 81 82 $len = (strlen($keyspace) - 1); 83 $str = ''; 84 85 for($i = 0; $i < $length; $i++){ 86 $str .= $keyspace[random_int(0, $len)]; 87 } 88 89 return $str; 90 } 91 92 /** 93 * Creates a new cryptographically secure random encryption key for use with `encrypt()` and `decrypt()` (returned in hexadecimal format) 94 * 95 * @see \sodium_crypto_secretbox_keygen() 96 * @see \sodium_crypto_secretbox()) 97 * 98 * @throws \SodiumException 99 */ 100 public static function createEncryptionKey():string{ 101 return sodium_bin2hex(sodium_crypto_secretbox_keygen()); 102 } 103 104 /** 105 * Encrypts the given `$data` with `$key`, formats the output according to `$format` [binary, base64, hex] 106 * 107 * @see \sodium_crypto_secretbox() 108 * @see \sodium_bin2base64() 109 * @see \sodium_bin2hex() 110 * 111 * @throws \SodiumException 112 */ 113 public static function encrypt(string $data, string $keyHex, int $format = self::ENCRYPT_FORMAT_HEX):string{ 114 $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 115 $box = sodium_crypto_secretbox($data, $nonce, sodium_hex2bin($keyHex)); 116 117 $out = match($format){ 118 self::ENCRYPT_FORMAT_BINARY => $nonce.$box, 119 self::ENCRYPT_FORMAT_BASE64 => Str::base64encode($nonce.$box), 120 self::ENCRYPT_FORMAT_HEX => sodium_bin2hex($nonce.$box), 121 default => throw new InvalidArgumentException('invalid format'), // @codeCoverageIgnore 122 }; 123 124 sodium_memzero($data); 125 sodium_memzero($keyHex); 126 sodium_memzero($nonce); 127 sodium_memzero($box); 128 129 return $out; 130 } 131 132 /** 133 * Decrypts the given `$encrypted` data with `$key` from input formatted according to `$format` [binary, base64, hex] 134 * 135 * @see \sodium_crypto_secretbox_open() 136 * @see \sodium_base642bin() 137 * @see \sodium_hex2bin() 138 * 139 * @throws \SodiumException 140 */ 141 public static function decrypt(string $encrypted, string $keyHex, int $format = self::ENCRYPT_FORMAT_HEX):string{ 142 143 $bin = match($format){ 144 self::ENCRYPT_FORMAT_BINARY => $encrypted, 145 self::ENCRYPT_FORMAT_BASE64 => Str::base64decode($encrypted), 146 self::ENCRYPT_FORMAT_HEX => sodium_hex2bin($encrypted), 147 default => throw new InvalidArgumentException('invalid format'), // @codeCoverageIgnore 148 }; 149 150 $nonce = substr($bin, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 151 $box = substr($bin, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 152 $data = sodium_crypto_secretbox_open($box, $nonce, sodium_hex2bin($keyHex)); 153 154 sodium_memzero($encrypted); 155 sodium_memzero($keyHex); 156 sodium_memzero($bin); 157 sodium_memzero($nonce); 158 sodium_memzero($box); 159 160 if($data === false){ 161 throw new RuntimeException('decryption failed'); // @codeCoverageIgnore 162 } 163 164 return $data; 165 } 166 167}