77 lines
3.2 KiB
PHP
77 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use app\common\service\qywx\QywxPromotionCodeCipher;
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
|
|
|
|
// 独立临时App根目录,不加载项目配置或业务数据库。
|
|
if (($argv[1] ?? '') === '--worker') {
|
|
new think\App($argv[2]);
|
|
echo (new QywxPromotionCodeCipher())->encrypt('cipher-concurrency-test');
|
|
exit(0);
|
|
}
|
|
|
|
function cipherCheck(bool $ok, string $message): void
|
|
{
|
|
if (!$ok) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
|
|
$root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'qywx_cipher_test_' . bin2hex(random_bytes(8));
|
|
mkdir($root, 0700);
|
|
new think\App($root);
|
|
$processes = [];
|
|
try {
|
|
// 多个进程首次启动必须共享同一完整密钥,不能读到空文件或覆盖对方的密钥。
|
|
for ($i = 0; $i < 6; $i++) {
|
|
$pipes = [];
|
|
$process = proc_open([PHP_BINARY, __FILE__, '--worker', $root],
|
|
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes,
|
|
null, null, ['bypass_shell' => true]);
|
|
cipherCheck(is_resource($process), 'start isolated cipher worker');
|
|
fclose($pipes[0]);
|
|
$processes[] = [$process, $pipes];
|
|
}
|
|
$cipher = new QywxPromotionCodeCipher();
|
|
$encrypted = [];
|
|
foreach ($processes as [$process, $pipes]) {
|
|
$value = stream_get_contents($pipes[1]);
|
|
$error = stream_get_contents($pipes[2]);
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
cipherCheck(proc_close($process) === 0 && $error === '', 'cipher worker completed without error');
|
|
cipherCheck($cipher->decrypt($value) === 'cipher-concurrency-test', 'concurrent processes share one persisted key');
|
|
$encrypted[] = $value;
|
|
}
|
|
$processes = [];
|
|
cipherCheck(count(array_unique($encrypted)) === 6, 'fresh nonce for every encryption');
|
|
$raw = base64_decode($encrypted[0], true);
|
|
$raw[15] = chr(ord($raw[15]) ^ 1);
|
|
$rejected = false;
|
|
try { $cipher->decrypt(base64_encode($raw)); } catch (RuntimeException) { $rejected = true; }
|
|
cipherCheck($rejected, 'tampered authentication tag rejected');
|
|
$rejected = false;
|
|
try { (new QywxPromotionCodeCipher(str_repeat('wrong-key', 8)))->decrypt($encrypted[0]); }
|
|
catch (RuntimeException) { $rejected = true; }
|
|
cipherCheck($rejected, 'wrong key cannot decrypt');
|
|
file_put_contents($root . '/runtime/qywx_promotion_private/welcome.key', 'incomplete-key');
|
|
$rejected = false;
|
|
try { (new QywxPromotionCodeCipher())->encrypt('test'); } catch (RuntimeException) { $rejected = true; }
|
|
cipherCheck($rejected, 'damaged persisted key fails closed instead of silently rotating');
|
|
} finally {
|
|
foreach ($processes as [$process, $pipes]) {
|
|
foreach ($pipes as $pipe) { if (is_resource($pipe)) { fclose($pipe); } }
|
|
if (is_resource($process)) { proc_close($process); }
|
|
}
|
|
$keyPath = $root . '/runtime/qywx_promotion_private/welcome.key';
|
|
if (is_file($keyPath)) { unlink($keyPath); }
|
|
if (is_dir(dirname($keyPath))) { rmdir(dirname($keyPath)); }
|
|
if (is_dir($root . '/runtime')) { rmdir($root . '/runtime'); }
|
|
rmdir($root);
|
|
}
|
|
echo "QYWX_PROMOTION_CODE_CIPHER_OK\n";
|