true, 'verboseOutput' => true ]); $test->describe('Base32・Base64', function($test): void { $test->it('RFC 4648の例と同じになるはず', function($test): void { $cases = ['' => ['', ''], 'f' => ['MY======', 'Zg=='], 'fo' => ['MZXQ====', 'Zm8='], 'foo' => ['MZXW6===', 'Zm9v'], 'foob' => ['MZXW6YQ=', 'Zm9vYg=='], 'fooba' => ['MZXW6YTB', 'Zm9vYmE='], 'foobar' => ['MZXW6YTBOI======', 'Zm9vYmFy']]; foreach ($cases as $in => [$b32, $b64]) { $in = (string)$in; $test->assertEquals($b32, BaseN::encode32($in), "「{$in}」のBase32"); $test->assertEquals($b64, BaseN::encode64($in), "「{$in}」のBase64"); $test->assertEquals($in, BaseN::decode32($b32)); $test->assertEquals($in, BaseN::decode64($b64)); } }); $test->it('任意のバイト列を元に戻せて、PHPのbase64と同じになるはず', function($test): void { for ($i = 1; $i < 200; $i += 7) { $d = random_bytes($i); $test->assertEquals(base64_encode($d), BaseN::encode64($d)); $test->assertEquals($d, BaseN::decode64(BaseN::encode64($d, true, false), true)); $test->assertEquals($d, BaseN::decode32(BaseN::encode32($d, false))); } $test->assertEquals(rtrim(strtr(base64_encode("\xfb\xff\xfe"), '+/', '-_'), '='), BaseN::encode64("\xfb\xff\xfe", true, false)); }); $test->it('秘密鍵は小文字・空白・ハイフンがあっても読めるはず', function($test): void { $test->assertEquals('Hello!', BaseN::decode32('jbsw y3dp-ee')); }); $test->it('不正な文字や長さは例外のはず', function($test): void { $test->assertThrows(fn() => BaseN::decode32('ABC1'), \InvalidArgumentException::class); $test->assertThrows(fn() => BaseN::decode32('ABC'), \InvalidArgumentException::class); $test->assertThrows(fn() => BaseN::decode64('Zm9v!'), \InvalidArgumentException::class); $test->assertThrows(fn() => BaseN::decode64('Zm9vY'), \InvalidArgumentException::class); $test->assertThrows(fn() => BaseN::decode64('Zm-v', false), \InvalidArgumentException::class); }); }); $test->describe('ワンタイムパスワード', function($test): void { $test->it('RFC 4226(HOTP)の例と同じになるはず', function($test): void { $exp = ['755224', '287082', '359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; foreach ($exp as $i => $code) $test->assertEquals($code, Totp::hotp('12345678901234567890', $i)); }); $test->it('RFC 6238(TOTP)の例と同じになるはず', function($test): void { $keys = ['sha1' => '12345678901234567890', 'sha256' => '12345678901234567890123456789012', 'sha512' => str_repeat('1234567890', 6).'1234']; $exp = [ 59 => ['94287082', '46119246', '90693936'], 1111111109 => ['07081804', '68084774', '25091201'], 1111111111 => ['14050471', '67062674', '99943326'], 1234567890 => ['89005924', '91819424', '93441116'], 2000000000 => ['69279037', '90698825', '38618901'], 20000000000 => ['65353130', '77737706', '47863826'], ]; foreach ($exp as $t => $codes) { $i = 0; foreach ($keys as $algo => $key) $test->assertEquals($codes[$i++], Totp::code(BaseN::encode32($key), $t, 8, 30, $algo), "{$t}の{$algo}"); } }); $test->it('前後30秒のずれを認め、同じコードは2度使えないはず', function($test): void { $s = Totp::secret(); $test->assertEquals(32, strlen($s)); $t = 1700000000; $code = Totp::code($s, $t); $step = intdiv($t, 30); $test->assertEquals($step, Totp::verify($s, $code, -1, $t)); $test->assertEquals($step, Totp::verify($s, substr($code, 0, 3).' '.substr($code, 3), -1, $t + 30)); $test->assertNull(Totp::verify($s, $code, -1, $t + 90)); $test->assertNull(Totp::verify($s, $code, $step, $t)); $test->assertNull(Totp::verify($s, '12345', -1, $t)); $test->assertNull(Totp::verify('不正な鍵', '123456', -1, $t)); }); $test->it('認証アプリ用のURIを作るはず', function($test): void { $test->assertEquals('otpauth://totp/Little%20Beast:suwako?secret=JBSWY3DPEHPK3PXP&issuer=Little%20Beast&algorithm=SHA1&digits=6&period=30', Totp::uri('JBSWY3DPEHPK3PXP', 'suwako', 'Little Beast')); }); }); $test->describe('QRコード', function($test): void { // OpenCVで読めると確かめたモジュールのMD5(黒が1、行毎) $md5 = fn(Qr $q) => md5(implode('', array_map(fn($r) => implode('', array_map(fn($d) => $d ? '1' : '0', $r)), $q->modules))); $test->it('型番とモードを選び、他のソフトで読めるのと同じ物を作るはず', function($test) use ($md5): void { $cases = [ ['otpauth://totp/Little%20Beast:suwako?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=Little%20Beast&algorithm=SHA1&digits=6&period=30', 'M', 8, '4d0a562a70f409cde3042c6414f73f7f'], ['HELLO WORLD', 'Q', 1, 'd63a9f9c0768b31980447c643e266e38'], ['01234567', 'H', 1, '08740c4c6bbf74c89a068540444b8a25'], ]; foreach ($cases as [$text, $level, $version, $hash]) { $q = new Qr($text, $level); $test->assertEquals($version, $q->version); $test->assertEquals($version * 4 + 17, $q->size); $test->assertEquals($hash, $md5($q)); } }); $test->it('入る量で型番が決まるはず', function($test): void { // 型番1・Lは数字41桁、英数字25文字、バイト17文字まで $test->assertEquals(1, (new Qr(str_repeat('7', 41), 'L'))->version); $test->assertEquals(2, (new Qr(str_repeat('7', 42), 'L'))->version); $test->assertEquals(1, (new Qr(str_repeat('A', 25), 'L'))->version); $test->assertEquals(2, (new Qr(str_repeat('A', 26), 'L'))->version); $test->assertEquals(1, (new Qr(str_repeat('a', 17), 'L'))->version); $test->assertEquals(2, (new Qr(str_repeat('a', 18), 'L'))->version); // 型番40・Lは2953バイトまで $test->assertEquals(40, (new Qr(str_repeat('a', 2953), 'L'))->version); $test->assertThrows(fn() => new Qr(str_repeat('a', 2954), 'L'), \InvalidArgumentException::class); $test->assertThrows(fn() => new Qr('a', 'X'), \InvalidArgumentException::class); }); $test->it('位置検出パターンと、正しい形式情報があるはず', function($test): void { foreach (['L', 'M', 'Q', 'H'] as $level) { $q = new Qr('二段階認証', $level, 7); $s = $q->size; foreach ([[0, 0], [$s - 7, 0], [0, $s - 7]] as [$x0, $y0]) { for ($i = 0; $i < 7; $i++) { $test->assertTrue($q->modules[$y0][$x0 + $i] && $q->modules[$y0 + 6][$x0 + $i] && $q->modules[$y0 + 3][$x0 + 3]); } } // 左上の形式情報:レベルとマスクの5ビット+BCH(マスク0x5412) $bits = 0; $pos = [[8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8]]; foreach ($pos as $i => [$x, $y]) if ($q->modules[$y][$x]) $bits |= 1 << $i; $bits ^= 0x5412; $data = $bits >> 10; $rem = $data; for ($i = 0; $i < 10; $i++) $rem = ($rem << 1) ^ (($rem >> 9) * 0x537); $test->assertEquals((($data << 10) | $rem) & 0x7fff, $bits, "{$level}の形式情報"); $test->assertEquals(['L' => 1, 'M' => 0, 'Q' => 3, 'H' => 2][$level], $data >> 3); $test->assertEquals($q->mask, $data & 7); } }); $test->it('PNGにすると周りに白があり、1モジュールが指定の画素数になるはず', function($test): void { $q = new Qr('HELLO WORLD', 'Q'); $b = (new Png())->decode((new Png())->encode($q->bitmap(3, 4))); $test->assertEquals((21 + 8) * 3, $b->width); $test->assertEquals([255, 255, 255, 255], $b->get(0, 0)); $test->assertEquals([0, 0, 0, 255], $b->get(12, 12)); $test->assertEquals([0, 0, 0, 255], $b->get(14, 14)); }); }); $test->printSummary();