Backbencher.dev

How To Write a File to AWS S3 Using Pure Node.js

Last updated on 29 Jan, 2024

If we are using Node.js, there is already an AWS SDK that can write a file to S3.

This article explains how to do it without the SDK. The generic steps to be followed are given in this AWS documentation.

For me it took a while and help from various other articles to finally implement the same using Node.js. Main challenge was in finding the right encryption methods.

Packages

We need crypto package to make use of different hashing algorithms. We need axios to make API requests.

const crypto = require("crypto");
import axios from "axios";

Function to Caculate HMAC SHA256

This function comes handy to do the hmac encryption with a key.

async function sign(key, msg) {
  // Convert the key and data to ArrayBuffer
  let keyBuffer = key;
  if (typeof key === "string") {
    keyBuffer = new TextEncoder().encode(key);
  }
  const dataBuffer = new TextEncoder().encode(msg);

  // Import the key
  const importedKey = await crypto.subtle.importKey(
    "raw",
    keyBuffer,
    { name: "HMAC", hash: { name: "SHA-256" } },
    false,
    ["sign"]
  );

  // Sign the data
  const signature = await crypto.subtle.sign(
    { name: "HMAC", hash: "SHA-256" },
    importedKey,
    dataBuffer
  );

  return Buffer.from(signature);
  //console.log(crypto.createHmac('sha256', key).update(msg, 'utf8').digest())
}

Function to Caculate SHA256

async function contentHash(content) {
  const msgUint8 = new TextEncoder().encode(content);
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
  const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  //const hash = crypto.createHash('sha256').update(content).digest('hex')
  return hashHex;
}

Function to Get Signature Key

async function getSignatureKey(key, dateStamp, regionName, serviceName) {
  const kDate = await sign(`AWS4${key}`, dateStamp);
  const kRegion = await sign(kDate, regionName);
  const kService = await sign(kRegion, serviceName);
  const kSigning = await sign(kService, "aws4_request");
  return kSigning;
}

Function to Create the String to Sign

function createStringToSign(datetime, region, service, requestHash) {
  const algorithm = "AWS4-HMAC-SHA256";
  const credentialScope = `${datetime.substring(
    0,
    8
  )}/${region}/${service}/aws4_request`;
  const stringToSign = `${algorithm}\n${datetime}\n${credentialScope}\n${requestHash}`;
  return stringToSign;
}

Function to Get UTC date

// Function to get the current date in the required format
function getFormattedDate() {
  const now = new Date();
  const year = now.getUTCFullYear();
  const month = ("0" + (now.getUTCMonth() + 1)).slice(-2);
  const day = ("0" + now.getUTCDate()).slice(-2);
  const hour = ("0" + now.getUTCHours()).slice(-2);
  const minute = ("0" + now.getUTCMinutes()).slice(-2);
  const second = ("0" + now.getUTCSeconds()).slice(-2);
  return {
    date: `${year}${month}${day}`,
    dateTime: `${year}${month}${day}T${hour}${minute}${second}Z`,
    utcString: `${now.toUTCString()}`,
  };
}

Init Function

This function sets the values like bucket name, accessKey, secret etc and starts the call stack.

async function init(contentArg, fileNameArg) {
  console.log({ contentArg, fileNameArg });
  const dateObj = getFormattedDate();
  console.log(dateObj);

  // Example usage
  const accessKey = "AKIA5Q3DN3MWQRKB7865";
  const secretKey = "uvzQGk14YWY0LX8RPwm9JZu2example/4qCFBL";
  const dateStamp = dateObj.date; // Replace with the current date in YYYYMMDD format
  const regionName = "us-east-1"; // Replace with your AWS region
  const serviceName = "s3"; // Replace with the AWS service you are using
  const datetime = dateObj.dateTime; // Replace with the timestamp of the request
  const bucketname = "your-bucket-name";
  const fileName = fileNameArg;
  const content = contentArg;
  const hashedContent = await contentHash(content);

  // Example Canonical Request (replace with your actual canonical request)
  const canonicalRequest = `PUT
/${fileName}

date:${dateObj.utcString}
host:${bucketname}.s3.amazonaws.com
x-amz-content-sha256:${hashedContent}
x-amz-date:${dateObj.dateTime}

date;host;x-amz-content-sha256;x-amz-date
${hashedContent}`;

  // Step 1: Calculate the Signature Key
  const signingKey = await getSignatureKey(
    secretKey,
    dateStamp,
    regionName,
    serviceName
  );

  // Step 2: Create the String to Sign
  const requestHash = crypto
    .createHash("sha256")
    .update(canonicalRequest)
    .digest("hex");
  const stringToSign = createStringToSign(
    datetime,
    regionName,
    serviceName,
    requestHash
  );

  // Step 3: Calculate the Signature
  const signature = (await sign(signingKey, stringToSign)).toString("hex");

  console.log(signature);

  const authHeader = `AWS4-HMAC-SHA256 Credential=${accessKey}/${dateObj.date}/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date,Signature=${signature}`;

  console.log(authHeader);

  const url = "https://your-bucket-name.s3.amazonaws.com/" + fileName;
  const authorizationHeader = authHeader;
  const contentSha256Header = hashedContent;
  const amzDateHeader = dateObj.dateTime;
  const dateHeader = dateObj.utcString;
  const data = content;

  const response = await axios.put(url, data, {
    headers: {
      Authorization: authorizationHeader,
      "x-amz-content-sha256": contentSha256Header,
      "x-amz-date": amzDateHeader,
      date: dateHeader,
    },
  });

  console.log(response.status);
  console.log(response.data);
}

init("hello world content", "filename.txt");
--- ○ ---
Joby Joseph
Web Architect