seal($iv12, $plain, $aad); // 暗号文||タグ(16バイト) * $plain = $aes->open($iv12, $sealed, $aad); // 改ざんされていればnull */ class Aes { private static ?array $t = null; private array $rk; private int $rounds; /** GHASHの表(4ビット毎、上位と下位の64ビット) */ private array $hh = []; private array $hl = []; // GHASHの4ビットの剰余(OpenSSLのrem_4bit) private const REM4 = [0x0000, 0x1C20, 0x3840, 0x2460, 0x7080, 0x6CA0, 0x48C0, 0x54E0, 0xE100, 0xFD20, 0xD940, 0xC560, 0x9180, 0x8DA0, 0xA9C0, 0xB5E0]; /** * @param string $key 16、24、32バイト */ public function __construct(string $key) { $len = strlen($key); if (!in_array($len, [16, 24, 32], true)) throw new \InvalidArgumentException('AES:鍵は16、24、32バイト'); self::tables(); $this->expand($key); // GHASHのH = E(K, 0) [$h0, $h1, $h2, $h3] = $this->encryptWords(0, 0, 0, 0); $vh = ($h0 << 32) | $h1; $vl = ($h2 << 32) | $h3; $hh = array_fill(0, 16, 0); $hl = array_fill(0, 16, 0); $hh[8] = $vh; $hl[8] = $vl; foreach ([4, 2, 1] as $i) { $t = (0xE1 << 56) & -($vl & 1); $vl = ($vh << 63) | (($vl >> 1) & PHP_INT_MAX); $vh = (($vh >> 1) & PHP_INT_MAX) ^ $t; $hh[$i] = $vh; $hl[$i] = $vl; } foreach ([2, 4, 8] as $i) { for ($j = 1; $j < $i; $j++) { $hh[$i + $j] = $hh[$i] ^ $hh[$j]; $hl[$i + $j] = $hl[$i] ^ $hl[$j]; } } $this->hh = $hh; $this->hl = $hl; } /** 1ブロック(16バイト)を暗号化する */ public function encryptBlock(string $block): string { if (strlen($block) !== 16) throw new \InvalidArgumentException('AES:ブロックは16バイト'); $w = unpack('N4', $block); return pack('N4', ...$this->encryptWords($w[1], $w[2], $w[3], $w[4])); } /** * GCMで暗号化する * * @param string $iv 12バイト * @param string $plain 平文 * @param string $aad 追加の認証データ * @return string 暗号文||タグ(16バイト) */ public function seal(string $iv, string $plain, string $aad = ''): string { if (strlen($iv) !== 12) throw new \InvalidArgumentException('AES-GCM:IVは12バイト'); $ct = $this->ctr($iv, $plain); return $ct.$this->tag($iv, $ct, $aad); } /** * GCMで復号する * * @param string $iv 12バイト * @param string $sealed 暗号文||タグ * @param string $aad 追加の認証データ * @return string|null 平文、改ざんされていればnull */ public function open(string $iv, string $sealed, string $aad = ''): ?string { if (strlen($iv) !== 12 || strlen($sealed) < 16) return null; $ct = substr($sealed, 0, -16); if (!hash_equals($this->tag($iv, $ct, $aad), substr($sealed, -16))) return null; return $this->ctr($iv, $ct); } /////////////// /** カウンタモード(カウンタは2から) */ private function ctr(string $iv, string $data): string { $len = strlen($data); if ($len === 0) return ''; [, $n0, $n1, $n2] = unpack('N3', $iv); $ks = ''; $blocks = intdiv($len + 15, 16); for ($i = 0; $i < $blocks; $i++) { $ks .= pack('N4', ...$this->encryptWords($n0, $n1, $n2, ($i + 2) & 0xFFFFFFFF)); } return $data ^ substr($ks, 0, $len); } /** タグ = E(K, J0) xor GHASH(aad, ct) */ private function tag(string $iv, string $ct, string $aad): string { $zh = 0; $zl = 0; foreach ([$aad, $ct] as $d) { $len = strlen($d); if ($len % 16) $d .= str_repeat("\0", 16 - $len % 16); for ($i = 0, $n = strlen($d); $i < $n; $i += 16) { $b = unpack('J2', $d, $i); [$zh, $zl] = $this->gmult($zh ^ $b[1], $zl ^ $b[2]); } } [$zh, $zl] = $this->gmult($zh ^ (strlen($aad) * 8), $zl ^ (strlen($ct) * 8)); [, $n0, $n1, $n2] = unpack('N3', $iv); $ek = pack('N4', ...$this->encryptWords($n0, $n1, $n2, 1)); return $ek ^ pack('J2', $zh, $zl); } /** X * H(GF(2^128)、OpenSSLのgcm_gmult_4bit) */ private function gmult(int $xh, int $xl): array { $H = $this->hh; $L = $this->hl; $mask = 0x0FFFFFFFFFFFFFFF; $b = $xl & 0xFF; $nhi = $b >> 4; $zh = $H[$b & 0xF]; $zl = $L[$b & 0xF]; for ($cnt = 15; ; ) { $rem = $zl & 0xF; $zl = ($zh << 60) | (($zl >> 4) & $mask); $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nhi]; $zl ^= $L[$nhi]; if (--$cnt < 0) break; $b = $cnt >= 8 ? ($xl >> (8 * (15 - $cnt))) & 0xFF : ($xh >> (8 * (7 - $cnt))) & 0xFF; $nhi = $b >> 4; $nlo = $b & 0xF; $rem = $zl & 0xF; $zl = ($zh << 60) | (($zl >> 4) & $mask); $zh = (($zh >> 4) & $mask) ^ (self::REM4[$rem] << 48) ^ $H[$nlo]; $zl ^= $L[$nlo]; } return [$zh, $zl]; } /** 4つの32ビットの語を暗号化する */ private function encryptWords(int $s0, int $s1, int $s2, int $s3): array { [$t0, $t1, $t2, $t3, $sb] = self::$t; $rk = $this->rk; $s0 ^= $rk[0]; $s1 ^= $rk[1]; $s2 ^= $rk[2]; $s3 ^= $rk[3]; $k = 4; for ($r = 1; $r < $this->rounds; $r++) { $u0 = $t0[$s0 >> 24] ^ $t1[($s1 >> 16) & 0xFF] ^ $t2[($s2 >> 8) & 0xFF] ^ $t3[$s3 & 0xFF] ^ $rk[$k]; $u1 = $t0[$s1 >> 24] ^ $t1[($s2 >> 16) & 0xFF] ^ $t2[($s3 >> 8) & 0xFF] ^ $t3[$s0 & 0xFF] ^ $rk[$k + 1]; $u2 = $t0[$s2 >> 24] ^ $t1[($s3 >> 16) & 0xFF] ^ $t2[($s0 >> 8) & 0xFF] ^ $t3[$s1 & 0xFF] ^ $rk[$k + 2]; $s3 = $t0[$s3 >> 24] ^ $t1[($s0 >> 16) & 0xFF] ^ $t2[($s1 >> 8) & 0xFF] ^ $t3[$s2 & 0xFF] ^ $rk[$k + 3]; $s0 = $u0; $s1 = $u1; $s2 = $u2; $k += 4; } return [ (($sb[$s0 >> 24] << 24) | ($sb[($s1 >> 16) & 0xFF] << 16) | ($sb[($s2 >> 8) & 0xFF] << 8) | $sb[$s3 & 0xFF]) ^ $rk[$k], (($sb[$s1 >> 24] << 24) | ($sb[($s2 >> 16) & 0xFF] << 16) | ($sb[($s3 >> 8) & 0xFF] << 8) | $sb[$s0 & 0xFF]) ^ $rk[$k + 1], (($sb[$s2 >> 24] << 24) | ($sb[($s3 >> 16) & 0xFF] << 16) | ($sb[($s0 >> 8) & 0xFF] << 8) | $sb[$s1 & 0xFF]) ^ $rk[$k + 2], (($sb[$s3 >> 24] << 24) | ($sb[($s0 >> 16) & 0xFF] << 16) | ($sb[($s1 >> 8) & 0xFF] << 8) | $sb[$s2 & 0xFF]) ^ $rk[$k + 3], ]; } /** 鍵の拡大(FIPS 197 5.2) */ private function expand(string $key): void { $sb = self::$t[4]; $nk = intdiv(strlen($key), 4); $this->rounds = $nk + 6; $w = array_values(unpack('N*', $key)); $rcon = 1; for ($i = $nk; $i < 4 * ($this->rounds + 1); $i++) { $t = $w[$i - 1]; if ($i % $nk === 0) { $t = (($t << 8) & 0xFFFFFFFF) | ($t >> 24); $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF]; $t ^= $rcon << 24; $rcon = (($rcon << 1) ^ (($rcon & 0x80) ? 0x1B : 0)) & 0xFF; } else if ($nk > 6 && $i % $nk === 4) { $t = ($sb[$t >> 24] << 24) | ($sb[($t >> 16) & 0xFF] << 16) | ($sb[($t >> 8) & 0xFF] << 8) | $sb[$t & 0xFF]; } $w[$i] = $w[$i - $nk] ^ $t; } $this->rk = $w; } /** S-boxとTテーブル(初めて使う時に作る) */ private static function tables(): void { if (self::$t !== null) return; $sb = array_fill(0, 256, 0); $p = 1; $q = 1; do { $p = ($p ^ ($p << 1) ^ (($p & 0x80) ? 0x1B : 0)) & 0xFF; $q ^= $q << 1; $q ^= $q << 2; $q ^= $q << 4; $q &= 0xFF; if ($q & 0x80) $q ^= 0x09; $x = $q ^ (($q << 1) | ($q >> 7)) ^ (($q << 2) | ($q >> 6)) ^ (($q << 3) | ($q >> 5)) ^ (($q << 4) | ($q >> 4)); $sb[$p] = ($x ^ 0x63) & 0xFF; } while ($p !== 1); $sb[0] = 0x63; $t0 = $t1 = $t2 = $t3 = []; for ($i = 0; $i < 256; $i++) { $s = $sb[$i]; $s2 = (($s << 1) ^ (($s & 0x80) ? 0x1B : 0)) & 0xFF; $s3 = $s2 ^ $s; $v = ($s2 << 24) | ($s << 16) | ($s << 8) | $s3; $t0[$i] = $v; $t1[$i] = (($v >> 8) | ($v << 24)) & 0xFFFFFFFF; $t2[$i] = (($v >> 16) | ($v << 16)) & 0xFFFFFFFF; $t3[$i] = (($v >> 24) | ($v << 8)) & 0xFFFFFFFF; } self::$t = [$t0, $t1, $t2, $t3, $sb]; } }