####################################################################################################################################
# S3 Authentication
#
# Contains the functions required to do S3 authentication.  It's a complicated topic and too much to cover here, but there is
# excellent documentation at http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html.
####################################################################################################################################
package pgBackRest::Storage::S3::Auth;

use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';

use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
use Exporter qw(import);
    our @EXPORT = qw();
use POSIX qw(strftime);

use pgBackRest::Common::Http::Common;
use pgBackRest::Common::Log;
use pgBackRest::LibC qw(:crypto);

####################################################################################################################################
# Constants
####################################################################################################################################
use constant S3                                                     => 's3';
use constant AWS4                                                   => 'AWS4';
use constant AWS4_REQUEST                                           => 'aws4_request';
use constant AWS4_HMAC_SHA256                                       => 'AWS4-HMAC-SHA256';

use constant S3_HEADER_AUTHORIZATION                                => 'authorization';
    push @EXPORT, qw(S3_HEADER_AUTHORIZATION);
use constant S3_HEADER_DATE                                         => 'x-amz-date';
    push @EXPORT, qw(S3_HEADER_DATE);
use constant S3_HEADER_CONTENT_SHA256                               => 'x-amz-content-sha256';
    push @EXPORT, qw(S3_HEADER_CONTENT_SHA256);
use constant S3_HEADER_HOST                                         => 'host';
    push @EXPORT, qw(S3_HEADER_HOST);
use constant S3_HEADER_TOKEN                                        => 'x-amz-security-token';
    push @EXPORT, qw(S3_HEADER_TOKEN);

use constant PAYLOAD_DEFAULT_HASH                                   => cryptoHashOne('sha256', '');
    push @EXPORT, qw(PAYLOAD_DEFAULT_HASH);

####################################################################################################################################
# s3DateTime - format date/time for authentication
####################################################################################################################################
sub s3DateTime
{
    # Assign function parameters, defaults, and log debug info
    my
    (
        $strOperation,
        $lTime,
    ) =
        logDebugParam
        (
            __PACKAGE__ . '::s3DateTime', \@_,
            {name => 'lTime', default => time(), trace => true},
        );

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'strDateTime', value => strftime("%Y%m%dT%H%M%SZ", gmtime($lTime)), trace => true}
    );
}

push @EXPORT, qw(s3DateTime);

####################################################################################################################################
# s3CanonicalRequest - strictly formatted version of the HTTP request used for signing
####################################################################################################################################
sub s3CanonicalRequest
{
    # Assign function parameters, defaults, and log debug info
    my
    (
        $strOperation,
        $strVerb,
        $strUri,
        $strQuery,
        $hHeader,
        $strPayloadHash,
    ) =
        logDebugParam
        (
            __PACKAGE__ . '::s3CanonicalRequest', \@_,
            {name => 'strVerb', trace => true},
            {name => 'strUri', trace => true},
            {name => 'strQuery', trace => true},
            {name => 'hHeader', trace => true},
            {name => 'strPayloadHash', trace => true},
        );

    # Create the canonical request
    my $strCanonicalRequest =
        "${strVerb}\n${strUri}\n${strQuery}\n";
    my $strSignedHeaders;

    foreach my $strHeader (sort(keys(%{$hHeader})))
    {
        if (lc($strHeader) ne $strHeader)
        {
            confess &log(ASSERT, "header '${strHeader}' must be lower case");
        }

        $strCanonicalRequest .= $strHeader . ":$hHeader->{$strHeader}\n";
        $strSignedHeaders .= (defined($strSignedHeaders) ? qw(;) : '') . lc($strHeader);
    }

    $strCanonicalRequest .= "\n${strSignedHeaders}\n${strPayloadHash}";

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'strCanonicalRequest', value => $strCanonicalRequest, trace => true},
        {name => 'strSignedHeaders', value => $strSignedHeaders, trace => true},
    );
}

push @EXPORT, qw(s3CanonicalRequest);

####################################################################################################################################
# s3SigningKey - signing keys last for seven days, but we'll regenerate every day because it doesn't seem too burdensome
####################################################################################################################################
my $hSigningKeyCache;                                               # Cache signing keys rather than regenerating them every time

sub s3SigningKey
{
    # Assign function parameters, defaults, and log debug info
    my
    (
        $strOperation,
        $strDate,
        $strRegion,
        $strSecretAccessKey,
    ) =
        logDebugParam
        (
            __PACKAGE__ . '::s3SigningKey', \@_,
            {name => 'strDate', trace => true},
            {name => 'strRegion', trace => true},
            {name => 'strSecretAccessKey', redact => true, trace => true},
        );

    # Check for signing key in cache
    my $strSigningKey = $hSigningKeyCache->{$strDate}{$strRegion}{$strSecretAccessKey};

    # If not found then generate it
    if (!defined($strSigningKey))
    {
        my $strDateKey = hmac_sha256($strDate, AWS4 . $strSecretAccessKey);
        my $strRegionKey = hmac_sha256($strRegion, $strDateKey);
        my $strServiceKey = hmac_sha256(S3, $strRegionKey);
        $strSigningKey = hmac_sha256(AWS4_REQUEST, $strServiceKey);

        # Cache the signing key
        $hSigningKeyCache->{$strDate}{$strRegion}{$strSecretAccessKey} = $strSigningKey;
    }

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'strSigningKey', value => $strSigningKey, redact => true, trace => true}
    );
}

push @EXPORT, qw(s3SigningKey);

####################################################################################################################################
# s3StringToSign - string that will be signed by the signing key for authentication
####################################################################################################################################
sub s3StringToSign
{
    # Assign function parameters, defaults, and log debug info
    my
    (
        $strOperation,
        $strDateTime,
        $strRegion,
        $strCanonicalRequestHash,
    ) =
        logDebugParam
        (
            __PACKAGE__ . '::s3StringToSign', \@_,
            {name => 'strDateTime', trace => true},
            {name => 'strRegion', trace => true},
            {name => 'strCanonicalRequestHash', trace => true},
        );

    my $strStringToSign =
        AWS4_HMAC_SHA256 . "\n${strDateTime}\n" . substr($strDateTime, 0, 8) . "/${strRegion}/" . S3 . '/' . AWS4_REQUEST . "\n" .
        $strCanonicalRequestHash;

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'strStringToSign', value => $strStringToSign, trace => true}
    );
}

push @EXPORT, qw(s3StringToSign);

####################################################################################################################################
# s3AuthorizationHeader - authorization string that will be used in the HTTP "authorization" header
####################################################################################################################################
sub s3AuthorizationHeader
{
    # Assign function parameters, defaults, and log debug info
    my
    (
        $strOperation,
        $strRegion,
        $strHost,
        $strVerb,
        $strUri,
        $strQuery,
        $strDateTime,
        $hHeader,
        $strAccessKeyId,
        $strSecretAccessKey,
        $strSecurityToken,
        $strPayloadHash,
    ) =
        logDebugParam
        (
            __PACKAGE__ . '::s3AuthorizationHeader', \@_,
            {name => 'strRegion', trace => true},
            {name => 'strHost', trace => true},
            {name => 'strVerb', trace => true},
            {name => 'strUri', trace => true},
            {name => 'strQuery', trace => true},
            {name => 'strDateTime', trace => true},
            {name => 'hHeader', required => false, trace => true},
            {name => 'strAccessKeyId', redact => true, trace => true},
            {name => 'strSecretAccessKey', redact => true, trace => true},
            {name => 'strSecurityToken', required => false, redact => true, trace => true},
            {name => 'strPayloadHash', trace => true},
        );

    # Delete the authorization header if it already exists. This could happen on a retry.
    delete($hHeader->{&S3_HEADER_AUTHORIZATION});

    # Add s3 required headers
    $hHeader->{&S3_HEADER_HOST} = $strHost;
    $hHeader->{&S3_HEADER_CONTENT_SHA256} = $strPayloadHash;
    $hHeader->{&S3_HEADER_DATE} = $strDateTime;

    # Add security token if defined
    if (defined($strSecurityToken))
    {
        $hHeader->{&S3_HEADER_TOKEN} = $strSecurityToken;
    }

    # Create authorization string
    my ($strCanonicalRequest, $strSignedHeaders) = s3CanonicalRequest(
        $strVerb, httpUriEncode($strUri, true), $strQuery, $hHeader, $strPayloadHash);
    my $strStringToSign = s3StringToSign($strDateTime, $strRegion, cryptoHashOne('sha256', $strCanonicalRequest));

    $hHeader->{&S3_HEADER_AUTHORIZATION} =
        AWS4_HMAC_SHA256 . " Credential=${strAccessKeyId}/" . substr($strDateTime, 0, 8) . "/${strRegion}/" . S3 . qw(/) .
            AWS4_REQUEST . ",SignedHeaders=${strSignedHeaders},Signature=" . hmac_sha256_hex($strStringToSign,
                s3SigningKey(substr($strDateTime, 0, 8), $strRegion, $strSecretAccessKey));

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'hHeader', value => $hHeader, trace => true},
        {name => 'strCanonicalRequest', value => $strCanonicalRequest, trace => true},
        {name => 'strSignedHeaders', value => $strSignedHeaders, trace => true},
        {name => 'strStringToSign', value => $strStringToSign, trace => true},
    );
}

push @EXPORT, qw(s3AuthorizationHeader);

1;
