php-bitpay-client/src/Bitpay/Bitauth.php

491 lines
15 KiB
PHP

<?php
/**
* The MIT License (MIT)
*
* Copyright (c) 2014 BitPay, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Bitpay;
use Bitpay\Util\SecureRandom;
use Bitpay\Util\Util;
use Bitpay\Util\Gmp;
use Bitpay\Util\Secp256k1;
/**
* This provides an easy interface for implementing Bitauth
*
* @package Bitauth
*/
class Bitauth
{
/**
* Initialization Vector
*/
const IV = '0000000000000000';
/**
*/
public function __construct()
{
if (!function_exists('openssl_open')) {
// TODO: Throw exception
die('FATAL Error in Bitauth::construct(): Your OpenSSL implementation is either too old or broken. Please contact your server administrator.');
}
if (!function_exists('mcrypt_list_algorithms')) {
// TODO: Throw exception
die('FATAL Error in Bitauth::construct(): Missing mcrypt PHP extension. Cannot continue.');
}
}
/**
* Generate Service Identification Number (SIN)
*
* @param void
* @return array
*/
public function generateSin()
{
$priKey = PrivateKey::create()->generate();
$pubKey = PublicKey::create()->setPrivateKey($priKey)->generate();
$sinKey = SinKey::create()->setPublicKey($pubKey)->generate();
return array(
'public' => $pubKey,
'private' => $priKey,
'sin' => $sinKey,
);
}
/**
* @return PublicKey
*/
public function getPublicKeyFromPrivateKey(\Bitpay\PrivateKey $privateKey)
{
return PublicKey::create()->generate($privateKey);
}
/**
* @return Sin
*/
public function getSinFromPublicKey(\Bitpay\PublicKey $publicKey)
{
// TODO
//return Sin::fromPubKey($publicKey);
}
/**
* Generates an ECDSA signature of $data
*
* @param string $data
* @param PrivateKey $privateKey
* @return string
*/
public function sign($data, \Bitpay\PrivateKey $privateKey)
{
return $privateKey->sign($data);
//if (!ctype_xdigit($privateKey->getHex())) {
// 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($privateKey->getHex()), 0, 2) != '0x') {
// $d = '0x'.$privateKey->getHex();
// } else {
// $d = $privateKey->getHex();
// }
// $k = SecureRandom::generateRandom(32);
// $k_hex = '0x'.strtolower(bin2hex($k));
// $n_hex = '0x'.Secp256k1::N;
// $a_hex = '0x'.Secp256k1::A;
// $p_hex = '0x'.Secp256k1::P;
// $Gx = '0x'.substr(Secp256k1::G, 0, 62);
// $Gy = '0x'.substr(Secp256k1::G, 64, 62);
// $P = new Point($Gx, $Gy);
// // Calculate a new curve point from Q=k*G (x1,y1)
// $R = Gmp::doubleAndAdd($k_hex, $P);
// $Rx_hex = Util::encodeHex($R->getX());
// $Ry_hex = Util::encodeHex($R->getY());
// while (strlen($Rx_hex) < 64) {
// $Rx_hex = '0'.$Rx_hex;
// }
// while (strlen($Ry_hex) < 64) {
// $Ry_hex = '0'.$Ry_hex;
// }
// // r = x1 mod n
// $r = gmp_strval(gmp_mod('0x'.$Rx_hex, $n_hex));
// // s = k^-1 * (e+d*r) mod n
// $edr = gmp_add($e, gmp_mul($d, $r));
// $invk = gmp_invert($k_hex, $n_hex);
// $kedr = gmp_mul($invk, $edr);
// $s = gmp_strval(gmp_mod($kedr, $n_hex));
// // The signature is the pair (r,s)
// $signature = array(
// 'r' => Util::encodeHex($r),
// 's' => Util::encodeHex($s),
// );
// while (strlen($signature['r']) < 64) {
// $signature['r'] = '0'.$signature['r'];
// }
// while (strlen($signature['s']) < 64) {
// $signature['s'] = '0'.$signature['s'];
// }
//} while (gmp_cmp($r, '0') <= 0 || gmp_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 string
* @param string
* @return string
*/
public static function serializeSig($r, $s)
{
for ($x = 0; $x < 256; $x++) {
$digits[$x] = chr($x);
}
$dec = Util::decodeHex($r);
$byte = '';
$seq = '';
$retval = array();
while (gmp_cmp($dec, '0') > 0) {
$dv = gmp_div($dec, '256');
$rem = gmp_strval(gmp_mod($dec, '256'));
$dec = $dv;
$byte = $byte.$digits[$rem];
}
$byte = strrev($byte);
// msb check
if (gmp_cmp('0x'.bin2hex($byte[0]), '0x80') >= 0) {
$byte = chr(0x00).$byte;
}
$retval['bin_r'] = bin2hex($byte);
$seq = chr(0x02).chr(strlen($byte)).$byte;
$dec = Util::decodeHex($s);
$byte = '';
while (gmp_cmp($dec, '0') > 0) {
$dv = gmp_div($dec, '256');
$rem = gmp_strval(gmp_mod($dec, '256'));
$dec = $dv;
$byte = $byte.$digits[$rem];
}
$byte = strrev($byte);
// msb check
if (gmp_cmp('0x'.bin2hex($byte[0]), '0x80') >= 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;
}
/**
* Verifies a previously generated ECDSA signature
*
* @param string
* @param string
* @param array
* @param array
* @param string
* @return boolean
*/
public function verifySignature($contract, $publicKey, $signature, $Q, $data)
{
$Gx = '0x'.substr(Secp256k1::G, 0, 62);
$Gy = '0x'.substr(Secp256k1::G, 64, 62);
$r = '0x'.$signature['r'];
$s = '0x'.$signature['s'];
$n_hex = '0x'.Secp256k1::N;
$a_hex = '0x'.Secp256k1::A;
$p_hex = '0x'.Secp256k1::P;
// check to see if r,s are in [1,n-1]
if (gmp_cmp($r, 1) <= 0 && gmp_cmp($r, $n_hex) > 0) {
die('FATAL Error in Bitauth::verifySignature(): r is out of range!');
}
if (gmp_cmp($s, 1) <= 0 && gmp_cmp($s, $n_hex) > 0) {
die('FATAL Error in Bitauth::verifySignature():s is out of range!');
}
// convert the hash of the hex message to decimal
$e = Util::decodeHex(hash('sha256', $data));
// calculate w = s^-1 (mod n)
$w = gmp_invert($s, $n_hex);
// calculate u1 = e*w (mod n)
$u1 = gmp_mod(gmp_mul($e, $w), $n_hex);
// calculate u2 = r*w (mod n)
$u2 = gmp_mod(gmp_mul($r, $w), $n_hex);
$P = array('x' => $Gx, 'y' => $Gy);
$Qx = '0x'.substr($Q, 2, 64);
$Qy = '0x'.substr($Q, 66, 64);
$Q = array('x' => $Qx, 'y' => $Qy);
// Get new point Z(x1,y1) = (u1 * G) + (u2 * Q)
$Za = Gmp::doubleAndAdd($u1, $P, $p_hex, $a_hex);
$Zb = Gmp::doubleAndAdd($u2, $Q, $p_hex, $a_hex);
$Z = Gmp::gmpPointAdd($Za, $Zb, $p_hex, $a_hex);
$Zx_hex = Util::encodeHex($Z['x']);
$Zy_hex = Util::encodeHex($Z['y']);
while (strlen($Zx_hex) < 64) {
$Zx_hex = '0'.$Zx_hex;
}
while (strlen($Zy_hex) < 64) {
$Zy_hex = '0'.$Zy_hex;
}
// Signature is valid if r is congruent to x1 (mod n)
// or in other words, if r - x1 is an integer multiple of n
$rsubx = gmp_sub($r, '0x'.$Zx_hex);
$rsubx_rem = gmp_div_r($rsubx, $n_hex);
if (gmp_cmp($rsubx_rem, '0') == 0) {
return true;
} else {
return false;
}
}
/**
* Determines if a SIN is valid or not, depending on
* rules defined in spec, see:
* https://en.bitcoin.it/wiki/Identity_protocol_v1
*
* @param string
* @return boolean
*/
public function validateSin($sin)
{
return (!empty($sin) && (substr($sin, 0, 1) == 'T'));
}
/**
* Returns the inititialization size used for a particular cypher
* type. Returns an integer IV size on success or boolean false
* on failure. If no IV is needed for the cypher type and mode,
* a zero is returned.
*
* @param string $cypher_type
* @return int|bool
*/
public function getIVSize($cypher_type = 'MCRYPT_TRIPLEDES')
{
$block_mode = 'cbc';
return mcrypt_get_iv_size($cypher_type, $block_mode);
}
/**
* Returns the maximum key size that can be used with a particular
* cypher type. Any key size equal to or less than the returned
* value are legal key sizes. Depending on if the local mycrypt
* extension is linked against 2.2 or 2.3/2.4 the block mode could
* be required, hence the if/else statement.
*
* @param string $cypher_type
* @return int
*/
public function getKeySize($cypher_type = 'MCRYPT_TRIPLEDES')
{
$block_mode = 'cbc';
$max_key_size = mcrypt_get_key_size($cipher_type);
if ($max_key_size !== false) {
return $max_key_size;
} else {
return mcrypt_get_key_size($cipher_type, $block_mode);
}
}
/**
* Returns a list of all supported mcrypt algorithms on the local system.
*
* @param none
* @return array
*/
public function getAlgos()
{
return mcrypt_list_algorithms();
}
/**
* Performs an internal self-test on the specified mcrypt algorithm and
* returns either boolean true/false depending on if the self-test passed
* or failed.
*
* @param string $cypher_type
* @return boolean
*/
public function algoSelfTest($cypher_type = 'MCRYPT_TRIPLEDES')
{
return mcrypt_module_self_test($cypher_type);
}
/**
*
* Encrypts $text based on your $key and $iv. The returned text is
* base-64 encoded to make it easier to work with in various scenarios.
* Default cypher is MCRYPT_TRIPLEDES but you can substitute depending
* on your specific encryption needs.
*
* @param string $text
* @param string $key
* @param string $iv
* @param int $bit_check
* @param string $cypher_type
* @return string $text
* @throws Exception $e
*
*/
public function encrypt($text, $key = '', $iv = '', $bit_check = 8, $cypher_type = 'MCRYPT_TRIPLEDES')
{
try {
/* Ensure the key & IV is the same for both encrypt & decrypt. */
if (!empty($text) && is_string($text)) {
$text_num = str_split($text, $bit_check);
$text_num = $bit_check - strlen($text_num[count($text_num) - 1]);
for ($i = 0; $i < $text_num; $i++) {
$text = $text.chr($text_num);
}
$cipher = mcrypt_module_open($cypher_type, '', 'cbc', '');
mcrypt_generic_init($cipher, $key, $iv);
$encrypted = mcrypt_generic($cipher, $text);
mcrypt_generic_deinit($cipher);
mcrypt_module_close($cipher);
return base64_encode($encrypted);
} else {
return $text;
}
} catch (Exception $e) {
return 'Error in Bitauth::Encrypt(): '.$e->getMessage();
}
}
/**
*
* Decrypts $text based on your $key and $iv. Make sure you use the same key
* and initialization vector that you used when encrypting the $text. Default
* cypher is MCRYPT_TRIPLEDES but you can substitute depending on the cypher
* used for encrypting the text - very important.
*
* @param string $encrypted_text
* @param string $key
* @param string $iv
* @param int $bit_check
* @param string $cypher_type
* @return string $text
* @throws Exception $e
*
*/
public function decrypt($encrypted_text, $key = '', $iv = '', $bit_check = 8, $cypher_type = 'MCRYPT_TRIPLEDES')
{
try {
/* Ensure the key & IV is the same for both encrypt & decrypt. */
if (!empty($encrypted_text)) {
$cipher = mcrypt_module_open($cypher_type, '', 'cbc', '');
mcrypt_generic_init($cipher, $key, $iv);
$decrypted = mdecrypt_generic($cipher, base64_decode($encrypted_text));
mcrypt_generic_deinit($cipher);
$last_char = substr($decrypted, -1);
for ($i = 0; $i < $bit_check - 1; $i++) {
if (chr($i) == $last_char) {
$decrypted = substr($decrypted, 0, strlen($decrypted) - $i);
break;
}
}
mcrypt_module_close($cipher);
return $decrypted;
} else {
return $encrypted_text;
}
} catch (Exception $e) {
return 'Error in Bitauth::Dncrypt(): '.$e->getMessage();
}
}
}