DigiByte-DigiID

 view release on metacpan or  search on metacpan

lib/DigiByte/DigiID.pm  view on Meta::CPAN


our @EXPORT_OK = qw(
  extract_nonce
  get_qrcode
  verify_signature
);

use Crypto::ECC;
use Crypt::OpenPGP::Digest;    ## RIPEMD160
use Data::UUID;
use Digest::SHA qw(sha256);
use Math::BigInt lib => 'GMP';
use MIME::Base64 qw(decode_base64);
use String::Pad qw(pad);
use URI::Escape qw(uri_escape);

my $STR_PAD_LEFT = 'l';
my %SECP256K1    = (
    a => 00,
    b => 07,
    prime =>
      '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F',
    x => '0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798',
    y => '0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8',
    order =>
      '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141',
);

sub get_qrcode {
    my ( $server_domain, %options ) = @_;

    my $nonce = $options{nonce} // Data::UUID->new->create_hex;

    my $path = $options{path} // '/callback';

    my $url = "digiid://$server_domain$path?x=$nonce";

    if ( $options{nossl} ) {
        $url .= '&u=1';
    }

    my $str = uri_escape($url);

    return (
        nonce    => $nonce,
        callback => $url,
        image =>
          "https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=$str",
    );
}

sub extract_nonce {
    my ($uri)   = @_;
    my ($nonce) = ( $uri =~ m/[\?\&]x=([^\&]+)/ );
    return $nonce;
}

sub verify_signature {
    my ( $address, $signature, $message, $testnet ) = @_;

    my $decoded_address = _base58check_decode( $address, $testnet );
    my @decoded_address = split //, $decoded_address;

    if (   length($decoded_address) != 21
        || ( $decoded_address[0] ne "\x1E" && !$testnet )
        || ( $decoded_address[0] ne "\x6F" && $testnet ) )
    {
        die "invalid DigiByte address";
    }

    my $decoded_signature = decode_base64($signature);
    my @decoded_signature = split //, $decoded_signature;

    if ( length($decoded_signature) != 65 ) {
        die "invalid signature";
    }

    my $recovery_flags = ord( $decoded_signature[0] ) - 27;

    if ( $recovery_flags < 0 || $recovery_flags > 7 ) {
        die "invalid signature type";
    }

    my $is_compressed = ( $recovery_flags & 4 ) != 0;

    my $message_hash = sha256(
        sha256(
                "\x19DigiByte Signed Message:\n"
              . _num_to_var_int_string( length($message) )
              . $message
        )
    );

    my $pubkey = do {
        my $r = _bin2gmp( substr( $decoded_signature, 1,  32 ) );
        my $s = _bin2gmp( substr( $decoded_signature, 33, 32 ) );
        my $e = _bin2gmp($message_hash);
        my $g = $Point->new(%SECP256K1);

        _recover_pubkey( $r, $s, $e, $recovery_flags, $g );
    };

    if ( !$pubkey ) {
        die 'unable to recover key';
    }

    my $point = $pubkey->point;

    my $pub_bin_str;

    ## see that the key we recovered is for the address given
    if ($is_compressed) {
        $pub_bin_str = ( _is_bignum_even( $point->y ) ? "\x02" : "\x03" )
          . pad( _gmp2bin( $point->x ), 32, $STR_PAD_LEFT, "\x00" );
    }
    else {
        $pub_bin_str = "\x04"
          . pad( _gmp2bin( $point->x ), 32, $STR_PAD_LEFT, "\x00" )
          . pad( _gmp2bin( $point->y ), 32, $STR_PAD_LEFT, "\x00" );
    }

    my $ripemd160 = Crypt::OpenPGP::Digest->new('RIPEMD160');

    my $derived_address;

    if ($testnet) {
        $derived_address = "\x6F" . $ripemd160->hash( sha256($pub_bin_str) );
    }
    else {
        $derived_address = "\x1E" . $ripemd160->hash( sha256($pub_bin_str) );
    }

    return $decoded_address eq $derived_address;
}

sub _base58check_decode {
    my ( $address, $testnet ) = @_;

    my $decoded_address = $address;

    $decoded_address =~
      tr{123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz}
                          {0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv};

    $decoded_address =~ s/^0+//;

    my $v = Math::BigInt->from_base( $decoded_address, 58 );

    $v = _gmp2bin($v);

    foreach my $chr ( split //, $address ) {
        if ( $chr ne '1' ) {
            last;
        }
        if ($testnet) {
            $v = "\x6F$v";
        }
        else {
            $v = "\x00$v";
        }
    }

    my $checksum = substr $v, -4;

    $v = substr $v, 0, -4;

    my $exp_check_sum = substr sha256( sha256($v) ), 0, 4;

    if ( $exp_check_sum ne $checksum ) {
        die "Invalid checksum";
    }

    return $v;
}

sub _num_to_var_int_string {
    my ($i) = @_;

    if ( $i < 0xfd ) {
        return chr($i);
    }
    elsif ( $i <= 0xffff ) {
        return pack( 'Cv', 0xfd, $i );
    }
    elsif ( $i <= 0xffffffff ) {
        return pack( 'CV', 0xfe, $i );
    }
    else {
        die 'int too large';
    }
}

sub _bin2gmp {
    my ($bin_str) = @_;

    my $v = Math::BigInt->new(0);

    foreach my $ch ( split //, $bin_str ) {
        $v *= 256;
        $v += ord $ch;
    }

    return $v;
}

sub _gmp2bin {
    my ($v) = @_;



( run in 0.582 second using v1.01-cache-2.11-cpan-0b5f733616e )