Hide Data in PNG Files Using JavaScript: A Step-by-Step Guide
Add to your RSS feed2 January 20257 min readTable of Contents
Hiding data inside images is an ingenious method of steganography, leveraging the PNG format's structure to store information inconspicuously. This article will explain how to embed and retrieve data within PNG files using the Least Significant Bit (LSB) technique, alongside practical JavaScript code examples.
Understanding PNG and LSB Technique
- PNG Structure: A PNG image stores information about each pixel. Every pixel contains three color channels (R, G, B) and an alpha channel for transparency.
- LSB (Least Significant Bit): The LSB of each channel can be altered to store additional data without noticeable changes in the image’s appearance.
Concept in Practice
- Data Storage: Convert the data into binary form and replace the LSBs of the pixel channels with the data bits.
- Capacity: Each pixel can hold 3 bits of data. A 1000x1000 image can store approximately 1 MB of data.
Code Implementation
Encoding Data into PNG
1 import { PNG } from 'pngjs';2 import fs from 'fs';34 function writeData(imageData, binaryData) {5 for (let i = 0, dataBitIndex = 0; i < imageData.length; i += 4) {6 for (let j = 0; j < 3; j++, dataBitIndex++) {7 if (dataBitIndex >= binaryData.length * 8) return imageData;8 let bit = (binaryData[Math.floor(dataBitIndex / 8)] >> (7 - (dataBitIndex % 8))) & 1;9 imageData[i + j] = (imageData[i + j] & 0xFE) | bit;10 }11 }12 return imageData;13 }1415 async function encode(inputPath, outputPath, message) {16 const binaryMessage = Buffer.from(message, 'utf-8');17 return new Promise((resolve) => {18 fs.createReadStream(inputPath)19 .pipe(new PNG())20 .on('parsed', function () {21 let lengthBuffer = Buffer.alloc(4);22 lengthBuffer.writeUInt32BE(binaryMessage.length, 0);2324 let totalData = Buffer.concat([lengthBuffer, binaryMessage]);25 writeData(this.data, totalData);2627 const output = fs.createWriteStream(outputPath);28 output.on('finish', resolve);29 this.pack().pipe(output);30 });31 });32 }
Decoding Data from PNG
1 function readData(imageData) {2 let bytes = [];3 let currentByte = 0;4 let dataBitIndex = 0;56 for (let i = 0; i < imageData.length; i += 4) {7 for (let j = 0; j < 3; j++) {8 let bit = imageData[i + j] & 1;9 currentByte = (currentByte << 1) | bit;10 dataBitIndex++;11 if (dataBitIndex % 8 === 0) {12 bytes.push(currentByte);13 currentByte = 0;14 }15 }16 }17 return Buffer.from(bytes);18 }1920 async function decode(inputPath) {21 return new Promise((resolve) => {22 fs.createReadStream(inputPath)23 .pipe(new PNG())24 .on('parsed', function () {25 const binaryData = readData(this.data);26 const messageLength = binaryData.readUInt32BE(0);27 const message = binaryData.slice(4, 4 + messageLength).toString('utf-8');28 resolve(message);29 });30 });31 }
PNG Steganography Class in TypeScript
1 import fs from 'node:fs';2 import crypto from 'node:crypto';3 import { PNG } from 'pngjs';4 import { gzipSync, gunzipSync } from 'zlib';5 import * as process from "node:process";67 export default class Steganography {89 public png: PNG;1011 public constructor(png: PNG) {12 if (png.data.length < 4) {13 throw new Error('Cannot use this PNG file.');14 }1516 this.png = png;17 }1819 protected computeHash(data: Buffer): Buffer {20 return crypto.createHash('sha256').update(data).digest();21 }2223 protected generateAESKey(key: string): Buffer {24 return crypto.createHash('sha256').update(key).digest();25 }2627 protected extractHiddenData(pixels: Buffer): Buffer {28 let bytes: number[] = [];29 let bitIndex = 0;30 let currentByte = 0;3132 for (let i = 0; i < pixels.length; i += 4) {33 for (let j = 0; j < 3; j++) {34 let bit = pixels[i + j] & 1;35 currentByte = (currentByte << 1) | bit;36 bitIndex++;3738 if (bitIndex % 8 === 0) {39 bytes.push(currentByte);40 currentByte = 0;41 }42 }43 }4445 return Buffer.from(bytes);46 }4748 protected embedData(pixels: Buffer, data: Buffer): Buffer {49 let outputBuffer = Buffer.from(pixels);50 let bitIndex = 0;5152 for (let i = 0; i < outputBuffer.length; i += 4) {53 for (let j = 0; j < 3; j++) {54 let bit = (bitIndex < data.length * 8)55 ? (data[Math.floor(bitIndex / 8)] >> (7 - (bitIndex % 8))) & 156 : crypto.randomInt(2);5758 outputBuffer[i + j] = (outputBuffer[i + j] & 0xFE) | bit;59 bitIndex++;60 }61 }6263 return outputBuffer;64 }6566 protected createClone(buffer: Buffer | null = null): Steganography {67 let newImage = new PNG({68 width: this.png.width,69 height: this.png.height70 });71 if (!buffer) {72 buffer = this.png.data;73 }74 buffer.copy(newImage.data);7576 return new Steganography(newImage);7778 }7980 protected getMaximumCapacity(): number {81 return Math.floor(this.png.data.length / 4) * 3 / 8;82 }8384 public static async loadPNG(path: string): Promise<Steganography> {85 return new Promise(resolve => {86 fs.createReadStream(path)87 .pipe(new PNG())88 .on('parsed', function () {89 resolve(new Steganography(this));90 });91 });92 }9394 public getAvailableSpace(): number {95 return this.getMaximumCapacity() - 4 - 32;96 }9798 public decodeImage(binary: boolean = false): string | Buffer {99 if (this.png.data.length < 96 * 4) {100 throw new Error('Cannot decode this container.');101 }102 let metadata = this.extractHiddenData(this.png.data.slice(0, 96 * 4));103 let length = metadata.readUInt32BE();104 let hash = metadata.slice(4, 36);105 let data = this.extractHiddenData(this.png.data).slice(36, 36 + length);106 if (!this.computeHash(data).equals(hash)) {107 throw new Error('Cannot decode this container.');108 }109 let decompressedData = gunzipSync(data);110111 return binary112 ? decompressedData113 : new TextDecoder().decode(decompressedData);114 }115116 public encodeImage(data: string | Buffer): Steganography {117 let binaryData = typeof data === 'string'118 ? Buffer.from(data, 'utf-8')119 : Buffer.from(data);120 let compressedData = gzipSync(binaryData);121 let length = Buffer.alloc(4);122 length.writeUInt32BE(compressedData.length, 0);123 let hash = this.computeHash(compressedData);124 let serializedData = Buffer.concat([length, hash, compressedData]);125 if (serializedData.length > this.getMaximumCapacity()) {126 throw new Error('Message is too large to encode.');127 }128129 return this.createClone(this.embedData(this.png.data, serializedData));130 }131132 public async saveImage(path: string): Promise<void> {133 let stream = fs.createWriteStream(path);134 this.png.pack().pipe(stream);135136 return new Promise(resolve => {137 stream.on('finish', resolve);138 });139 }140141 public encodeWithEncryption(key: string, data: string | Buffer): Steganography {142 let aesKey = this.generateAESKey(key);143 let binaryData = typeof data === 'string'144 ? Buffer.from(data, 'utf-8')145 : Buffer.from(data);146 let iv = crypto.randomBytes(16);147 let cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);148 let encryptedData = Buffer.concat([cipher.update(binaryData), cipher.final()]);149 let finalData = Buffer.concat([iv, encryptedData]);150151 return this.encodeImage(finalData);152 }153154 public decodeWithEncryption(key: string, binary: boolean = false): string | Buffer {155 let aesKey = this.generateAESKey(key);156 let encodedData = <Buffer>this.decodeImage(true);157 let iv = encodedData.slice(0, 16);158 let encryptedData = encodedData.slice(16);159 let decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);160 let decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]);161162 return binary ? decryptedData : new TextDecoder().decode(decryptedData);163 }164165 public encodeFile(path: string): Steganography {166 let dataBuffer = fs.readFileSync(path);167 return this.encodeImage(dataBuffer);168 }169170 public decodeFile(path: string): void {171 let decodedData = this.decodeImage(true);172 fs.writeFileSync(path, decodedData);173 }174175 public encodeFileWithEncryption(key: string, path: string): Steganography {176 let dataBuffer = fs.readFileSync(path);177 return this.encodeWithEncryption(key, dataBuffer);178 }179180 public decodeFileWithEncryption(key: string, path: string): void {181 let decodedData = this.decodeWithEncryption(key, true);182 fs.writeFileSync(path, decodedData);183 }184 }
The Stenography class allows hiding, extracting, and optionally encrypting data in PNG files by modifying their pixel data.
Core Methods
Data Manipulation
hashData(binaryData: Buffer): Buffer
- Creates a SHA-256 hash of the provided binary data.
- Ensures data integrity during decoding.
deriveAESKey(key: string): Buffer
- Derives a secure AES key using SHA-256 hashing of a given key string.
mask(pixels: Buffer, data: Buffer): Buffer
- Encodes data into the least significant bits (LSB) of the image's pixel data.
- Ensures encoded data remains visually indistinguishable in the image.
unmask(pixels: Buffer): Buffer
- Extracts hidden data from the LSBs of the image's pixel data.
Image Handling
clone(buffer: Buffer | null = null): Stenography
- Creates a copy of the current PNG object, optionally using a provided buffer.
getAvailableEncodeBytes(): number
- Calculates the maximum data size (in bytes) that can be encoded in the image.
saveToFile(path: string): Promise<void>
- Saves the modified PNG object to a file.
openPNG(path: string): Promise<Stenography>
- Static method to load and parse a PNG file, returning a Stenography instance.
Data Encoding and Decoding
encode(data: string | Buffer): Stenography
- Compresses, hashes, and embeds the provided data into the PNG image.
- Throws an error if the data exceeds the available space.
decode(binary: boolean = false): string | Buffer
- Extracts and decompresses data from the image.
- Validates the integrity of the data using its hash.
encodeWithKey(key: string, data: string | Buffer): Stenography
- Encrypts the data with AES-256 before encoding it in the image.
decodeWithKey(key: string, binary: boolean = false): string | Buffer
- Decrypts AES-encrypted data after extracting it from the image.
File Operations
encodeFile(fromDataPath: string): Stenography
- Reads a file, encodes its contents into the PNG image.
decodeFile(toDataPath: string): void
- Extracts and saves hidden file data from the image.
encodeFileWithKey(key: string, fromDataPath: string): Stenography
- Encrypts and embeds a file’s contents into the PNG image.
decodeFileWithKey(key: string, toDataPath: string): void
- Extracts and decrypts a file’s data from the image.
Key Concepts
- Steganography: Hides data within the LSBs of pixel channels (RGB). Human eyes cannot detect such minor changes.
- Data Compression: Uses gzipSync to minimize the size of data before encoding.
- Encryption: Supports AES-256 encryption for secure data storage.
- Error Handling: Includes various validations to ensure data integrity and compatibility.
Advanced Ideas and Enhancements
- Encryption: Enhance security by encrypting the data before embedding it.
- Non-Sequential Pixel Selection: Use algorithms like elliptic curves to determine pixel positions for data storage, adding obfuscation.
- Noise Addition: Introduce subtle noise to obscure data concealment.
Practical Uses
- Storing sensitive files within an image.
- Hiding secure messages with AES encryption.
- Embedding metadata for forensic purposes.
Conclusion
With minimal visual impact, you can hide significant amounts of data in PNG files using the LSB method. By applying encryption, noise, and advanced pixel selection strategies, the data can remain undetectable even under scrutiny.