WordPress makes two assumptions this project cannot accept: that identity comes from an email address, and that a site lives at one server. Both are things somebody else can take away — a mailbox is rented from a provider who can close it or be compelled to open it, and a server is one seizure from being gone. Sirius Press replaces the first and hedges the second. Signing in means signing a challenge with the key that controls a CashAddress. The address is recovered from the signature, so nothing is typed but the signature itself, and the result is an ordinary WordPress session cookie — roles, capabilities, nonces and the REST API never learn the login was different. Three ways to produce one: a wallet the browser already exposes, a phrase used once in the page and wiped, or a signature pasted in from any BIP-137 wallet, which needs no JavaScript and lets the key stay on a machine that never touches the web. There is no password reset, and the recovery page says so plainly rather than offering a form that cannot work. A reset mechanism is by construction a way to take an account from its owner, and it is always easier to attack than the cryptography it bypasses. Publishing a post also exports it as static HTML to the name's storage on Sia, signed by the key that owns the name, so the site keeps answering when the server does not. Email as a feature is untouched. wp_mail() still works, SMTP still sends, and contact forms still deliver to addresses real people typed. Only mail to the site's own unroutable placeholder addresses is diverted to an in-app inbox. The objection was to email as identity, not to email. Core is pinned and patched rather than vendored. WordPress 7.1.1 is 149 MB and 5,008 files; the fork's entire core diff is 75 lines in wp-admin/install.php. Carrying the former to express the latter would bury the patch where nobody reviews it and make every clone of the monorepo pay for it. Upstream releases still merge through tools/update-wordpress.sh, which reapplies the series and says exactly which hunk needs a human. The cryptography is implemented twice — PHP on the server, JavaScript in the page — because the server must verify and the browser must sign. Both are pinned against libauth, the library the Sirius portal wallet and the BNS gateway already use, so a disagreement of one byte fails the test suite rather than presenting as a rejected login at three in the morning. 132 checks, no framework, about a second.
182 lines
5.6 KiB
PHP
182 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' );
|
|
}
|
|
}
|