183 lines
5.6 KiB
PHP
183 lines
5.6 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Arbitrary-precision integers, on whatever the host happens to have.
|
||
|
|
*
|
||
|
|
* Sirius Press has to do secp256k1 arithmetic inside PHP because the whole
|
||
|
|
* identity model is "prove you hold the key". Shared hosts are wildly
|
||
|
|
* inconsistent about which big-number extension they ship: some have GMP,
|
||
|
|
* many only have BCMath, a few have both. Rather than make the fork refuse to
|
||
|
|
* install on half the hosts it targets, every curve operation goes through
|
||
|
|
* this shim and picks a backend at load time.
|
||
|
|
*
|
||
|
|
* GMP is roughly 20x faster and is preferred. BCMath is the fallback; a login
|
||
|
|
* verification costs a few hundred milliseconds there, which is survivable
|
||
|
|
* because it happens once per session, not once per page.
|
||
|
|
*
|
||
|
|
* Numbers are opaque handles: GMP objects on the GMP backend, decimal strings
|
||
|
|
* on the BCMath one. Never do arithmetic on them directly — always via here.
|
||
|
|
*
|
||
|
|
* @package SiriusPress
|
||
|
|
*/
|
||
|
|
|
||
|
|
defined( 'ABSPATH' ) || defined( 'SP_CLI' ) || exit;
|
||
|
|
|
||
|
|
final class SP_BN {
|
||
|
|
|
||
|
|
const BACKEND_GMP = 'gmp';
|
||
|
|
const BACKEND_BC = 'bc';
|
||
|
|
|
||
|
|
/** @var string|null One of the BACKEND_* constants, or null if neither exists. */
|
||
|
|
private static $backend = null;
|
||
|
|
|
||
|
|
/** Resolve the backend once. Returns null when the host has neither extension. */
|
||
|
|
public static function backend() {
|
||
|
|
if ( null === self::$backend ) {
|
||
|
|
if ( extension_loaded( 'gmp' ) ) {
|
||
|
|
self::$backend = self::BACKEND_GMP;
|
||
|
|
} elseif ( extension_loaded( 'bcmath' ) ) {
|
||
|
|
self::$backend = self::BACKEND_BC;
|
||
|
|
bcscale( 0 );
|
||
|
|
} else {
|
||
|
|
self::$backend = '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return '' === self::$backend ? null : self::$backend;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** True when this host can do wallet crypto at all. */
|
||
|
|
public static function available() {
|
||
|
|
return null !== self::backend();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------- input
|
||
|
|
|
||
|
|
/** @param string $hex Unprefixed hex, any length, case-insensitive. */
|
||
|
|
public static function from_hex( $hex ) {
|
||
|
|
$hex = ltrim( strtolower( (string) $hex ), '0' );
|
||
|
|
if ( '' === $hex ) {
|
||
|
|
return self::from_int( 0 );
|
||
|
|
}
|
||
|
|
if ( self::BACKEND_GMP === self::backend() ) {
|
||
|
|
return gmp_init( $hex, 16 );
|
||
|
|
}
|
||
|
|
// BCMath has no base conversion, so fold the hex in one nibble at a
|
||
|
|
// time. 64 iterations of bcmul/bcadd — negligible next to the curve.
|
||
|
|
$out = '0';
|
||
|
|
$len = strlen( $hex );
|
||
|
|
for ( $i = 0; $i < $len; $i++ ) {
|
||
|
|
$out = bcadd( bcmul( $out, '16' ), (string) hexdec( $hex[ $i ] ) );
|
||
|
|
}
|
||
|
|
return $out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @param string $bin Raw bytes, big-endian. */
|
||
|
|
public static function from_bin( $bin ) {
|
||
|
|
return self::from_hex( bin2hex( $bin ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function from_int( $n ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_init( (string) $n, 10 ) : (string) $n;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --------------------------------------------------------------- output
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param mixed $a
|
||
|
|
* @param int $pad Zero-pad to this many hex characters (0 = no padding).
|
||
|
|
*/
|
||
|
|
public static function to_hex( $a, $pad = 64 ) {
|
||
|
|
if ( self::BACKEND_GMP === self::backend() ) {
|
||
|
|
$hex = gmp_strval( $a, 16 );
|
||
|
|
} else {
|
||
|
|
$hex = '';
|
||
|
|
$n = $a;
|
||
|
|
if ( 0 === bccomp( $n, '0' ) ) {
|
||
|
|
$hex = '0';
|
||
|
|
}
|
||
|
|
while ( bccomp( $n, '0' ) > 0 ) {
|
||
|
|
$hex = dechex( (int) bcmod( $n, '16' ) ) . $hex;
|
||
|
|
$n = bcdiv( $n, '16' );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( $pad > 0 && strlen( $hex ) < $pad ) {
|
||
|
|
$hex = str_repeat( '0', $pad - strlen( $hex ) ) . $hex;
|
||
|
|
}
|
||
|
|
return $hex;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Fixed-width big-endian bytes. */
|
||
|
|
public static function to_bin( $a, $bytes = 32 ) {
|
||
|
|
return hex2bin( self::to_hex( $a, $bytes * 2 ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------ arithmetic
|
||
|
|
|
||
|
|
public static function add( $a, $b ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_add( $a, $b ) : bcadd( $a, $b );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function sub( $a, $b ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_sub( $a, $b ) : bcsub( $a, $b );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function mul( $a, $b ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_mul( $a, $b ) : bcmul( $a, $b );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Always returns a non-negative residue, matching gmp_mod's sign rule. */
|
||
|
|
public static function mod( $a, $m ) {
|
||
|
|
if ( self::BACKEND_GMP === self::backend() ) {
|
||
|
|
return gmp_mod( $a, $m );
|
||
|
|
}
|
||
|
|
$r = bcmod( $a, $m );
|
||
|
|
return bccomp( $r, '0' ) < 0 ? bcadd( $r, $m ) : $r;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function pow_mod( $a, $e, $m ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_powm( $a, $e, $m ) : bcpowmod( $a, $e, $m );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** -1, 0 or 1. */
|
||
|
|
public static function cmp( $a, $b ) {
|
||
|
|
return self::BACKEND_GMP === self::backend() ? gmp_cmp( $a, $b ) : bccomp( $a, $b );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function is_zero( $a ) {
|
||
|
|
return 0 === self::cmp( $a, self::from_int( 0 ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function is_odd( $a ) {
|
||
|
|
if ( self::BACKEND_GMP === self::backend() ) {
|
||
|
|
return 1 === gmp_intval( gmp_mod( $a, gmp_init( 2 ) ) );
|
||
|
|
}
|
||
|
|
return '1' === bcmod( $a, '2' );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Modular inverse by Fermat's little theorem: a^(m-2) mod m.
|
||
|
|
*
|
||
|
|
* Only correct for prime moduli, which is all this library ever uses (the
|
||
|
|
* field prime p and the group order n are both prime). Saves carrying an
|
||
|
|
* extended-Euclid implementation that BCMath would make painful.
|
||
|
|
*/
|
||
|
|
public static function inv_mod( $a, $m ) {
|
||
|
|
return self::pow_mod( $a, self::sub( $m, self::from_int( 2 ) ), $m );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The scalar's bits, most-significant first, as a string of '0'/'1'.
|
||
|
|
*
|
||
|
|
* Point multiplication walks these. Deriving them from hex rather than by
|
||
|
|
* repeated division keeps the BCMath path from doing 256 bcdiv calls.
|
||
|
|
*/
|
||
|
|
public static function bits( $a ) {
|
||
|
|
$hex = self::to_hex( $a, 0 );
|
||
|
|
$bits = '';
|
||
|
|
$len = strlen( $hex );
|
||
|
|
for ( $i = 0; $i < $len; $i++ ) {
|
||
|
|
$bits .= str_pad( decbin( hexdec( $hex[ $i ] ) ), 4, '0', STR_PAD_LEFT );
|
||
|
|
}
|
||
|
|
return ltrim( $bits, '0' );
|
||
|
|
}
|
||
|
|
}
|