JavaScript Development Space

Hide Data in PNG Files Using JavaScript: A Step-by-Step Guide

Add to your RSS feed2 January 20257 min read
Hide Data in PNG Files Using JavaScript: A Step-by-Step Guide

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

js
1 import { PNG } from 'pngjs';
2 import fs from 'fs';
3
4 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 }
14
15 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);
23
24 let totalData = Buffer.concat([lengthBuffer, binaryMessage]);
25 writeData(this.data, totalData);
26
27 const output = fs.createWriteStream(outputPath);
28 output.on('finish', resolve);
29 this.pack().pipe(output);
30 });
31 });
32 }

Decoding Data from PNG

js
1 function readData(imageData) {
2 let bytes = [];
3 let currentByte = 0;
4 let dataBitIndex = 0;
5
6 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 }
19
20 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

ts
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";
6
7 export default class Steganography {
8
9 public png: PNG;
10
11 public constructor(png: PNG) {
12 if (png.data.length < 4) {
13 throw new Error('Cannot use this PNG file.');
14 }
15
16 this.png = png;
17 }
18
19 protected computeHash(data: Buffer): Buffer {
20 return crypto.createHash('sha256').update(data).digest();
21 }
22
23 protected generateAESKey(key: string): Buffer {
24 return crypto.createHash('sha256').update(key).digest();
25 }
26
27 protected extractHiddenData(pixels: Buffer): Buffer {
28 let bytes: number[] = [];
29 let bitIndex = 0;
30 let currentByte = 0;
31
32 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++;
37
38 if (bitIndex % 8 === 0) {
39 bytes.push(currentByte);
40 currentByte = 0;
41 }
42 }
43 }
44
45 return Buffer.from(bytes);
46 }
47
48 protected embedData(pixels: Buffer, data: Buffer): Buffer {
49 let outputBuffer = Buffer.from(pixels);
50 let bitIndex = 0;
51
52 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))) & 1
56 : crypto.randomInt(2);
57
58 outputBuffer[i + j] = (outputBuffer[i + j] & 0xFE) | bit;
59 bitIndex++;
60 }
61 }
62
63 return outputBuffer;
64 }
65
66 protected createClone(buffer: Buffer | null = null): Steganography {
67 let newImage = new PNG({
68 width: this.png.width,
69 height: this.png.height
70 });
71 if (!buffer) {
72 buffer = this.png.data;
73 }
74 buffer.copy(newImage.data);
75
76 return new Steganography(newImage);
77
78 }
79
80 protected getMaximumCapacity(): number {
81 return Math.floor(this.png.data.length / 4) * 3 / 8;
82 }
83
84 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 }
93
94 public getAvailableSpace(): number {
95 return this.getMaximumCapacity() - 4 - 32;
96 }
97
98 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);
110
111 return binary
112 ? decompressedData
113 : new TextDecoder().decode(decompressedData);
114 }
115
116 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 }
128
129 return this.createClone(this.embedData(this.png.data, serializedData));
130 }
131
132 public async saveImage(path: string): Promise<void> {
133 let stream = fs.createWriteStream(path);
134 this.png.pack().pipe(stream);
135
136 return new Promise(resolve => {
137 stream.on('finish', resolve);
138 });
139 }
140
141 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]);
150
151 return this.encodeImage(finalData);
152 }
153
154 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()]);
161
162 return binary ? decryptedData : new TextDecoder().decode(decryptedData);
163 }
164
165 public encodeFile(path: string): Steganography {
166 let dataBuffer = fs.readFileSync(path);
167 return this.encodeImage(dataBuffer);
168 }
169
170 public decodeFile(path: string): void {
171 let decodedData = this.decodeImage(true);
172 fs.writeFileSync(path, decodedData);
173 }
174
175 public encodeFileWithEncryption(key: string, path: string): Steganography {
176 let dataBuffer = fs.readFileSync(path);
177 return this.encodeWithEncryption(key, dataBuffer);
178 }
179
180 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

  1. Steganography: Hides data within the LSBs of pixel channels (RGB). Human eyes cannot detect such minor changes.
  2. Data Compression: Uses gzipSync to minimize the size of data before encoding.
  3. Encryption: Supports AES-256 encryption for secure data storage.
  4. 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.

JavaScript Development Space

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.