# U2F second factor authentication
#
# This plugin handle authentications to ask U2F second factor for users that
# have registered their U2F key
package Lemonldap::NG::Portal::2F::U2F;

use strict;
use Mouse;
use JSON qw(from_json to_json);
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_OK
  PE_ERROR
  PE_U2FFAILED
  PE_SENDRESPONSE
  PE_BADCREDENTIALS
);

our $VERSION = '2.0.10';

extends qw(
  Lemonldap::NG::Portal::Main::SecondFactor
  Lemonldap::NG::Portal::Lib::U2F
);

# INITIALIZATION

has rule   => ( is => 'rw' );
has prefix => ( is => 'ro', default => 'u' );
has logo   => ( is => 'rw', default => 'u2f.png' );

sub init {
    my ($self) = @_;

    # If self registration is enabled and "activation" is just set to
    # "enabled", replace the rule to detect if user has registered its key
    if (    $self->conf->{u2fSelfRegistration}
        and $self->conf->{u2fActivation} eq '1' )
    {
        $self->conf->{u2fActivation} =
          '$_2fDevices && $_2fDevices =~ /"type":\s*"U2F"/s';
    }
    return 0
      unless ( $self->Lemonldap::NG::Portal::Main::SecondFactor::init()
        and $self->Lemonldap::NG::Portal::Lib::U2F::init() );

    1;
}

# RUNNING METHODS

# Main method
sub run {
    my ( $self, $req, $token ) = @_;

    my $checkLogins = $req->param('checkLogins');
    $self->logger->debug("U2F: checkLogins set") if $checkLogins;

    my $stayconnected = $req->param('stayconnected');
    $self->logger->debug("U2F: stayconnected set") if $stayconnected;

    # Check if user is registered
    if ( my $res = $self->loadUser( $req, $req->sessionInfo ) ) {
        return PE_ERROR     if ( $res == -1 );
        return PE_U2FFAILED if ( $res == 0 );

        # Get a challenge (from first key)
        my $data = eval {
            from_json( $req->data->{crypter}->[0]->authenticationChallenge );
        };

        if ($@) {
            $self->logger->error( Crypt::U2F::Server::u2fclib_getError() );
            return PE_ERROR;
        }

        # Get registered keys
        my @rk =
          map { { keyHandle => $_->{keyHandle}, version => $data->{version} } }
          @{ $req->data->{crypter} };

        $self->ott->updateToken( $token, __ch => $data->{challenge} );

        $self->logger->debug("Prepare U2F verification");
        $self->logger->debug( " -> Send challenge: " . $data->{challenge} );

        # Serialize data
        $data = to_json( {
                challenge      => $data->{challenge},
                appId          => $data->{appId},
                registeredKeys => \@rk
            }
        );

        my $tmp = $self->p->sendHtml(
            $req,
            'u2fcheck',
            params => {
                MAIN_LOGO     => $self->conf->{portalMainLogo},
                SKIN          => $self->p->getSkin($req),
                DATA          => $data,
                TOKEN         => $token,
                CHECKLOGINS   => $checkLogins,
                STAYCONNECTED => $stayconnected
            }
        );

        $req->response($tmp);
        return PE_SENDRESPONSE;
    }
    return PE_U2FFAILED;
}

sub verify {
    my ( $self, $req, $session ) = @_;
    my $crypter;

    # Check U2F signature
    if (    my $resp = $req->param('signature')
        and my $challenge = $req->param('challenge') )
    {
        unless ( $self->loadUser( $req, $session ) == 1 ) {
            $req->error(PE_ERROR);
            return $self->fail($req);
        }

        $self->logger->debug("Get challenge: $challenge");

        unless ( $session->{__ch} and $session->{__ch} eq $challenge ) {
            $self->userLogger->error(
                "U2F challenge changed by user: $session->{__ch} / $challenge");
            $req->error(PE_BADCREDENTIALS);
            return $self->fail($req);
        }
        delete $session->{__ch};

        $self->logger->debug("Get signature: $resp");
        my $data = eval { JSON::from_json($resp) };
        if ($@) {
            $self->logger->error("U2F response error: $@");
            $req->error(PE_ERROR);
            return $self->fail($req);
        }
        $crypter = $_
          foreach grep { $_->{keyHandle} eq $data->{keyHandle} }
          @{ $req->data->{crypter} };
        unless ($crypter) {
            $self->userLogger->error("Unregistered U2F key");
            $req->error(PE_BADCREDENTIALS);
            return $self->fail($req);
        }

        if ( not $crypter->setChallenge($challenge) ) {
            $self->logger->error(
                $@ ? $@ : Crypt::U2F::Server::Simple::lastError() );
            $req->error(PE_ERROR);
            return $self->fail($req);
        }
        if ( $crypter->authenticationVerify($resp) ) {
            $self->userLogger->info('U2F signature verified');
            return PE_OK;
        }
        else {
            $self->userLogger->notice( 'Invalid U2F signature for '
                  . $session->{ $self->conf->{whatToTrace} } . ' ('
                  . Crypt::U2F::Server::u2fclib_getError()
                  . ')' );
            $req->error(PE_U2FFAILED);
            return $self->fail($req);
        }
    }
    else {
        $self->userLogger->notice( 'No valid U2F response for user'
              . $session->{ $self->conf->{whatToTrace} } );
        $req->authResult(PE_U2FFAILED);
        return $self->fail($req);
    }
}

sub fail {
    my ( $self, $req ) = @_;
    $req->response(
        $self->p->sendHtml(
            $req,
            'u2fcheck',
            params => {
                MAIN_LOGO       => $self->conf->{portalMainLogo},
                AUTH_ERROR      => $req->error,
                AUTH_ERROR_TYPE => $req->error_type,
                SKIN            => $self->p->getSkin($req),
                FAILED          => 1
            }
        )
    );
    return PE_SENDRESPONSE;
}

sub loadUser {
    my ( $self, $req, $session ) = @_;
    my ( $kh, $uk, $_2fDevices );
    my @u2fs = ();

    if ( $session->{_2fDevices} ) {
        $self->logger->debug("Loading 2F Devices ...");

        # Read existing 2FDevices
        $_2fDevices =
          eval { from_json( $session->{_2fDevices}, { allow_nonref => 1 } ); };
        if ($@) {
            $self->logger->error("Bad encoding in _2fDevices: $@");
            return PE_ERROR;
        }
        $self->logger->debug("2F Device(s) found");

        $self->logger->debug("Looking for registered U2F key(s) ...");
        foreach (@$_2fDevices) {
            if ( $_->{type} eq 'U2F' ) {
                unless ( $_->{_userKey} and $_->{_keyHandle} ) {
                    $self->logger->error(
"Missing required U2F attributes in storage ($session->{_2fDevices})"
                    );
                    next;
                }
                $self->logger->debug( "Found U2F key -> _userKey = "
                      . $_->{_userKey}
                      . " / _keyHandle = "
                      . $_->{_keyHandle} );
                $_->{_userKey} = $self->decode_base64url( $_->{_userKey} );
                push @u2fs, $_;
            }
        }
    }

    # Manage multi u2f keys
    my @crypters;
    if (@u2fs) {
        $self->logger->debug("Generating crypter(s) with uk & kh");

        foreach (@u2fs) {
            $kh = $_->{_keyHandle};
            $uk = $_->{_userKey};
            $self->logger->debug("Append crypter with kh -> $kh");
            my $c = $self->crypter( keyHandle => $kh, publicKey => $uk );
            if ($c) {
                push @crypters, $c;
            }
            else {
                $self->logger->error(
                    'U2F error: ' . Crypt::U2F::Server::u2fclib_getError() );
            }
        }
        return -1 unless @crypters;

        $req->data->{crypter} = \@crypters;
        return 1;
    }
    else {
        $self->userLogger->info("U2F : user not registered");
        return 0;
    }
}

1;
