#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
# $Id$
#------------------------------------------------------------
package Common::Crypto;

use strict;
use warnings;

use Data::Dumper;

use Time::HiRes;
use MIME::Base64;
use Crypt::CBC;
use JSON;


use lib '/app/tools/common/lib';
use Common::Util qw(escape_mysql_regexp escape_mysql_like);
use Common::DB::ItemCollection;
use Common::Assert;

use constant kKMSScriptPath => '/app/tools/common/bin/kms-scripts/';

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self->_init(@_);
}

sub _init {
    my ( $self, %args ) = @_;

    return $self;
}

sub GenerateDatakey {
    # TODO: Consider using a different profile.  Also, 'dev-rs'test' should be renamed when
    # TODO: this is rolled to production.
    # TODO: What happens if you try to do this without a valid profile?  E.g., remote dev environment?
    # TODO:  Make sure we deal with error messages.
    my $cmd = "aws kms generate-data-key --profile sysmail --key-id alias/dev-rs-test --key-spec AES_256";
    print STDERR "D: Common::Crypto->GenerateDatakey:  cmd = $cmd\n"; # XXX
    my $result = `$cmd`;
    my $content = decode_json( $result );

    print STDERR "Common::Crypto->GenerateDatakey:  content = ". Dumper($content) . "\n"; # XXX

    my $plainkey = $content->{Plaintext};        # The plaintext datakey (in Base64 format)
    my $encKey   = $content->{CiphertextBlob};   # The encrypted datakey (in Base64 format)
    my $encKeyDecoded   = decode_base64($encKey);

    # The caller should store the CiphertextBlob for later use.  For convenience,
    # the plaintext key is also returned so the caller can immedicately perform
    # crypto activities.
    #
    # In either case, the encrypted and plain datakeys are in Base64 format via the 'Plaintext'
    # and 'CiphertextBlob' keys.
    return $content;
}

# GetPlaintextkey64 - get the plaintext data key associated with an encrypted key
# returns: Base64 encoded plaintext key, or undef if unable to decrypt the key
sub GetPlaintextkey64 {
    my $self           = shift;
    my $encryptedKey64 = shift;

    # This is a shell script which sets up a call to 'aws kms decrypt' to decrypt the key
    # The script uses the AWS "sysmail" profile to communicate with KMS.

    # If we're running as a web app, make sure that the credentials file can be found
    my $user = `whoami`;
    chomp $user;
    my $awsCredentials = '';
    if ( $user eq 'apache' ) {
        $awsCredentials = 'AWS_SHARED_CREDENTIALS_FILE=/var/www/.aws/credentials ';
    }
    my $scriptPath = Common::Util::returnFirstDefined( $ENV{KMS_SCRIPT_PATH},  kKMSScriptPath );
    $scriptPath .= '/' if ( $scriptPath !~ /\/$/ ); # make sure we have a trailing slash

    my $cmd = $awsCredentials . $scriptPath . "kms-decrypt -s \"$encryptedKey64\"";

    my $plainkey64 = `$cmd`;
    chomp $plainkey64;
    return $plainkey64;
}

sub Encrypt64NNNN {
    my $self   = shift;
    my %args   = @_;
    my $secret = $args{secret};
    my $cipher = $args{cipher};

    return  encode_base64( $cipher->encrypt($secret),'' ) . 'NNNN';
}

sub Decrypt64NNNN {
    my $self   = shift;
    my %args   = @_;
    my $secret = $args{secret};
    my $cipher = $args{cipher};

    if ( isBase64NNNN($secret) ) {
        return $cipher->decrypt( decode_base64($secret) );
    }
    print STDERR "ERROR: Common::Crypto->Decrypt64NNNN - unable to decrypt '$secret'\n"; # TODO: Need to log this
    return $secret;
}

sub _getCipher { # return a cipher object suitable for encryption/decryption activities
    my $self      = shift;
    my %args      = @_;

    # Must specify either the encrypted datakey or the plaintext datakey
    my $encryptedKey64 = $args{key64};           # encrypted key (Base64 encoded)
    my $plaintextKey64 = $args{plaintextKey64};  # plaintext key (Base64 encoded)

    if ( !$encryptedKey64 && !$plaintextKey64 ) {
        # This can happen if the aws profile isn't available.
        print STDERR "ERROR: Neither 'key64' or 'plaintextkey64' argument provided !!!  args = ".Dumper(\%args) . "\n";
        return;
    }

    if ( !$plaintextKey64 ) {
        $plaintextKey64 = $self->GetPlaintextkey64( $encryptedKey64 );
    }

    my $cipher = Crypt::CBC->new(
        {   
            'key'         => decode_base64( $plaintextKey64 ),
            'pbkdf'       => 'opensslv2', # Use the salted SHA-256 method that was the default in versions of OpenSSL through v1.1.0.
            'cipher'      => 'Crypt::Cipher::AES',
        }   
    );
    return $cipher;
}

sub envelope_encrypt2 {
    my $self = shift;
    my %args = @_;

    my $datakey64 = $args{datakey64};  # plaintext key (Base64 encoded)
    my $secret    = $args{secret};     # data to be encrypted (recommend Base64 encoding if contains spaces or punctuation)

}

# Name: envelope_encrypt
# Desc: Encrypts secret data using the supplied plaintext datakey
# Args:
#   datakey64 - the plaintext datakey (in Base64 format)
#   secret - the data to encrypt.
#      TODO: This data must be Base64 encoded if if contains any spaces or punctuation.  This is because we're using a command-line
#      script to perform the encryption and certain characters will cause the command to fail.
#
sub envelope_encrypt {
    my $self = shift;
    my %args = @_;
    my $datakey64 = $args{datakey64};  # plaintext key (Base64 encoded)
    my $secret    = $args{secret};     # data to be encrypted (recommend Base64 encoding if contains spaces or punctuation)

    my $scriptPath = Common::Util::returnFirstDefined( $ENV{KMS_SCRIPT_PATH},  kKMSScriptPath );
    $scriptPath .= '/' if ( $scriptPath !~ /\/$/ ); # make sure we have a trailing slash

    my $secret64 = encode_base64($secret);
    my $cmd = $scriptPath .  "kms-envelopeencrypt -K $datakey64 -p $secret64";

    my $encryptedData = `$cmd`;
    chomp $encryptedData;
    return $encryptedData;
}

sub envelope_decrypt {
    my $self = shift;
    my %args = @_;
    my $datakey64 = $args{datakey64};  # plaintext key (Base64 encoded)
    my $public    = $args{public};     # data to be decrypted (Base64 encoded, single string no newlines)

    my $scriptPath = Common::Util::returnFirstDefined( $ENV{KMS_SCRIPT_PATH},  kKMSScriptPath );
    $scriptPath .= '/' if ( $scriptPath !~ /\/$/ ); # make sure we have a trailing slash

    my $cmd = $scriptPath . "kms-envelopedecrypt -K $datakey64 -s $public";
    my $decryptedData64 = `$cmd`;

    return $decryptedData64;

}

# This just checks if the specified string meets our cryteria of something
# encrypted.  E.g., a Base64-encoded string with a 'NNNN' suffix.
#
sub isBase64NNNN {
    my $s = shift;
    if ( $s && $s =~ /(.*)NNNN$/ ) { # strip off the delimiter
        return isBase64($1);
    }
    return undef;
}

sub isBase64 {
    my $s    = shift;

    return ( $s =~ m{
        ^ 
        (?: [A-Za-z0-9+/]{4} )*
        (?: [A-Za-z0-9+/]{2} [AEIMQUYcgkosw048] = 
        |   [A-Za-z0-9+/] [AQgw] ==
        )?
        \z
    }x) ? 1 : 0;
}

1;
