$size, 'sha256' => hash_final($hash)]; } /** Caller verifies the complete result before emitting any plaintext. */ public static function decrypt($input, $output, string $key): array { if (self::read($input, 8) !== self::MAGIC) throw new \RuntimeException('Invalid container.'); $state = sodium_crypto_secretstream_xchacha20poly1305_init_pull(self::read($input, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES), $key); $hash = hash_init('sha256'); $size = 0; for (;;) { $length = unpack('Nlength', self::read($input, 4))['length']; if ($length < SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES || $length > self::CHUNK + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES) throw new \RuntimeException('Invalid record size.'); $result = sodium_crypto_secretstream_xchacha20poly1305_pull($state, self::read($input, $length), self::MAGIC); if ($result === false) throw new \RuntimeException('Authentication failed.'); [$plain, $tag] = $result; if ($tag === SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Reject even one trailing byte after the authenticated final record. if ($plain !== '' || fread($input, 1) !== '') throw new \RuntimeException('Invalid final record.'); sodium_memzero($state); return ['size' => $size, 'sha256' => hash_final($hash)]; } if ($tag !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE) throw new \RuntimeException('Invalid record tag.'); $size += strlen($plain); hash_update($hash, $plain); self::write($output, $plain); } } }