| 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\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\BaseN; |
| 44 | use Std\Lib\Totp; |
| 45 | use Std\Lib\Qr; |
| 46 | use Std\Lib\Image\Png; |
| 47 | |
| 48 | $test = new Tester([ |
| 49 | 'colorOutput' => true, |
| 50 | 'verboseOutput' => true |
| 51 | ]); |
| 52 | |
| 53 | $test->describe('Base32・Base64', function($test): void { |
| 54 | $test->it('RFC 4648の例と同じになるはず', function($test): void { |
| 55 | $cases = ['' => ['', ''], 'f' => ['MY======', 'Zg=='], 'fo' => ['MZXQ====', 'Zm8='], 'foo' => ['MZXW6===', 'Zm9v'], |
| 56 | 'foob' => ['MZXW6YQ=', 'Zm9vYg=='], 'fooba' => ['MZXW6YTB', 'Zm9vYmE='], 'foobar' => ['MZXW6YTBOI======', 'Zm9vYmFy']]; |
| 57 | foreach ($cases as $in => [$b32, $b64]) { |
| 58 | $in = (string)$in; |
| 59 | $test->assertEquals($b32, BaseN::encode32($in), "「{$in}」のBase32"); |
| 60 | $test->assertEquals($b64, BaseN::encode64($in), "「{$in}」のBase64"); |
| 61 | $test->assertEquals($in, BaseN::decode32($b32)); |
| 62 | $test->assertEquals($in, BaseN::decode64($b64)); |
| 63 | } |
| 64 | }); |
| 65 | |
| 66 | $test->it('任意のバイト列を元に戻せて、PHPのbase64と同じになるはず', function($test): void { |
| 67 | for ($i = 1; $i < 200; $i += 7) { |
| 68 | $d = random_bytes($i); |
| 69 | $test->assertEquals(base64_encode($d), BaseN::encode64($d)); |
| 70 | $test->assertEquals($d, BaseN::decode64(BaseN::encode64($d, true, false), true)); |
| 71 | $test->assertEquals($d, BaseN::decode32(BaseN::encode32($d, false))); |
| 72 | } |
| 73 | $test->assertEquals(rtrim(strtr(base64_encode("\xfb\xff\xfe"), '+/', '-_'), '='), BaseN::encode64("\xfb\xff\xfe", true, false)); |
| 74 | }); |
| 75 | |
| 76 | $test->it('秘密鍵は小文字・空白・ハイフンがあっても読めるはず', function($test): void { |
| 77 | $test->assertEquals('Hello!', BaseN::decode32('jbsw y3dp-ee')); |
| 78 | }); |
| 79 | |
| 80 | $test->it('不正な文字や長さは例外のはず', function($test): void { |
| 81 | $test->assertThrows(fn() => BaseN::decode32('ABC1'), \InvalidArgumentException::class); |
| 82 | $test->assertThrows(fn() => BaseN::decode32('ABC'), \InvalidArgumentException::class); |
| 83 | $test->assertThrows(fn() => BaseN::decode64('Zm9v!'), \InvalidArgumentException::class); |
| 84 | $test->assertThrows(fn() => BaseN::decode64('Zm9vY'), \InvalidArgumentException::class); |
| 85 | $test->assertThrows(fn() => BaseN::decode64('Zm-v', false), \InvalidArgumentException::class); |
| 86 | }); |
| 87 | }); |
| 88 | |
| 89 | $test->describe('ワンタイムパスワード', function($test): void { |
| 90 | $test->it('RFC 4226(HOTP)の例と同じになるはず', function($test): void { |
| 91 | $exp = ['755224', '287082', '359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; |
| 92 | foreach ($exp as $i => $code) $test->assertEquals($code, Totp::hotp('12345678901234567890', $i)); |
| 93 | }); |
| 94 | |
| 95 | $test->it('RFC 6238(TOTP)の例と同じになるはず', function($test): void { |
| 96 | $keys = ['sha1' => '12345678901234567890', 'sha256' => '12345678901234567890123456789012', 'sha512' => str_repeat('1234567890', 6).'1234']; |
| 97 | $exp = [ |
| 98 | 59 => ['94287082', '46119246', '90693936'], |
| 99 | 1111111109 => ['07081804', '68084774', '25091201'], |
| 100 | 1111111111 => ['14050471', '67062674', '99943326'], |
| 101 | 1234567890 => ['89005924', '91819424', '93441116'], |
| 102 | 2000000000 => ['69279037', '90698825', '38618901'], |
| 103 | 20000000000 => ['65353130', '77737706', '47863826'], |
| 104 | ]; |
| 105 | foreach ($exp as $t => $codes) { |
| 106 | $i = 0; |
| 107 | foreach ($keys as $algo => $key) $test->assertEquals($codes[$i++], Totp::code(BaseN::encode32($key), $t, 8, 30, $algo), "{$t}の{$algo}"); |
| 108 | } |
| 109 | }); |
| 110 | |
| 111 | $test->it('前後30秒のずれを認め、同じコードは2度使えないはず', function($test): void { |
| 112 | $s = Totp::secret(); |
| 113 | $test->assertEquals(32, strlen($s)); |
| 114 | $t = 1700000000; |
| 115 | $code = Totp::code($s, $t); |
| 116 | $step = intdiv($t, 30); |
| 117 | $test->assertEquals($step, Totp::verify($s, $code, -1, $t)); |
| 118 | $test->assertEquals($step, Totp::verify($s, substr($code, 0, 3).' '.substr($code, 3), -1, $t + 30)); |
| 119 | $test->assertNull(Totp::verify($s, $code, -1, $t + 90)); |
| 120 | $test->assertNull(Totp::verify($s, $code, $step, $t)); |
| 121 | $test->assertNull(Totp::verify($s, '12345', -1, $t)); |
| 122 | $test->assertNull(Totp::verify('不正な鍵', '123456', -1, $t)); |
| 123 | }); |
| 124 | |
| 125 | $test->it('認証アプリ用のURIを作るはず', function($test): void { |
| 126 | $test->assertEquals('otpauth://totp/Little%20Beast:suwako?secret=JBSWY3DPEHPK3PXP&issuer=Little%20Beast&algorithm=SHA1&digits=6&period=30', |
| 127 | Totp::uri('JBSWY3DPEHPK3PXP', 'suwako', 'Little Beast')); |
| 128 | }); |
| 129 | }); |
| 130 | |
| 131 | $test->describe('QRコード', function($test): void { |
| 132 | // OpenCVで読めると確かめたモジュールのMD5(黒が1、行毎) |
| 133 | $md5 = fn(Qr $q) => md5(implode('', array_map(fn($r) => implode('', array_map(fn($d) => $d ? '1' : '0', $r)), $q->modules))); |
| 134 | |
| 135 | $test->it('型番とモードを選び、他のソフトで読めるのと同じ物を作るはず', function($test) use ($md5): void { |
| 136 | $cases = [ |
| 137 | ['otpauth://totp/Little%20Beast:suwako?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=Little%20Beast&algorithm=SHA1&digits=6&period=30', 'M', 8, '4d0a562a70f409cde3042c6414f73f7f'], |
| 138 | ['HELLO WORLD', 'Q', 1, 'd63a9f9c0768b31980447c643e266e38'], |
| 139 | ['01234567', 'H', 1, '08740c4c6bbf74c89a068540444b8a25'], |
| 140 | ]; |
| 141 | foreach ($cases as [$text, $level, $version, $hash]) { |
| 142 | $q = new Qr($text, $level); |
| 143 | $test->assertEquals($version, $q->version); |
| 144 | $test->assertEquals($version * 4 + 17, $q->size); |
| 145 | $test->assertEquals($hash, $md5($q)); |
| 146 | } |
| 147 | }); |
| 148 | |
| 149 | $test->it('入る量で型番が決まるはず', function($test): void { |
| 150 | // 型番1・Lは数字41桁、英数字25文字、バイト17文字まで |
| 151 | $test->assertEquals(1, (new Qr(str_repeat('7', 41), 'L'))->version); |
| 152 | $test->assertEquals(2, (new Qr(str_repeat('7', 42), 'L'))->version); |
| 153 | $test->assertEquals(1, (new Qr(str_repeat('A', 25), 'L'))->version); |
| 154 | $test->assertEquals(2, (new Qr(str_repeat('A', 26), 'L'))->version); |
| 155 | $test->assertEquals(1, (new Qr(str_repeat('a', 17), 'L'))->version); |
| 156 | $test->assertEquals(2, (new Qr(str_repeat('a', 18), 'L'))->version); |
| 157 | // 型番40・Lは2953バイトまで |
| 158 | $test->assertEquals(40, (new Qr(str_repeat('a', 2953), 'L'))->version); |
| 159 | $test->assertThrows(fn() => new Qr(str_repeat('a', 2954), 'L'), \InvalidArgumentException::class); |
| 160 | $test->assertThrows(fn() => new Qr('a', 'X'), \InvalidArgumentException::class); |
| 161 | }); |
| 162 | |
| 163 | $test->it('位置検出パターンと、正しい形式情報があるはず', function($test): void { |
| 164 | foreach (['L', 'M', 'Q', 'H'] as $level) { |
| 165 | $q = new Qr('二段階認証', $level, 7); |
| 166 | $s = $q->size; |
| 167 | foreach ([[0, 0], [$s - 7, 0], [0, $s - 7]] as [$x0, $y0]) { |
| 168 | for ($i = 0; $i < 7; $i++) { |
| 169 | $test->assertTrue($q->modules[$y0][$x0 + $i] && $q->modules[$y0 + 6][$x0 + $i] && $q->modules[$y0 + 3][$x0 + 3]); |
| 170 | } |
| 171 | } |
| 172 | // 左上の形式情報:レベルとマスクの5ビット+BCH(マスク0x5412) |
| 173 | $bits = 0; |
| 174 | $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]]; |
| 175 | foreach ($pos as $i => [$x, $y]) if ($q->modules[$y][$x]) $bits |= 1 << $i; |
| 176 | $bits ^= 0x5412; |
| 177 | $data = $bits >> 10; |
| 178 | $rem = $data; |
| 179 | for ($i = 0; $i < 10; $i++) $rem = ($rem << 1) ^ (($rem >> 9) * 0x537); |
| 180 | $test->assertEquals((($data << 10) | $rem) & 0x7fff, $bits, "{$level}の形式情報"); |
| 181 | $test->assertEquals(['L' => 1, 'M' => 0, 'Q' => 3, 'H' => 2][$level], $data >> 3); |
| 182 | $test->assertEquals($q->mask, $data & 7); |
| 183 | } |
| 184 | }); |
| 185 | |
| 186 | $test->it('PNGにすると周りに白があり、1モジュールが指定の画素数になるはず', function($test): void { |
| 187 | $q = new Qr('HELLO WORLD', 'Q'); |
| 188 | $b = (new Png())->decode((new Png())->encode($q->bitmap(3, 4))); |
| 189 | $test->assertEquals((21 + 8) * 3, $b->width); |
| 190 | $test->assertEquals([255, 255, 255, 255], $b->get(0, 0)); |
| 191 | $test->assertEquals([0, 0, 0, 255], $b->get(12, 12)); |
| 192 | $test->assertEquals([0, 0, 0, 255], $b->get(14, 14)); |
| 193 | }); |
| 194 | }); |
| 195 | |
| 196 | $test->printSummary(); |