UNPKG

node-bury

Version:

A cryptographic and steganographic image library for nodejs.

1,109 lines (970 loc) 43 kB
/** * File: Bury.js * Author: J. Ian Lindsay * Date: 2015.11.19 * * Copyright (c) 2015 J. Ian Lindsay * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * * * Author's BTC address: 17da1aqXEhdqMkbEq66nc2n5DeAnrnNbsK * * This library is meant to embed an encrypted message into the noise-floor of a carrier image. * The message is first compressed, then encrypted, then treated as a bitstream and modulated into * the carrier. The data to be written to the carrier is organized like this.... * * +--------+------------------------+----------+ * | HEADER | MESSAGE DATA | CHECKSUM | * +--------+------------------------+----------+ * | | | * | | +-- MD5, stored as binary (16 bytes). See Note0. * | | * | +-- ( IV + ENCRYPT( COMPRESS([FILENAME] + MESSAGE) ) ) * | * +--ACTIVE CHANNELS: 3-bits // Which channels are used to encode the data? See Note1. * VERSION: 2 bytes // The version of this program that wrote the image. * HEADER LENGTH: 1 byte // The length of this data structure. * MESSAGE PARAMS: 1 byte // Control bits for how the message is handled. See Note5. * CHANNEL PARAMS: 1 byte // These are reserved for later use, but will deal with carrier pre-processing. * PAYLOAD SIZE: 4 bytes // The size of the payload, including the checksum, but NOT the header. * * ============================================================================================================================ * Note0: Regarding the checksum * The MD5 checksum is the final 16-bytes of the bitstream. It is stored as binary, and its length is included in the * payload_size field of the header. The checksum only relates to the MESSAGE DATA, and not to the HEADER. * ============================================================================================================================ * * ============================================================================================================================ * Note1: Regarding the first important pixel * The channel-parameters are always stored in the pixel at offset 0 (Note3). That pixel's least-significant bits * are taken to mean which channels were used to encode everything else. Suppose the first pixel was (in RGB) #425523... * RED CHANNEL ENABLED? 0x42 % 0x01 = 0 = FALSE * GREEN CHANNEL ENABLED? 0x55 % 0x01 = 1 = TRUE * BLUE CHANNEL ENABLED? 0x23 % 0x01 = 1 = TRUE * * The HEADER_LENGTH parameter does not account for these 3-bits. * * All data (including the rest of the HEADER) will respect the constraint so determined. Typically, you would want to use * every availible channel to keep the noise profile consistent and maximize capacity (or minimize carrier size). But a * possible reason to use less than the maximum would be to overlay many messages (up to 3) in the same carrier with * different passwords. * ============================================================================================================================ * * * ============================================================================================================================ * Note3: Regarding parameters derived from the password * The password is the indirect source for offset and stride. The most-significant byte of the password's SHA256 hash is taken * to be the offset of the HEADER, The next two bytes are the number of hash rounds on the password. The fourth byte is * used to derive the maximum stride size. And the rest of the bytes are XOR'd to create the seed for the RNG. * ============================================================================================================================ * * * ============================================================================================================================ * Note5: Control bits that affect messages * The following is a table of bitmasks and how they relate to message options. Bits not defined here ought to be set randomly. * 0x01: Compress message prior to encryption. * 0x02: Enable encryption. As of version 0x01, this is always enabled, and ignored on read. * 0x04: Prepend filename to stream before compression/encryption. See Note6. * ============================================================================================================================ * * ============================================================================================================================ * Note6: Storing files * If the encrypting party loaded their message from a file, this feature will be enabled unless they specifically disabled * it (more about that later). It is possible to determine if the feature is enabled by checking that the appropriate bit * is set in the MESSAGE_PARAMS field (Note5). * * If the feature is enabled, the filename that was stored in the carrier will be truncated (or padded) to 32-bytes and * prepended to the data before compression (and therefore, before encryption as well). The file extension (if present) * will be preserved, regardless of padding and truncation of the rest of the filename. * * When the decrypting party successfully decodes the message, they can set write_file = path-to-dir, and the file will be * re-consituted on their filesystem. This is DANGEROUS on webservers running this code, as an attacker could bypass many * security layers related to file uploads. Then again... you can also leverage it to your advantage (putting back-doors * for arbitrary script into your systems). * ============================================================================================================================ * * * ============================================================================================================================ * Note7: Compression choices * Compression and encryption go together _really_ well for reasons I won't delve into here. * The way I saw it, there was no reason to support multiple compression algos. If the user wants to compress, there is no * reason to pussyfoot around. Go for density, since it carries the most extreme entropic benefit. * Speed? We aren't stremaing data. Be patient. The compression algo must be binary-safe. * I chose BZip2. * ============================================================================================================================ */ 'use strict' var util = require('util'); // TODO: Remove when done debugging. var fs = require('fs'); // File i/o var gd = require('node-gd'); // Image manipulation library. var binbuf = require('bufferpack'); // Bleh... typelessness.... var compressjs = require('compressjs'); // Compression library. var CryptoJS = require("crypto-js"); // Hash var rng = require('mersenne'); // We can't seed Math.random(). var bzip2 = compressjs.Bzip2; // These are global constants for the library. var VERSION_CODE = 0x02; // The version of the program. Will be included in the carrier. var MIN_PASS_LENGTH = 8; // The length of the smallest password we will tolerate. var HEADER_LENGTH = 9; // Length of the header (in bytes). var LOG_DEBUG = 7; var LOG_INFO = 5; var LOG_ERR = 2; var STR_PAD_LEFT = 1; var STR_PAD_RIGHT = 2; var STR_PAD_BOTH = 3; /* * Thanks, David (from StackOverflow) * http://stackoverflow.com/users/60682/david * Modified somewhat. Original was... * * Javascript string pad * http://www.webtoolkit.info/ */ var pad = function(str, len, pad, dir) { if (typeof(len) == "undefined") { var len = 32; } if (typeof(pad) == "undefined") { var pad = ' '; } if (typeof(dir) == "undefined") { var dir = STR_PAD_LEFT; } if (len + 1 >= str.length) { switch (dir){ case STR_PAD_LEFT: str = Array(len + 1 - str.length).join(pad) + str; break; case STR_PAD_BOTH: var right = Math.ceil((padlen = len - str.length) / 2); var left = padlen - right; str = Array(left+1).join(pad) + str + Array(right+1).join(pad); break; default: str = str + Array(len + 1 - str.length).join(pad); break; } } return str; }; /** * The hash and crypto deal with word arrays. But for sanity's sake, we sometimes * need to access them byte-wise. * Assumes big-endian. * @return an array of bytes. */ var toByteArray = function(word_array) { var byte_array = []; for (var i = 0; i < word_array.length; i++) { byte_array[i*4+0] = 0x000000FF & (word_array[i] >> 24); byte_array[i*4+1] = 0x000000FF & (word_array[i] >> 16); byte_array[i*4+2] = 0x000000FF & (word_array[i] >> 8); byte_array[i*4+3] = 0x000000FF & (word_array[i]); } return byte_array; }; /* Assumes big-endian. */ var toWordArray = function(byte_array) { var word_array = []; var temp_word = 0; var w = 0; for (var i = 0; i < byte_array.length; i++) { temp_word += byte_array[i] << ((3-(i%4))*8); if (3 == i%4) { // This is the end of a word. word_array[w++] = temp_word; temp_word = 0; } } return word_array; }; /* * Thanks, Eli... * http://stackoverflow.com/questions/2128157/javascript-equivalent-to-c-strncmp-compare-string-for-length */ var strncmp = function(a, b, n){ return a.substring(0, n) == b.substring(0, n); } /* * Taken from: * http://phpjs.org/functions/basename/ * discuss at: http://phpjs.org/functions/basename/ * original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) * improved by: Ash Searle (http://hexmen.com/blog/) * improved by: Lincoln Ramsay * improved by: djmix * improved by: Dmitry Gorelenkov */ function basename(path, suffix) { var b = path; var lastChar = b.charAt(b.length - 1); if (lastChar === '/' || lastChar === '\\') b = b.slice(0, -1); b = b.replace(/^.*[\/\\]/g, ''); if (typeof suffix === 'string' && b.substr(b.length - suffix.length) == suffix) { b = b.substr(0, b.length - suffix.length); } return b; } /** * Returns a string of length 32. Always. */ var normalize_filename = function(base_name) { var base_len = base_name.length; if (base_len == 0) { return ' ThisFileExtensionWasBad.txt'; } var base = (base_len > 32) ? base_name.substr(base_len-32) : pad(base_name); return base; } /** * Given the password, derive the following parameters.... * 0) Offset (in pixels) * 1) Hash round count. * 2) RNG seed * 3) Maximum stride range. * 4) Key material via the number from step 1. * * Without knowing the key, it should be made as difficult as possible to * mine the resulting image for patterns, and it ought to be as unlikely * as possible to guess it on accident. * * NOTE: RNG implementation will affect the consistency of this function's output. */ var deriveParamsFromKey = function(pw) { var params = {}; var t_initial = (new Date).getTime(); var hash = CryptoJS.SHA256(pw); // Give us back 32 bytes. var hash_arr = toByteArray(hash.words); // Need to access it byte-wise... params.offset = hash_arr[0]; // Where does the first header byte go? params.max_stride = 2+(hash_arr[3] % 14); // Make sure max-stride falls between 2 and 16 pixels. params.hash = hash_arr; // How many hash rounds should we run on the password? Limit it to 9000. // We don't want to go over 9000. var rounds = ((hash_arr[1] * 256) + hash_arr[2]) % 9000; // Use the remaining bits to seed the RNG for arythmic stride. var mixer_array = []; for (var i = 0; i < 7; i++) { mixer_array[0] = hash_arr[(i+4)] ^ mixer_array[0]; mixer_array[1] = hash_arr[(i+11)] ^ mixer_array[1]; mixer_array[2] = hash_arr[(i+18)] ^ mixer_array[2]; mixer_array[3] = hash_arr[(i+25)] ^ mixer_array[3]; } // Recombine into a 32-bit integer... params.stride_seed = (((mixer_array[0] *16777216) % 128) + (mixer_array[1] * 65536) + (mixer_array[2] * 256) + mixer_array[3]); // Spin the password around for awhile... for (var i = 0; i < rounds; i++) hash = CryptoJS.SHA256(hash); params.key = toByteArray(hash.words); // Now we have the 256-bit key. params.ms_required = (new Date).getTime() - t_initial; params.rounds = rounds; return params; }; /** * Instancing this object represents a full operation on a carrier. Either encrypting or decyrpting. * The carrier_path and password parameters are required for both operations. * The options parameter is always optional, and if not supplied, defaults will be used. */ function Bury(carrier_path, password, options) { /************************************************************************** * These are the options that can be supplied to the bury operation. * * The members below represents an exhaustive list, and the default * * values for each option. * **************************************************************************/ options = options ? options : {}; // Enabled carrier channels. No alpha support due to it standing out like a flare that says: 'ANOMALY!'. // It should also be noted that not using all of the channels makes the statistical profile of the // noise asymetrical. No human being could ever see this with their eyes, but a machine might. var enableRed = options.hasOwnProperty('enableRed') ? options.enableRed : true; var enableGreen = options.hasOwnProperty('enableGreen') ? options.enableGreen : true; var enableBlue = options.hasOwnProperty('enableBlue') ? options.enableBlue : true; // DEBUG OPTION Set to true to expose the affected pixels in the image. var visibleResult = options.hasOwnProperty('visibleResult') ? options.visibleResult : false; // Crush the message prior to encrypting? var compress = options.hasOwnProperty('compress') ? options.compress : true; // Should the output image be scaled to a minimum-size needed to fit the message? var rescaleCarrier = options.hasOwnProperty('rescaleCarrier') ? options.rescaleCarrier : true; // If supplied, this function will be called when there is a result ready. var callback = options.hasOwnProperty('callback') ? options.callback : false; // DEBUG OPTION How noisy should this class be about what it's doing? var verbosity = options.hasOwnProperty('verbosity') ? options.verbosity : LOG_INFO; /* These options apply to treatment of filenames for embedded files. */ // Encrypt only: If the user sets this to false, we will not store file information. var store_filename = options.hasOwnProperty('storeFilename') ? options.storeFilename : true; // Decrypt only: Should we write an output file, if applicable? Ignored for encryption. var write_file = options.hasOwnProperty('writeFile') ? options.writeFile : true; /************************************************************************** * Everything below this block is internal machinary of the class. * **************************************************************************/ /* These are instance variables for manipulating the carrier image. */ var __image = false; // Our working copy of the carrier. var __x = 0; // Cursor within the image. var __y = 0; // Cursor within the image. var __bitCursor = 0; // Used to keep track of how many bits we've (de)modulated. /* Variables for the cryptographic operations. */ var __iv_size = -1; // The size of the cipher's initialization vector. var __ciphertext = ''; var __plaintext = ''; var __key = ''; // Key material for the cipher algo. var __header = ''; // Prepended to the ciphertext to aid choice about length. var __payload_size = -1; // The size of the message after encryption and compression. Not the header. /* These parameters are derived from the password. Do not set them directly. */ var __max_size = -1; // Used to decide how much plaintext we can stuff into the carrier. var __max_stride = -1; // How much range should we allow in the arhythmic stride? var __offset = -1; // The first pixel to mean something. var __stride_seed = -1; // Use an arythmic stride between relevant pixels. var __strides = []; // Count off the intervals between pixels. var __usablePixels = 0; // How many pixels are we capable of using? // Holds the filename if setMessage() is called with a path. var __file_name_info = false; /* Logging is done this way to make redicrection of output more convenient. */ var log_error = function(body, v) { v = v ? v : LOG_DEBUG; if (v <= verbosity) console.log(body); }; /** * Call to shink the carrier to the minimum size required to store the bitstream. * Replaces the carrier image with the rescaled version. * Maintains aspect ratio. * Checks for adequate size. * Regenerates strides. * * Returns true on success, false on failure. */ var rescale_carrier = function() { var return_value = false; var bits = __payload_size * 8; var ratio = Math.max(__x, __y) / Math.min(__x, __y); var required_pixels = __offset; var bpp = getBitsPerPixel(); // How many bits-per-pixel can we have? var n = 0; while ((bits > 0) && (__strides[n])) { required_pixels += __strides[n++]; bits = bits - bpp; } log_error('Need a total of ' + required_pixels + ' pixels to store the given message with given password.'); log_error('Need a total of ' + required_pixels + ' pixels to store the given message with given password.'); n = Math.ceil(Math.sqrt(required_pixels / ratio)); var width = n; var height = n; if (__x >= __y) width = Math.ceil(width * ratio); else height = Math.ceil(height * ratio); var img = gd.createTrueColorSync(width, height); if (img) { if (__image.copyResized(img, 0, 0, 0, 0, width, height, __x, __y)) { if ((height * width) < (__x * __y)) { // Did we actually shrink the carrier? if ((height * width) >= required_pixels) { // Do we have enough space in the new carrier? __image.destroy(); __image = img; __x = img.width; __y = img.height; log_error('Scaled carrier into minimum required size for the given password: (' + __x + ', ' + __y + ').', LOG_INFO); __strides = []; // We will need to truncate the stride array because our image has shrunk. demarcate_strides(); } else log_error('Somehow we scaled the carrier and now it doesn\'t have enough space. Using the original carrier...', LOG_WARNING); } else log_error('Somehow we scaled the carrier and it got larger. Using the original carrier...', LOG_WARNING); return_value = true; } else log_error('Failed to scale the carrier.', LOG_ERR); } else log_error('Failed to create the scaled carrier..', LOG_ERR); return return_value; } /** * We need a truecolor image to do our trick. Save the user from vimself if ve submits * an image that isn't up to spec. */ var upgrade_color = function() { var img = gd.createTrueColorSync(__x, __y); __image.copy(img, 0, 0, 0, 0, __x, __y); __image.destroy(); __image = img; log_error('Resampled image into truecolor.', LOG_WARNING); } /************************************************************************** * These functions deal with deriving parameters from the key material. * **************************************************************************/ /** * Projective function that will run the arythmic stride as far out as the carrier * will allow, and save the results as an array of integers. The modulator will * need this array later to lay the data down into the proper pixels. */ var demarcate_strides = function() { if (__stride_seed >= 0) { rng.seed(__stride_seed); var usable_pixels = 0; // How many pixels can we use? var total_remaining = (__x * __y) - __offset; // Total remaining pixels. while (total_remaining > 0) { var delta = rng.rand(__max_stride-1)+1; total_remaining = total_remaining - delta; if (total_remaining > 0) { usable_pixels++; __strides.push(delta); } } log_error('There are ' + usable_pixels + ' usable pixels.', LOG_INFO); findMaxPayloadSize(); } else { log_error("Somehow there is no seed value.", LOG_WARNING); } }; /* * Given the stride info, figure out how much data we can pack into the carrier. */ var findMaxPayloadSize = function() { var enabled_channels = enableRed ? 'Red ' : ''; enabled_channels += enableGreen ? 'Green ' : ''; enabled_channels += enableBlue ? 'Blue ' : ''; log_error('Enabled channels: ' + enabled_channels); var bpp = getBitsPerPixel(); var raw_pixels = (__x * __y) - __offset; var stride_pix = __strides.length; __max_size = Math.floor((bpp * stride_pix) / 8); // The gross size. log_error('Maximum message size is ' + __max_size + ' bytes.', LOG_INFO); } /** * Returns an integer that indicates how many bits we can fit into each pixel using the current settings. */ var getBitsPerPixel = function() { var bpp = (enableRed) ? 1:0; // How many bits-per-pixel can we have? bpp += (enableGreen) ? 1:0; bpp += (enableBlue) ? 1:0; return bpp; } /************************************************************************** * Functions related to shoveling the message into the carrier image. * * Compress, encrypt, measure. * * Decide if we can fit it in the image. If we can, we might try. * * If we try, we need to write the header. * * * * Optionally rescale the image. * **************************************************************************/ /** * We need to record which channels we are going to make use of. * Record those pixels at the offset. */ var set_channel_spec = function() { var j = __offset % __x; var i = Math.floor(__offset / __x); var temp = __image.imageColorAt(j, i); var red = ((temp >> 16) & 0xFE) | (enableRed ? 0x01:0x00); var green = ((temp >> 8) & 0xFE) | (enableGreen ? 0x01:0x00); var blue = (temp & 0xFE) | (enableBlue ? 0x01:0x00); __image.setPixel(j, i, __image.colorAllocate(red, green, blue)); log_error('Wrote ('+red+', '+green+', '+blue+') (R, G, B) to offset ' + __offset + '.'); } /* * Encrypt the plaintext. */ var encrypt = function() { var return_value = true; var message_params = 0x00; if (store_filename) { if (__file_name_info.length != 32) { log_error('Filename was not 32 bytes. storing it generically...', LOG_WARNING); __file_name_info = ' bad_filename.txt'; } __plaintext = __file_name_info + __plaintext; } __plaintext = __plaintext.toString('binary'); var compressed = (compress) ? bzip2.compressBlock(__plaintext, __plaintext.length, 9) : __plaintext; var cipherObj = CryptoJS.AES.encrypt( compressed.toString('binary'), __key.toString('binary'), { mode: CryptoJS.mode.CFB, padding: CryptoJS.pad.ZeroPadding } ); var nu_iv = toByteArray(cipherObj.iv.words); var encrypted = toByteArray(cipherObj.ciphertext.words); //console.log(util.inspect(cipherObj.iv.words)); //console.log(util.inspect(nu_iv)); //console.log(util.inspect(toWordArray(nu_iv))); var checksum = toByteArray(CryptoJS.MD5(encrypted).words); var message_params = message_params | ((compress) ? 0x01:0x00); message_params = message_params | ((store_filename) ? 0x04:0x00); var payload_length = (encrypted.length + nu_iv.length + checksum.length); __ciphertext = new Buffer(payload_length + HEADER_LENGTH, 'binary'); //console.log(JSON.stringify(__ciphertext, null, 3)); if (binbuf.packTo('<HxBx', __ciphertext, 0, [VERSION_CODE, message_params])) { if (binbuf.packTo('>I', __ciphertext, 5, [payload_length])) { if (binbuf.packTo(nu_iv.length+'B', __ciphertext, HEADER_LENGTH, nu_iv.toString('binary'))) { if (binbuf.packTo(encrypted.length+'B', __ciphertext, (HEADER_LENGTH+nu_iv.length), encrypted)) { if (binbuf.packTo(checksum.length+'B', __ciphertext, payload_length-16, checksum)) { log_error('Packed payload. Ready for modulation.', LOG_DEBUG); __payload_size = __ciphertext.length; // Record the number of bytes to modulate. if (compress) { var pt_len = __plaintext.length; var comp_len = compressed.length; log_error('Compressed '+pt_len+' bytes into '+comp_len+'.', LOG_INFO); } if (store_filename) { log_error('Prepended filename to plaintext: '+__file_name_info, LOG_INFO); } log_error('Encrypted payload with header is '+__payload_size+' bytes.', LOG_INFO); return_value = true; } else log_error('Failed to pack checksum into payload.', LOG_ERR); } else log_error('Failed to pack ciphertext into payload.', LOG_ERR); } else log_error('Failed to pack IV into payload.', LOG_ERR); } else log_error('Failed to pack length into payload.', LOG_ERR); } else log_error('Failed to pack header into payload.', LOG_ERR); return return_value; } /* * Embed the header and ciphertext into the carrier. */ var modulate = function() { set_channel_spec(); // Record the channels in use. __bitCursor = 0; var initial = __offset + __strides[0]; // The offset stores the active channel settings. log_error('Initial pixel of modulation: (' + get_x_coords_by_linear(initial) + ', ' + get_y_coords_by_linear(initial) + ') (x, y).'); // Visit each usable pixel and modulate it. var abs_pix = __offset; for (var n = 0; n < __strides.length; n++) { var abs_pix = abs_pix + __strides[n]; var i = get_x_coords_by_linear(abs_pix); var j = get_y_coords_by_linear(abs_pix); var temp = __image.imageColorAt(i, j); var red = (temp >> 16) & 0xFF; var green = (temp >> 8) & 0xFF; var blue = (temp) & 0xFF; var bit; if (visibleResult) { if (enableRed) bit = getBit(); if (enableBlue) bit = getBit(); if (enableGreen) bit = getBit(); if (bit === false) { red = 0x00; blue = 0x00; green = 0xff; } else { green = 0x00; blue = 0x00; red = 0xff; } } else { if (enableRed) { bit = getBit(); if (bit !== false) red = (red & 0xFE) + bit; } if (enableBlue) { bit = getBit(); if (bit !== false) blue = (blue & 0xFE) + bit; } if (enableGreen) { bit = getBit(); if (bit !== false) green = (green & 0xFE) + bit; } } __image.setPixel(i, j, __image.colorAllocate(red, green, blue)); } return true; } /** * Given image coordinates, get the bit to be embedded in that pixel. * Otherwise, returns 0 or 1, as the case may dictate. */ var getBit = function() { var return_value = false; if (__bitCursor < (__payload_size * 8)) { var byte = Math.floor(__bitCursor / 8); var bit = __bitCursor % 8; var mask = 0x01 << bit; var feed = __ciphertext[byte]; return_value = (feed & mask) ? 0x01:0x00; __bitCursor++; } else { return_value = (visibleResult) ? false: (rng.rand(1)) ? 0x01:0x00; } return return_value; } /** * Helper function that returns the x-component of an image co-ordinate if * we give it a linear length argument. */ var get_x_coords_by_linear = function(linear) { return linear % __x; } /** * Helper function that returns the y-component of an image co-ordinate if * we give it a linear length argument. */ var get_y_coords_by_linear = function(linear) { return Math.floor(linear / __x); } /************************************************************************** * Functions related to getting the message out of the image. * **************************************************************************/ /** * Before we can read the header, we need to know which channels it is spread * across. */ var get_channel_spec = function() { var temp = __image.imageColorAt( __offset % __x, Math.floor(__offset / __x) ); enableRed = ((temp >> 16) & 0x01) ? true : false; enableGreen = ((temp >> 8) & 0x01) ? true : false; enableBlue = (temp & 0x01) ? true : false; } /* * Decrypt the ciphertext. */ var decrypt = function() { var return_value = true; __iv_size = 16; // We need the size of the IV... var nu_iv = __ciphertext.slice(0, __iv_size); var ct = __ciphertext.slice(__iv_size); var decrypted = CryptoJS.AES.decrypt( ct.toString('binary'), __key, {iv: nu_iv} ); //console.log(util.inspect(decrypted)) var decompressed = (compress) ? bzip2.decompressFile(decrypted) : decrypted; __file_name_info = store_filename ? decompressed.slice(0, 32).toString('binary').trim() : ''; __plaintext = store_filename ? decompressed.slice(32).toString('binary').trim() : decompressed.toString('binary').trim(); if (compress) log_error('Compression inflated '+decrypted.length+' bytes into '+decompressed.length+' bytes.', LOG_INFO); if (store_filename) log_error('Retrieved file name: '+__file_name_info, LOG_INFO); return return_value; } /* * Extract the header and ciphertext from the carrier. */ var demodulate = function() { get_channel_spec(); var all_bytes = [0x00]; var byte = 0; var bit = 0; var initial = __offset + __strides[0]; // The offset stores the active channel settings. log_error('Initial pixel of demodulation: ('+get_x_coords_by_linear(initial)+', '+get_y_coords_by_linear(initial)+') (x, y).'); // Visit each usable pixel and demodulate it. var abs_pix = __offset; for (var n = 0; n < __strides.length; n++) { abs_pix = abs_pix + __strides[n]; var i = get_x_coords_by_linear(abs_pix); var j = get_y_coords_by_linear(abs_pix); var temp = __image.imageColorAt(i, j); if (enableRed) { all_bytes[byte] = (all_bytes[byte] >> 1) + (((temp >> 16) & 0x01) << 7); bit++; if (bit % 8 == 0) all_bytes[++byte] = 0x00; } if (enableBlue) { all_bytes[byte] = (all_bytes[byte] >> 1) + (((temp) & 0x01) << 7); bit++; if (bit % 8 == 0) all_bytes[++byte] = 0x00; } if (enableGreen) { all_bytes[byte] = (all_bytes[byte] >> 1) + (((temp >> 8) & 0x01) << 7); bit++; if (bit % 8 == 0) all_bytes[++byte] = 0x00; } } // This function call makes a choice about the data we just read, // and unifies the channels into a single coherrant bit-stream, or // it errors. if (decodeHeader(all_bytes)) { if (verify_checksum()) { log_error('Message passed checksum.', LOG_INFO); return true; } else log_error('Message failed checksum.', LOG_ERR); } else log_error('Failed to decode the header.', LOG_ERR); return false; } var decodeHeader = function(bytes) { // First, we need to find the header... var ver = binbuf.unpack('<H', bytes, 0); var msg_params = binbuf.unpack('<B', bytes, 3); __payload_size = binbuf.unpack('>I', bytes, 5); compress = (msg_params & 0x0001) ? true : false; store_filename = (msg_params & 0x0004) ? true : false; __ciphertext = bytes.slice(HEADER_LENGTH); if (VERSION_CODE == ver) { log_error('Found a payload length of '+__payload_size+' bytes.'); return true; } else { log_error('Version code mismatch. File was written by version '+ver+' and this is version '+VERSION_CODE+'.', LOG_ERR); return false; } } /** * The last 16 bytes of the ciphertext will be a checksum for the encrypted message. * The header has already been removed from the cipher text, so no need to tip-toe around it. * Returns true if the message checks ok. * False otherwise. */ var verify_checksum = function() { var msg = __ciphertext.slice(0, __payload_size-16); var chksum = __ciphertext.slice(__payload_size-16); var hash = CryptoJS.MD5(msg); __ciphertext = msg; return (!strncmp(chksum.toString(), hash.toString(), 32)); } /** * Setting the message. */ this.setMessage = function(message, name_override) { var return_value = false; if (message) { if (__plaintext.length == 0) { if (fs.lstatSync(message).isFile()) { log_error('Message looks like a path to a file.', LOG_INFO); // if (is_readable(message)) { __plaintext = fs.readFileSync(message); if (store_filename) { if (name_override) message = name_override; // Facilitates HTML forms. var base = basename(message); __file_name_info = normalize_filename(base); log_error('Will use filename: '+__file_name_info, LOG_INFO); } log_error('Loaded '+__plaintext.length+' raw message bytes from file.', LOG_INFO); // } // else log_error('Provided message file is not readable.', LOG_INFO); } else if (message.length > 0) { log_error('Message looks like a string.', LOG_INFO); // Must pad the message... var padded_len = Math.floor(message.length) + (message.length % 16) ? 16 : 0; message = pad(message, padded_len, ' ', STR_PAD_RIGHT).toString('binary'); __plaintext = new Buffer(message, 'binary'); store_filename = false; // No need for this. } else log_error('Message must be either a path or a string.', LOG_ERR); } else log_error('Plaintext has already been set.', LOG_ERR); } else log_error('Message length is zero.', LOG_ERR); // If we loaded a message successfully, try to encrypt it and fit it into the carrier. if (__plaintext.length > 0) { __iv_size = 16; // We need the size of the IV... if (__iv_size) { if (encrypt()) { if (__payload_size <= __max_size) { // Only scale the image down. Never up. To do otherwise exposes the message. if (rescaleCarrier) rescale_carrier(); if (modulate()) { return_value = true; } else log_error('Modulation failed.', LOG_ERR); } else log_error('Encryption produced a payload of '+__payload_size+' bytes, which is '+(__payload_size - __max_size)+' bytes too large.', LOG_ERR); } else log_error('Encryption failed.', LOG_ERR); } else log_error('Bad cipher/mode combination.', LOG_ERR); } return return_value; } /** * Tries to retreive a message from the carrier and the given password. */ this.getMessage = function() { var return_value = false; if (__image) { if (demodulate()) { if (decrypt()) { if (store_filename) { if (write_file) { fs.writeFile(__file_name_info, __plaintext, 'utf8', function(err) { if (err) { log_error('Failed to write to file: '+__file_name_info+' because '+err, LOG_WARNING); } else { log_error('Wrote '+__plaintext.length+' bytes to '+__file_name_info, LOG_INFO); } } ); } } return_value = __plaintext; } else log_error('Decryption failed.', LOG_ERR); } else log_error('Demodulation failed.', LOG_ERR); } else log_error('No carrier loaded.', LOG_ERR); return return_value; } /** * Dumps the image as a base64 string (no parameter given), or a file (if a path was provided. */ this.outputImage = function(output_path, callback) { if (output_path) { __image.savePng(output_path, function(err) { if (err) { log_error('Failed to save PNG file.', LOG_ERR); } if (callback) callback(err) }); } else { return new Buffer(__image.pngPtr(), 'binary').toString('base64'); } } /** * Return the filename. */ this.filename = function() { return __file_name_info; } /** * Clean up our mess. */ this.destroyImage = function() { if (__image) __image.destroy(); __image = false; } if (password.length < MIN_PASS_LENGTH) { console.log('Password is too short. You must supply a password with at least ' + MIN_PASS_LENGTH + ' characters.'); } else { var params = deriveParamsFromKey(password); __key = params.key; __stride_seed = params.stride_seed; __max_stride = params.max_stride; __offset = params.offset; } /** * Try to load the carrier file specified by the argument. * Returns true on success and false on failure. */ if (fs.existsSync(carrier_path)) { var ptr = carrier_path.lastIndexOf('.'); if (ptr > 0) { // Gee... I sure hope we have a file extension... switch (carrier_path.substring(ptr).toLowerCase()) { case '.bmp': __image = gd.createFromWBMP(carrier_path); break; case '.gif': __image = gd.createFromGif(carrier_path); break; case '.png': __image = gd.createFromPng(carrier_path); break; case '.jpeg': case '.jpg': __image = gd.createFromJpeg(carrier_path); break; default: log_error(carrier_path + ' does not have a supported file extention. Failing, because: no carrier.', LOG_ERR); } if (__image) { __x = __image.width; __y = __image.height; log_error('Loaded carrier with size ('+__x+', '+__y+').'); if (!__image.trueColor) upgrade_color(); demarcate_strides(); } else { log_error('We got to a point where we ought to have an image, and we don\'t.', LOG_ERR); } } else { log_error('Cannot determine file extention.', LOG_ERR); } } else { log_error('Bad path. Doesn\'t exist, or isn\'t a file.', LOG_ERR); } }; /** * Report our version. */ Bury.getVersion = function() { return ('0x'+pad(VERSION_CODE.toString(16), 2, '0')); }; /** * Takes two (or three) passwords and tests them for mutual compatibility. This is needed only in cases * where you want to overlay more than one message (up to three, total) in the same carrier. * Returns true if the passwords are compatible. False otherwise. * * Compatibility is defined as the condition where no password results in an offset or a stride that overwrites * the first byte of a header from any other password. */ Bury.testPasswordCompatibility = function(pass0, pass1, pass2) { var return_value = false; var params0 = {}; var params1 = {}; var params2 = {}; var strides0 = []; var strides1 = []; var strides2 = []; params0 = deriveParamsFromKey(pass0); console.log('pass0: "' + pass0 + '"'); console.log(JSON.stringify(params0, null, 2)); params1 = deriveParamsFromKey(pass1); console.log('pass1: "' + pass1 + '"'); console.log(JSON.stringify(params1, null, 2)); var test_limit = Math.max(params0.offset, params1.offset); if (pass2) { params2 = deriveParamsFromKey(pass2); test_limit = Math.max(test_limit, params2.offset); console.log('pass2: "' + pass2 + '"'); console.log(JSON.stringify(params2, null, 2)); } console.log('The test only needs to find strides up to ' + test_limit + ' bytes.'); rng.seed(params0.stride_seed); var i = 0; var n = params0.offset; while (n < test_limit) { n += rng.rand(params0.max_stride); strides0.push(n); } rng.seed(params1.stride_seed); i = 0; n = params1.offset;; while (n < test_limit) { n += rng.rand(params1.max_stride); strides1.push(n); } if (pass2) { rng.seed(params2.stride_seed); i = 0; n = params2.offset;; while (n < test_limit) { n += rng.rand(params2.max_stride); strides2.push(n); } if ((strides1.indexOf(params2.offset) >= 0) || (strides0.indexOf(params2.offset) >= 0)) { } else if ((strides2.indexOf(params1.offset) >= 0) || (strides0.indexOf(params1.offset) >= 0)) { } else if ((strides1.indexOf(params0.offset) >= 0) || (strides2.indexOf(params0.offset) >= 0)) { } else { return_value = true; } } else { if ((strides1.indexOf(params0.offset) >= 0) || (strides0.indexOf(params1.offset) >= 0)) { } else { return_value = true; } } return return_value; }; module.exports = Bury;