|
|
@@ -15,28 +15,46 @@ final class TokenIssuer
|
|
|
{
|
|
|
private const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
|
|
+ /** Required input length for the base32 encoder. 20 × 8 = 160 = 32 × 5. */
|
|
|
+ private const ENTROPY_BYTES = 20;
|
|
|
+
|
|
|
public function issue(TokenKind $kind): string
|
|
|
{
|
|
|
- return sprintf('irdb_%s_%s', $kind->code(), self::base32Encode(random_bytes(20)));
|
|
|
+ return sprintf('irdb_%s_%s', $kind->code(), self::base32Encode(random_bytes(self::ENTROPY_BYTES)));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Encodes 20 raw bytes (160 bits) as 32 base32 chars (no padding).
|
|
|
+ * Encodes exactly 20 raw bytes (160 bits) as 32 base32 chars.
|
|
|
+ *
|
|
|
+ * 160 ÷ 5 = 32 with zero remainder — every base32 char in the output
|
|
|
+ * carries 5 useful bits, there is no trailing-bit ambiguity, no
|
|
|
+ * padding, and exactly one canonical encoding per input. SEC_REVIEW
|
|
|
+ * F39 noted a "the trailing 5-bit chunk is zero-padded" concern; the
|
|
|
+ * concern was based on the previous unreachable `if (strlen($chunk)
|
|
|
+ * < 5)` branch in this method, which never fires for a 20-byte
|
|
|
+ * input. We now assert the length up front and refuse anything else
|
|
|
+ * — any future caller that passes the wrong length crashes loudly
|
|
|
+ * rather than silently emitting a non-canonical / shorter encoding.
|
|
|
*/
|
|
|
private static function base32Encode(string $bytes): string
|
|
|
{
|
|
|
+ if (strlen($bytes) !== self::ENTROPY_BYTES) {
|
|
|
+ throw new \InvalidArgumentException(sprintf(
|
|
|
+ 'TokenIssuer::base32Encode requires exactly %d bytes; got %d',
|
|
|
+ self::ENTROPY_BYTES,
|
|
|
+ strlen($bytes),
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
$bits = '';
|
|
|
- for ($i = 0, $n = strlen($bytes); $i < $n; ++$i) {
|
|
|
+ for ($i = 0; $i < self::ENTROPY_BYTES; ++$i) {
|
|
|
$bits .= str_pad(decbin(ord($bytes[$i])), 8, '0', STR_PAD_LEFT);
|
|
|
}
|
|
|
|
|
|
$out = '';
|
|
|
- for ($i = 0, $n = strlen($bits); $i < $n; $i += 5) {
|
|
|
- $chunk = substr($bits, $i, 5);
|
|
|
- if (strlen($chunk) < 5) {
|
|
|
- $chunk = str_pad($chunk, 5, '0');
|
|
|
- }
|
|
|
- $out .= self::BASE32_ALPHABET[bindec($chunk)];
|
|
|
+ // 160 bits / 5 = 32 iterations exactly; no partial trailing chunk.
|
|
|
+ for ($i = 0; $i < 160; $i += 5) {
|
|
|
+ $out .= self::BASE32_ALPHABET[bindec(substr($bits, $i, 5))];
|
|
|
}
|
|
|
|
|
|
return $out;
|