Skip to main content

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

· 4 min read

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");