diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aac8991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/bin/** +/build/cache/ +/build/docs/ +/build/dist/ +/build/logs/ +/docs/_build/ +/node_modules/ +/vendor/ +/.settings/ +.project +.buildpath +composer.lock +composer.phar +.DS_Store +build/.DS_Store +docs/.DS_Store +examples/.DS_Store \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a35b226 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "bitpay/key-utils", + "description": "BitPay Utils pack for cryptography", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Antonio Buedo", + "email": "sales-engineering@bitpay.com" + } + ], + "autoload": { + "psr-4": { + "": "src/" + } + }, + "require": { + "ext-bcmath": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-json": "*", + "ext-iconv": "*", + "ext-gmp": "*" + } +} diff --git a/examples.php b/examples.php new file mode 100644 index 0000000..d88fd8e --- /dev/null +++ b/examples.php @@ -0,0 +1,87 @@ +load(__DIR__ . '/secure/SecurePathPlusYourClientName.key'); +} catch (Exception $ex) { +// Check if the loaded keys is a valid key + if (!$privateKey->isValid()) { + $privateKey->generate(); + } + +// Encrypt and store it securely. +// This Master password could be one for all keys or a different one for each merchant + $storageEngine->persist($privateKey); +} + +/** + * Generate the public key from the private key every time (no need to store the public key). + **/ +try { + $publicKey = $privateKey->getPublicKey(); +} catch (Exception $ex) { + echo $ex->getMessage(); +} + +/** + * Derive the SIN from the public key. + **/ +$sin = $publicKey->getSin()->__toString(); + +/** + * Use the SIN to request a pairing code and token. + * The pairing code has to be approved in the BitPay Dashboard + * THIS is just a cUrl example, which explains how to use the key pair for signing requests + **/ +$resourceUrl = 'https://test.bitpay.com/tokens'; + +$facade = 'merchant'; + +$postData = json_encode([ + 'id' => $sin, + 'facade' => $facade +]); + +$curlCli = curl_init($resourceUrl); + +curl_setopt($curlCli, CURLOPT_HTTPHEADER, [ + 'x-accept-version: 2.0.0', + 'Content-Type: application/json', + 'x-identity' => $publicKey->__toString(), + 'x-signature' => $privateKey->sign($resourceUrl . $postData), +]); + +curl_setopt($curlCli, CURLOPT_CUSTOMREQUEST, 'POST'); +curl_setopt($curlCli, CURLOPT_POSTFIELDS, stripslashes($postData)); +curl_setopt($curlCli, CURLOPT_RETURNTRANSFER, true); + +$result = curl_exec($curlCli); +$resultData = json_decode($result, TRUE); +curl_close($curlCli); + +if (array_key_exists('error', $resultData)) { + echo $resultData['error']; + exit; +} + +/** + * Example of a pairing Code returned from the BitPay API + * which needs to be APPROVED on the BitPay Dashboard before being able to use it. + **/ +echo $resultData['data'][0]['pairingCode']; + +/** End of request **/ \ No newline at end of file diff --git a/src/BitPayKeyUtils/KeyHelper/Key.php b/src/BitPayKeyUtils/KeyHelper/Key.php new file mode 100644 index 0000000..b0ec71e --- /dev/null +++ b/src/BitPayKeyUtils/KeyHelper/Key.php @@ -0,0 +1,111 @@ +id = $id; + } + + /** + * Returns a new instance of self. + * + * @param string $id + * @return KeyInterface + */ + public static function create($id = null) + { + $class = get_called_class(); + + return new $class($id); + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getHex() + { + return $this->hex; + } + + /** + * @return string + */ + public function getDec() + { + return $this->dec; + } + + /** + * @inheritdoc + */ + public function serialize() + { + return serialize( + array( + $this->id, + $this->x, + $this->y, + $this->hex, + $this->dec, + ) + ); + } + + /** + * @inheritdoc + */ + public function unserialize($data) + { + list( + $this->id, + $this->x, + $this->y, + $this->hex, + $this->dec + ) = unserialize($data); + } + + /** + * @return boolean + */ + public function isGenerated() + { + return (!empty($this->hex)); + } +} diff --git a/src/BitPayKeyUtils/KeyHelper/KeyInterface.php b/src/BitPayKeyUtils/KeyHelper/KeyInterface.php new file mode 100644 index 0000000..64b790a --- /dev/null +++ b/src/BitPayKeyUtils/KeyHelper/KeyInterface.php @@ -0,0 +1,21 @@ +hex; + } + + /* + * Use this method if you have a hex-encoded private key + * and you want to initialize your private key. + * If you have a private key, you can derive your public key + * and also your sin. + * @param string + */ + public function setHex($hex) + { + $this->hex = $hex; + try { + $this->dec = Util::decodeHex($this->hex); + } catch (Exception $e) { + } + } + + /** + * @return PublicKey + * @throws Exception + */ + public function getPublicKey() + { + if (null === $this->publicKey) { + $this->publicKey = new PublicKey(); + $this->publicKey->setPrivateKey($this); + $this->publicKey->generate(); + } + + return $this->publicKey; + } + + /** + * Generates an EC private key + * + * @return PrivateKey + * @throws Exception + */ + public function generate() + { + if (!empty($this->hex)) { + return $this; + } + + do { + $privateKey = SecureRandom::generateRandom(32); + $this->hex = strtolower(bin2hex($privateKey)); + } while (Math::cmp('0x' . $this->hex, '1') <= 0 || Math::cmp('0x' . $this->hex, '0x' . Secp256k1::N) >= 0); + + $this->dec = Util::decodeHex($this->hex); + + return $this; + } + + /** + * Checks to see if the private key value is not empty and + * the hex form only contains hexits and the decimal form + * only contains devimal digits. + * + * @return boolean + */ + public function isValid() + { + return ($this->hasValidDec() && $this->hasValidHex()); + } + + /** + * @return boolean + */ + public function hasValidDec() + { + return (!empty($this->dec) || ctype_digit($this->dec)); + } + + /** + * @return boolean + */ + public function hasValidHex() + { + return (!empty($this->hex) || ctype_xdigit($this->hex)); + } + + /** + * Creates an ECDSA signature of $message + * + * @param $data + * @return string + * @throws Exception + */ + public function sign($data) + { + if (!ctype_xdigit($this->hex)) { + throw new Exception('The private key must be in hex format.'); + } + + if (empty($data)) { + throw new Exception('You did not provide any data to sign.'); + } + + $e = Util::decodeHex(hash('sha256', $data)); + + do { + if (substr(strtolower($this->hex), 0, 2) != '0x') { + $d = '0x' . $this->hex; + } else { + $d = $this->hex; + } + + $k = SecureRandom::generateRandom(32); + + $k_hex = '0x' . strtolower(bin2hex($k)); + $n_hex = '0x' . Secp256k1::N; + + + $Gx = '0x' . substr(Secp256k1::G, 2, 64); + $Gy = '0x' . substr(Secp256k1::G, 66, 64); + + $P = new Point($Gx, $Gy); + + // Calculate a new curve point from Q=k*G (x1,y1) + $R = Util::doubleAndAdd($k_hex, $P); + + $Rx_hex = Util::encodeHex($R->getX()); + + $Rx_hex = str_pad($Rx_hex, 64, '0', STR_PAD_LEFT); + + // r = x1 mod n + $r = Math::mod('0x' . $Rx_hex, $n_hex); + + // s = k^-1 * (e+d*r) mod n + $edr = Math::add($e, Math::mul($d, $r)); + $invk = Math::invertm($k_hex, $n_hex); + $kedr = Math::mul($invk, $edr); + + $s = Math::mod($kedr, $n_hex); + + // The signature is the pair (r,s) + $signature = array( + 'r' => Util::encodeHex($r), + 's' => Util::encodeHex($s), + ); + + $signature['r'] = str_pad($signature['r'], 64, '0', STR_PAD_LEFT); + $signature['s'] = str_pad($signature['s'], 64, '0', STR_PAD_LEFT); + } while (Math::cmp($r, '0') <= 0 || Math::cmp($s, '0') <= 0); + + $sig = array( + 'sig_rs' => $signature, + 'sig_hex' => self::serializeSig($signature['r'], $signature['s']), + ); + + return $sig['sig_hex']['seq']; + } + + /** + * ASN.1 DER encodes the signature based on the form: + * 0x30 + size(all) + 0x02 + size(r) + r + 0x02 + size(s) + s + * http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + * + * @param $r + * @param $s + * @return array + * @throws Exception + */ + private static function serializeSig($r, $s) + { + $dec = ''; + $byte = ''; + $seq = ''; + + $digits = array(); + $retval = array(); + + for ($x = 0; $x < 256; $x++) { + $digits[$x] = chr($x); + } + + $dec = Util::decodeHex($r); + + while (Math::cmp($dec, '0') > 0) { + $dv = Math::div($dec, '256'); + $rem = Math::mod($dec, '256'); + $dec = $dv; + $byte = $byte . $digits[$rem]; + } + + $byte = strrev($byte); + + // msb check + if (Math::cmp('0x' . bin2hex($byte[0]), '0' . 'x80') >= 0) { + $byte = chr(0x00) . $byte; + } + + $retval['bin_r'] = bin2hex($byte); + $seq = chr(0x02) . chr(strlen($byte)) . $byte; + $dec = Util::decodeHex($s); + + $byte = ''; + + while (Math::cmp($dec, '0') > 0) { + $dv = Math::div($dec, '256'); + $rem = Math::mod($dec, '256'); + $dec = $dv; + $byte = $byte . $digits[$rem]; + } + + $byte = strrev($byte); + + // msb check + if (Math::cmp('0x' . bin2hex($byte[0]), '0' . 'x80') >= 0) { + $byte = chr(0x00) . $byte; + } + + $retval['bin_s'] = bin2hex($byte); + $seq = $seq . chr(0x02) . chr(strlen($byte)) . $byte; + $seq = chr(0x30) . chr(strlen($seq)) . $seq; + $retval['seq'] = bin2hex($seq); + + return $retval; + } + + /** + * Decodes PEM data to retrieve the keypair. + * + * @param string $pem_data The data to decode. + * @return array The keypair info. + * @throws Exception + */ + public function pemDecode($pem_data) + { + $beg_ec_text = '-----BEGIN EC PRIVATE KEY-----'; + $end_ec_text = '-----END EC PRIVATE KEY-----'; + + $decoded = ''; + + $ecpemstruct = array(); + + $pem_data = str_ireplace($beg_ec_text, '', $pem_data); + $pem_data = str_ireplace($end_ec_text, '', $pem_data); + $pem_data = str_ireplace("\r", '', trim($pem_data)); + $pem_data = str_ireplace("\n", '', trim($pem_data)); + $pem_data = str_ireplace(' ', '', trim($pem_data)); + + $decoded = bin2hex(base64_decode($pem_data)); + + if (strlen($decoded) < 230) { + throw new Exception('Invalid or corrupt secp256k1 key provided. Cannot decode the supplied PEM data.'); + } + + $ecpemstruct = array( + 'oct_sec_val' => substr($decoded, 14, 64), + 'obj_id_val' => substr($decoded, 86, 10), + 'bit_str_val' => substr($decoded, 106), + ); + + if ($ecpemstruct['obj_id_val'] != '2b8104000a') { + throw new Exception('Invalid or corrupt secp256k1 key provided. Cannot decode the supplied PEM data.'); + } + + $private_key = $ecpemstruct['oct_sec_val']; + $public_key = $ecpemstruct['bit_str_val']; + + if (strlen($private_key) < 64 || strlen($public_key) < 128) { + throw new Exception('Invalid or corrupt secp256k1 key provided. Cannot decode the supplied PEM data.'); + } + + $this->pemDecoded = array('private_key' => $private_key, 'public_key' => $public_key); + + return $this->pemDecoded; + } + + /** + * Encodes keypair data to PEM format. + * + * @param array $keypair The keypair info. + * @return string The data to decode. + * @throws Exception + */ + public function pemEncode($keypair) + { + if (is_array($keypair) && (strlen($keypair[0]) < 64 || strlen($keypair[1]) < 128)) { + throw new Exception('Invalid or corrupt secp256k1 keypair provided. Cannot decode the supplied PEM data.'); + } + + $dec = ''; + $byte = ''; + $beg_ec_text = ''; + $end_ec_text = ''; + $ecpemstruct = array(); + $digits = array(); + + for ($x = 0; $x < 256; $x++) { + $digits[$x] = chr($x); + } + + $ecpemstruct = array( + 'sequence_beg' => '30', + 'total_len' => '74', + 'int_sec_beg' => '02', + 'int_sec_len' => '01', + 'int_sec_val' => '01', + 'oct_sec_beg' => '04', + 'oct_sec_len' => '20', + 'oct_sec_val' => $keypair[0], + 'a0_ele_beg' => 'a0', + 'a0_ele_len' => '07', + 'obj_id_beg' => '06', + 'obj_id_len' => '05', + 'obj_id_val' => '2b8104000a', + 'a1_ele_beg' => 'a1', + 'a1_ele_len' => '44', + 'bit_str_beg' => '03', + 'bit_str_len' => '42', + 'bit_str_val' => '00' . $keypair[1], + ); + + $beg_ec_text = '-----BEGIN EC PRIVATE KEY-----'; + $end_ec_text = '-----END EC PRIVATE KEY-----'; + + $dec = trim(implode($ecpemstruct)); + + if (strlen($dec) < 230) { + throw new Exception('Invalid or corrupt secp256k1 keypair provided. Cannot encode the supplied data.'); + } + + $dec = Util::decodeHex('0x' . $dec); + + while (Math::cmp($dec, '0') > 0) { + $dv = Math::div($dec, '256'); + $rem = Math::mod($dec, '256'); + $dec = $dv; + $byte = $byte . $digits[$rem]; + } + + $byte = $beg_ec_text . "\r\n" . chunk_split(base64_encode(strrev($byte)), 64) . $end_ec_text; + + $this->pemEncoded = $byte; + + return $byte; + } +} diff --git a/src/BitPayKeyUtils/KeyHelper/PublicKey.php b/src/BitPayKeyUtils/KeyHelper/PublicKey.php new file mode 100644 index 0000000..1818dc6 --- /dev/null +++ b/src/BitPayKeyUtils/KeyHelper/PublicKey.php @@ -0,0 +1,150 @@ +setPrivateKey($private); + + return $public; + } + + /** + * @param PrivateKey $privateKey + * @return KeyInterface + */ + public function setPrivateKey(PrivateKey $privateKey) + { + $this->privateKey = $privateKey; + + return $this; + } + + /** + * Returns the compressed public key value + * + * @return string + */ + public function __toString() + { + if (is_null($this->x)) { + return ''; + } + + if (Math::mod('0x' . $this->y, '0' . 'x02') == '1') { + return sprintf('03%s', $this->x); + } else { + return sprintf('02%s', $this->x); + } + } + + /** + * Checks to see if the public key is not blank and contains + * valid decimal and hex valules for this->hex & this->dec + * + * @return boolean + */ + public function isValid() + { + return ((!empty($this->hex) && ctype_xdigit($this->hex)) && (!empty($this->dec) && ctype_digit($this->dec))); + } + + /** + * @return SinKey + * @throws Exception + */ + public function getSin() + { + if (empty($this->hex)) { + $this->generate(); + } + + if (null === $this->sin) { + $this->sin = new SinKey(); + $this->sin->setPublicKey($this); + $this->sin->generate(); + } + + return $this->sin; + } + + /** + * Generates an uncompressed and compressed EC public key. + * + * @param PrivateKey $privateKey + * + * @return PublicKey + * @throws Exception + */ + public function generate(PrivateKey $privateKey = null) + { + if ($privateKey instanceof PrivateKey) { + $this->setPrivateKey($privateKey); + } + + if (!empty($this->hex)) { + return $this; + } + + if (is_null($this->privateKey)) { + throw new Exception('Please `setPrivateKey` before you generate a public key'); + } + + if (!$this->privateKey->isGenerated()) { + $this->privateKey->generate(); + } + + if (!$this->privateKey->isValid()) { + throw new Exception('Private Key is invalid and cannot be used to generate a public key'); + } + + $point = new Point( + '0x' . substr(Secp256k1::G, 2, 64), + '0x' . substr(Secp256k1::G, 66, 64) + ); + + $R = Util::doubleAndAdd( + '0x' . $this->privateKey->getHex(), + $point + ); + + $RxHex = Util::encodeHex($R->getX()); + $RyHex = Util::encodeHex($R->getY()); + + $RxHex = str_pad($RxHex, 64, '0', STR_PAD_LEFT); + $RyHex = str_pad($RyHex, 64, '0', STR_PAD_LEFT); + + $this->x = $RxHex; + $this->y = $RyHex; + $this->hex = sprintf('%s%s', $RxHex, $RyHex); + $this->dec = Util::decodeHex($this->hex); + + return $this; + } +} diff --git a/src/BitPayKeyUtils/KeyHelper/SinKey.php b/src/BitPayKeyUtils/KeyHelper/SinKey.php new file mode 100644 index 0000000..da5fb6c --- /dev/null +++ b/src/BitPayKeyUtils/KeyHelper/SinKey.php @@ -0,0 +1,103 @@ +value; + } + + /** + * @param PublicKey + * @return SinKey + */ + public function setPublicKey(PublicKey $publicKey) + { + $this->publicKey = $publicKey; + + return $this; + } + + /** + * Generates a Service Identification Number (SIN), see: + * https://en.bitcoin.it/wiki/Identity_protocol_v1 + * + * @return SinKey + * @throws Exception + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function generate() + { + if (is_null($this->publicKey)) { + throw new Exception('Public Key has not been set'); + } + + $compressedValue = $this->publicKey; + + if (empty($compressedValue)) { + throw new Exception('The Public Key needs to be generated.'); + } + + $step1 = Util::sha256(Util::binConv($compressedValue), true); + + $step2 = Util::ripe160($step1); + + $step3 = sprintf( + '%s%s%s', + self::SIN_VERSION, + self::SIN_TYPE, + $step2 + ); + + $step4 = Util::twoSha256(Util::binConv($step3), true); + + $step5 = substr(bin2hex($step4), 0, 8); + + $step6 = $step3 . $step5; + + $this->value = Base58::encode($step6); + + return $this; + } + + /** + * Checks to make sure that this SIN is a valid object. + * + * @return boolean + */ + public function isValid() + { + return (!is_null($this->value) && (substr($this->value, 0, 1) == 'T')); + } +} diff --git a/src/BitPayKeyUtils/Math/BcEngine.php b/src/BitPayKeyUtils/Math/BcEngine.php new file mode 100644 index 0000000..f004ac1 --- /dev/null +++ b/src/BitPayKeyUtils/Math/BcEngine.php @@ -0,0 +1,242 @@ +input($a); + $b = $this->input($b); + + return bcadd($a, $b); + } + + public function input($x) + { + if (empty($x)) { + return '0'; + } + $x = strtolower(trim($x)); + if (preg_match('/^(-?)0x([0-9a-f]+)$/', $x, $matches)) { + $sign = $matches[1]; + $hex = $matches[2]; + + for ($dec = '0', $i = 0; $i < strlen($hex); $i++) { + $current = strpos('0123456789abcdef', $hex[$i]); + $dec = bcadd(bcmul($dec, 16), $current); + } + + return $sign . $dec; + + } elseif (preg_match('/^-?[0-9]+$/', $x)) { + return $x; + } else { + throw new Exception("The input must be a numeric string in decimal or hexadecimal (with leading 0x) format.\n" . var_export($x, true)); + } + + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return int + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function cmp($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + return bccomp($a, $b); + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return string|null + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function div($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + return bcdiv($a, $b); + } + + /** + * Finds inverse number $inv for $num by modulus $mod, such as: + * $inv * $num = 1 (mod $mod) + * + * @param string $num + * @param string $mod + * @return string + * @access public + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function invertm($num, $mod) + { + $num = $this->input($num); + $mod = $this->input($mod); + + $x = '1'; + $y = '0'; + $num1 = $mod; + + do { + $tmp = bcmod($num, $num1); + + $q = bcdiv($num, $num1); + + $num = $num1; + + $num1 = $tmp; + + $tmp = bcsub($x, bcmul($y, $q)); + + $x = $y; + + $y = $tmp; + + } while (bccomp($num1, '0')); + + if (bccomp($x, '0') < 0) { + $x = bcadd($x, $mod); + } + + if (substr($num, 0, 1) === '-') { + $x = bcsub($mod, $x); + } + + return $x; + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return string|null + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function mod($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + if (substr($a, 0, 1) === '-') { + return bcadd(bcmod($a, $b), $b); + } + + return bcmod($a, $b); + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return string + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function mul($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + return bcmul($a, $b); + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return string + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function pow($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + return bcpow($a, $b); + } + + /** + * @param String $a Numeric String + * @param String $b Numeric String + * @return string + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function sub($a, $b) + { + $a = $this->input($a); + $b = $this->input($b); + + return bcsub($a, $b); + } + + /** + * Function to determine if two numbers are + * co-prime according to the Euclidean algo. + * + * @param string $a First param to check. + * @param string $b Second param to check. + * @return bool Whether the params are cp. + */ + public function coprime($a, $b) + { + $small = 0; + $diff = 0; + while (bccomp($a, '0') > 0 && bccomp($b, '0') > 0) { + if (bccomp($a, $b) == -1) { + $small = $a; + $diff = bcmod($b, $a); + } + if (bccomp($a, $b) == 1) { + $small = $b; + $diff = bcmod($a, $b); + } + if (bccomp($a, $b) == 0) { + $small = $a; + $diff = bcmod($b, $a); + } + $a = $small; + $b = $diff; + } + if (bccomp($a, '1') == 0) { + return true; + } + + return false; + } +} diff --git a/src/BitPayKeyUtils/Math/EngineInterface.php b/src/BitPayKeyUtils/Math/EngineInterface.php new file mode 100644 index 0000000..3416682 --- /dev/null +++ b/src/BitPayKeyUtils/Math/EngineInterface.php @@ -0,0 +1,57 @@ +password = base64_encode($password); + $this->unencoded_password = $password; + } + + /** + * @inheritdoc + */ + public function persist(KeyInterface $key) + { + $path = $key->getId(); + $data = serialize($key); + $encoded = bin2hex(openssl_encrypt( + $data, + self::METHOD, + $this->password, + 1, + self::IV + )); + + file_put_contents($path, $encoded); + } + + /** + * @inheritdoc + * @throws Exception + * @throws Exception + * @throws Exception + */ + public function load($id) + { + if (!is_file($id)) { + throw new Exception(sprintf('Could not find "%s"', $id)); + } + + if (!is_readable($id)) { + throw new Exception(sprintf('"%s" cannot be read, check permissions', $id)); + } + + $encoded = file_get_contents($id); + $decoded = openssl_decrypt(Util::binConv($encoded), self::METHOD, $this->password, 1, self::IV); + + if (false === $decoded) { + $decoded = openssl_decrypt(Util::binConv($encoded), self::METHOD, $this->unencoded_password, 1, self::IV); + } + + if (false === $decoded) { + throw new Exception('Could not decode key'); + } + + return unserialize($decoded); + } +} diff --git a/src/BitPayKeyUtils/Storage/FilesystemStorage.php b/src/BitPayKeyUtils/Storage/FilesystemStorage.php new file mode 100644 index 0000000..61acd5b --- /dev/null +++ b/src/BitPayKeyUtils/Storage/FilesystemStorage.php @@ -0,0 +1,39 @@ +getId(); + file_put_contents($path, serialize($key)); + } + + /** + * @inheritdoc + * @throws Exception + * @throws Exception + */ + public function load($id) + { + if (!is_file($id)) { + throw new Exception(sprintf('Could not find "%s"', $id)); + } + + if (!is_readable($id)) { + throw new Exception(sprintf('"%s" cannot be read, check permissions', $id)); + } + + return unserialize(file_get_contents($id)); + } +} diff --git a/src/BitPayKeyUtils/Storage/StorageInterface.php b/src/BitPayKeyUtils/Storage/StorageInterface.php new file mode 100644 index 0000000..a5df5f1 --- /dev/null +++ b/src/BitPayKeyUtils/Storage/StorageInterface.php @@ -0,0 +1,23 @@ + 0) { + $q = Math::div($x, 58); + $r = Math::mod($x, 58); + $output_string .= substr($code_string, intval($r), 1); + $x = $q; + } + + for ($i = 0; $i < $dataLen && substr($data, $i, 2) == '00'; $i += 2) { + $output_string .= substr($code_string, 0, 1); + } + + $output_string = strrev($output_string); + + return $output_string; + } + + /** + * Decodes $data from BASE-58 format + * + * @param string $data + * + * @return string + * @throws Exception + * @throws Exception + */ + public static function decode($data) + { + $dataLen = strlen($data); + + for ($return = '0', $i = 0; $i < $dataLen; $i++) { + $current = strpos(self::BASE58_CHARS, $data[$i]); + $return = Math::mul($return, '58'); + $return = Math::add($return, $current); + } + + $return = Util::encodeHex($return); + + for ($i = 0; $i < $dataLen && substr($data, $i, 1) == '1'; $i++) { + $return = '00' . $return; + } + + if (strlen($return) % 2 != 0) { + $return = '0' . $return; + } + + return $return; + } +} diff --git a/src/BitPayKeyUtils/Util/CurveParameterInterface.php b/src/BitPayKeyUtils/Util/CurveParameterInterface.php new file mode 100644 index 0000000..0fd15e9 --- /dev/null +++ b/src/BitPayKeyUtils/Util/CurveParameterInterface.php @@ -0,0 +1,24 @@ += 4.3.0, PHP 5) + * + * @param bool + * @param bool + * @param int + * @return array|void + */ + final public function backtrace($print = false, $options = false, $limit = 0) + { + if ($print == true) { + return debug_print_backtrace($options, $limit); + } else { + return debug_backtrace($options, $limit); + } + } + + /** + * Get the last occurred error and returns an associative + * array describing the last error with keys "type", "message", + * "file" and "line". If the error has been caused by a PHP + * internal function then the "message" begins with its name. + * Returns NULL if there hasn't been an error yet. + * (PHP 5 >= 5.2.0) + * + * @param void + * @return array + */ + final public function last() + { + return error_get_last(); + } + + /** + * Send an error message to the defined error handling + * routines. Returns true on success or false on failure. + * The possible values for $message_type are: 0 = system log, + * 1 = email to $destination, 2 = depricated, 3 = appended + * to file $destination, 4 = sent to SAPI log handler. + * (PHP 4, PHP 5) + * + * @param string + * @param int + * @param string + * @param string + * @return bool + */ + final public function log($message, $message_type = 0, $destination = '', $extra_headers = '') + { + return error_log((string)$message, $message_type = 0, $destination = '', $extra_headers = ''); + } + + /** + * Sets which PHP errors are reported or returns the old + * error_reporting level or the current level if no level + * parameter is given. + * (PHP 4, PHP 5) + * + * @param bool + * @return int + */ + final public function reporting($level = false) + { + if ($level !== false) { + return error_reporting($level); + } else { + return error_reporting(); + } + } + + /** + * Sets or restores either the error or exception handler + * based on the $type and $action parameters. + * (PHP 4 >= 4.0.1, PHP 5) + * + * @param string + * @param string + * @param mixed + * @param int + * return mixed + * @return bool|callable|mixed|null + */ + final public function handler($type = 'error', $action = 'restore', $callable_handler = false, $error_types = null) + { + if (empty($error_types)) { + $error_types = E_ALL | E_STRICT; + } + switch (strtolower($type)) { + case 'error': + switch (strtolower($action)) { + case 'restore': + return restore_error_handler(); + break; + case 'set': + return set_error_handler($callable_handler, $error_types); + break; + default: + return false; + } + break; + case 'exception': + switch (strtolower($action)) { + case 'restore': + return restore_exception_handler(); + break; + case 'set': + return set_exception_handler($callable_handler); + break; + default: + return false; + } + break; + default: + return false; + } + } + + /** + * Generates a user-level error/warning/notice message. + * This function returns FALSE if wrong $error_type is + * specified, TRUE otherwise. The $error_msg param is + * limited to 1024 bytes. + * (PHP 4 >= 4.0.1, PHP 5) + * + * @param string + * @param int + * @return bool + */ + final public function raise($error_msg, $error_type = E_USER_NOTICE) + { + return trigger_error($error_msg, $error_type); + } +} diff --git a/src/BitPayKeyUtils/Util/Fingerprint.php b/src/BitPayKeyUtils/Util/Fingerprint.php new file mode 100644 index 0000000..9741741 --- /dev/null +++ b/src/BitPayKeyUtils/Util/Fingerprint.php @@ -0,0 +1,55 @@ + $v) { + if (in_array(strtolower($k), $serverVariables)) { + self::$sigData[] = $v; + } + } + + self::$sigData[] = phpversion(); + self::$sigData[] = get_current_user(); + self::$sigData[] = php_uname('s') . php_uname('n') . php_uname('m') . PHP_OS . PHP_SAPI . ICONV_IMPL . ICONV_VERSION; + self::$sigData[] = sha1_file(__FILE__); + + self::$finHash = implode(self::$sigData); + self::$finHash = sha1(str_ireplace(' ', '', self::$finHash) . strlen(self::$finHash) . metaphone(self::$finHash)); + self::$finHash = sha1(self::$finHash); + + return self::$finHash; + } +} diff --git a/src/BitPayKeyUtils/Util/Point.php b/src/BitPayKeyUtils/Util/Point.php new file mode 100644 index 0000000..568af52 --- /dev/null +++ b/src/BitPayKeyUtils/Util/Point.php @@ -0,0 +1,90 @@ +x = (string)$x; + $this->y = (string)$y; + } + + /** + * @return string + */ + public function __toString() + { + if ($this->isInfinity()) { + return self::INFINITY; + } + + return sprintf('(%s, %s)', $this->x, $this->y); + } + + /** + * @return boolean + */ + public function isInfinity() + { + return (self::INFINITY == $this->x || self::INFINITY == $this->y); + } + + /** + * @return string + */ + public function getX() + { + return $this->x; + } + + /** + * @return string + */ + public function getY() + { + return $this->y; + } + + /** + * @inheritdoc + */ + public function serialize() + { + return serialize(array($this->x, $this->y)); + } + + /** + * @inheritdoc + */ + public function unserialize($data) + { + list( + $this->x, + $this->y + ) = unserialize($data); + } +} diff --git a/src/BitPayKeyUtils/Util/PointInterface.php b/src/BitPayKeyUtils/Util/PointInterface.php new file mode 100644 index 0000000..ca23bd5 --- /dev/null +++ b/src/BitPayKeyUtils/Util/PointInterface.php @@ -0,0 +1,33 @@ + 0) { + $q = Math::div($dec, 16); + $rem = Math::mod($dec, 16); + $dec = $q; + + $hex = substr(self::HEX_CHARS, intval($rem), 1) . $hex; + } + + return $hex; + } + + public static function doubleAndAdd($hex, PointInterface $point, CurveParameterInterface $parameters = null) + { + if (null === $parameters) { + $parameters = new Secp256k1(); + } + try { + $tmp = self::decToBin($hex); + } catch (Exception $e) { + } + + $n = strlen($tmp) - 1; + $S = new Point(PointInterface::INFINITY, PointInterface::INFINITY); + + + while ($n >= 0) { + try { + $S = self::pointDouble($S); + } catch (Exception $e) { + } + + if ($tmp[$n] == 1) { + try { + $S = self::pointAdd($S, $point); + } catch (Exception $e) { + } + } + $n--; + } + + return new Point($S->getX(), $S->getY()); + } + + /** + * This method returns a binary string representation of + * the decimal number. Used for the doubleAndAdd() method. + * + * @see http://php.net/manual/en/function.decbin.php but for large numbers + * + * @param string + * @return string + * @throws Exception + * @throws Exception + * @throws Exception + */ + public static function decToBin($dec) + { + if (substr(strtolower($dec), 0, 2) == '0x') { + $dec = self::decodeHex(substr($dec, 2)); + } + + $bin = ''; + while (Math::cmp($dec, '0') > 0) { + if (Math::mod($dec, 2) == '1') { + $bin .= '1'; + } else { + $bin .= '0'; + } + $prevDec = $dec; + $dec = Math::div($dec, 2); + //sanity check to avoid infinite loop + if (Math::cmp($prevDec, $dec) < 1) { + throw new Exception('Math library has unexpected behavior, please report the following information to support@bitpay.com. Math Engine is: ' . Math::getEngineName() . '. PHP Version is: ' . phpversion() . '.'); + } + } + + return $bin; + } + + /** + * Decodes a hexadecimal value into decimal. + * + * @param string $hex + * @return string + * @throws Exception + * @throws Exception + */ + public static function decodeHex($hex) + { + if (!is_string($hex) || !ctype_xdigit($hex) && '0x' != substr($hex, 0, 2)) { + throw new Exception('Argument must be a string of hex digits.'); + } + + $hex = strtolower($hex); + + // if it has a prefix of 0x this needs to be trimed + if (substr($hex, 0, 2) == '0x') { + $hex = substr($hex, 2); + } + + $hexLen = strlen($hex); + for ($dec = '0', $i = 0; $i < $hexLen; $i++) { + $current = strpos(self::HEX_CHARS, $hex[$i]); + $dec = Math::add(Math::mul($dec, 16), $current); + } + + return $dec; + } + + /** + * Point multiplication method 2P = R where + * s = (3xP2 + a)/(2yP) mod p + * xR = s2 - 2xP mod p + * yR = -yP + s(xP - xR) mod p + * + * @param PointInterface $point + * @param CurveParameterInterface + * @return Point|PointInterface + * @throws Exception + * @throws Exception + */ + public static function pointDouble(PointInterface $point, CurveParameterInterface $parameters = null) + { + if ($point->isInfinity()) { + return $point; + } + + if (null === $parameters) { + $parameters = new Secp256k1(); + } + + $p = $parameters->pHex(); + $a = $parameters->aHex(); + + $s = 0; + $R = array( + 'x' => 0, + 'y' => 0, + ); + + // Critical math section + try { + $m = Math::add(Math::mul(3, Math::mul($point->getX(), $point->getX())), $a); + $o = Math::mul(2, $point->getY()); + $n = Math::invertm($o, $p); + $n2 = Math::mod($o, $p); + $st = Math::mul($m, $n); + $st2 = Math::mul($m, $n2); + $s = Math::mod($st, $p); + $s2 = Math::mod($st2, $p); + $xmul = Math::mul(2, $point->getX()); + $smul = Math::mul($s, $s); + $xsub = Math::sub($smul, $xmul); + $xmod = Math::mod($xsub, $p); + $R['x'] = $xmod; + $ysub = Math::sub($point->getX(), $R['x']); + $ymul = Math::mul($s, $ysub); + $ysub2 = Math::sub(0, $point->getY()); + $yadd = Math::add($ysub2, $ymul); + + $R['y'] = Math::mod($yadd, $p); + + } catch (Exception $e) { + throw new Exception('Error in Util::pointDouble(): ' . $e->getMessage()); + } + + return new Point($R['x'], $R['y']); + } + + /** + * Point addition method P + Q = R where: + * s = (yP - yQ)/(xP - xQ) mod p + * xR = s2 - xP - xQ mod p + * yR = -yP + s(xP - xR) mod p + * + * @param PointInterface $P + * @param PointInterface $Q + * @return Point|PointInterface + * @throws Exception + */ + public static function pointAdd(PointInterface $P, PointInterface $Q) + { + if ($P->isInfinity()) { + return $Q; + } + + if ($Q->isInfinity()) { + return $P; + } + + if ($P->getX() == $Q->getX() && $P->getY() == $Q->getY()) { + return self::pointDouble(new Point($P->getX(), $P->getY())); + } + + $p = '0x' . Secp256k1::P; + $a = '0x' . Secp256k1::A; + $s = 0; + $R = array( + 'x' => 0, + 'y' => 0, + 's' => 0, + ); + + // Critical math section + try { + $m = Math::sub($P->getY(), $Q->getY()); + $n = Math::sub($P->getX(), $Q->getX()); + $o = Math::invertm($n, $p); + $st = Math::mul($m, $o); + $s = Math::mod($st, $p); + + $R['x'] = Math::mod( + Math::sub( + Math::sub( + Math::mul($s, $s), + $P->getX() + ), + $Q->getX() + ), + $p + ); + $R['y'] = Math::mod( + Math::add( + Math::sub( + 0, + $P->getY() + ), + Math::mul( + $s, + Math::sub( + $P->getX(), + $R['x'] + ) + ) + ), + $p + ); + + $R['s'] = $s; + } catch (Exception $e) { + throw new Exception('Error in Util::pointAdd(): ' . $e->getMessage()); + } + + return new Point($R['x'], $R['y']); + } + + /** + * Converts hex value into octet (byte) string + * + * @param string + * + * @return string + */ + public static function binConv($hex) + { + $rem = ''; + $dv = ''; + $byte = ''; + $digits = array(); + + for ($x = 0; $x < 256; $x++) { + $digits[$x] = chr($x); + } + + if (substr(strtolower($hex), 0, 2) != '0x') { + $hex = '0x' . strtolower($hex); + } + + while (Math::cmp($hex, 0) > 0) { + $dv = Math::div($hex, 256); + $rem = Math::mod($hex, 256); + $hex = $dv; + $byte = $byte . $digits[$rem]; + } + + return strrev($byte); + } + + /** + * Checks dependencies for the library + * + * @return array list of each requirement, boolean true if met, string error message if not as value + */ + public static function checkRequirements() + { + $requirements = array(); + + // PHP Version + if (!defined('PHP_VERSION_ID')) { + $version = explode('.', PHP_VERSION); + define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2])); + } + if (PHP_VERSION_ID < 50400) { + $requirements['PHP'] = 'Your PHP version, ' . PHP_VERSION . ', is too low. PHP version >= 5.4 is required.'; + } else { + $requirements['PHP'] = true; + } + + // OpenSSL Extension + if (!extension_loaded('openssl')) { + $requirements['OpenSSL'] = 'The OpenSSL PHP extension could not be found.'; + } else { + $requirements['OpenSSL'] = true; + } + + // JSON Extension + if (!extension_loaded('json')) { + $requirements['JSON'] = 'The JSON PHP extension could not be found.'; + } else { + $requirements['JSON'] = true; + } + + // cURL Extension + if (!extension_loaded('curl')) { + $requirements['cURL'] = 'The cURL PHP extension could not be found.'; + } else { + $requirements['cURL'] = true; + $curl_version = curl_version(); + $ssl_supported = ($curl_version['features'] & CURL_VERSION_SSL); + if (!$ssl_supported) { + $requirements['cURL.SSL'] = 'The cURL PHP extension does not have SSL support.'; + } else { + $requirements['cURL.SSL'] = true; + } + } + + // Math + if (!extension_loaded('bcmath') && !extension_loaded('gmp')) { + $requirements['Math'] = 'Either the BC Math or GMP PHP extension is required. Neither could be found.'; + } else { + $requirements['Math'] = true; + } + + return $requirements; + } +}