diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61ead86 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/README.md b/README.md index 5b60707..8d49229 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # hd-wallet-derive -A command-line tool that derives bip32 addresses and private keys. +A command-line tool that derives bip32 addresses and private keys for Bitcoin and Ethereum. -Derivation reports show privkey (wif encoded), xprv, xpub, and address. +Derivation reports show privkey (wif encoded), xprv, xpub, and addresses for Bitcoin and Ethereum . Input can be a xprv key, xpub key, or bip39 mnemonic string (eg 12 words) with optional password. @@ -131,6 +131,20 @@ $ ./hd-wallet-derive.php -g --key=xpub6BfKpqjTwvH21wJGWEfxLppb8sU7C6FJge2kWb9315 +-----------------------------------------------------------------------------------------------------------------+------------------------------------+ ``` +## We can view ethereum addresses. + +``` +$ ./hd-wallet-derive.php --key=xpub6BfKpqjTwvH21wJGWEfxLppb8sU7C6FJge2kWb9315oP4ZVqCXG29cdUtkyu7YQhHyfA5nt63nzcNZHYmqXYHDxYo8mm1Xq1dAC7YtodwUR --cols=path,address,eth_address --numderive=3 -g + ++------+------------------------------------+--------------------------------------------+ +| path | address | eth_address | ++------+------------------------------------+--------------------------------------------+ +| m/0 | 1FZKdR3E7S1UPvqsuqStXAhZiovntFirge | 0xc7eE60fFD437cf206A4E6deFaEd020c54b63d3f5 | +| m/1 | 12UMERLGAHKe5PQPaSYX8sczr52rSAg2Mi | 0x96790F426AC663989605B806Ac8360891bD76359 | +| m/2 | 1Pyk8NLx3gaXSng7XhKoNMLfBffUsJGAjr | 0x76580a4cD31C5EC607a713C922Fd3dE278Ab49c1 | ++------+------------------------------------+--------------------------------------------+ +``` + ## We can get results in a variety of additional formats ### simple list @@ -246,34 +260,38 @@ The report may be printed in the following formats: Options: -g go! ( required ) - + --key= xpriv or xpub key --mnemonic= bip39 seed words note: either key or nmemonic is required. - + --mnemonic-pw= optionally specify password for mnemonic. - + + --numderive= Number of keys to derive. default=10 + + --startderive= Starting key index to derive. default=0 + --cols= a csv list of columns, or "all" all: - (path,xprv,xpub,privkey,address,index) + (path,address,xprv,xpub,privkey,pubkey,pubkeyhash,index,eth_address) default: - (path,privkey,address) + (path,address,privkey) --outfile= specify output file path. --format= txt|csv|json|jsonpretty|html|list|all default=txt - + if 'all' is specified then a file will be created for each format with appropriate extension. only works when outfile is specified. - + 'list' prints only the first column. see --cols - + --path= bip32 path to derive, relative to provided key (m). eg "", "m/0" or "m/1" default = "m" - + --includeroot include root key as first element of report. - + --logfile= path to logfile. if not present logs to stdout. --loglevel= debug,info,specialinfo,warning,exception,fatalerror default = info diff --git a/composer.json b/composer.json index 5020c6d..5a56a74 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,7 @@ "ext-json": "*", "php": ">=5.5", "dan-da/texttable-php": "^1.0", - "dan-da/strictmode-php": "^1.0" - }, - - "require-dev": { + "dan-da/strictmode-php": "^1.0", + "kornrunner/keccak": "^1.0" } } diff --git a/hd-wallet-derive.php b/hd-wallet-derive.php index 72c5dde..732830c 100755 --- a/hd-wallet-derive.php +++ b/hd-wallet-derive.php @@ -60,6 +60,7 @@ function get_cli_params() { 'mnemonic-pw:', 'outfile:', 'numderive:', + 'startderive:', 'includeroot', 'path:', 'format:', 'cols:', @@ -112,6 +113,7 @@ function process_cli_params( $params ) { $params['format'] = @$params['format'] ?: 'txt'; $params['cols'] = @$params['cols'] ?: 'all'; $params['numderive'] = @$params['numderive'] ?: 10; + $params['startderive'] = @$params['startderive'] ?: 0; $params['includeroot'] = isset($params['includeroot'] ); return [$params, $success]; @@ -153,6 +155,8 @@ function print_help() { --mnemonic-pw= optionally specify password for mnemonic. --numderive= Number of keys to derive. default=10 + + --startderive= Starting key index to derive. default=0 --cols= a csv list of columns, or "all" all: diff --git a/lib/wallet_derive.class.php b/lib/wallet_derive.class.php index 4cf7640..60f8f36 100644 --- a/lib/wallet_derive.class.php +++ b/lib/wallet_derive.class.php @@ -7,11 +7,20 @@ use \BitWasp\Bitcoin\Address; use \BitWasp\Bitcoin\Key\Deterministic\HierarchicalKeyFactory; use \BitWasp\Buffertools\Buffer; +use \BitWasp\Bitcoin\Address\PayToPubKeyHashAddress; // For Bip39 Mnemonics use \BitWasp\Bitcoin\Mnemonic\Bip39\Bip39SeedGenerator; use BitWasp\Bitcoin\Mnemonic\MnemonicFactory; +// For ethereum addresses +use kornrunner\Keccak; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Serializer\Key\PublicKeySerializer; +use BitWasp\Bitcoin\Crypto\EcAdapter\EcAdapterFactory; +use Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer; +use Mdanter\Ecc\EccFactory; + // For generating html tables. require_once __DIR__ . '/html_table.class.php'; @@ -36,6 +45,40 @@ private function get_params() { return $this->params; } + private function getEthereumAddress(PublicKeyInterface $publicKey){ + static $pubkey_serializer = null; + static $point_serializer = null; + if(!$pubkey_serializer){ + $adapter = EcAdapterFactory::getPhpEcc(Bitcoin::getMath(), Bitcoin::getGenerator()); + $pubkey_serializer = new PublicKeySerializer($adapter); + $point_serializer = new UncompressedPointSerializer(EccFactory::getAdapter()); + } + + $pubKey = $pubkey_serializer->parse($publicKey->getHex()); + $point = $pubKey->getPoint(); + $upk = $point_serializer->serialize($point); + $upk = hex2bin(substr($upk, 2)); + + $keccak = Keccak::hash($upk, 256); + $eth_address_lower = strtolower(substr($keccak, -40)); + + $hash = Keccak::hash($eth_address_lower, 256); + $eth_address = ''; + for($i = 0; $i < 40; $i++) { + // the nth letter should be uppercase if the nth digit of casemap is 1 + $char = substr($eth_address_lower, $i, 1); + + if(ctype_digit($char)) + $eth_address .= $char; + else if('0' <= $hash[$i] && $hash[$i] <= '7') + $eth_address .= strtolower($char); + else + $eth_address .= strtoupper($char); + } + + return '0x'. $eth_address; + } + /* Derives child keys/addresses for a given key. */ public function derive_keys($key) { @@ -49,16 +92,20 @@ public function derive_keys($key) { $master = HierarchicalKeyFactory::fromExtended($key, $network); - $start = 0; - $end = $params['numderive']; + $start = $params['startderive']; + $end = $params['startderive'] + $params['numderive']; if( $params['includeroot'] ) { - $address = $master->getPublicKey()->getAddress()->getAddress(); + $publicKey = $master->getPublicKey(); + $address = new PayToPubKeyHashAddress($publicKey->getPubKeyHash()); + $address = $address->getAddress(); + $xprv = $master->isPrivate() ? $master->toExtendedKey($network) : null; $wif = $master->isPrivate() ? $master->getPrivateKey()->toWif($network) : null; - $pubkey = $master->getPublicKey()->getHex(); - $pubkeyhash = $master->getPublicKey()->getPubKeyHash()->getHex(); + $pubkey = $publicKey->getHex(); + $pubkeyhash = $publicKey->getPubKeyHash()->getHex(); $xpub = $master->toExtendedPublicKey($network); + $eth_address = $this->getEthereumAddress($publicKey); $addrs[] = array( 'xprv' => $xprv, 'privkey' => $wif, @@ -66,6 +113,7 @@ public function derive_keys($key) { 'pubkeyhash' => $pubkey, 'xpub' => $xpub, 'address' => $address, + 'eth_address' => $eth_address, 'index' => null, 'path' => 'm'); } @@ -82,12 +130,17 @@ public function derive_keys($key) { // fixme: hack for copay/multisig. maybe should use a callback? if(method_exists($key, 'getPublicKey')) { // bip32 path - $address = $key->getPublicKey()->getAddress()->getAddress(); + $publicKey = $key->getPublicKey(); + $address = new PayToPubKeyHashAddress($publicKey->getPubKeyHash()); + $address = $address->getAddress(); + $xprv = $key->isPrivate() ? $key->toExtendedKey($network) : null; $priv_wif = $key->isPrivate() ? $key->getPrivateKey()->toWif($network) : null; - $pubkey = $key->getPublicKey()->getHex(); - $pubkeyhash = $key->getPublicKey()->getPubKeyHash()->getHex(); + $pubkey = $publicKey->getHex(); + $pubkeyhash = $publicKey->getPubKeyHash()->getHex(); $xpub = $key->toExtendedPublicKey($network); + + $eth_address = $this->getEthereumAddress($publicKey); } else { throw new Exception("multisig keys not supported"); @@ -98,6 +151,7 @@ public function derive_keys($key) { 'pubkeyhash' => $pubkeyhash, 'xpub' => $xpub, 'address' => $address, + 'eth_address' => $eth_address, 'index' => $i, 'path' => $path); } @@ -124,7 +178,7 @@ static public function mnemonicToKey($mnemonic, $password=null) { /* Returns all columns available for reports */ static public function all_cols() { - return ['path', 'address', 'xprv', 'xpub', 'privkey', 'pubkey', 'pubkeyhash', 'index']; + return ['path', 'address', 'xprv', 'xpub', 'privkey', 'pubkey', 'pubkeyhash', 'index', 'eth_address']; } /* Returns default reporting columns