| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib\Crypto; |
| 39 | |
| 40 | /** |
| 41 | * AES(128・192・256ビットの鍵)とGCM(AEAD) |
| 42 | * |
| 43 | * 例: |
| 44 | * $aes = new Aes($key); |
| 45 | * $sealed = $aes->seal($iv12, $plain, $aad); // 暗号文||タグ(16バイト) |
| 46 | * $plain = $aes->open($iv12, $sealed, $aad); // 改ざんされていればnull |
| 47 | */ |
| 48 | class Aes { |
| 49 | private static ?array $t = null; |
| 50 | |
| 51 | private array $rk; |
| 52 | private int $rounds; |
| 53 | /** GHASHの表(4ビット毎、上位と下位の64ビット) */ |
| 54 | private array $hh = []; |
| 55 | private array $hl = []; |
| 56 | |
| 57 | // GHASHの4ビットの剰余(OpenSSLのrem_4bit) |
| 58 | private const REM4 = [0x0000, 0x1C20, 0x3840, 0x2460, 0x7080, 0x6CA0, 0x48C0, 0x54E0, |
| 59 | 0xE100, 0xFD20, 0xD940, 0xC560, 0x9180, 0x8DA0, 0xA9C0, 0xB5E0]; |
| 60 | |
| 61 | /** |
| 62 | * @param string $key 16、24、32バイト |
| 63 | */ |
| 64 | public function __construct(string $key) { |
| 65 | $len = strlen($key); |
| 66 | if (!in_array($len, [16, 24, 32], true)) throw new \InvalidArgumentException('AES:鍵は16、24、32バイト'); |
| 67 | self::tables(); |
| 68 | $this->expand($key); |
| 69 | |
| 70 | // GHASHのH = E(K, 0) |
| 71 | [$h0, $h1, $h2, $h3] = $this->encryptWords(0, 0, 0, 0); |
| 72 | $vh = ($h0 << 32) | $h1; |
| 73 | $vl = ($h2 << 32) | $h3; |
| 74 | $hh = array_fill(0, 16, 0); |
| 75 | $hl = array_fill(0, 16, 0); |
| 76 | $hh[8] = $vh; |
| 77 | $hl[8] = $vl; |
| 78 | foreach ([4, 2, 1] as $i) { |
| 79 | $t = (0xE1 << 56) & -($vl & 1); |
| 80 | $vl = ($vh << 63) | (($vl >> 1) & PHP_INT_MAX); |
| 81 | $vh = (($vh >> 1) & PHP_INT_MAX) ^ $t; |
| 82 | $hh[$i] = $vh; |
| 83 | $hl[$i] = $vl; |
| 84 | } |
| 85 | foreach ([2, 4, 8] as $i) { |
| 86 | for ($j = 1; $j < $i; $j++) { |
| 87 | $hh[$i + $j] = $hh[$i] ^ $hh[$j]; |
| 88 | $hl[$i + $j] = $hl[$i] ^ $hl[$j]; |
| 89 | } |
| 90 | } |
| 91 | $this->hh = $hh; |
| 92 | $this->hl = $hl; |
| 93 | } |
| 94 | |
| 95 | /** 1ブロック(16バイト)を暗号化する */ |
| 96 | public function encryptBlock(string $block): string { |
| 97 | if (strlen($block) !== 16) throw new \InvalidArgumentException('AES:ブロックは16バイト'); |
| 98 | $w = unpack('N4', $block); |
| 99 | return pack('N4', ...$this->encryptWords($w[1], $w[2], $w[3], $w[4])); |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * GCMで暗号化する |
| 104 | * |
| 105 | * @param string $iv 12バイト |
| 106 | * @param string $plain 平文 |
| 107 | * @param string $aad 追加の認証データ |
| 108 | * @return string 暗号文||タグ(16バイト) |
| 109 | */ |
| 110 | public function seal(string $iv, string $plain, string $aad = ''): string { |
| 111 | if (strlen($iv) !== 12) throw new \InvalidArgumentException('AES-GCM:IVは12バイト'); |
| 112 | $ct = $this->ctr($iv, $plain); |
| 113 | return $ct.$this->tag($iv, $ct, $aad); |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * GCMで復号する |
| 118 | * |
| 119 | * @param string $iv 12バイト |
| 120 | * @param string $sealed 暗号文||タグ |
| 121 | * @param string $aad 追加の認証データ |
| 122 | * @return string|null 平文、改ざんされていればnull |
| 123 | */ |
| 124 | public function open(string $iv, string $sealed, string $aad = ''): ?string { |
| 125 | if (strlen($iv) !== 12 || strlen($sealed) < 16) return null; |
| 126 | $ct = substr($sealed, 0, -16); |
| 127 | if (!hash_equals($this->tag($iv, $ct, $aad), substr($sealed, -16))) return null; |
| 128 | return $this->ctr($iv, $ct); |
| 129 | } |
| 130 | |
| 131 | /////////////// |
| 132 | |
| 133 | /** カウンタモード(カウンタは2から) */ |
| 134 | private function ctr(string $iv, string $data): string { |
| 135 | $len = strlen($data); |
| 136 | if ($len === 0) return ''; |
| 137 | [, $n0, $n1, $n2] = unpack('N3', $iv); |
| 138 | $ks = ''; |
| 139 | $blocks = intdiv($len + 15, 16); |
| 140 | for ($i = 0; $i < $blocks; $i++) { |
| 141 | $ks .= pack('N4', ...$this->encryptWords($n0, $n1, $n2, ($i + 2) & 0xFFFFFFFF)); |
| 142 | } |
| 143 | return $data ^ substr($ks, 0, $len); |
| 144 | } |
| 145 | |
| 146 | /** タグ = E(K, J0) xor GHASH(aad, ct) */ |
| 147 | private function tag(string $iv, string $ct, string $aad): string { |
| 148 | $zh = 0; |
| 149 | $zl = 0; |
| 150 | foreach ([$aad, $ct] as $d) { |
| 151 | $len = strlen($d); |
| 152 | if ($len % 16) $d .= str_repeat("\0", 16 - $len % 16); |
| 153 | for ($i = 0, $n = strlen($d); $i < $n; $i += 16) { |
| 154 | $b = unpack('J2', $d, $i); |
| 155 | [$zh, $zl] = $this->gmult($zh ^ $b[1], $zl ^ $b[2]); |
| 156 | } |
| 157 | } |
| 158 | [$zh, $zl] = $this->gmult($zh ^ (strlen($aad) * 8), $zl ^ (strlen($ct) * 8)); |
| 159 | |
| 160 | [, $n0, $n1, $n2] = unpack('N3', $iv); |
| 161 | $ek = pack('N4', ...$this->encryptWords($n0, $n1, $n2, 1)); |
| 162 | return $ek ^ pack('J2', $zh, $zl); |
| 163 | } |
| 164 | |
| 165 | /** X * H(GF(2^128)、OpenSSLのgcm_gmult_4bit) */ |
| 166 | private function gmult(int $xh, int $xl): array { |
| 167 | $H = $this->hh; |
| 168 | $L = $this->hl; |
| 169 | $mask = 0x0FFFFFFFFFFFFFFF; |
| 170 | |
| 171 | $b = $xl & 0xFF; |
| 172 | $nhi = $b >> 4; |
| 173 | $zh = $H[$b & 0xF]; |
| 174 | $zl = $L[$b & 0xF]; |
| 175 | |
| 176 | for ($cnt = 15; ; ) { |
| 177 | $rem = $zl & 0xF; |
| 178 | $zl = ($zh << 60) | (($zl >> 4) & $mask); |
| 179 | $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nhi]; |
| 180 | $zl ^= $L[$nhi]; |
| 181 | if (--$cnt < 0) break; |
| 182 | |
| 183 | $b = $cnt >= 8 ? ($xl >> (8 * (15 - $cnt))) & 0xFF : ($xh >> (8 * (7 - $cnt))) & 0xFF; |
| 184 | $nhi = $b >> 4; |
| 185 | $nlo = $b & 0xF; |
| 186 | $rem = $zl & 0xF; |
| 187 | $zl = ($zh << 60) | (($zl >> 4) & $mask); |
| 188 | $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nlo]; |
| 189 | $zl ^= $L[$nlo]; |
| 190 | } |
| 191 | return [$zh, $zl]; |
| 192 | } |
| 193 | |
| 194 | /** 4つの32ビットの語を暗号化する */ |
| 195 | private function encryptWords(int $s0, int $s1, int $s2, int $s3): array { |
| 196 | [$t0, $t1, $t2, $t3, $sb] = self::$t; |
| 197 | $rk = $this->rk; |
| 198 | $s0 ^= $rk[0]; |
| 199 | $s1 ^= $rk[1]; |
| 200 | $s2 ^= $rk[2]; |
| 201 | $s3 ^= $rk[3]; |
| 202 | $k = 4; |
| 203 | for ($r = 1; $r < $this->rounds; $r++) { |
| 204 | $u0 = $t0[$s0 >> 24] ^ $t1[($s1 >> 16) & 0xFF] ^ $t2[($s2 >> 8) & 0xFF] ^ $t3[$s3 & 0xFF] ^ $rk[$k]; |
| 205 | $u1 = $t0[$s1 >> 24] ^ $t1[($s2 >> 16) & 0xFF] ^ $t2[($s3 >> 8) & 0xFF] ^ $t3[$s0 & 0xFF] ^ $rk[$k + 1]; |
| 206 | $u2 = $t0[$s2 >> 24] ^ $t1[($s3 >> 16) & 0xFF] ^ $t2[($s0 >> 8) & 0xFF] ^ $t3[$s1 & 0xFF] ^ $rk[$k + 2]; |
| 207 | $s3 = $t0[$s3 >> 24] ^ $t1[($s0 >> 16) & 0xFF] ^ $t2[($s1 >> 8) & 0xFF] ^ $t3[$s2 & 0xFF] ^ $rk[$k + 3]; |
| 208 | $s0 = $u0; |
| 209 | $s1 = $u1; |
| 210 | $s2 = $u2; |
| 211 | $k += 4; |
| 212 | } |
| 213 | return [ |
| 214 | (($sb[$s0 >> 24] << 24) | ($sb[($s1 >> 16) & 0xFF] << 16) | ($sb[($s2 >> 8) & 0xFF] << 8) | $sb[$s3 & 0xFF]) ^ $rk[$k], |
| 215 | (($sb[$s1 >> 24] << 24) | ($sb[($s2 >> 16) & 0xFF] << 16) | ($sb[($s3 >> 8) & 0xFF] << 8) | $sb[$s0 & 0xFF]) ^ $rk[$k + 1], |
| 216 | (($sb[$s2 >> 24] << 24) | ($sb[($s3 >> 16) & 0xFF] << 16) | ($sb[($s0 >> 8) & 0xFF] << 8) | $sb[$s1 & 0xFF]) ^ $rk[$k + 2], |
| 217 | (($sb[$s3 >> 24] << 24) | ($sb[($s0 >> 16) & 0xFF] << 16) | ($sb[($s1 >> 8) & 0xFF] << 8) | $sb[$s2 & 0xFF]) ^ $rk[$k + 3], |
| 218 | ]; |
| 219 | } |
| 220 | |
| 221 | /** 鍵の拡大(FIPS 197 5.2) */ |
| 222 | private function expand(string $key): void { |
| 223 | $sb = self::$t[4]; |
| 224 | $nk = intdiv(strlen($key), 4); |
| 225 | $this->rounds = $nk + 6; |
| 226 | $w = array_values(unpack('N*', $key)); |
| 227 | $rcon = 1; |
| 228 | for ($i = $nk; $i < 4 * ($this->rounds + 1); $i++) { |
| 229 | $t = $w[$i - 1]; |
| 230 | if ($i % $nk === 0) { |
| 231 | $t = (($t << 8) & 0xFFFFFFFF) | ($t >> 24); |
| 232 | $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF]; |
| 233 | $t ^= $rcon << 24; |
| 234 | $rcon = (($rcon << 1) ^ (($rcon & 0x80) ? 0x1B : 0)) & 0xFF; |
| 235 | } else if ($nk > 6 && $i % $nk === 4) { |
| 236 | $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF]; |
| 237 | } |
| 238 | $w[$i] = $w[$i - $nk] ^ $t; |
| 239 | } |
| 240 | $this->rk = $w; |
| 241 | } |
| 242 | |
| 243 | /** S-boxとTテーブル(初めて使う時に作る) */ |
| 244 | private static function tables(): void { |
| 245 | if (self::$t !== null) return; |
| 246 | $sb = array_fill(0, 256, 0); |
| 247 | $p = 1; |
| 248 | $q = 1; |
| 249 | do { |
| 250 | $p = ($p ^ ($p << 1) ^ (($p & 0x80) ? 0x1B : 0)) & 0xFF; |
| 251 | $q ^= $q << 1; |
| 252 | $q ^= $q << 2; |
| 253 | $q ^= $q << 4; |
| 254 | $q &= 0xFF; |
| 255 | if ($q & 0x80) $q ^= 0x09; |
| 256 | $x = $q ^ (($q << 1) | ($q >> 7)) ^ (($q << 2) | ($q >> 6)) ^ (($q << 3) | ($q >> 5)) ^ (($q << 4) | ($q >> 4)); |
| 257 | $sb[$p] = ($x ^ 0x63) & 0xFF; |
| 258 | } while ($p !== 1); |
| 259 | $sb[0] = 0x63; |
| 260 | |
| 261 | $t0 = $t1 = $t2 = $t3 = []; |
| 262 | for ($i = 0; $i < 256; $i++) { |
| 263 | $s = $sb[$i]; |
| 264 | $s2 = (($s << 1) ^ (($s & 0x80) ? 0x1B : 0)) & 0xFF; |
| 265 | $s3 = $s2 ^ $s; |
| 266 | $v = ($s2 << 24) | ($s << 16) | ($s << 8) | $s3; |
| 267 | $t0[$i] = $v; |
| 268 | $t1[$i] = (($v >> 8) | ($v << 24)) & 0xFFFFFFFF; |
| 269 | $t2[$i] = (($v >> 16) | ($v << 16)) & 0xFFFFFFFF; |
| 270 | $t3[$i] = (($v >> 24) | ($v << 8)) & 0xFFFFFFFF; |
| 271 | } |
| 272 | self::$t = [$t0, $t1, $t2, $t3, $sb]; |
| 273 | } |
| 274 | } |