├── .gitignore ├── LICENSE ├── README.md ├── bin └── speaking-jpg ├── img ├── backup.jpg ├── hidden-msg-2.jpg ├── hidden-msg.jpg ├── hidden-msg.jpg.hex └── schneesturm.jpg.hex ├── package.json ├── src ├── crypt.js ├── index.js └── options.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wolfram Hempel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # speaking-jpg 2 | A simple tool to hide encrypted text messages inside jpeg images. 3 | 4 | ## Why? 5 | I stumbled upon [a comment on Hackernews](https://news.ycombinator.com/item?id=14825675) the other day. A secure messaging app that used Tor just passed a security audit and the commenter argued that while this would be safe, once your phone is seized by authorities your use of Tor for messaging would stick out like a sore thumb. 6 | 7 | So why not use something way less conspicuous? Speaking-jpg allows you to embed messages in normal jpeg images that can be uploaded and shared via email or social media. Only if the counterparty knows that a message is contained AND has the same key they can retrieve and decrypt the message. 8 | 9 | ## Installation: 10 | 11 | ``` 12 | npm install speaking-jpg -g 13 | ``` 14 | 15 | ## Usage 16 | Embed a message into a jpg 17 | 18 | ```shell 19 | speaking-jpg create 20 | --in=path/to/img.jpg 21 | --out=path/to/manipulatedimg.jpg 22 | --msg="message you want to embed" 23 | --key="encryptionpassword" 24 | ``` 25 | 26 | Read a message from a jpg 27 | 28 | ```shell 29 | speaking-jpg read 30 | --in=path/to/img.jpg 31 | --key="encryptionpassword" 32 | ``` 33 | 34 | ## Limitations 35 | - Max message length is 65,000 bytes (Sixty-Five-Thousand) 36 | - Image processing (e.g. resizing upon upload) might strip out the comment. 37 | 38 | ## How does it work? 39 | speaking jpg embeds a comment byte marker into the jpg's meta data section, followed by the total length and a random byte series to identify the comment as speaking-jpg one. Image viewers ignore this segment when parsing the file. 40 | 41 | The message itself is stored as aes-256-ctr encrypted utf8 bytes. 42 | 43 | ## Example 44 | ### Before: 45 | ![img](https://user-images.githubusercontent.com/5931248/28672046-7c10d624-72d6-11e7-8776-60c838c3d297.jpg) 46 | ### After: 47 | ![hello-world](https://user-images.githubusercontent.com/5931248/28672058-84ccac84-72d6-11e7-8a47-3e37fa1cbc4b.jpg) 48 | 49 | Note how the unedited image will look identical visually to the edited image. 50 | 51 | ![image](https://user-images.githubusercontent.com/5931248/28672335-64d87704-72d7-11e7-882b-d6869be64b9a.png) 52 | -------------------------------------------------------------------------------- /bin/speaking-jpg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../src/index'); -------------------------------------------------------------------------------- /img/backup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframHempel/speaking-jpg/ddf1b920fc3e915157ac47214b13f16490b35847/img/backup.jpg -------------------------------------------------------------------------------- /img/hidden-msg-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframHempel/speaking-jpg/ddf1b920fc3e915157ac47214b13f16490b35847/img/hidden-msg-2.jpg -------------------------------------------------------------------------------- /img/hidden-msg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframHempel/speaking-jpg/ddf1b920fc3e915157ac47214b13f16490b35847/img/hidden-msg.jpg -------------------------------------------------------------------------------- /img/hidden-msg.jpg.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframHempel/speaking-jpg/ddf1b920fc3e915157ac47214b13f16490b35847/img/hidden-msg.jpg.hex -------------------------------------------------------------------------------- /img/schneesturm.jpg.hex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolframHempel/speaking-jpg/ddf1b920fc3e915157ac47214b13f16490b35847/img/schneesturm.jpg.hex -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speaking-jpg", 3 | "description": "A tool to hide encrypted messages in jpg images", 4 | "version": "1.0.0", 5 | "author": "Wolfram Hempel", 6 | "homepage": "https://github.com/WolframHempel/speaking-jpg", 7 | "repository": "https://github.com/WolframHempel/speaking-jpg.git", 8 | "license": "MIT", 9 | "scripts": {}, 10 | "main": "src/index", 11 | "bin": { 12 | "speaking-jpg": "bin/speaking-jpg" 13 | }, 14 | "keywords": [ 15 | "encryption", 16 | "jpg", 17 | "secret", 18 | "communication" 19 | ], 20 | "dependencies": { 21 | "minimist": "^1.2.0" 22 | }, 23 | "files": [ 24 | "src", 25 | "bin" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/crypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const algorithm = 'aes-256-ctr'; 3 | 4 | exports.encrypt = function (buffer, password){ 5 | var cipher = crypto.createCipher(algorithm,password) 6 | return Buffer.concat([cipher.update(buffer),cipher.final()]); 7 | }; 8 | 9 | exports.decrypt = function(buffer, password){ 10 | var decipher = crypto.createDecipher(algorithm,password) 11 | return Buffer.concat([decipher.update(buffer) , decipher.final()]); 12 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ); 2 | const opts = require( './options.js' ); 3 | const crypto = require('crypto'); 4 | const START_MARKER = Buffer.from([ 32, 154, 66, 56, 174, 23, 77 ]); 5 | const COMMENT_BYTES = Buffer.from([ 0xFF, 0xFE ]); 6 | const ALGORITHM = 'aes-256-ctr'; 7 | const imgData = fs.readFileSync( opts.in ); 8 | 9 | if( opts.action === 'create' ) { 10 | if( retrieveMessage() !== null ) throw new Error( 'Image already contains a message' ); 11 | const message = createMessage(); 12 | const manipulatedImgData = Buffer.concat([ imgData.slice( 0, 2 ), message, imgData.slice( 2 ) ]); 13 | fs.writeFileSync( opts.out, manipulatedImgData ); 14 | console.log( 'Done! Hidden message in ' + opts.out ); 15 | } else { 16 | const encryptedMessage = retrieveMessage(); 17 | if( encryptedMessage === null ) throw new Error( 'No message found in ' + opts.in ); 18 | console.log( 'Message: ' + decrypt( encryptedMessage, opts.key ).toString( 'utf8' ) ); 19 | } 20 | 21 | function createMessage() { 22 | const encryptedMessage = encrypt( Buffer.from( opts.msg, 'utf8' ), opts.key ); 23 | const length = Buffer.from( getInt64Bytes( 2 + START_MARKER.byteLength + encryptedMessage.byteLength ) ); 24 | return Buffer.concat([ COMMENT_BYTES, length, START_MARKER, encryptedMessage]); 25 | } 26 | 27 | function retrieveMessage() { 28 | const startIndex = imgData.indexOf( START_MARKER ); 29 | if( startIndex === -1 ) return null; 30 | const msgLength = intFromBytes([ imgData[ startIndex - 2 ], imgData[ startIndex - 1 ] ]); 31 | return imgData.slice( startIndex + START_MARKER.byteLength, msgLength + 4 ); 32 | } 33 | 34 | function encrypt(buffer, password){ 35 | var cipher = crypto.createCipher(ALGORITHM,password) 36 | return Buffer.concat([cipher.update(buffer),cipher.final()]); 37 | }; 38 | 39 | function decrypt(buffer, password){ 40 | var decipher = crypto.createDecipher(ALGORITHM,password) 41 | return Buffer.concat([decipher.update(buffer) , decipher.final()]); 42 | }; 43 | 44 | function intFromBytes( x ){ 45 | var val = 0; 46 | for (var i = 0; i < x.length; ++i) { 47 | val += x[i]; 48 | if (i < x.length-1) { 49 | val = val << 8; 50 | } 51 | } 52 | return val; 53 | } 54 | 55 | function getInt64Bytes( x ){ 56 | var bytes = []; 57 | var i = 2; 58 | do { 59 | bytes[--i] = x & (255); 60 | x = x>>8; 61 | } while ( i ) 62 | return bytes; 63 | } -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | const argv = require('minimist')(process.argv.slice(2)); 2 | 3 | if( argv._.length !== 1 ) { 4 | throw new Error( 'Please specify on operation (either create or read)' ) 5 | } 6 | 7 | if( ['create', 'read'].indexOf( argv._[ 0 ] ) === -1 ) { 8 | throw new Error( 'Unknown operation ' + argv._[ 0 ] + ' - please specify either create or read' ); 9 | } 10 | 11 | if( !argv.in ) { 12 | throw new Error( 'Missing parameter in, please specify a source image to read from' ); 13 | } 14 | 15 | if( !argv.key ) { 16 | throw new Error( 'Missing parameter key, please specify a key to encrypt the message with' ); 17 | } 18 | 19 | argv.action = argv._[ 0 ]; 20 | 21 | if( argv.action === 'create' ) { 22 | if( !argv.out ) { 23 | throw new Error( 'Missing parameter out, please specify a file to write to' ); 24 | } 25 | 26 | if( !argv.msg ) { 27 | throw new Error( 'Missing parameter msg, please specify a message to hide in the file' ); 28 | } 29 | } 30 | 31 | module.exports = argv; -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | minimist: 4 | version "1.2.0" 5 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 6 | 7 | --------------------------------------------------------------------------------