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}