| 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 | * RSA(鍵の作成、PEM、PKCS#1 v1.5・PSSの署名、OAEPの暗号化) |
| 42 | * |
| 43 | * 例: |
| 44 | * $key = Rsa::generate(2048); |
| 45 | * file_put_contents('priv.pem', $key->toPem()); |
| 46 | * $sig = Rsa::fromPem($priv)->sign($data); // RSASSA-PKCS1-v1_5、SHA-256 |
| 47 | * $ok = Rsa::fromPem($pub)->verify($data, $sig); |
| 48 | * $ct = Rsa::fromPem($pub)->encrypt($secret, 'sha1'); // RSAES-OAEP |
| 49 | * |
| 50 | * 秘密鍵の計算はCRTとブラインディングを使い、結果を公開鍵で確認する。 |
| 51 | */ |
| 52 | class Rsa { |
| 53 | public const OID = '1.2.840.113549.1.1.1'; |
| 54 | public const OID_PSS = '1.2.840.113549.1.1.10'; |
| 55 | |
| 56 | // DigestInfoの先頭(RFC 8017 9.2) |
| 57 | private const DIGEST_INFO = [ |
| 58 | 'sha1' => "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", |
| 59 | 'sha224' => "\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c", |
| 60 | 'sha256' => "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20", |
| 61 | 'sha384' => "\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30", |
| 62 | 'sha512' => "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40", |
| 63 | ]; |
| 64 | |
| 65 | public readonly \GMP $n; |
| 66 | public readonly \GMP $e; |
| 67 | /** 法の大きさ(ビット、バイト) */ |
| 68 | public readonly int $bits; |
| 69 | public readonly int $bytes; |
| 70 | |
| 71 | private ?\GMP $d = null; |
| 72 | private ?\GMP $p = null; |
| 73 | private ?\GMP $q = null; |
| 74 | private ?\GMP $dp = null; |
| 75 | private ?\GMP $dq = null; |
| 76 | private ?\GMP $qi = null; |
| 77 | |
| 78 | private function __construct(\GMP $n, \GMP $e) { |
| 79 | if (gmp_cmp($n, 0) <= 0 || gmp_cmp($e, 1) <= 0) throw new \InvalidArgumentException('RSA:不正な鍵'); |
| 80 | $this->n = $n; |
| 81 | $this->e = $e; |
| 82 | $this->bits = strlen(gmp_strval($n, 2)); |
| 83 | $this->bytes = intdiv($this->bits + 7, 8); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * 新しい鍵を作る |
| 88 | * |
| 89 | * @param int $bits 法の大きさ(1024以上、普通は2048か4096) |
| 90 | * @param int $e 公開指数 |
| 91 | * @return self |
| 92 | */ |
| 93 | public static function generate(int $bits = 2048, int $e = 65537): self { |
| 94 | if ($bits < 1024 || $bits % 2) throw new \InvalidArgumentException('RSA:鍵の大きさは1024以上の偶数'); |
| 95 | $E = gmp_init($e); |
| 96 | |
| 97 | while (true) { |
| 98 | $p = self::prime(intdiv($bits, 2), $E); |
| 99 | $q = self::prime(intdiv($bits, 2), $E); |
| 100 | // pとqが近過ぎない事(FIPS 186-4 B.3.1) |
| 101 | if (strlen(gmp_strval(gmp_abs(gmp_sub($p, $q)), 2)) <= intdiv($bits, 2) - 100) continue; |
| 102 | $n = gmp_mul($p, $q); |
| 103 | if (strlen(gmp_strval($n, 2)) !== $bits) continue; |
| 104 | return self::fromParts($n, $E, null, $p, $q); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /** 最上位の2ビットを立てた、p-1がeと互いに素な素数 */ |
| 109 | private static function prime(int $bits, \GMP $e): \GMP { |
| 110 | while (true) { |
| 111 | $b = random_bytes(intdiv($bits + 7, 8)); |
| 112 | $b[0] = chr(ord($b[0]) | 0xC0); |
| 113 | $c = gmp_nextprime(gmp_import($b)); |
| 114 | if (strlen(gmp_strval($c, 2)) !== $bits) continue; |
| 115 | if (gmp_cmp(gmp_gcd(gmp_sub($c, 1), $e), 1) !== 0) continue; |
| 116 | if (gmp_prob_prime($c, 40) === 0) continue; |
| 117 | return $c; |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | /** n、e、(d、p、q)から作る */ |
| 122 | private static function fromParts(\GMP $n, \GMP $e, ?\GMP $d = null, ?\GMP $p = null, ?\GMP $q = null): self { |
| 123 | $key = new self($n, $e); |
| 124 | if ($p !== null && $q !== null) { |
| 125 | if (gmp_cmp(gmp_mul($p, $q), $n) !== 0) throw new \InvalidArgumentException('RSA:p・qとnが合わない'); |
| 126 | $phi = gmp_lcm(gmp_sub($p, 1), gmp_sub($q, 1)); |
| 127 | $d ??= gmp_invert($e, $phi) ?: null; |
| 128 | if ($d === null) throw new \InvalidArgumentException('RSA:dを計算出来ない'); |
| 129 | $key->p = $p; |
| 130 | $key->q = $q; |
| 131 | $key->dp = gmp_mod($d, gmp_sub($p, 1)); |
| 132 | $key->dq = gmp_mod($d, gmp_sub($q, 1)); |
| 133 | $key->qi = gmp_invert($q, $p) ?: null; |
| 134 | } |
| 135 | $key->d = $d; |
| 136 | return $key; |
| 137 | } |
| 138 | |
| 139 | /////////////// |
| 140 | // 読み込み・書き出し |
| 141 | |
| 142 | /** |
| 143 | * PEMから読む(PRIVATE KEY、RSA PRIVATE KEY、PUBLIC KEY、RSA PUBLIC KEY) |
| 144 | * |
| 145 | * @param string $pem |
| 146 | * @return self |
| 147 | * @throws \InvalidArgumentException RSAの鍵ではない、又は暗号化されている場合 |
| 148 | */ |
| 149 | public static function fromPem(string $pem): self { |
| 150 | foreach (Asn1::unpem($pem) as $b) { |
| 151 | if (!empty($b['encrypted']) || $b['label'] === 'ENCRYPTED PRIVATE KEY') { |
| 152 | throw new \InvalidArgumentException('RSA:暗号化された鍵は未対応'); |
| 153 | } |
| 154 | switch ($b['label']) { |
| 155 | case 'PRIVATE KEY': |
| 156 | case 'PUBLIC KEY': |
| 157 | case 'RSA PRIVATE KEY': |
| 158 | case 'RSA PUBLIC KEY': |
| 159 | return self::fromDer($b['der']); |
| 160 | } |
| 161 | } |
| 162 | throw new \InvalidArgumentException('RSA:鍵が見つからない'); |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * DERから読む(PKCS#8、SubjectPublicKeyInfo、PKCS#1の秘密鍵・公開鍵) |
| 167 | * |
| 168 | * @param string $der |
| 169 | * @return self |
| 170 | * @throws \InvalidArgumentException |
| 171 | */ |
| 172 | public static function fromDer(string $der): self { |
| 173 | try { |
| 174 | $c = Asn1::expect(Asn1::parse($der), Asn1::SEQUENCE, 2); |
| 175 | |
| 176 | // SubjectPublicKeyInfo:SEQUENCE { AlgorithmIdentifier, BIT STRING } |
| 177 | if ($c[0]['tag'] === Asn1::SEQUENCE && $c[1]['tag'] === Asn1::BIT_STRING) { |
| 178 | self::checkAlgorithm($c[0]); |
| 179 | return self::fromDer(Asn1::toBits($c[1])); |
| 180 | } |
| 181 | |
| 182 | // PKCS#8:SEQUENCE { version, AlgorithmIdentifier, OCTET STRING, … } |
| 183 | if ($c[0]['tag'] === Asn1::INTEGER && $c[1]['tag'] === Asn1::SEQUENCE && ($c[2]['tag'] ?? 0) === Asn1::OCTET_STRING) { |
| 184 | self::checkAlgorithm($c[1]); |
| 185 | return self::fromDer($c[2]['value']); |
| 186 | } |
| 187 | |
| 188 | // PKCS#1の公開鍵:SEQUENCE { n, e } |
| 189 | if (count($c) === 2) return new self(Asn1::toInt($c[0]), Asn1::toInt($c[1])); |
| 190 | |
| 191 | // PKCS#1の秘密鍵:SEQUENCE { version, n, e, d, p, q, dp, dq, qi } |
| 192 | if (count($c) >= 9 && gmp_sign(Asn1::toInt($c[0])) === 0) { |
| 193 | [$n, $e, $d, $p, $q] = array_map([Asn1::class, 'toInt'], array_slice($c, 1, 5)); |
| 194 | return self::fromParts($n, $e, $d, $p, $q); |
| 195 | } |
| 196 | } catch (\UnexpectedValueException $ex) { |
| 197 | throw new \InvalidArgumentException('RSA:壊れた鍵('.$ex->getMessage().')'); |
| 198 | } |
| 199 | |
| 200 | throw new \InvalidArgumentException('RSA:未対応の鍵の形式'); |
| 201 | } |
| 202 | |
| 203 | /** |
| 204 | * SubjectPublicKeyInfoのアルゴリズムを確認する |
| 205 | * |
| 206 | * @throws \InvalidArgumentException RSA(又はRSA-PSS)ではない場合 |
| 207 | */ |
| 208 | private static function checkAlgorithm(array $alg): void { |
| 209 | $oid = Asn1::toOid(Asn1::children($alg)[0]); |
| 210 | if ($oid !== self::OID && $oid !== self::OID_PSS) throw new \InvalidArgumentException("RSA:RSAの鍵ではない({$oid})"); |
| 211 | } |
| 212 | |
| 213 | public function isPrivate(): bool { |
| 214 | return $this->d !== null; |
| 215 | } |
| 216 | |
| 217 | /** 公開鍵(SubjectPublicKeyInfoのDER) */ |
| 218 | public function publicDer(): string { |
| 219 | return Asn1::seq( |
| 220 | Asn1::seq(Asn1::oid(self::OID), Asn1::null()), |
| 221 | Asn1::bits(Asn1::seq(Asn1::int($this->n), Asn1::int($this->e))), |
| 222 | ); |
| 223 | } |
| 224 | |
| 225 | /** 公開鍵(「BEGIN PUBLIC KEY」のPEM) */ |
| 226 | public function publicPem(): string { |
| 227 | return Asn1::pem($this->publicDer(), 'PUBLIC KEY'); |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * 秘密鍵(PKCS#8、「BEGIN PRIVATE KEY」のPEM) |
| 232 | * |
| 233 | * @throws \LogicException 公開鍵の場合 |
| 234 | */ |
| 235 | public function toPem(): string { |
| 236 | if ($this->p === null) throw new \LogicException('RSA:秘密鍵ではない'); |
| 237 | $pkcs1 = Asn1::seq(Asn1::int(0), Asn1::int($this->n), Asn1::int($this->e), Asn1::int($this->d), |
| 238 | Asn1::int($this->p), Asn1::int($this->q), Asn1::int($this->dp), Asn1::int($this->dq), Asn1::int($this->qi)); |
| 239 | return Asn1::pem(Asn1::seq(Asn1::int(0), Asn1::seq(Asn1::oid(self::OID), Asn1::null()), Asn1::octets($pkcs1)), 'PRIVATE KEY'); |
| 240 | } |
| 241 | |
| 242 | /////////////// |
| 243 | // 署名 |
| 244 | |
| 245 | /** |
| 246 | * 署名する(RSASSA-PKCS1-v1_5) |
| 247 | * |
| 248 | * @param string $msg メッセージ |
| 249 | * @param string $hash sha1、sha256、sha384、sha512等 |
| 250 | * @return string 署名($this->bytesバイト) |
| 251 | */ |
| 252 | public function sign(string $msg, string $hash = 'sha256'): string { |
| 253 | return $this->private($this->pkcs1Encode($msg, $hash)); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * 署名を確認する(RSASSA-PKCS1-v1_5) |
| 258 | * |
| 259 | * @param string $msg メッセージ |
| 260 | * @param string $sig 署名 |
| 261 | * @param string $hash ハッシュ |
| 262 | * @return bool |
| 263 | */ |
| 264 | public function verify(string $msg, string $sig, string $hash = 'sha256'): bool { |
| 265 | $em = $this->public($sig); |
| 266 | return $em !== null && hash_equals($this->pkcs1Encode($msg, $hash), $em); |
| 267 | } |
| 268 | |
| 269 | private function pkcs1Encode(string $msg, string $hash): string { |
| 270 | if (!isset(self::DIGEST_INFO[$hash])) throw new \InvalidArgumentException("RSA:未対応のハッシュ:{$hash}"); |
| 271 | $t = self::DIGEST_INFO[$hash].hash($hash, $msg, true); |
| 272 | if ($this->bytes < strlen($t) + 11) throw new \InvalidArgumentException('RSA:鍵が小さ過ぎる'); |
| 273 | return "\x00\x01".str_repeat("\xFF", $this->bytes - strlen($t) - 3)."\x00".$t; |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * 署名する(RSASSA-PSS、MGF1は同じハッシュ) |
| 278 | * |
| 279 | * @param string $msg メッセージ |
| 280 | * @param string $hash ハッシュ |
| 281 | * @param int|null $saltLen ソルトの長さ(null=ハッシュの長さ) |
| 282 | * @return string |
| 283 | */ |
| 284 | public function signPss(string $msg, string $hash = 'sha256', ?int $saltLen = null): string { |
| 285 | $mHash = hash($hash, $msg, true); |
| 286 | $hLen = strlen($mHash); |
| 287 | $saltLen ??= $hLen; |
| 288 | $emBits = $this->bits - 1; |
| 289 | $emLen = intdiv($emBits + 7, 8); |
| 290 | if ($emLen < $hLen + $saltLen + 2) throw new \InvalidArgumentException('RSA:鍵が小さ過ぎる'); |
| 291 | |
| 292 | $salt = $saltLen > 0 ? random_bytes($saltLen) : ''; |
| 293 | $h = hash($hash, str_repeat("\0", 8).$mHash.$salt, true); |
| 294 | $db = str_repeat("\0", $emLen - $saltLen - $hLen - 2)."\x01".$salt; |
| 295 | $masked = $db ^ self::mgf1($h, $emLen - $hLen - 1, $hash); |
| 296 | $masked[0] = chr(ord($masked[0]) & (0xFF >> (8 * $emLen - $emBits))); |
| 297 | return $this->private(str_pad($masked.$h."\xBC", $this->bytes, "\0", STR_PAD_LEFT)); |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * 署名を確認する(RSASSA-PSS) |
| 302 | * |
| 303 | * @param string $msg メッセージ |
| 304 | * @param string $sig 署名 |
| 305 | * @param string $hash ハッシュ(MGF1も同じ) |
| 306 | * @param int|null $saltLen ソルトの長さ(null=ハッシュの長さ、-1=何でも) |
| 307 | * @return bool |
| 308 | */ |
| 309 | public function verifyPss(string $msg, string $sig, string $hash = 'sha256', ?int $saltLen = null): bool { |
| 310 | $em = $this->public($sig); |
| 311 | if ($em === null) return false; |
| 312 | |
| 313 | $mHash = hash($hash, $msg, true); |
| 314 | $hLen = strlen($mHash); |
| 315 | $emBits = $this->bits - 1; |
| 316 | $emLen = intdiv($emBits + 7, 8); |
| 317 | $em = substr($em, -$emLen); |
| 318 | if (strlen($em) !== $emLen || $emLen < $hLen + 2 || substr($em, -1) !== "\xBC") return false; |
| 319 | if ($this->bytes > $emLen && ltrim(substr($this->public($sig), 0, $this->bytes - $emLen), "\0") !== '') return false; |
| 320 | |
| 321 | $masked = substr($em, 0, $emLen - $hLen - 1); |
| 322 | $h = substr($em, $emLen - $hLen - 1, $hLen); |
| 323 | $topMask = 0xFF >> (8 * $emLen - $emBits); |
| 324 | if (ord($masked[0]) & ~$topMask & 0xFF) return false; |
| 325 | |
| 326 | $db = $masked ^ self::mgf1($h, strlen($masked), $hash); |
| 327 | $db[0] = chr(ord($db[0]) & $topMask); |
| 328 | $one = strpos($db, "\x01"); |
| 329 | if ($one === false || ltrim(substr($db, 0, $one), "\0") !== '') return false; |
| 330 | $salt = substr($db, $one + 1); |
| 331 | if ($saltLen !== null && $saltLen >= 0 && strlen($salt) !== $saltLen) return false; |
| 332 | if ($saltLen === null && strlen($salt) !== $hLen) return false; |
| 333 | |
| 334 | return hash_equals($h, hash($hash, str_repeat("\0", 8).$mHash.$salt, true)); |
| 335 | } |
| 336 | |
| 337 | /////////////// |
| 338 | // 暗号化 |
| 339 | |
| 340 | /** |
| 341 | * 暗号化する(RSAES-OAEP、MGF1は同じハッシュ) |
| 342 | * |
| 343 | * @param string $msg 平文 |
| 344 | * @param string $hash ハッシュ(OpenSSLのOPENSSL_PKCS1_OAEP_PADDINGはsha1) |
| 345 | * @param string $label ラベル |
| 346 | * @return string |
| 347 | */ |
| 348 | public function encrypt(string $msg, string $hash = 'sha1', string $label = ''): string { |
| 349 | $lHash = hash($hash, $label, true); |
| 350 | $hLen = strlen($lHash); |
| 351 | $k = $this->bytes; |
| 352 | if (strlen($msg) > $k - 2 * $hLen - 2) throw new \InvalidArgumentException('RSA:メッセージが長過ぎる'); |
| 353 | |
| 354 | $db = $lHash.str_repeat("\0", $k - strlen($msg) - 2 * $hLen - 2)."\x01".$msg; |
| 355 | $seed = random_bytes($hLen); |
| 356 | $maskedDb = $db ^ self::mgf1($seed, $k - $hLen - 1, $hash); |
| 357 | $maskedSeed = $seed ^ self::mgf1($maskedDb, $hLen, $hash); |
| 358 | return $this->public("\x00".$maskedSeed.$maskedDb, false); |
| 359 | } |
| 360 | |
| 361 | /** |
| 362 | * 復号する(RSAES-OAEP) |
| 363 | * |
| 364 | * @param string $ct 暗号文 |
| 365 | * @param string $hash ハッシュ |
| 366 | * @param string $label ラベル |
| 367 | * @return string|null 平文、失敗したらnull |
| 368 | */ |
| 369 | public function decrypt(string $ct, string $hash = 'sha1', string $label = ''): ?string { |
| 370 | if (strlen($ct) !== $this->bytes) return null; |
| 371 | $em = $this->private($ct); |
| 372 | $lHash = hash($hash, $label, true); |
| 373 | $hLen = strlen($lHash); |
| 374 | if ($this->bytes < 2 * $hLen + 2) return null; |
| 375 | |
| 376 | $maskedSeed = substr($em, 1, $hLen); |
| 377 | $maskedDb = substr($em, 1 + $hLen); |
| 378 | $seed = $maskedSeed ^ self::mgf1($maskedDb, $hLen, $hash); |
| 379 | $db = $maskedDb ^ self::mgf1($seed, strlen($maskedDb), $hash); |
| 380 | |
| 381 | $one = strpos($db, "\x01", $hLen); |
| 382 | $bad = ($em[0] !== "\0") | !hash_equals($lHash, substr($db, 0, $hLen)) | ($one === false); |
| 383 | if ($bad || ltrim(substr($db, $hLen, $one - $hLen), "\0") !== '') return null; |
| 384 | return substr($db, $one + 1); |
| 385 | } |
| 386 | |
| 387 | /////////////// |
| 388 | // 計算 |
| 389 | |
| 390 | /** MGF1(RFC 8017 B.2.1) */ |
| 391 | private static function mgf1(string $seed, int $len, string $hash): string { |
| 392 | $out = ''; |
| 393 | for ($i = 0; strlen($out) < $len; $i++) $out .= hash($hash, $seed.pack('N', $i), true); |
| 394 | return substr($out, 0, $len); |
| 395 | } |
| 396 | |
| 397 | /** 公開鍵の計算(m^e mod n) */ |
| 398 | private function public(string $data, bool $check = true): ?string { |
| 399 | if (strlen($data) !== $this->bytes) { |
| 400 | if ($check) return null; |
| 401 | throw new \InvalidArgumentException('RSA:長さが違う'); |
| 402 | } |
| 403 | $m = gmp_import($data); |
| 404 | if (gmp_cmp($m, $this->n) >= 0) return null; |
| 405 | return $this->i2osp(gmp_powm($m, $this->e, $this->n)); |
| 406 | } |
| 407 | |
| 408 | /** 秘密鍵の計算(CRT、ブラインディング、結果の確認) */ |
| 409 | private function private(string $data): string { |
| 410 | if ($this->d === null) throw new \LogicException('RSA:秘密鍵ではない'); |
| 411 | $c = gmp_import($data); |
| 412 | if (gmp_cmp($c, $this->n) >= 0) throw new \InvalidArgumentException('RSA:値が大き過ぎる'); |
| 413 | |
| 414 | // ブラインディング:c' = c * r^e |
| 415 | do { |
| 416 | $r = gmp_mod(gmp_import(random_bytes($this->bytes + 8)), $this->n); |
| 417 | $ri = gmp_invert($r, $this->n); |
| 418 | } while ($ri === false || gmp_sign($r) === 0); |
| 419 | $cb = gmp_mod(gmp_mul($c, gmp_powm($r, $this->e, $this->n)), $this->n); |
| 420 | |
| 421 | if ($this->p !== null && $this->qi !== null) { |
| 422 | $m1 = gmp_powm($cb, $this->dp, $this->p); |
| 423 | $m2 = gmp_powm($cb, $this->dq, $this->q); |
| 424 | $h = gmp_mod(gmp_mul($this->qi, gmp_sub($m1, $m2)), $this->p); |
| 425 | $mb = gmp_add($m2, gmp_mul($h, $this->q)); |
| 426 | } else { |
| 427 | $mb = gmp_powm($cb, $this->d, $this->n); |
| 428 | } |
| 429 | $m = gmp_mod(gmp_mul($mb, $ri), $this->n); |
| 430 | |
| 431 | // 計算の誤り(故障攻撃)を防ぐ |
| 432 | if (gmp_cmp(gmp_powm($m, $this->e, $this->n), $c) !== 0) throw new \RuntimeException('RSA:計算に失敗'); |
| 433 | return $this->i2osp($m); |
| 434 | } |
| 435 | |
| 436 | private function i2osp(\GMP $x): string { |
| 437 | return str_pad(gmp_export($x), $this->bytes, "\0", STR_PAD_LEFT); |
| 438 | } |
| 439 | } |