*/ private static array $kdfCache = []; private static ?bool $scryptReady = null; public static function decrypt(array $keystore, string $password): ?string { $crypto = $keystore['crypto'] ?? $keystore['Crypto'] ?? null; if (! is_array($crypto)) { return null; } $cipher = strtolower((string) ($crypto['cipher'] ?? '')); $keyLen = match ($cipher) { 'aes-128-ctr' => 16, 'aes-192-ctr' => 24, 'aes-256-ctr' => 32, default => 0, }; if ($keyLen === 0) { return null; } $ciphertext = self::fromHex($crypto['ciphertext'] ?? null); $mac = self::fromHex($crypto['mac'] ?? null); $iv = self::fromHex($crypto['cipherparams']['iv'] ?? null); if ($ciphertext === null || $mac === null || $iv === null || strlen($iv) !== 16) { return null; } $derived = self::deriveKey($crypto, $password); if ($derived === null || strlen($derived) < $keyLen) { return null; } $macOk = hash_equals($mac, self::keccak256(substr($derived, -$keyLen).$ciphertext)) || hash_equals($mac, self::keccak256(substr($derived, 16, 16).$ciphertext)); if (! $macOk) { return null; } $plain = openssl_decrypt($ciphertext, $cipher, substr($derived, 0, $keyLen), OPENSSL_RAW_DATA, $iv); if (! is_string($plain) || $plain === '') { return null; } return $plain; } public static function scryptReady(): bool { if (self::$scryptReady !== null) { return self::$scryptReady; } $out = self::scryptPython('a', 'b', 2, 1, 1, 16); self::$scryptReady = is_string($out) && strlen($out) === 16; return self::$scryptReady; } /** * @param array $kdfparams * @return array */ public static function encrypt(string $plaintext, string $password, array $kdfparams = []): array { $salt = $kdfparams['salt'] ?? bin2hex(random_bytes(32)); if (is_string($salt) && ctype_xdigit($salt)) { $saltHex = strtolower($salt); } else { $saltHex = bin2hex((string) $salt); } $n = (int) ($kdfparams['n'] ?? 16); $r = (int) ($kdfparams['r'] ?? 8); $p = (int) ($kdfparams['p'] ?? 1); $dklen = (int) ($kdfparams['dklen'] ?? 32); $iv = random_bytes(16); $derived = Scrypt::hash($password, hex2bin($saltHex) ?: '', $n, $r, $p, $dklen); $ciphertext = openssl_encrypt($plaintext, 'aes-128-ctr', substr($derived, 0, 16), OPENSSL_RAW_DATA, $iv); if (! is_string($ciphertext)) { throw new \RuntimeException('aes-128-ctr encrypt failed'); } return [ 'version' => 3, 'type' => 'mnemonic', 'crypto' => [ 'cipher' => 'aes-128-ctr', 'cipherparams' => ['iv' => bin2hex($iv)], 'ciphertext' => bin2hex($ciphertext), 'kdf' => 'scrypt', 'kdfparams' => [ 'dklen' => $dklen, 'n' => $n, 'p' => $p, 'r' => $r, 'salt' => $saltHex, ], 'mac' => bin2hex(self::keccak256(substr($derived, 16, 16).$ciphertext)), ], ]; } /** * @param array $crypto */ private static function deriveKey(array $crypto, string $password): ?string { $kdf = strtolower((string) ($crypto['kdf'] ?? '')); $params = is_array($crypto['kdfparams'] ?? null) ? $crypto['kdfparams'] : []; $dklen = (int) ($params['dklen'] ?? 32); $salt = self::fromHex($params['salt'] ?? '') ?? ''; if ($dklen < 16) { return null; } if ($kdf === 'scrypt') { $n = (int) ($params['n'] ?? 0); $r = (int) ($params['r'] ?? 0); $p = (int) ($params['p'] ?? 0); if ($n < 2 || $r < 1 || $p < 1) { return null; } return self::scrypt($password, $salt, $n, $r, $p, $dklen); } if ($kdf === 'pbkdf2') { $c = (int) ($params['c'] ?? 0); $prf = strtolower((string) ($params['prf'] ?? 'hmac-sha256')); if ($c < 1 || ! str_contains($prf, 'sha256')) { return null; } return hash_pbkdf2('sha256', $password, $salt, $c, $dklen, true); } return null; } private static function scrypt(string $password, string $salt, int $n, int $r, int $p, int $dklen): ?string { $cacheKey = hash('sha256', $password."\0".$salt."\0{$n}\0{$r}\0{$p}\0{$dklen}"); if (isset(self::$kdfCache[$cacheKey])) { return self::$kdfCache[$cacheKey]; } $out = null; if ($n >= 256) { $out = self::scryptPython($password, $salt, $n, $r, $p, $dklen); } else { try { $out = Scrypt::hash($password, $salt, $n, $r, $p, $dklen); } catch (\Throwable) { $out = null; } } if ($out !== null) { self::$kdfCache[$cacheKey] = $out; } return $out; } private static function scryptPython(string $password, string $salt, int $n, int $r, int $p, int $dklen): ?string { $python = self::pythonBinary(); if ($python === null) { return null; } $code = <<<'PY' import sys pw = bytes.fromhex(sys.argv[1]) salt = bytes.fromhex(sys.argv[2]) n, r, p, dk = (int(sys.argv[i]) for i in range(3, 7)) mem = 128 * r * n + 8 * 1024 * 1024 try: import hashlib sys.stdout.buffer.write(hashlib.scrypt(pw, salt=salt, n=n, r=r, p=p, dklen=dk, maxmem=mem)) raise SystemExit(0) except SystemExit: raise except Exception: pass try: from Crypto.Protocol.KDF import scrypt sys.stdout.buffer.write(scrypt(pw, salt, dk, N=n, r=r, p=p)) except Exception: sys.exit(2) PY; $cmd = [ $python, '-c', $code, bin2hex($password), bin2hex($salt), (string) $n, (string) $r, (string) $p, (string) $dklen, ]; $spec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; $proc = @proc_open($cmd, $spec, $pipes); if (! is_resource($proc)) { return null; } fclose($pipes[0]); $out = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); $codeStatus = proc_close($proc); if ($codeStatus !== 0 || ! is_string($out) || strlen($out) !== $dklen) { return null; } return $out; } private static function pythonBinary(): ?string { $configured = [ trim((string) config('coruna.channel_builder.python', '')), trim((string) config('coruna.channel_builder_new.python', '')), ]; foreach ($configured as $bin) { if ($bin !== '') { return $bin; } } foreach ([ base_path('channel-builder/.venv/bin/python'), base_path('channel-builder-new/.venv/bin/python'), ] as $venv) { if (self::venvPythonExists($venv)) { return $venv; } } return 'python3'; } /** * Detect .venv/bin/python without following the symlink into /usr/bin * (open_basedir typically allows the project root only). */ private static function venvPythonExists(string $path): bool { clearstatcache(true, $path); if (@is_link($path)) { return true; } return @is_file($path); } private static function keccak256(string $data): string { return hex2bin(Keccak::hash($data, 256)) ?: ''; } private static function fromHex(mixed $value): ?string { if (! is_string($value)) { return null; } if ($value === '') { return ''; } $hex = preg_replace('/[^0-9a-fA-F]/', '', $value) ?? ''; if ($hex === '' || strlen($hex) % 2 !== 0) { return null; } $bin = @hex2bin($hex); return is_string($bin) ? $bin : null; } }