AWS Customer Key Encryption

Sunday, Jun 15, 2025| Tags: perl, python, aws, localstack

DISCLAIMER: Image is generated using ChatGPT.


1. Introduction

2. Customer Provided Keys (SSE-C)

3. Using aws

4. Using Python

5. Using Perl


Introduction



Continuing the blog series on AWS encryption, this post focuses on Server-Side Encryption using Customer-Provided Keys.

Previously, I covered the other two types of Server-Side Encryption i.e. S3 Encryption and KMS Encryption.

In this type of encryption, customer generates and manages the encryption key.

AWS does not store customer keys, they must be provided with every API request.

It uses AES-256, Advanced Encryption Standard with 256-bit keys.

256-bit keys offer 2256 possible combinations.

It’s considered virtually impervious to brute-force attacks.


Using aws



Create raw key.


(myenv) $ export RAW_KEY=$(openssl rand 32)
(myenv) $ echo $RAW_KEY
-kPz0^ALGR*Ҭ>MK

Generate base64 encoded raw key.


(myenv) $ export BASE64_KEY=$(echo -n "$RAW_KEY" | base64)
(myenv) $ echo $BASE64_KEY
igUt1GtQyHru7aswXkGoTJmAR39/HVIqtNKsxz7hTUs=

Generate md5 key for raw key.


(myenv) $ export MD5_KEY=$(echo -n "$RAW_KEY" | openssl dgst -md5 -binary | base64)
(myenv) $ echo $MD5_KEY
MFY4ZCjuzXvBZfIbk2LeMQ==

Let’s create the bucket now.


(myenv) $ aws s3 mb s3://sse-c-encrypted-bucket
make_bucket: sse-c-encrypted-bucket

Upload file with customer key using AES-256 algorithm.


(myenv) $ aws s3api put-object \
  --bucket sse-c-encrypted-bucket \
  --key secret-file.txt \
  --body secret-file.txt \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "$BASE64_KEY" \
  --sse-customer-key-md5 "$MD5_KEY"
{
    "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
    "ChecksumCRC64NVME": "QtYNnUr9Eos=",
    "ChecksumType": "FULL_OBJECT",
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "MFY4ZCjuzXvBZfIbk2LeMQ=="
}

Fetch file meta.


(myenv) $ aws s3api head-object \
  --bucket sse-c-encrypted-bucket \
  --key secret-file.txt \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "$BASE64_KEY"  \
  --sse-customer-key-md5 "$MD5_KEY"
{
    "AcceptRanges": "bytes",
    "LastModified": "2025-06-15T02:43:54+00:00",
    "ContentLength": 31,
    "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {},
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "ICjoBPuHgpRhQCUQ4hCC1Q=="
}

Copy file with key.


(myenv) $ aws s3api copy-object \
  --bucket sse-c-encrypted-bucket \
  --key copy_of_secret-file.txt \
  --copy-source "sse-c-encrypted-bucket/secret-file.txt" \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "$BASE64_KEY" \
  --sse-customer-key-md5 "$MD5_KEY" \
  --copy-source-sse-customer-algorithm AES256 \
  --copy-source-sse-customer-key "$BASE64_KEY" \
  --copy-source-sse-customer-key-md5 "$MD5_KEY"
{
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "qTQAjYHjCvO+vfQwrK4l8Q==",
    "CopyObjectResult": {
        "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
        "LastModified": "2025-06-15T03:12:23+00:00",
        "ChecksumCRC64NVME": "QtYNnUr9Eos="
    }
}

Fetch copied file meta.


(myenv) $ aws s3api head-object \
  --bucket sse-c-encrypted-bucket \
  --key copy_of_secret-file.txt \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "$BASE64_KEY" \
  --sse-customer-key-md5 "$MD5_KEY"
{
    "AcceptRanges": "bytes",
    "LastModified": "2025-06-15T03:12:23+00:00",
    "ContentLength": 31,
    "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {},
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "qTQAjYHjCvO+vfQwrK4l8Q=="
}

Try downloading the file without key.


(myenv) $ aws s3api get-object \
  --bucket sse-c-encrypted-bucket \
  --key secret-file.txt \
  downloaded.txt

An error occurred (InvalidRequest) when calling the GetObject operation: The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.

Now download file with the customer key.


(myenv) $ aws s3api get-object \
  --bucket sse-c-encrypted-bucket \
  --key secret-file.txt \
  downloaded.txt \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "$BASE64_KEY" \
  --sse-customer-key-md5 "$MD5_KEY"
{
    "AcceptRanges": "bytes",
    "LastModified": "2025-06-15T00:25:54+00:00",
    "ContentLength": 31,
    "ETag": "\"5c1c7463b3b94cd0e204abf746a42671\"",
    "ChecksumCRC64NVME": "QtYNnUr9Eos=",
    "ChecksumType": "FULL_OBJECT",
    "ContentType": "binary/octet-stream",
    "Metadata": {},
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "MFY4ZCjuzXvBZfIbk2LeMQ=="
}

Cleanup now:


(myenv) $ aws s3 rm s3://sse-c-encrypted-bucket --recursive
delete: s3://sse-c-encrypted-bucket/copy_of_secret-file.txt
delete: s3://sse-c-encrypted-bucket/secret-file.txt

(myenv) $ aws s3 rb s3://sse-c-encrypted-bucket
remove_bucket: sse-c-encrypted-bucket

Using Python



Here is the Python implementation of the above.

File: sse-c.py


#!/usr/bin/env python3

import os
import boto3
import base64
import hashlib
import argparse
from botocore.config import Config
from botocore.exceptions import ClientError

ENDPOINT_URL         = "http://localhost:4566"
ACCESS_KEY_ID        = "test"
SECRET_ACCESS_KEY    = "test"
REGION               = "eu-west-1"
DEFAULT_FILE         = 'secret-file.txt'
DEFAULT_BUCKET       = 'sse-c-encrypted-bucket'
DEFAULT_CUSTOMER_KEY = 'CustomerSecretKey'

class MyArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        if message:
            sys.stderr.write(f"ERROR: {message}\n\n")
        self.print_help()
        sys.exit(2)

def create_s3_client():
    return boto3.client(
        's3',
        endpoint_url = ENDPOINT_URL,
        aws_access_key_id = ACCESS_KEY_ID,
        aws_secret_access_key = SECRET_ACCESS_KEY,
        config = Config(signature_version='s3v4'),
        region_name = REGION
    )

def generate_encryption_key(customer_key):
    return hashlib.sha256(customer_key.encode("utf-8")).digest()

def get_sse_c_headers(enc_key):
    return {
        'SSECustomerAlgorithm': 'AES256',
        'SSECustomerKey': base64.b64encode(enc_key).decode('utf-8'),
        'SSECustomerKeyMD5': base64.b64encode(hashlib.md5(enc_key).digest()).decode('utf-8'),
    }

def make_bucket(s3, bucket_name):
    try:
        s3.create_bucket(Bucket=bucket_name,
            CreateBucketConfiguration={
                'LocationConstraint': REGION
            })
        print(f"Bucket {bucket_name} created.")
    except Exception as e:
        print(f"Error creating bucket: {e}")

def upload_file(s3, bucket_name, file, enc_key):
    try:
        headers = get_sse_c_headers(enc_key)

        s3.put_object(
            Bucket = bucket_name,
            Key = file,
            Body = open(file, 'r', encoding='utf-8').read().encode('utf-8'),
            **headers
        )
        print(f"Uploaded {file} to {bucket_name}.")
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidRequest':
            print(f"Failed to upload file with key: {e}")
    except Exception as e:
        print(f"Error uploading file with key: {e}")

def copy_file(s3, bucket_name, file, enc_key):
    try:
        headers = get_sse_c_headers(enc_key)
        s3.copy_object(
            Bucket=bucket_name,
            Key=f"copy_of_{file}",
            CopySource=f"{bucket_name}/{file}",
            SSECustomerAlgorithm=headers['SSECustomerAlgorithm'],
            SSECustomerKey=headers['SSECustomerKey'],
            SSECustomerKeyMD5=headers['SSECustomerKeyMD5'],
            CopySourceSSECustomerAlgorithm=headers['SSECustomerAlgorithm'],
            CopySourceSSECustomerKey=headers['SSECustomerKey'],
            CopySourceSSECustomerKeyMD5=headers['SSECustomerKeyMD5'],
        )
        print(f"Successfully copied to 'copy_of_{file}' with maintained encryption.")
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidRequest':
            print(f"Failed to copy without key: {e}")
    except Exception as e:
        print(f"Error copying file with key: {e}")

def download_file(s3, bucket_name, file, enc_key):
    try:
        headers = get_sse_c_headers(enc_key)
        response = s3.get_object(
            Bucket = bucket_name,
            Key = file,
            **headers
        )
        content = response['Body'].read().decode('utf-8')
        print(f"Downloaded content: {content}")
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidRequest':
            print(f"Failed to downloaded with key: {e}")
    except Exception as e:
        print(f"Error downloading file with key: {e}")

def download_file_without_key(s3, bucket_name, file):
    try:
        s3.get_object(
            Bucket = bucket_name,
            Key = file
        )
        print(f"ERROR: downloaded successfully without key.")
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidRequest':
            print(f"Failed to downloaded without key: {e}")
    except Exception as e:
        print(f"Error downloading file without key: {e}")

def fetch_file_meta(s3, bucket_name, file, enc_key):
    try:
        headers = get_sse_c_headers(enc_key)

        response = s3.head_object(
            Bucket = bucket_name,
            Key = file,
            **headers
        )
        print("Object metadata:")
        print(f"ETag: {response['ETag']}")
        print(f"Size: {response['ContentLength']} bytes")
        print(f"Encryption: {response.get('ServerSideEncryption', 'None')}")
        print(f"SSE-C Algorithm: {response.get('SSECustomerAlgorithm', 'None')}")
        print(f"SSE-C Key MD5: {response.get('SSECustomerKeyMD5', 'None')}")
    except ClientError as e:
        if e.response['Error']['Code'] == 'InvalidRequest':
            print(f"Failed to retrieving file metadata: {e}")
    except Exception as e:
        print(f"Error fetching file meta: {e}")

def cleanup(s3, bucket_name):
    try:
        objects = s3.list_objects_v2(Bucket=bucket_name)
        if 'Contents' in objects:
            for obj in objects['Contents']:
                s3.delete_object(Bucket=bucket_name, Key=obj['Key'])

        s3.delete_bucket(Bucket=bucket_name)
        print(f"Bucket '{bucket_name}' deleted successfully.")
    except Exception as e:
        print(f"Error deleting bucket: {e}")

def main():
    parser = MyArgumentParser(description="SSE-C Encryption")

    # Regular arguments
    regular_args = [
        ("--bucket", DEFAULT_BUCKET, f"Bucket name, default {DEFAULT_BUCKET}"),
        ("--file", DEFAULT_FILE, f"File to upload/download, default {DEFAULT_FILE}"),
        ('--customer-key', DEFAULT_CUSTOMER_KEY, "Customer key, default {DEFAULT_CUSTOMER_KEY}"),
    ]

    for flag, default, help_text, *type_hint in regular_args:
        kwargs = {"help": help_text}
        if default is not None:
            kwargs["default"] = default
        parser.add_argument(flag, **kwargs)

    # Mutually exclusive action flags
    action_group = parser.add_mutually_exclusive_group(required=True)
    action_flags = [
        ("--make-bucket", "Create bucket"),
        ("--upload", "Upload file"),
        ("--copy-file", "Copy file"),
        ("--download", "Download file"),
        ("--fetch-file-meta", "Fetch file metadata"),
        ("--cleanup", "Remove bucket and its content"),
    ]

    for flag, help_text in action_flags:
        action_group.add_argument(flag, action='store_true', help=help_text)

    args    = parser.parse_args()
    s3      = create_s3_client()
    enc_key = generate_encryption_key(args.customer_key)

    if args.make_bucket:
        make_bucket(s3, args.bucket)

    if args.upload:
        upload_file(s3, args.bucket, args.file, enc_key)

    if args.copy_file:
        copy_file(s3, args.bucket, args.file, enc_key)

    if args.download:
        print(f"Downloading {args.file} with key")
        download_file(s3, args.bucket, args.file, enc_key)
        print(f"Downloading {args.file} without key")
        download_file_without_key(s3, args.bucket, args.file)

    if args.fetch_file_meta:
        fetch_file_meta(s3, args.bucket, args.file, enc_key)

    if args.cleanup:
        cleanup(s3, args.bucket)

if __name__ == '__main__':
    main()

Test the code now:


(myenv) $ py sse-c.py --help
usage: sse-c.py [-h] [--bucket BUCKET] [--file FILE] [--customer-key CUSTOMER_KEY] (--make-bucket | --upload | --copy-file | --download | --fetch-file-meta | --cleanup)

SSE-C Encryption

options:
  -h, --help            show this help message and exit
  --bucket BUCKET       Bucket name, default sse-c-encrypted-bucket
  --file FILE           File to upload/download, default secret-file.txt
  --customer-key CUSTOMER_KEY
                        Customer key, default {DEFAULT_CUSTOMER_KEY}
  --make-bucket         Create bucket
  --upload              Upload file
  --copy-file           Copy file
  --download            Download file
  --fetch-file-meta     Fetch file metadata
  --cleanup             Remove bucket and its content

Create bucket


$ py sse-c.py --bucket sse-c-encrypted-bucket --make-bucket
Bucket sse-c-encrypted-bucket created.

Upload file to bucket


$ py sse-c.py --bucket sse-c-encrypted-bucket --file secret-file.txt --upload
Uploaded secret-file.txt to sse-c-encrypted-bucket.

Fetch file metadata


$ py sse-c.py --bucket sse-c-encrypted-bucket --file secret-file.txt --fetch-file-meta
Object metadata:
ETag: "5c1c7463b3b94cd0e204abf746a42671"
Size: 31 bytes
Encryption: AES256
SSE-C Algorithm: AES256
SSE-C Key MD5: dovR6EvoX5kk8922+aAvNQ==

Copy file with key


$ py sse-c.py --bucket sse-c-encrypted-bucket --file secret-file.txt --copy-file
Successfully copied to 'copy_of_secret-file.txt' with maintained encryption.

Download file locally with key and without key


$ py sse-c.py --bucket sse-c-encrypted-bucket --file secret-file.txt --download
Downloading secret-file.txt with key
Downloaded content: This is a secret file content.

Downloading secret-file.txt without key
Failed to downloaded without key.

Cleanup in the end


$ py sse-c.py --bucket sse-c-encrypted-bucket --cleanup
Bucket 'sse-c-encrypted-bucket' deleted successfully.

Using Perl



Now, Perl implementation:

File: sse-c.pl


#!/usr/bin/env perl

use v5.38;
use Encode;
use Try::Tiny;
use File::Slurp;
use Getopt::Long;
use File::Basename;
use Digest::MD5 qw(md5);
use Digest::SHA qw(sha256);
use MIME::Base64 qw(encode_base64 decode_base64);

use Paws;
use Paws::Credential::Explicit;

my $REGION               = 'eu-west-1';
my $ENDPOINT             = 'http://localhost:4566';
my $ACCESS_KEY           = 'test';
my $SECRET_KEY           = 'test';
my $DEFAULT_FILE         = 'secret-file.txt';
my $DEFAULT_BUCKET       = 'sse-c-encrypted-bucket';
my $DEFAULT_CUSTOMER_KEY = 'CustomerSecretKey';

my %opts;
GetOptions(
    'bucket=s'          => \$opts{bucket},
    'file=s'            => \$opts{file},
    'customer-key=s'    => \$opts{customer_key},
    'make-bucket'       => \$opts{make_bucket},
    'upload'            => \$opts{upload},
    'copy-file'         => \$opts{copy_file},
    'download'          => \$opts{download},
    'fetch-file-meta'   => \$opts{fetch_file_meta},
    'cleanup'           => \$opts{cleanup},
    'help'              => \$opts{help},
) or show_help_and_exit(1);

show_help_and_exit(0) if $opts{help};

# Enforce only one action is selected
my @actions = qw/
    make_bucket
    upload
    copy_file
    download
    fetch_file_meta
    cleanup
/;

my $count   = grep { defined $opts{$_} } @actions;
show_help_and_exit(0, "You must specify exactly one of: --" . join(', --', @actions))
    unless $count == 1;

$opts{bucket}       = $DEFAULT_BUCKET       unless defined $opts{bucket};
$opts{file}         = $DEFAULT_FILE         unless defined $opts{file};
$opts{customer_key} = $DEFAULT_CUSTOMER_KEY unless defined $opts{customer_key};

my $s3 = create_s3_client();
my $enc_key = generate_encryption_key($opts{customer_key});

if ($opts{make_bucket}) {
    make_bucket($s3, $opts{bucket});
}

if ($opts{upload}) {
    upload_file($s3, $opts{bucket}, $opts{file}, $enc_key);
}

if ($opts{copy_file}) {
    copy_file($s3, $opts{bucket}, $opts{file}, $enc_key);
}

if ($opts{download}) {
    download_file($s3, $opts{bucket}, $opts{file}, $enc_key);
    download_file_without_enc_key($s3, $opts{bucket}, $opts{file});
}

if ($opts{fetch_file_meta}) {
    fetch_file_meta($s3, $opts{bucket}, $opts{file}, $enc_key);
}

if ($opts{cleanup}) {
    cleanup($s3, $opts{bucket});
}

#
#
# HELPER SUBROUTINES

sub show_help_and_exit($exit_code) {

    say <<"END_HELP";
usage: $0 [--help] [--bucket BUCKET] [--file FILE]
          [--customer-key CUSTOMER_KEY]
          [--make-bucket]
          [--upload] [--copy-file] [--download]
          [--fetch-file-meta] [--cleanup]

SSE-C Encryption

options:
  --help                      Show this help message and exit
  --bucket BUCKET             Bucket name, default $DEFAULT_BUCKET
  --file FILE                 File to upload/download, default $DEFAULT_FILE
  --customer-key CUSTOMER_KEY Customer key, default $DEFAULT_CUSTOMER_KEY
  --make-bucket               Create bucket
  --upload                    Upload file
  --copy-file                 Copy file
  --download                  Download file
  --fetch-file-meta           Fetch file metadata
  --cleanup                   Remove file and bucket
END_HELP

    exit $exit_code;
}

sub error_and_die($message) {
    my $level = 1;
    my ($sub_name, $line);

    while (1) {
        my @caller = caller($level++) or last;
        ($sub_name, $line) = @caller[3,2];
        last unless $sub_name =~ /^(?:Try::Tiny|eval|catch|__ANON__|try|main::)/;
    }

    my $clean_error = $message;
    $clean_error =~ s/\s+at\s+.*?line\s+\d+\.?$//;

    die "ERROR in $sub_name (at line $line): $clean_error\n";
}

sub generate_encryption_key($customer_key) {
    return sha256(Encode::encode_utf8($customer_key));
}

sub create_s3_client {
    return Paws->service('S3',
        region      => $REGION,
        endpoint    => $ENDPOINT,
        credentials => Paws::Credential::Explicit->new(
            access_key => $ACCESS_KEY,
            secret_key => $SECRET_KEY,
        ),
    );
}

sub make_bucket($s3, $bucket_name) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;

    try {
        $s3->CreateBucket(
            Bucket => $bucket_name,
            CreateBucketConfiguration => { LocationConstraint => $REGION },
        );

        say "Bucket $bucket_name created.";
    }
    catch {
        die "ERROR: unable to create bucket: $_\n";
    };
}

sub upload_file($s3, $bucket_name, $file, $enc_key) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;
    error_and_die("Missing file.")        unless defined $file;
    error_and_die("Missing enc key.")     unless defined $enc_key;

    try {
        my $key_b64 = encode_base64($enc_key, '');
        my $md5_raw = md5($enc_key);
        my $md5_b64 = encode_base64($md5_raw, '');

        $s3->PutObject(
            Bucket => $bucket_name,
            Key    => $file,
            Body   => read_file($file, binmode => ':raw'),
            SSECustomerAlgorithm => 'AES256',
            SSECustomerKey       => $key_b64,
            SSECustomerKeyMD5    => $md5_b64,
        );
        say "File '$file' uploaded with SSE-C encryption.";
    }
    catch {
        die "Error uploading file with SSE-C encryption: $_";
    };
}

sub copy_file($s3, $bucket_name, $file, $enc_key) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;
    error_and_die("Missing file.")        unless defined $file;
    error_and_die("Missing enc key.")     unless defined $enc_key;

    try {
        my $key_b64 = encode_base64($enc_key, '');
        my $md5_raw = md5($enc_key);
        my $md5_b64 = encode_base64($md5_raw, '');

        $s3->CopyObject(
            Bucket     => $bucket_name,
            Key        => "copy_of_$file",
            CopySource => "$bucket_name/$file",
            CopySourceSSECustomerAlgorithm => 'AES256',
            CopySourceSSECustomerKey       => $key_b64,
            CopySourceSSECustomerKeyMD5    => $md5_b64,
            SSECustomerAlgorithm           => 'AES256',
            SSECustomerKey                 => $key_b64,
            SSECustomerKeyMD5              => $md5_b64,
        );
        say "Successfully copied to 'copy_of_$file' with maintained encryption.";
    }
    catch {
        die "Error copying file with SSE-C encryption: $_";
    };
}

sub download_file($s3, $bucket_name, $file, $enc_key) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;
    error_and_die("Missing file.")        unless defined $file;
    error_and_die("Missing enc key.")     unless defined $enc_key;

    try {
        my $key_b64 = encode_base64($enc_key, '');
        my $md5_raw = md5($enc_key);
        my $md5_b64 = encode_base64($md5_raw, '');

        my $response = $s3->GetObject(
            Bucket               => $bucket_name,
            Key                  => $file,
            SSECustomerAlgorithm => 'AES256',
            SSECustomerKey       => $key_b64,
            SSECustomerKeyMD5    => $md5_b64,
        );

        my $content;
        if (ref $response->Body eq 'SCALAR') {
            $content = ${ $response->Body };
        } elsif (ref $response->Body && $response->Body->can('getline')) {
            local $/;
            $content = $response->Body->getline();
        } else {
            $content = $response->Body;  # fallback
        }

        say "Successfully downloaded encrypted object:\n$content";
    }
    catch {
        die "ERROR: unable to download file with enc key: $_\n";
    };
}

sub download_file_without_enc_key($s3, $bucket_name, $file) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;
    error_and_die("Missing file.")        unless defined $file;

    try {
        my $response = $s3->GetObject(
            Bucket => $bucket_name,
            Key    => $file,
        );
        say "Unexpected success downloading without SSE-C key.";
    }
    catch {
        say "Expected failure when downloading without SSE-C key.";
    };
}

sub fetch_file_meta($s3, $bucket_name, $file, $enc_key) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;
    error_and_die("Missing file.")        unless defined $file;
    error_and_die("Missing enc key.")     unless defined $enc_key;

    try {
        my $key_b64 = encode_base64($enc_key, '');
        my $md5_raw = md5($enc_key);
        my $md5_b64 = encode_base64($md5_raw, '');

        my $resp = $s3->HeadObject(
            Bucket => $bucket_name,
            Key    => $file,
            SSECustomerAlgorithm => 'AES256',
            SSECustomerKey       => $key_b64,
            SSECustomerKeyMD5    => $md5_b64,
        );

        say "Object metadata:";
        say "ETag: " . $resp->ETag;
        say "Size: " . $resp->ContentLength . " bytes";
        say "Encryption: " . ($resp->ServerSideEncryption // 'None');
        say "SSE-C Algorithm: " . ($resp->SSECustomerAlgorithm // 'None');
        say "SSE-C Key MD5: " . ($resp->SSECustomerKeyMD5 // 'None');
    }
    catch {
        die "ERROR: unable to view file meta: $_\n";
    };
}

sub cleanup($s3, $bucket_name) {
    error_and_die("Missing S3 client.")   unless defined $s3;
    error_and_die("Missing bucket name.") unless defined $bucket_name;

    try {
        my $resp = $s3->ListObjectsV2(Bucket => $bucket_name);
        for my $obj (@{ $resp->Contents }) {
            say " Delete key: ", $obj->{Key};
            $s3->DeleteObject(Bucket => $bucket_name, Key => $obj->{Key});
        }
        $s3->DeleteBucket(Bucket => $bucket_name);
        say "Bucket '$bucket_name' deleted successfully.";
    }
    catch {
        die "ERROR: unable to cleanup: $_\n";
    };
}

Test the code now:


$ perl sse-c.pl --help
usage: sse-c.pl [--help] [--bucket BUCKET] [--file FILE]
          [--customer-key CUSTOMER_KEY]
          [--make-bucket]
          [--upload] [--copy-file] [--download]
          [--fetch-file-meta] [--cleanup]

SSE-C Encryption

options:
  --help                      Show this help message and exit
  --bucket BUCKET             Bucket name, default sse-c-encrypted-bucket
  --file FILE                 File to upload/download, default secret-file.txt
  --customer-key CUSTOMER_KEY Customer key, default CustomerSecretKey
  --make-bucket               Create bucket
  --upload                    Upload file
  --copy-file                 Copy file
  --download                  Download file
  --fetch-file-meta           Fetch file metadata
  --cleanup                   Remove file and bucket

Create bucket


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --make-bucket
Bucket sse-c-encrypted-bucket created.

Upload file to bucket


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --file secret-file.txt --upload
File 'secret-file.txt' uploaded with SSE-C encryption.

Fetch file metadata


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --file secret-file.txt --fetch-file-meta
Object metadata:
ETag: "5c1c7463b3b94cd0e204abf746a42671"
Size: 31 bytes
Encryption: None
SSE-C Algorithm: AES256
SSE-C Key MD5: dovR6EvoX5kk8922+aAvNQ==

Copy file with key


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --file secret-file.txt --copy-file
Successfully copied to 'copy_of_secret-file.txt' with maintained encryption.

Download file locally with key and without key


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --file secret-file.txt --download
Successfully downloaded encrypted object:
This is a secret file content.

Expected failure when downloading without SSE-C key.

Cleanup in the end


$ perl sse-c.pl --bucket sse-c-encrypted-bucket --cleanup
 Delete key: copy_of_secret-file.txt
 Delete key: secret-file.txt
Bucket 'sse-c-encrypted-bucket' deleted successfully.

What next?


The next post would be about AWS S3 Client-Side Encryption.



Happy Hacking !!!

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me