This guide explains how to implement a secure system for encrypting images, uploading them to a cloud storage solution (Cloudinary), and serving them when needed. We’ll walk through each step in detail, from setting up key generation to encrypting and decrypting images, uploading to Cloudinary, and securely serving them. By the end, you'll understand how everything ties together to form a robust, secure image handling process.
Step 1: Key Generation
To secure the encryption and decryption process, we'll use RSA (asymmetric cryptography) to encrypt the AES (symmetric) key used for image encryption.
Generating RSA Keys
import crypto from "crypto";
import fs from "fs";
// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048, // Key size in bits
publicKeyEncoding: {
type: "spki", // Recommended format for public key
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8", // Recommended format for private key
format: "pem",
},
});
// Save public key to a file
fs.writeFileSync("public_key.pem", publicKey);
console.log("Public key saved to public_key.pem");
// Save private key to a file
fs.writeFileSync("private_key.pem", privateKey);
console.log("Private key saved to private_key.pem");
This generates an RSA key pair and saves the keys to files for use in encryption and decryption.
Step 2: Setting Up Image Uploads
We’ll use multer
, a middleware for handling file uploads in Node.js, to handle user-uploaded images.
Installing Multer
npm install multer
Configuring Multer
import multer from "multer";
import path from "path";
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/"); // Directory to save uploaded files
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1E9);
cb(null, `${uniqueSuffix}-${file.originalname}`);
},
});
export const upload = multer({ storage });
This code configures Multer to save uploaded images to a directory called uploads/
.
Step 3: Encrypting Images
We'll use AES-GCM (a symmetric encryption algorithm) to encrypt the image data and RSA to securely store the encryption key.
Encryption Function
import crypto from "crypto";
import fs from "fs";
// Load RSA public key
const publicKey = fs.readFileSync("public_key.pem", "utf8");
export const encryptImage = (imageFile) => {
const algorithm = "aes-256-gcm";
const key = crypto.randomBytes(32); // AES key
const iv = crypto.randomBytes(16); // Initialization vector
const cipher = crypto.createCipheriv(algorithm, key, iv);
// Read the image file into a buffer
const imageBuffer = fs.readFileSync(imageFile.path);
let encrypted = cipher.update(imageBuffer);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const authTag = cipher.getAuthTag();
// Encrypt the AES key with RSA public key
const encryptedKey = crypto.publicEncrypt(publicKey, key);
return {
iv: iv.toString("hex"),
encryptedData: encrypted.toString("base64"),
authTag: authTag.toString("hex"),
encryptedKey: encryptedKey.toString("base64"),
};
};
This function encrypts the image file using AES-GCM and encrypts the AES key with the RSA public key.
Step 4: Uploading to Cloudinary
Installing Cloudinary SDK
npm install cloudinary
Configuring Cloudinary
import { v2 as cloudinary } from "cloudinary";
cloudinary.config({
cloud_name: "your-cloud-name", // Replace with your Cloudinary cloud name
api_key: "your-api-key", // Replace with your Cloudinary API key
api_secret: "your-api-secret", // Replace with your Cloudinary API secret
});
Uploading Encrypted Images
import fs from "fs";
export const uploadToCloudinary = async (encryptedData, fileName) => {
const tempFilePath = `temp/${fileName}.json`;
fs.writeFileSync(tempFilePath, JSON.stringify(encryptedData));
const result = await cloudinary.uploader.upload(tempFilePath, {
resource_type: "raw",
folder: "encrypted_images",
});
fs.unlinkSync(tempFilePath); // Delete temporary file
return result.secure_url; // Return the uploaded file URL
};
This uploads the encrypted image data as a JSON file to Cloudinary.
Step 5: Decrypting and Serving Images
Decryption Function
// Load RSA private key
const privateKey = fs.readFileSync("private_key.pem", "utf8");
export const decryptImage = (encryptedData) => {
const { encryptedData: data, encryptedKey, iv, authTag } = encryptedData;
// Decrypt the AES key using RSA private key
const key = crypto.privateDecrypt(privateKey, Buffer.from(encryptedKey, "base64"));
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
decipher.setAuthTag(Buffer.from(authTag, "hex"));
let decrypted = decipher.update(Buffer.from(data, "base64"));
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted;
};
This function decrypts the AES-encrypted image using the RSA private key and the provided metadata.
Serving Decrypted Images
import axios from "axios";
export const decodeUserImageController = async (req, res) => {
const { version, public_id } = req.params;
if (!public_id) {
res.status(400).send("Invalid request: public_id is required");
return;
}
// Fetch encrypted data from Cloudinary
const encryptedData = (await axios.get(`https://res.cloudinary.com/your-cloud-name/raw/upload/v${version}/${public_id}.json`)).data;
// Decrypt the image
const decrypted = decryptImage(encryptedData);
// Serve the decrypted image
res.type("image/png").send(decrypted);
};
This code fetches the encrypted image metadata from Cloudinary, decrypts it, and serves it as a response.
Conclusion
By following this guide, you can implement a secure system for encrypting and storing sensitive images. Using a combination of AES-GCM for encryption and RSA for key security ensures strong protection. Cloudinary provides a scalable storage solution for encrypted data, and the ability to decrypt and serve images on demand makes this system both flexible and secure.