package Crypt::Perl::ECDSA::Parse;

=encoding utf-8

=head1 NAME

Crypt::Perl::ECDSA::Parse - ECDSA key parsing

=head1 SYNOPSIS

    use Crypt::Perl::ECDSA::Parse ();

    #These accept either DER or PEM, native format or PKCS8.
    #
    my $prkey = Crypt::Perl::ECDSA::Parse::private($buffer);
    my $pbkey = Crypt::Perl::ECDSA::Parse::public($buffer);

=head1 DISCUSSION

See L<Crypt::Perl::ECDSA::PrivateKey> and L<Crypt::Perl::ECDSA::PublicKey>
for descriptions of the interfaces of these two classes.

=cut

use strict;
use warnings;

use Try::Tiny;

use Module::Load ();

use Crypt::Perl::ASN1 ();
use Crypt::Perl::PKCS8 ();
use Crypt::Perl::ToDER ();
use Crypt::Perl::ECDSA::ECParameters ();

sub private {
    my ($pem_or_der) = @_;

    Module::Load::load('Crypt::Perl::ECDSA::PrivateKey');

    Crypt::Perl::ToDER::ensure_der($pem_or_der);

    my $asn1 = _private_asn1();
    my $asn1_ec = $asn1->find('ECPrivateKey');

    my $struct;
    try {
        $struct = $asn1_ec->decode($pem_or_der);
    }
    catch {
        my $ec_err = $_;

        my $asn1_pkcs8 = $asn1->find('PrivateKeyInfo');

        try {
            my $pk8_struct = $asn1_pkcs8->decode($pem_or_der);

            #It still might succeed, even if this is wrong, so don’t die().
            if ( $pk8_struct->{'privateKeyAlgorithm'}{'algorithm'} ne Crypt::Perl::ECDSA::PrivateKey->OID_ecPublicKey() ) {
                warn "Unknown private key algorithm OID: “$pk8_struct->{'privateKeyAlgorithm'}{'algorithm'}”";
            }

            my $asn1_params = $asn1->find('EcpkParameters');
            my $params = $asn1_params->decode($pk8_struct->{'privateKeyAlgorithm'}{'parameters'});

            $struct = $asn1_ec->decode($pk8_struct->{'privateKey'});
            $struct->{'parameters'} = $params;
        }
        catch {
            die "Failed to decode private key as either ECDSA native ($ec_err) or PKCS8 ($_)";
        };
    };

    my $key_parts = {
        version => $struct->{'version'},
        private => Crypt::Perl::BigInt->from_bytes($struct->{'privateKey'}),
        public => Crypt::Perl::BigInt->from_bytes($struct->{'publicKey'}[0]),
    };

    return Crypt::Perl::ECDSA::PrivateKey->new($key_parts, $struct->{'parameters'});
}

sub public {
    my ($pem_or_der) = @_;

    Module::Load::load('Crypt::Perl::ECDSA::PublicKey');

    Crypt::Perl::ToDER::ensure_der($pem_or_der);

    my $asn1 = _public_asn1();
    my $asn1_ec = $asn1->find('ECPublicKey');

    my $struct;
    try {
        $struct = $asn1_ec->decode($pem_or_der);
    }
    catch {
        my $ec_err = $_;

        my $asn1_pkcs8 = $asn1->find('SubjectPublicKeyInfo');

        try {
            my $spk_struct = $asn1_pkcs8->decode($pem_or_der);

            #It still might succeed, even if this is wrong, so don’t die().
            if ( $spk_struct->{'algorithm'}{'algorithm'} ne Crypt::Perl::ECDSA::PublicKey->OID_ecPublicKey() ) {
                warn "Unknown private key algorithm OID: “$spk_struct->{'algorithm'}{'algorithm'}”";
            }

            my $asn1_params = $asn1->find('EcpkParameters');
            my $params = $asn1_params->decode($spk_struct->{'algorithm'}{'parameters'});

            $struct = { publicKey => $spk_struct->{'subjectPublicKey'} };
            $struct->{'keydata'}{'parameters'} = $params;
        }
        catch {
            die "Failed to decode public key as either ECDSA native ($ec_err) or PKCS8 ($_)";
        };
    };

    return Crypt::Perl::ECDSA::PublicKey->new(
        $struct->{'publicKey'}[0],
        $struct->{'keydata'}{'parameters'},
    );
}

sub jwk {
    my ($hr) = @_;

    Module::Load::load($_) for qw(
        Crypt::Perl::ECDSA::NIST
        Crypt::Perl::ECDSA::EC::DB
        Crypt::Perl::Math
        MIME::Base64
    );

    my $curve_name = Crypt::Perl::ECDSA::NIST::get_curve_name_for_nist($hr->{'crv'});
    my $curve_hr = Crypt::Perl::ECDSA::EC::DB::get_curve_data_by_name($curve_name);

    my $keylen = $curve_hr->{'p'}->bit_length();
    my $pub_half_byte_length = Crypt::Perl::Math::ceil( $keylen / 8 );

    my $x = MIME::Base64::decode_base64url($hr->{'x'});
    my $y = MIME::Base64::decode_base64url($hr->{'y'});

    #Make sure both halves are the proper length.
    substr($_, 0, 0) = ("\0" x ($pub_half_byte_length - length)) for ($x, $y);

    my $public = Crypt::Perl::BigInt->from_bytes("\x{04}$x$y");

    if ($hr->{'d'}) {
        Module::Load::load('Crypt::Perl::ECDSA::PrivateKey');
        Module::Load::load('Crypt::Perl::JWK');

        my %args = (
            version => 1,
            public => $public,
            private => Crypt::Perl::JWK::jwk_num_to_bigint($hr->{'d'}),
        );

        return Crypt::Perl::ECDSA::PrivateKey->new_by_curve_name(\%args, $curve_name);
    }

    Module::Load::load('Crypt::Perl::ECDSA::PublicKey');
    return Crypt::Perl::ECDSA::PublicKey->new_by_curve_name( $public, $curve_name);
}

#----------------------------------------------------------------------

sub _private_asn1 {
    my $template = join("\n", Crypt::Perl::ECDSA::PrivateKey->ASN1_PRIVATE(), Crypt::Perl::PKCS8::ASN1());

    return Crypt::Perl::ASN1->new()->prepare($template);
}

sub _public_asn1 {
    my $template = join("\n", Crypt::Perl::ECDSA::PublicKey->ASN1_PUBLIC(), Crypt::Perl::PKCS8::ASN1());

    return Crypt::Perl::ASN1->new()->prepare($template);
}

1;
