├── .gitignore ├── README.md ├── package.json ├── src ├── Utils │ └── Stenography.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | src/tests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS PNG Stenography 2 | 3 | ### Hide your message or file into PNG picture! 4 | 5 | ## Hide text message into picture 6 | 7 | ``` 8 | npm i png-stenography 9 | ``` 10 | 11 | ```javascript 12 | //Encode 13 | let input = await Stenography.openPNG('input.png'); 14 | await input.encode('My Message').saveToFile('output.png'); 15 | 16 | //Decode 17 | let output = await Stenography.openPNG('output.png'); 18 | let message = output.decode(); //My Message 19 | ``` 20 | 21 | ## Hide encrypted message into picture 22 | 23 | ```javascript 24 | let key = 'My AES Key'; 25 | 26 | //Encode 27 | let input = await Stenography.openPNG('input.png'); 28 | await input.encodeWithKey(key, 'My Message').saveToFile('output.png'); 29 | 30 | //Decode 31 | let output = await Stenography.openPNG('output.png'); 32 | let message = output.decodeWithKey(key); //My message 33 | ``` 34 | 35 | ## Hide file into picture 36 | 37 | ```javascript 38 | //Encode 39 | let input = await Stenography.openPNG('input.png'); 40 | await input.encodeFile('data.zip').saveToFile('output.png'); 41 | 42 | //Decode 43 | let output = await Stenography.openPNG('output.png'); 44 | output.decodeFile('result.zip'); 45 | ``` 46 | 47 | ## Hide encrypted file into picture 48 | 49 | ```javascript 50 | let key = 'My AES Key'; 51 | 52 | //Encode 53 | let input = await Stenography.openPNG('input.png'); 54 | await input.encodeFileWithKey(key, 'data.zip').saveToFile('output.png'); 55 | 56 | //Decode 57 | let output = await Stenography.openPNG('output.png'); 58 | output.decodeFileWithKey(key, 'result.zip'); 59 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "pngjs": "^7.0.0" 4 | }, 5 | "devDependencies": { 6 | "@types/node": "^18.0.3", 7 | "@types/pngjs" : "^6.0.5", 8 | "typescript": "^4.7.4" 9 | }, 10 | "name": "png-stenography", 11 | "description": "Hide your message or file into PNG picture", 12 | "version": "1.0.1", 13 | "main": "dist/index.js", 14 | "module": "dist/index.js", 15 | "umd:main": "dist/index.js", 16 | "scripts": { 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "files": [ 22 | "dist" 23 | ], 24 | "repository": { 25 | "type" : "git", 26 | "url" : "git+https://github.com/in4in-dev/png-stenography" 27 | }, 28 | "prepare": "tsc", 29 | "types": "dist/index.d.ts" 30 | } -------------------------------------------------------------------------------- /src/Utils/Stenography.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 Stenography 8 | { 9 | 10 | public png : PNG; 11 | 12 | public constructor(png : PNG) { 13 | 14 | if(png.data.length < 4){ 15 | throw new Error('Cant use this PNG file'); 16 | } 17 | 18 | this.png = png; 19 | 20 | } 21 | 22 | protected hashData(binaryData : Buffer) : Buffer 23 | { 24 | return crypto.createHash('sha256').update(binaryData).digest(); 25 | } 26 | 27 | protected deriveAESKey(key : string) : Buffer 28 | { 29 | return crypto.createHash('sha256').update(key).digest(); 30 | } 31 | 32 | protected unmask(pixels : Buffer) : Buffer 33 | { 34 | 35 | let bytes: number[] = []; 36 | let dataBitIndex = 0; 37 | let currentByte = 0; 38 | 39 | for (let i = 0; i < pixels.length; i += 4) { 40 | 41 | for (let j = 0; j < 3; j++) { 42 | 43 | let bit = pixels[i + j] & 1; 44 | 45 | currentByte = (currentByte << 1) | bit; 46 | dataBitIndex++; 47 | 48 | if (dataBitIndex % 8 === 0) { 49 | bytes.push(currentByte); 50 | currentByte = 0; 51 | } 52 | 53 | } 54 | 55 | } 56 | 57 | return Buffer.from(bytes); 58 | 59 | } 60 | 61 | protected mask(pixels : Buffer, data : Buffer) : Buffer 62 | { 63 | 64 | let outputBuffer = Buffer.from(pixels); 65 | 66 | let dataBitIndex = 0; 67 | 68 | for (let i = 0; i < outputBuffer.length; i += 4) { 69 | 70 | for (let j = 0; j < 3; j++) { 71 | 72 | let bit = (dataBitIndex < data.length * 8) 73 | ? (data[Math.floor(dataBitIndex / 8)] >> (7 - (dataBitIndex % 8))) & 1 74 | : crypto.randomInt(2) 75 | 76 | outputBuffer[i + j] = (outputBuffer[i + j] & 0xFE) | bit; 77 | dataBitIndex++; 78 | 79 | } 80 | 81 | } 82 | 83 | return outputBuffer; 84 | 85 | } 86 | 87 | protected clone(buffer : Buffer | null = null) : Stenography 88 | { 89 | 90 | let outputPicture = new PNG({ 91 | width: this.png.width, 92 | height: this.png.height 93 | }); 94 | 95 | if(!buffer){ 96 | buffer = this.png.data; 97 | } 98 | 99 | buffer.copy(outputPicture.data); 100 | 101 | return new Stenography( 102 | outputPicture 103 | ); 104 | 105 | } 106 | 107 | protected getAvailableEncodeBytes() : number 108 | { 109 | return Math.floor(this.png.data.length / 4) * 3 / 8; 110 | } 111 | 112 | 113 | /** 114 | * Открыть существующий файл 115 | */ 116 | public static async openPNG(path : string) : Promise 117 | { 118 | 119 | return new Promise(resolve => { 120 | 121 | return fs.createReadStream(path) 122 | .pipe(new PNG()) 123 | .on('parsed', function () { 124 | resolve( 125 | new Stenography(this) 126 | ); 127 | }); 128 | 129 | }); 130 | 131 | } 132 | 133 | /** 134 | * Свободное место в хранилище 135 | */ 136 | public getMemorySize() : number 137 | { 138 | return this.getAvailableEncodeBytes() - 4 - 32; 139 | } 140 | 141 | /** 142 | * Раскодировать изображение 143 | */ 144 | public decode(binary : boolean = false) : string | Buffer 145 | { 146 | 147 | if(this.png.data.length < 96 * 4){ 148 | throw new Error('Cant decode this container'); 149 | } 150 | 151 | let meta = this.unmask( 152 | this.png.data.slice(0, 96 * 4) 153 | ); 154 | 155 | let length = meta.readUInt32BE(); 156 | let hash = meta.slice(4, 36); 157 | 158 | let data = this.unmask(this.png.data).slice(36, 36 + length); 159 | 160 | if(!this.hashData(data).equals(hash)){ 161 | throw new Error('Cant decode this container'); 162 | } 163 | 164 | let unzippedData = gunzipSync(data); 165 | 166 | return binary 167 | ? unzippedData 168 | : new TextDecoder().decode( 169 | unzippedData 170 | ); 171 | 172 | } 173 | 174 | /** 175 | * Закодировать изображение 176 | */ 177 | public encode(data : string | Buffer) : Stenography 178 | { 179 | 180 | let binaryData = typeof data === 'string' 181 | ? Buffer.from(data, 'utf-8') 182 | : Buffer.from(data); 183 | 184 | /** 185 | * Сжимаем для экономии места 186 | */ 187 | let compressedBinaryData = gzipSync(binaryData); 188 | 189 | /** 190 | * Записываем длину данных 191 | */ 192 | let length = Buffer.alloc(4); 193 | length.writeUInt32BE(compressedBinaryData.length, 0); 194 | 195 | /** 196 | * Записываем хэш данных 197 | */ 198 | let hash = this.hashData(compressedBinaryData); 199 | 200 | /** 201 | * Собираем все вместе 202 | */ 203 | let serializedData = Buffer.concat([ 204 | length, 205 | hash, 206 | compressedBinaryData 207 | ]); 208 | 209 | if (serializedData.length > this.getAvailableEncodeBytes()) { 210 | throw new Error('Message is too long'); 211 | } 212 | 213 | return this.clone( 214 | this.mask(this.png.data, serializedData) 215 | ); 216 | 217 | } 218 | 219 | /** 220 | * Сохранение картинки 221 | */ 222 | public async saveToFile(path : string) : Promise 223 | { 224 | 225 | let stream = fs.createWriteStream(path); 226 | 227 | this.png.pack().pipe(stream); 228 | 229 | return new Promise(resolve => { 230 | stream.on('finish', resolve); 231 | }); 232 | 233 | } 234 | 235 | /** 236 | * Закодировать изображение с AES ключом 237 | */ 238 | public encodeWithKey(key : string, data : string | Buffer) : Stenography 239 | { 240 | 241 | let cryptoKey = this.deriveAESKey(key); 242 | 243 | let binaryData = typeof data === 'string' 244 | ? Buffer.from(data, 'utf-8') 245 | : Buffer.from(data); 246 | 247 | let iv = crypto.randomBytes(16); 248 | let cipher = crypto.createCipheriv('aes-256-cbc', cryptoKey, iv); 249 | let encryptedData = Buffer.concat([cipher.update(binaryData), cipher.final()]); 250 | 251 | let finalData = Buffer.concat([iv, encryptedData]); 252 | 253 | return this.encode(finalData); 254 | 255 | } 256 | 257 | /** 258 | * Раскодировать изображение с AES ключом 259 | */ 260 | public decodeWithKey(key : string, binary : boolean = false) : string | Buffer 261 | { 262 | 263 | let cryptoKey = crypto.createHash('sha256').update(key).digest(); 264 | 265 | let encodedData = this.decode(true); 266 | 267 | let iv = encodedData.slice(0, 16); 268 | let encryptedData = encodedData.slice(16); 269 | 270 | let decipher = crypto.createDecipheriv('aes-256-cbc', cryptoKey, iv); 271 | let decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); 272 | 273 | // Возвращаем расшифрованные данные 274 | return binary ? decryptedData : new TextDecoder().decode(decryptedData); 275 | 276 | } 277 | 278 | /** 279 | * Закодировать файл внутрь изображения 280 | */ 281 | public encodeFile(fromDataPath : string) : Stenography 282 | { 283 | 284 | let dataBuffer = fs.readFileSync(fromDataPath); 285 | 286 | return this.encode(dataBuffer); 287 | 288 | } 289 | 290 | /** 291 | * Раскодировать файл внутри изображения 292 | */ 293 | public decodeFile(toDataPath : string) : void 294 | { 295 | 296 | let decode = this.decode(true); 297 | 298 | fs.writeFileSync(toDataPath, decode); 299 | 300 | } 301 | 302 | /** 303 | * Закодировать файл внутрь изображения с AES ключом 304 | */ 305 | public encodeFileWithKey(key : string, fromDataPath : string) : Stenography 306 | { 307 | 308 | let dataBuffer = fs.readFileSync(fromDataPath); 309 | 310 | return this.encodeWithKey(key, dataBuffer); 311 | 312 | } 313 | 314 | /** 315 | * Раскодировать файл внутри изображения с AES ключем 316 | */ 317 | public decodeFileWithKey(key : string, toDataPath : string) : void 318 | { 319 | 320 | let decode = this.decodeWithKey(key, true); 321 | 322 | fs.writeFileSync(toDataPath, decode); 323 | 324 | } 325 | 326 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Stenography from "./Utils/Stenography"; 2 | 3 | export {Stenography} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES6", 5 | "typeRoots": [ 6 | "node_modules/@types" 7 | ], 8 | "sourceRoot": "/", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "outDir": "dist", 12 | "allowSyntheticDefaultImports": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "pretty": true, 17 | "sourceMap": true, 18 | "inlineSources": true, 19 | "declaration": true, 20 | "allowJs": true, 21 | "noEmit": false, 22 | "esModuleInterop": true, 23 | "resolveJsonModule": true, 24 | "importHelpers": true, 25 | "skipLibCheck": true, 26 | "strict": true 27 | }, 28 | "include": [ 29 | "src/**/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------