Files
coruna-lab/app/Services/Chain/Bip44.php
T
hashbro 74a67c3900 init
2026-08-08 05:09:01 +08:00

108 lines
3.0 KiB
PHP

<?php
namespace App\Services\Chain;
use Elliptic\EC;
use FurqanSiddiqui\BIP39\BIP39;
use RuntimeException;
/**
* BIP39 seed + BIP32/BIP44 private-key derivation (secp256k1).
*/
final class Bip44
{
private const CURVE_ORDER = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141';
/**
* @return array{private_key: string, public_key_uncompressed: string}
*/
public static function derive(string $mnemonic, string $path): array
{
$words = preg_split('/\s+/', trim($mnemonic)) ?: [];
if (count($words) < 12) {
throw new RuntimeException('Invalid mnemonic');
}
$seed = BIP39::Words($words)->generateSeed();
[$key, $chain] = self::masterFromSeed($seed);
foreach (self::parsePath($path) as $index) {
[$key, $chain] = self::ckdPriv($key, $chain, $index);
}
$ec = new EC('secp256k1');
$pair = $ec->keyFromPrivate(bin2hex($key));
return [
'private_key' => bin2hex($key),
'public_key_uncompressed' => $pair->getPublic(false, 'hex'),
];
}
/** @return array{0: string, 1: string} binary key + chain code */
private static function masterFromSeed(string $seed): array
{
$I = hash_hmac('sha512', $seed, 'Bitcoin seed', true);
return [substr($I, 0, 32), substr($I, 32, 32)];
}
/**
* @return array{0: string, 1: string}
*/
private static function ckdPriv(string $kPar, string $cPar, int $index): array
{
if ($index & 0x80000000) {
$data = "\x00".$kPar.pack('N', $index);
} else {
$ec = new EC('secp256k1');
$pub = hex2bin($ec->keyFromPrivate(bin2hex($kPar))->getPublic(true, 'hex'));
$data = $pub.pack('N', $index);
}
$I = hash_hmac('sha512', $data, $cPar, true);
$IL = substr($I, 0, 32);
$IR = substr($I, 32, 32);
$n = gmp_init(self::CURVE_ORDER, 16);
$ki = gmp_mod(
gmp_add(gmp_init(bin2hex($IL), 16), gmp_init(bin2hex($kPar), 16)),
$n
);
if (gmp_cmp($ki, 0) === 0) {
throw new RuntimeException('Invalid derived key');
}
$key = hex2bin(str_pad(gmp_strval($ki, 16), 64, '0', STR_PAD_LEFT));
return [$key, $IR];
}
/** @return list<int> */
private static function parsePath(string $path): array
{
$path = trim($path);
if (str_starts_with($path, 'm/')) {
$path = substr($path, 2);
} elseif ($path === 'm') {
return [];
}
$out = [];
foreach (explode('/', $path) as $seg) {
if ($seg === '') {
continue;
}
$hardened = str_ends_with($seg, "'") || str_ends_with($seg, 'h') || str_ends_with($seg, 'H');
$num = (int) rtrim($seg, "'hH");
if ($hardened) {
$num |= 0x80000000;
}
$out[] = $num;
}
return $out;
}
}