Instantly share code, notes, and snippets.
Created
March 25, 2026 07:36
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save santrancisco/5d98d4ba5da30323a06653a3c7d6f9ea to your computer and use it in GitHub Desktop.
fileshare s3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # kms_s3_upload_utils.sh | |
| # Utilities for KMS public-key retrieval, S3 presigned URL generation, | |
| # and envelope-encrypted file upload. | |
| # ============================================================================= | |
| set -euo pipefail | |
| # --------------------------------------------------------------------------- | |
| # PART 1 — generate_upload_credentials | |
| # Pulls the public key from a KMS asymmetric key and generates a presigned | |
| # S3 PUT URL, then returns both as a JSON payload. | |
| # | |
| # Usage: | |
| # generate_upload_credentials <kms_key_arn> <s3_bucket> <s3_object_key> [ttl_seconds] | |
| # | |
| # Example: | |
| # generate_upload_credentials \ | |
| # "arn:aws:kms:ap-southeast-1:123456789012:key/mrk-abc123" \ | |
| # "my-secure-bucket" \ | |
| # "uploads/payload.enc" \ | |
| # 3600 | |
| # --------------------------------------------------------------------------- | |
| generate_upload_credentials() { | |
| local kms_arn="${1:?KMS ARN required}" | |
| local bucket="${2:?S3 bucket required}" | |
| local object_key="${3:?S3 object key required}" | |
| local ttl="${4:-3600}" # default 1 hour | |
| # --- 1a. Fetch the public key from KMS ----------------------------------- | |
| local kms_response | |
| kms_response=$(aws kms get-public-key \ | |
| --key-id "$kms_arn" \ | |
| --output json) | |
| # The public key is returned as a base64-encoded DER blob. | |
| local pubkey_b64 | |
| pubkey_b64=$(echo "$kms_response" | jq -r '.PublicKey') | |
| local key_spec | |
| key_spec=$(echo "$kms_response" | jq -r '.KeySpec') | |
| local key_usage | |
| key_usage=$(echo "$kms_response" | jq -r '.KeyUsage') | |
| # Convert base64-DER → PEM so openssl can consume it later. | |
| local pubkey_pem | |
| pubkey_pem=$(printf '%s\n' \ | |
| "-----BEGIN PUBLIC KEY-----" \ | |
| "$(echo "$pubkey_b64" | fold -w 64)" \ | |
| "-----END PUBLIC KEY-----") | |
| # --- 1b. Generate presigned S3 PUT URL ----------------------------------- | |
| # Resolve bucket region: env var takes priority, otherwise auto-detect. | |
| local bucket_region="${S3_BUCKET_REGION:-}" | |
| if [[ -z "$bucket_region" ]]; then | |
| bucket_region=$(aws s3api get-bucket-location \ | |
| --bucket "$bucket" \ | |
| --query 'LocationConstraint' \ | |
| --output text) | |
| # us-east-1 returns "None" from get-bucket-location | |
| [[ "$bucket_region" == "None" ]] && bucket_region="us-east-1" | |
| # echo "[info] Auto-detected bucket region: $bucket_region (set S3_BUCKET_REGION to override)" | |
| fi | |
| local presigned_url | |
| presigned_url=$(AWS_DEFAULT_REGION="$bucket_region" aws s3 presign \ | |
| "s3://${bucket}/${object_key}" \ | |
| --expires-in "$ttl") | |
| # --- 1c. Assemble JSON response ------------------------------------------ | |
| jq -n \ | |
| --arg kms_arn "$kms_arn" \ | |
| --arg key_spec "$key_spec" \ | |
| --arg key_usage "$key_usage" \ | |
| --arg pubkey_pem "$pubkey_pem" \ | |
| --arg url "$presigned_url" \ | |
| --arg bucket "$bucket" \ | |
| --arg object_key "$object_key" \ | |
| --arg region "$bucket_region" \ | |
| --argjson ttl "$ttl" \ | |
| '{ | |
| kms_arn: $kms_arn, | |
| key_spec: $key_spec, | |
| key_usage: $key_usage, | |
| public_key_pem: $pubkey_pem, | |
| upload: { | |
| presigned_url: $url, | |
| bucket: $bucket, | |
| object_key: $object_key, | |
| region: $region, | |
| expires_in_seconds: $ttl | |
| } | |
| }' | |
| } | |
| # --------------------------------------------------------------------------- | |
| # PART 2 — encrypt_and_upload | |
| # Reads the JSON produced by generate_upload_credentials, performs | |
| # envelope encryption on a local file, and uploads the result to S3. | |
| # | |
| # Envelope encryption layout | |
| # ┌─────────────────────────────────────────────┐ | |
| # │ envelope.enc │ | |
| # │ ├── encrypted_dek.bin (RSA-OAEP wrapped) │ | |
| # │ ├── iv.bin (AES-256-CBC IV) │ | |
| # │ └── ciphertext.bin (AES-256-CBC data) │ | |
| # └─────────────────────────────────────────────┘ | |
| # All three are tar'd into a single .enc.tar before upload. | |
| # | |
| # Usage: | |
| # encrypt_and_upload <credentials_json_file> <plaintext_file> | |
| # | |
| # Example: | |
| # encrypt_and_upload credentials.json secret_document.pdf | |
| # --------------------------------------------------------------------------- | |
| encrypt_and_upload() { | |
| local creds_file="${1:?Credentials JSON file required}" | |
| local plaintext_file="${2:?Plaintext file required}" | |
| [[ -f "$creds_file" ]] || { echo "ERROR: credentials file not found: $creds_file"; exit 1; } | |
| [[ -f "$plaintext_file" ]] || { echo "ERROR: plaintext file not found: $plaintext_file"; exit 1; } | |
| # --- 2a. Parse credentials ----------------------------------------------- | |
| local pubkey_pem bucket object_key bucket_region | |
| pubkey_pem=$(jq -r '.public_key_pem' "$creds_file") | |
| bucket=$(jq -r '.upload.bucket' "$creds_file") | |
| object_key=$(jq -r '.upload.object_key' "$creds_file") | |
| bucket_region=$(jq -r '.upload.region' "$creds_file") | |
| [[ "$pubkey_pem" != "null" ]] || { echo "ERROR: public_key_pem missing in credentials"; exit 1; } | |
| [[ "$bucket" != "null" ]] || { echo "ERROR: upload.bucket missing in credentials"; exit 1; } | |
| [[ "$object_key" != "null" ]] || { echo "ERROR: upload.object_key missing in credentials"; exit 1; } | |
| [[ "$bucket_region" != "null" ]] || { echo "ERROR: upload.region missing in credentials"; exit 1; } | |
| # --- 2b. Create a secure temp workspace ---------------------------------- | |
| local tmpdir="" | |
| tmpdir=$(mktemp -d) | |
| trap '[[ -n "$tmpdir" ]] && rm -rf "$tmpdir"' EXIT | |
| local pem_file="$tmpdir/recipient_public.pem" | |
| local dek_file="$tmpdir/dek.bin" # plaintext DEK (32 bytes) | |
| local enc_dek_file="$tmpdir/encrypted_dek.bin" | |
| local iv_file="$tmpdir/iv.bin" # AES IV (16 bytes) | |
| local ciphertext_file="$tmpdir/ciphertext.bin" | |
| local bundle_file="$tmpdir/envelope.enc.tar" | |
| echo "$pubkey_pem" > "$pem_file" | |
| # --- 2c. Generate a random 256-bit Data Encryption Key (DEK) + IV -------- | |
| echo "[1/5] Generating DEK and IV..." | |
| openssl rand 32 > "$dek_file" | |
| openssl rand 16 > "$iv_file" | |
| # --- 2d. Wrap the DEK with the KMS RSA public key (RSA-OAEP SHA-256) ----- | |
| echo "[2/5] Encrypting DEK with KMS public key (RSA-OAEP)..." | |
| openssl pkeyutl \ | |
| -encrypt \ | |
| -pubin \ | |
| -inkey "$pem_file" \ | |
| -in "$dek_file" \ | |
| -out "$enc_dek_file" \ | |
| -pkeyopt rsa_padding_mode:oaep \ | |
| -pkeyopt rsa_oaep_md:sha256 \ | |
| -pkeyopt rsa_mgf1_md:sha256 | |
| # --- 2e. Encrypt the plaintext file with AES-256-CBC --------------------- | |
| echo "[3/5] Encrypting file with AES-256-CBC..." | |
| openssl enc -aes-256-cbc -nosalt \ | |
| -in "$plaintext_file" \ | |
| -out "$ciphertext_file" \ | |
| -K "$(xxd -p -c 256 "$dek_file")" \ | |
| -iv "$(xxd -p -c 256 "$iv_file")" | |
| # Wipe the plaintext DEK from disk immediately after use | |
| openssl rand 32 > "$dek_file" && rm -f "$dek_file" | |
| # --- 2f. Bundle all envelope parts into a single tar archive ------------- | |
| echo "[4/5] Bundling envelope..." | |
| tar -cf "$bundle_file" \ | |
| -C "$tmpdir" \ | |
| encrypted_dek.bin iv.bin ciphertext.bin | |
| local bundle_size | |
| bundle_size=$(wc -c < "$bundle_file" | tr -d ' ') | |
| echo " Bundle size: ${bundle_size} bytes" | |
| # --- 2g. Upload using AWS CLI — avoids ALL presigned URL signature issues | |
| # aws s3 cp handles SigV4 signing internally, so there's no risk of curl | |
| # adding unexpected headers (Content-Type, Transfer-Encoding, etc.) that | |
| # weren't included in the presigned signature. | |
| echo "[5/5] Uploading to S3..." | |
| if aws s3 cp "$bundle_file" "s3://${bucket}/${object_key}" \ | |
| --region "$bucket_region" \ | |
| --content-type "application/octet-stream"; then | |
| echo "" | |
| echo "✅ Upload successful" | |
| echo " Object: s3://${bucket}/${object_key}" | |
| else | |
| echo "❌ Upload failed" | |
| exit 1 | |
| fi | |
| if [[ "$http_status" == "200" ]]; then | |
| echo "" | |
| echo "✅ Upload successful (HTTP $http_status)" | |
| echo " Object: $(jq -r '.upload.bucket' "$creds_file")/$(jq -r '.upload.object_key' "$creds_file")" | |
| else | |
| echo "❌ Upload failed (HTTP $http_status)" | |
| exit 1 | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # PART 3 — download_and_decrypt | |
| # Downloads the envelope bundle from S3, uses KMS to unwrap the DEK, | |
| # then decrypts the payload with openssl. | |
| # | |
| # Usage: | |
| # download_and_decrypt <kms_key_arn> <s3_bucket> <s3_object_key> <output_file> | |
| # | |
| # Example: | |
| # download_and_decrypt \ | |
| # "arn:aws:kms:ap-southeast-1:123456789012:key/mrk-abc123" \ | |
| # "my-secure-bucket" \ | |
| # "uploads/payload.enc.tar" \ | |
| # decrypted_output.pdf | |
| # --------------------------------------------------------------------------- | |
| download_and_decrypt() { | |
| local kms_arn="${1:?KMS ARN required}" | |
| local bucket="${2:?S3 bucket required}" | |
| local object_key="${3:?S3 object key required}" | |
| local output_file="${4:?Output file path required}" | |
| local tmpdir="" | |
| tmpdir=$(mktemp -d) | |
| trap '[[ -n "$tmpdir" ]] && rm -rf "$tmpdir"' EXIT | |
| local bundle_file="$tmpdir/envelope.enc.tar" | |
| local enc_dek_file="$tmpdir/encrypted_dek.bin" | |
| local iv_file="$tmpdir/iv.bin" | |
| local ciphertext_file="$tmpdir/ciphertext.bin" | |
| local dek_file="$tmpdir/dek.bin" | |
| # --- 3a. Download the bundle from S3 ------------------------------------- | |
| echo "[1/4] Downloading from S3..." | |
| aws s3 cp "s3://${bucket}/${object_key}" "$bundle_file" | |
| # --- 3b. Extract the three envelope components --------------------------- | |
| echo "[2/4] Extracting envelope..." | |
| tar -xf "$bundle_file" -C "$tmpdir" | |
| for f in encrypted_dek.bin iv.bin ciphertext.bin; do | |
| [[ -f "$tmpdir/$f" ]] || { echo "ERROR: missing $f in bundle"; exit 1; } | |
| done | |
| # --- 3c. Unwrap the DEK via KMS Decrypt ---------------------------------- | |
| echo "[3/4] Decrypting DEK via KMS..." | |
| # KMS expects the ciphertext as base64; we pipe the raw binary through base64. | |
| local enc_dek_b64 | |
| enc_dek_b64=$(base64 < "$enc_dek_file") | |
| aws kms decrypt \ | |
| --key-id "$kms_arn" \ | |
| --ciphertext-blob "fileb://$enc_dek_file" \ | |
| --encryption-algorithm RSAES_OAEP_SHA_256 \ | |
| --query 'Plaintext' \ | |
| --output text \ | |
| | base64 --decode > "$dek_file" | |
| # --- 3d. Decrypt the ciphertext with AES-256-CBC ------------------------- | |
| echo "[4/4] Decrypting payload..." | |
| openssl enc -d -aes-256-cbc -nosalt \ | |
| -in "$ciphertext_file" \ | |
| -out "$output_file" \ | |
| -K "$(xxd -p -c 256 "$dek_file")" \ | |
| -iv "$(xxd -p -c 256 "$iv_file")" | |
| # Wipe DEK immediately | |
| openssl rand 32 > "$dek_file" && rm -f "$dek_file" | |
| echo "" | |
| echo "✅ Decryption successful → $output_file" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # CLI entrypoint | |
| # --------------------------------------------------------------------------- | |
| usage() { | |
| cat <<EOF | |
| Usage: | |
| $0 gen-creds <kms_key_arn> <s3_bucket> <s3_object_key> [ttl_seconds] | |
| $0 encrypt <credentials_json_file> <plaintext_file> | |
| $0 decrypt <kms_key_arn> <s3_bucket> <s3_object_key> <output_file> | |
| Examples: | |
| # Step 1 — generate credentials and save to file | |
| $0 gen-creds \\ | |
| arn:aws:kms:ap-southeast-1:123456789012:key/mrk-abc \\ | |
| my-bucket uploads/payload.enc.tar 3600 > credentials.json | |
| # Step 2 — encrypt a file and upload it | |
| $0 encrypt credentials.json ./sensitive_data.pdf | |
| # Step 3 — download and decrypt on the receiving end | |
| $0 decrypt \\ | |
| arn:aws:kms:ap-southeast-1:123456789012:key/mrk-abc \\ | |
| my-bucket uploads/payload.enc.tar \\ | |
| ./recovered_output.pdf | |
| EOF | |
| exit 1 | |
| } | |
| case "${1:-}" in | |
| gen-creds) shift; generate_upload_credentials "$@" ;; | |
| encrypt) shift; encrypt_and_upload "$@" ;; | |
| decrypt) shift; download_and_decrypt "$@" ;; | |
| *) usage ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment