<|start_filename|>Makefile<|end_filename|>
all: SIGNED.md

$(VERSION):
	bash version.sh $(VERSION)

version: $(VERSION)

typo.js: main.js QWERTY.keyboard dictionary default.help LICENSE $(VERSION)
	bash build.sh

typo.js.asc: typo.js
	keybase sign typo.js

.npmignore: .gitignore
	sort -ru .gitignore > .npmignore
	echo '.gitignore .npmignore Makefile *.sh' | tr ' ' '\n' >> .npmignore

.kbignore: .npmignore
	sort -ru .npmignore > .kbignore
	echo package.json >> .kbignore

SIGNED.md: typo.js.asc .kbignore
	keybase dir sign

verify:
	keybase dir verify
	keybase verify typo.js.asc

ifdef VERSION
tag: SIGNED.md
	git commit -am 'Signed PGP:E6B74303'
	git tag v$(VERSION)
endif

clean:
	rm -fv typo.js
	git checkout SIGNED.md typo.js.asc

.PHONY: clean version SIGNED.md verify tag



<|start_filename|>main.js<|end_filename|>
/*  ----------------------------------------------------------------------------
 *  typo v0.4.12
 *  
 *  Hide secret information in typographical errors
 *  
 *  Author:  <NAME> (<EMAIL>)
 *  Date:    April 9, 2015
 *  
 *  See 'typo --help'
 *  
 *  PGP: 57F8 9653 7461 1F9C EEF9 578B FBDC 955C E6B7 4303
 *  
 *  Bitcoin: 1NxChtv1R6q6STF9rq1BZsZ4jUKDh5MsQg
 *  
 *  http://manishjethani.com/
 *  
 *  Copyright (c) 2015 <NAME>
 *  
 *  Permission to use, copy, modify, and/or distribute this software for any
 *  purpose with or without fee is hereby granted, provided that the above
 *  copyright notice and this permission notice appear in all copies.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 *  SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 *  IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *  ------------------------------------------------------------------------- */

var crypto   = require('crypto');
var fs       = require('fs');
var os       = require('os');
var path     = require('path');
var readline = require('readline');
var stream   = require('stream');

var _name    = 'typo';
var _version = '0.4.12';

var QWERTY  = !'<%= package %>' && '<%= keyboard %>';

var WORDS   = !'<%= package %>' && '<%= dictionary %>'.split('\n');

var HELP_TEXT     = !'<%= package %>' && '<%= help %>';
var LICENSE_TEXT  = !'<%= package %>' && '<%= license %>';

var dictionary = {};

var rules = {};
var rulesetOrder = [];

var wordCharacter = /[A-Za-z'-]/;

var wordPattern = /^'?[A-Za-z]+-?[A-Za-z]+'?[A-Za-z]'?$/;

var say = function () {};

var colors = {
  'black':    { open: 30, close: 39 },
  'red':      { open: 31, close: 39 },
  'green':    { open: 32, close: 39 },
  'yellow':   { open: 33, close: 39 },
  'blue':     { open: 34, close: 39 },
  'magenta':  { open: 35, close: 39 },
  'cyan':     { open: 36, close: 39 },
  'white':    { open: 37, close: 39 },
  'gray':     { open: 90, close: 39 },
  'grey':     { open: 90, close: 39 },
};

var backgroundColors = {
  'black':    { open: 40, close: 49 },
  'red':      { open: 41, close: 49 },
  'green':    { open: 42, close: 49 },
  'yellow':   { open: 43, close: 49 },
  'blue':     { open: 44, close: 49 },
  'magenta':  { open: 45, close: 49 },
  'cyan':     { open: 46, close: 49 },
  'white':    { open: 47, close: 49 },
};

var textStyles = {
  'bold':           { open:  1, close: 22 },
  'dim':            { open:  2, close: 22 },
  'italic':         { open:  3, close: 23 },
  'underline':      { open:  4, close: 24 },
  'inverse':        { open:  7, close: 27 },
  'hidden':         { open:  8, close: 28 },
  'strikethrough':  { open:  9, close: 29 },
};

function sayImpl(prefix) {
  if (!prefix) {
    return console.error;
  } else if (typeof prefix === 'function') {
    return function () {
      console.error.apply(console, [ prefix() ].concat(
            sliceArguments(arguments)));
    };
  } else {
    return function () {
      console.error.apply(console, [ prefix ].concat(
            sliceArguments(arguments)));
    };
  }
}

function sliceArguments(begin, end) {
  return Array.prototype.slice.call(sliceArguments.caller.arguments,
      begin, end);
}

function async(func) {
  var args = sliceArguments(1);
  process.nextTick(function () {
    func.apply(null, args);
  });
}

function chain(list, errorCallback, doneCallback) {
  // This function lets you chain function calls so the output of one is the
  // the input to the next. If any of them throws an error, it goes to the
  // error callback. Once the list has been exhausted, the final result goes to
  // the done callback.

  var params = sliceArguments(3);

  var func = list.shift();

  if (func) {
    params.push(function (error) {
      if (error) {
        errorCallback(error);
      } else {
        chain.apply(null, [ list, errorCallback, doneCallback ]
              .concat(sliceArguments(1)));
      }
    });
  } else {
    func = doneCallback;
  }

  async(function () {
    try {
      func.apply(null, params);
    } catch (error) {
      errorCallback(error);
    }
  });
}

function die() {
  if (arguments.length > 0) {
    console.error.apply(console, arguments);
  }

  process.exit(1);
}

function dieOnExit() {
  process.exitCode = 1;
}

function logError(error) {
  if (error) {
    console.error(error.toString());
  }
}

function parseArgs(args) {
  // This is another cool function. It parses command line arguments of two
  // kinds: '--long-name[=<value>]' and '-n [<value>]'
  // 
  // If the value is omitted, it's assumed to be a boolean true.
  // 
  // You can pass in default values and a mapping of short names to long names
  // as the first and second arguments respectively.

  var rest = sliceArguments(1);

  var defaultOptions  = typeof rest[0] === 'object' && rest.shift() || {};
  var shortcuts       = typeof rest[0] === 'object' && rest.shift() || {};

  var expect = null;
  var stop = false;

  var obj = Object.create(defaultOptions);

  obj = Object.defineProperty(obj, '...', { value: [] });
  obj = Object.defineProperty(obj, '!?',  { value: [] });

  // Preprocessing.
  args = args.reduce(function (newArgs, arg) {
    if (!stop) {
      if (arg === '--') {
        stop = true;

      // Split '-xyz' into '-x', '-y', '-z'.
      } else if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') {
        arg = arg.slice(1).split('').map(function (v) { return '-' + v });
      }
    }

    return newArgs.concat(arg);
  },
  []);

  stop = false;

  return args.reduce(function (obj, arg, index) {
    var single = !stop && arg[0] === '-' && arg[1] !== '-';

    if (!(single && !(arg = shortcuts[arg]))) {
      if (!stop && arg.slice(0, 2) === '--') {
        if (arg.length > 2) {
          var eq = arg.indexOf('=');

          if (eq === -1) {
            eq = arg.length;
          }

          var name = arg.slice(2, eq);

          if (!single && !defaultOptions.hasOwnProperty(name)) {
            obj['!?'].push(arg.slice(0, eq));

            return obj;
          }

          if (single && eq === arg.length - 1) {
            obj[expect = name] = '';

            return obj;
          }

          obj[name] = typeof defaultOptions[name] === 'boolean'
              && eq === arg.length
              || arg.slice(eq + 1);

        } else {
          stop = true;
        }
      } else if (expect) {
        obj[expect] = arg;

      } else if (rest.length > 0) {
        obj[rest.shift()] = arg;

      } else {
        obj['...'].push(arg);
      }

    } else if (single) {
      obj['!?'].push(args[index]);
    }

    expect = null;

    return obj;
  },
  obj);
}

function prettyBuffer(buffer) {
  return (buffer.toString('hex').toUpperCase().match(/.{2}/g) || []).join(' ');
}

function hash(message, algorithm) {
  return crypto.Hash(algorithm || 'sha256').update(message).digest();
}

function stringDistance(s, t) {
  var a = new Array(t.length + 1);
  for (var x = 0; x < a.length; x++) {
    a[x] = x;
  }

  for (var j = 1; j <= s.length; j++) {
    var p = a[0]++;
    for (var k = 1; k <= t.length; k++) {
      var o = a[k];
      if (s[j - 1] === t[k - 1]) {
        a[k] = p;
      } else {
        a[k] = Math.min(a[k - 1] + 1, a[k] + 1, p + 1);
      }
      p = o;
    }
  }

  return a[t.length];
}

function shuffle(array) {
  for (var m = array.length - 1; m >= 0; m--) {
    var x = 0 | Math.random() * (m + 1);
    if (x !== m) {
      var p = array[x];
      array[x] = array[m];
      array[m] = p;
    }
  }
  return array;
}

function sortBy(array, prop) {
  return array.sort(function (a, b) {
    return -(a[prop] < b[prop]) || +(a[prop] > b[prop]);
  });
}

function findCloseMatches(string, candidateList, distanceThreshold) {
  if (arguments.length < 3) {
    distanceThreshold = 1;
  }

  var matches = candidateList.map(function (candidate) {
    // Split candidate into individual components. e.g. 'output-file' becomes a
    // list containing 'output', 'file', and 'output-file'.
    var candidateWords = candidate.split('-');
    if (candidateWords.length > 1) {
      candidateWords.push(candidate);
    }

    var distance = candidateWords.reduce(function (distance, word) {
      // Take the lowest distance.
      return Math.min(distance, stringDistance(string, word));
    },
    Infinity);

    return {
      candidate: candidate,
      distance:  distance
    };

  }).filter(function (match) {
    return match.distance <= distanceThreshold;
  });

  sortBy(matches, 'distance');

  return matches.map(function (match) { return match.candidate });
}

function typeMatch(one, other, type, exempt) {
  // Check that every property of the given type in one object is also of the
  // same type in the other object.
  return Object.keys(one).every(function (key) {
    return typeof one[key] !== type
        || typeof other[key] === type
        || exempt && exempt.indexOf(key) !== -1;
  });
}

function trigrams(word) {
  // Return three-letter sequences for the word.

  // Example: 'hello' ...

  var seq = [
    // '^he', 'lo$'
    '^' + word.slice(0, 2),
    word.slice(word.length - 2) + '$'
  ];

  // 'hel', 'ell', 'llo'
  for (var i = 0; i < word.length - 2; i++) {
    seq.push(word.slice(i, i + 3));
  }

  return seq;
}

function parseTabularData(data) {
  if (data == null) {
    return null;
  }

  var lines = data.toString().split('\n');
  var records = lines.filter(function (line) {
    return line.match(/^[^#]/);
  });

  return records.map(function (record) {
    return record.split('\t');
  });
}

function stringToBuffer(string, format) {
  var buffer = null;

  switch (format) {
  case 'hex':
    string = '0'.slice(0, string.length % 2) + string;
  case 'base64':
    buffer = new Buffer(string, format);
    break;
  default:
    buffer = new Buffer(string);
  }

  return buffer;
}

function bufferToString(buffer, format) {
  var string = null;

  switch (format) {
  case 'hex':
  case 'base64':
    string = buffer.toString(format);
    break;
  default:
    string = buffer.toString();
  }

  return string;
}

function slurp(callback) {
  var input = '';

  process.stdin.setEncoding('utf8');

  process.stdin.on('readable', function () {
    var chunk = process.stdin.read();
    if (chunk !== null) {
      input += chunk;
    }
  });

  process.stdin.on('end', function () {
    callback(null, input);
  });
}

function slurpFile(filename, callback) {
  fs.readFile(filename, { encoding: 'utf8' }, callback);
}

function slurpFileSync(filename) {
  return fs.readFileSync(filename, { encoding: 'utf8' });
}

function dumpFile(filename, stream, transformer) {
  if (!stream) {
    stream = process.stdout;
  }

  if (transformer) {
    transformer.pipe(stream);
    stream = transformer;
  }

  fs.createReadStream(filename, { encoding: 'utf8' }).pipe(stream);
}

function readInput(filename, callback) {
  if (filename) {
    say('Reading input from file ' + filename);

    slurpFile(filename, callback);

  } else {
    say('Reading input from stdin');

    slurp(callback);
  }
}

function writeOutput(string, filename) {
  if (string != null) {
    if (filename) {
      say('Writing output to file ' + filename);

      fs.writeFileSync(filename, string);

    } else {
      say('Writing output to stdout');

      process.stdout.write(string);
    }
  }
}

function prompt(label, quiet, callback) {
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
    throw new Error('No TTY.');
  }

  if (arguments.length > 0) {
    callback = arguments[arguments.length - 1];
    if (typeof callback !== 'function') {
      callback = null;
    }
  }

  if (typeof quiet !== 'boolean') {
    quiet = false;
  }

  if (typeof label === 'string') {
    process.stdout.write(label);
  }

  var rl = readline.createInterface({
    input: process.stdin,
    // The quiet argument is for things like passwords. It turns off standard
    // output so nothing is displayed.
    output: !quiet && process.stdout || null,
    terminal: true
  });

  rl.on('line', function (line) {
    rl.close();

    if (quiet) {
      process.stdout.write(os.EOL);
    }

    if (callback) {
      callback(null, line);
    }
  });
}

function deriveKey(password, salt, length) {
  return crypto.pbkdf2Sync(password, salt, 0x100000, length, 'sha256');
}

function encrypt(buffer, password, salt, authenticated) {
  var keyLength = 48;
  var algorithm = 'aes-256-ctr';

  if (authenticated) {
    keyLength = 44;
    algorithm = 'aes-256-gcm';
  }

  var key = deriveKey(password, salt, keyLength);
  var cipher = crypto.createCipheriv(algorithm, key.slice(0, 32),
      key.slice(32));

  var encrypted = Buffer.concat([ cipher.update(buffer), cipher.final() ]);

  if (authenticated) {
    // Attach 16-byte authentication tag.
    encrypted = Buffer.concat([ encrypted, cipher.getAuthTag() ]);
  }

  return encrypted;
}

function decrypt(buffer, password, salt, authenticated) {
  var keyLength = 48;
  var algorithm = 'aes-256-ctr';

  if (authenticated) {
    keyLength = 44;
    algorithm = 'aes-256-gcm';
  }

  var key = deriveKey(password, salt, keyLength);
  var decipher = crypto.createDecipheriv(algorithm, key.slice(0, 32),
      key.slice(32));

  if (authenticated) {
    decipher.setAuthTag(buffer.slice(-16));
    buffer = buffer.slice(0, -16);
  }

  return Buffer.concat([ decipher.update(buffer), decipher.final() ]);
}

function wordValue(word) {
  // The value of a word is the lower half of the first octet of its SHA-256
  // digest.
  // 
  // e.g. 'colour' is 6 (hash: 'd6838c35...')
  return hash(word, 'sha256')[0] & 0xF;
}

function isColorName(name) {
  return Object.keys(colors).indexOf(name) !== -1;
}

function isTextStyleName(name) {
  return Object.keys(textStyles).indexOf(name) !== -1;
}

function colorize(text, color, table) {
  if (!table) {
    table = colors;
  }

  var open  = '';
  var close = '';

  var obj = table[color];

  if (obj) {
    open  = obj.open;
    close = obj.close;
  }

  return '\u001b[' + open + 'm' + text + '\u001b[' + close + 'm';
}

function textStylize(text, textStyle) {
  return colorize(text, textStyle, textStyles);
}

function stylize(text, style) {
  var props = style && style.match(/([^\s]+)/g) || [];

  var colorProps = props.filter(isColorName);
  var textStyleProps = props.filter(isTextStyleName);

  // Foreground and background colors respectively.
  text = colorize(text, colorProps[0]);
  text = colorize(text, colorProps[1], backgroundColors);

  // Text styles.
  text = textStyleProps.reduce(textStylize, text);

  return text;
}

function printVersion() {
  console.log(_name + ' v' + _version);
}

function printHelp() {
  if (HELP_TEXT) {
    process.stdout.write(HELP_TEXT);
    process.stdout.write(os.EOL);
    return;
  }

  dumpFile(path.join(__dirname, 'default.help'));
}

function printLicense() {
  if (LICENSE_TEXT) {
    process.stdout.write(LICENSE_TEXT);
    process.stdout.write(os.EOL);
    return;
  }

  dumpFile(path.join(__dirname, 'LICENSE'));
}

function printSource() {
  dumpFile(__filename);
}

function printUsage() {
  var seeHelp = os.EOL + os.EOL + "See '" + _name + " --help'."
              + os.EOL + os.EOL;

  var breakAt = '\n\n';

  if (HELP_TEXT) {
    process.stderr.write(HELP_TEXT.slice(0, HELP_TEXT.indexOf(breakAt)));
    process.stderr.write(seeHelp);
    return;
  }

  var cut = false;

  var x = new stream.Transform({ decodeStrings: false });
  x._transform = function (chunk, encoding, callback) {
    if (!cut) {
      var br = chunk.indexOf(breakAt);
      if (br !== -1) {
        cut = true;
        this.push(chunk.slice(0, br));
      } else {
        this.push(chunk);
      }
      callback();
    }
  };
  x._flush = function (callback) {
    this.push(seeHelp);
    callback();
  };

  dumpFile(path.join(__dirname, 'default.help'), process.stderr, x);
}

function printCloseMatches(string, candidateList) {
  var closeMatches = findCloseMatches(string, candidateList, 2);

  if (closeMatches.length > 1) {
    console.error('Did you mean one of these?');
  } else if (closeMatches.length === 1) {
    console.error('Did you mean this?');
  }

  closeMatches.forEach(function (v) {
    console.error('\t' + v);
  });
}

function loadDictionary(filename) {
  say('Loading dictionary' + (filename ? ' file ' + filename : ''));

  var defaultFilename = path.join(__dirname, 'dictionary');
  var defaultWords = WORDS;

  var words = filename ? slurpFileSync(filename).split('\n')
      : defaultWords  || slurpFileSync(defaultFilename).split('\n');

  words.forEach(function (word) {
    trigrams(word).forEach(function (v) {
      dictionary[v] = dictionary[v] + 1 || 1;
    });
  });
}

function loadKeyboard(filename) {
  say('Loading keyboard ' + (filename ? 'file ' + filename : 'QWERTY'));

  var defaultFilename = path.join(__dirname, 'QWERTY.keyboard');
  var defaultLayout = QWERTY;

  var keyboard = [];

  var layout = filename ? slurpFileSync(filename)
      : defaultLayout  || slurpFileSync(defaultFilename);

  layout.split('\n').forEach(function (row) {
    var keys = row.split('');
    if (keys.length > 0) {
      keyboard.push(keys);
    }
  });

  var ruleset = [];

  var addRule = function (pattern, substitution, weight) {
    ruleset = ruleset.concat(createRule(pattern, substitution, weight));
  };

  for (var i = 0; i < keyboard.length; i++) {
    for (var j = 0; j < keyboard[i].length; j++) {
      var c = (keyboard[i][j] || '').toLowerCase();

      if (c.match(/[a-z]/)) {
        for (var k = j - 1; k <= j + 1; k += 2) {
          var x = (keyboard[i][k] || '').toLowerCase();

          if (x.match(/[a-z]/)) {
            var p = '([^' + c + x + '][^' + c + x + '])' + c
                  + '([^' + c + x + '][^' + c + x + '])';

            // Insertions (aka "fat fingers")
            addRule(p, '$1' + c + x + '$2', 4);
            addRule(p, '$1' + x + c + '$2');

            // Substitutions (wrong key)
            addRule(p, '$1' + x     + '$2');
          }
        }

        // Transpositions
        addRule('([^' + c + '])' + c + '([a-z])(?!\\2)', '$1$2' + c, 4);

        // Shift typos (e.g. "THe")
        addRule('^([A-Z])' + c, '$1' + c.toUpperCase());
      }
    }
  }

  rules['keyboard'] = ruleset;

  rulesetOrder.push('keyboard');
}

function createRule(pattern, substitution, weight) {
  if (arguments.length < 3) {
    return { re: new RegExp(pattern), sub: substitution };
  }

  if (isNaN(weight = +weight)) {
    weight = 1;
  }

  var array = [];
  for (var i = 0; i < weight; i++) {
    array.push(createRule(pattern, substitution));
  }
  return array;
}

function loadRulesetFile(filename, alias) {
  var data = slurpFileSync(filename);
  var records = parseTabularData(data);

  var ruleset = records.reduce(function (ruleset, fields) {
    return ruleset.concat(createRule.apply(null, fields.slice(0, 2)));
  },
  []);

  rules[alias || filename] = ruleset;

  return ruleset;
}

function loadRules(name) {
  if (rules.hasOwnProperty(name)) {
    return rules[name];
  }

  say('Loading ruleset ' + name);

  return loadRulesetFile(path.join(__dirname, name + '.rules'), name);
}

function rulesetAvailable(name) {
  try {
    fs.accessSync(path.join(__dirname, name + '.rules'));
    return true;
  } catch (error) {
    return false;
  }
}

function loadRulesets(spec, filename) {
  if (filename) {
    rulesetOrder = 'custom'.split(' ');

    say('Loading ruleset file ' + filename);

    loadRulesetFile(filename, 'custom');

  } else {
    if (spec != null) {
      rulesetOrder = spec.match(/([^ ,]+)/g) || [];

    } else {
      if (rulesetAvailable('misspelling')) {
        rulesetOrder.push('misspelling');
      }

      if (rulesetAvailable('grammatical')) {
        rulesetOrder.push('grammatical');
      }
    }

    rulesetOrder.forEach(loadRules);
  }
}

function shuffleRules(name) {
  shuffle(rules[name] || []);
}

function mapOptions(options, names, values) {
  names.forEach(function (n) {
    var v = values.shift();
    if (v !== undefined && !options.hasOwnProperty(n)) {
      options[n] = v;
    }
  });
}

function readPassword(password, callback) {
  if (password === true) {
    prompt('Password: ', true, callback);
  } else {
    async(callback, null, typeof password === 'string' ? password : null);
  }
}

function checkPlausibility(typo) {
  // Check if the typo is 'plausible' (note: quotes).
  var n = trigrams(typo.toLowerCase()).reduce(function (a, v) {
    return a + !!dictionary[v];
  },
  0);

  // If every three-letter sequence in the word occurs at least once in the
  // dictionary, we consider it 'plausible'.
  return n / typo.length >= 1;
}

function generateTypos(word) {
  if (!word.match(wordPattern)) {
    return [];
  }

  var collection = [];

  // Bookkeeping.
  var book = {};

  rulesetOrder.forEach(function (name) {
    if (!rules.hasOwnProperty(name)) {
      // Ruleset is not available.
      return;
    }

    rules[name].forEach(function (rule) {
      var mutation = word.replace(rule.re, rule.sub);

      if (mutation === word
          // Include every mutation no more than once.
          || book.hasOwnProperty(mutation)

          // For QWERTY typos, include the typo only if it passes the
          // 'plausibility' test.
          || (name === 'keyboard' && !checkPlausibility(mutation))
          ) {
        return;
      }

      collection.push(mutation);

      book[mutation] = true;
    });
  });

  return collection;
}

function processWord(word, buffer, offset) {
  // Take the low 4 bits.
  var nibble = 0xF & buffer[offset];

  generateTypos(word).some(function (candidate) {
    if (wordValue(candidate) === nibble) {
      // This typo works.
      word = candidate;

      return true;
    }
  });

  return word;
}

function extractTypos(original, modified) {
  // Compare the two texts to get all the typos.
  var typos = [];

  var offset = 0;

  var i = -1;
  var j = -1;
  var k = -1;

  var c = null;

  for (i = 0; i < modified.length; i++) {
    if (modified[i] !== original[i + offset]) {
      // We've hit a typo!

      var word = '';

      // Add every character until the beginning of the word.
      for (j = i - 1; j >= 0; j--) {
        c = modified[j];

        if (!c.match(wordCharacter)) {
          break;
        }

        word = c + word;
      }

      // Now add every character until the end of the word.
      for (j = i; j < modified.length; j++) {
        c = modified[j];

        if (!c.match(wordCharacter)) {
          break;
        }

        word += c;
      }

      // This is the piece of information we're looking for.
      typos.push(word);

      // Stay in sync with the original text.
      for (k = i + offset; k < original.length; k++) {
        if (!original[k].match(wordCharacter)) {
          break;
        }
      }

      i = j;
      offset = k - j;
    }
  }

  return typos;
}

function extractTyposFromMarkup(markup) {
  var typos = [];

  // Look for our custom markup, which is of the form '{[s/typo/correction/]}'
  var index = markup.indexOf('{[s/');
  while (index !== -1) {
    var x1 = markup.indexOf('/', index + 4) + 1;
    var x2 = markup.indexOf('/', x1);

    // Extract typo and save it.
    typos.push(markup.slice(index + 4, x1 - 1));

    // Also do the substitution on the input text.
    markup = markup.slice(0, index) + markup.slice(x1, x2)
      + markup.slice(x2 + 3);

    index = markup.indexOf('{[s/');
  }

  // By the end of the loop we have both the list of typos and the original
  // text.
  return {
    typos:     typos,
    original:  markup
  };
}

function encryptSecret(secret, format, password, covertext, nosalt,
    authenticated) {
  var salt = hash(!nosalt && covertext || '');
  var extraSalt = !nosalt && crypto.randomBytes(2) || new Buffer(0);

  var encrypted = encrypt(stringToBuffer(secret, format),
      password || '',
      Buffer.concat([ salt, extraSalt ]),
      authenticated);

  return Buffer.concat([ extraSalt, encrypted ]);
}

function decryptPayload(payload, format, password, covertext, nosalt,
    authenticated) {
  var ciphertextBegin = !nosalt ? 2 : 0;

  var salt = hash(!nosalt && covertext || '');
  var extraSalt = payload.slice(0, ciphertextBegin);

  var decrypted = decrypt(payload.slice(ciphertextBegin),
      password || '',
      Buffer.concat([ salt, extraSalt ]),
      authenticated);

  return bufferToString(decrypted, format);
}

function encode(message, secret, password, options) {
  var result = null;

  if (password) {
    say('Password:', new Array(2 + Math.floor(Math.random() * 15)).join('*'));
  }

  say('Encrypting ...');

  var buffer = encryptSecret(secret, options.format, password, message,
      options.deterministic || options.nosalt,
      options.authenticated);

  say('Encrypted secret:', prettyBuffer(buffer));

  say('Buffer size: ' + buffer.length);

  var random = null;
  var odd = false;

  if (!options.deterministic) {
    try {
      random = crypto.randomBytes(2);

      // One in two times add an extra meaningless typo just to throw 'em off.
      // By default we always have an even number of typos. This helps.
      odd = random[1] >= 128;
    } catch (error) {
    }
  }

  // This is the ratio of the total number of typos to the message length. It's
  // the rate at which typos should be introduced. We want to make sure the
  // typos are spread out more or less evenly.
  var density = (buffer.length * 2 + odd) / message.length;

  say('Density: ' + (density * 1000).toFixed(4) + ' per thousand');

  // This is how much we try to squeeze the information into the message.
  var multiplier = 1.0;

  do {
    say('Trying with multiplier ' + multiplier.toFixed(4));

    var workingBuffer = new Buffer(buffer);

    var word = '';
    var count = 0;

    var targetDensity = density * multiplier;

    result = '';

    for (var i = 0; i < message.length; i++) {
      var c = message[i];

      if (c.match(wordCharacter)) {
        word += c;
      } else {
        if (word) {
          // Here we're dividing count by two and rounding down. The offset
          // into the buffer is half of the number of typos already introduced,
          // because each typo carries only 4 bits of information.
          var offset = count >>> 1;
          var newWord = null;

          if (offset < buffer.length) {
            // Adjust the bar for letting in the next typo based on the current
            // rate.
            var bar = count / i / targetDensity || 0;

            if (bar < 1.0) {
              newWord = processWord(word, workingBuffer, offset);
            } else {
              newWord = word;
            }

          } else if (odd) {
            // Throw in the extra typo.
            newWord = processWord(word, random, 0);

          } else {
            newWord = word;
          }

          var replacement = newWord;

          if (newWord !== word) {
            if (options.markup) {
              replacement = '{[s/' + newWord + '/' + word + '/]}';

              // Once you're satisfied with the result, open in Vim and do:
              // %s/{\[s\/\([^\/]\+\)\/[^\/]\+\/\]}/\1/g

            } else if (options.highlight) {
              replacement = stylize(replacement, options.highlight);
            }

            if (offset < buffer.length) {
              // Bring the next 4 bits into position.
              workingBuffer[offset] >>>= 4;
            } else {
              odd = false;
            }

            if (++count >>> 1 >= buffer.length && !odd) {
              // Optimization: We have enough typos now, let's add the rest of
              // the text and get out of this loop.
              result += replacement;
              result += message.slice(i);
              break;
            }
          }

          result += replacement;
          word = '';
        }

        result += c;
      }
    }

    say('Score: ' + count + ' / ' + buffer.length * 2);

    // Try again if required with a higher density.
  } while (count >>> 1 < buffer.length && (multiplier *= 1.1) <= 10.0);

  if (count >>> 1 < buffer.length) {
    // This is the main problem. The input text simply isn't big enough for the
    // secret. For example, you can't encode 'Hello, world!' in 'A quick brown
    // fox jumped over the lazy dog.'
    throw new Error('Not enough text.');
  }

  return result;
}

function decode(message, password, options) {
  var original = null;

  var typos = null;

  say('Extracting typos');

  if (options.original != null) {
    // If we have the original text, we're only interested in extracting the
    // typos.
    typos = extractTypos(original = options.original, message);

  } else {
    // If the original text hasn't been provided, then we assume the input text
    // contains substitution markup, and we try to extract both the list of
    // typos and the original text out of it.
    var obj = extractTyposFromMarkup(message);

    original = obj.original;
    typos = obj.typos;
  }

  if (typos.length % 2 === 1) {
    // Ignore any odd typo at the end.
    typos.pop();
  }

  var buffer = new Buffer(typos.length / 2);

  say('Buffer size: ' + buffer.length);

  for (var i = 0; i < typos.length; i++) {
    var d = wordValue(typos[i]);

    // Read the encrypted secret 4 bits at a time. The even ones are the low 4
    // bits, the odd ones are the high 4 bits.
    if (i % 2 === 0) {
      buffer[i >>> 1] = d;
    } else {
      buffer[i >>> 1] |= d << 4;
    }
  }

  say('Encrypted secret:', prettyBuffer(buffer));

  if (password) {
    say('Password:', new Array(2 + Math.floor(Math.random() * 15)).join('*'));
  }

  say('Decrypting ...');

  // Finally, decrypt the buffer to get the original secret.
  return decryptPayload(buffer, options.format, password, original,
      options.nosalt,
      options.authenticated);
}

function query(q, options) {
  say('Generating typos');

  var data = generateTypos(q || '').map(function (typo) {
    var value = wordValue(typo);

    var grams = trigrams(typo.toLowerCase());
    var hits = grams.reduce(function (a, v) {
      return a + (dictionary[v] || 0);
    },
    0);

    var score = hits / grams.length;

    return { typo: typo, value: value, score: score };
  });

  // Sort by score.
  sortBy(data, 'score').reverse();

  return data.map(function (record) {
    return [
      stylize(record.typo, options.highlight),
      record.value.toString(16).toUpperCase(),
      record.score.toFixed(4),
    ].join('\t');

  }).join(os.EOL);
}

function run() {
  if (process.argv.length <= 2) {
    // No arguments.
    dieOnExit();
    printUsage();
    return;
  }

  var defaultOptions = {
    'version':         false,
    'help':            false,
    'license':         false,
    'view-source':     false,
    'highlight':       null,
    'verbose':         false,
    'secret':          null,
    'decode':          false,
    'file':            null,
    'output-file':     null,
    'original-file':   null,
    'format':          null,
    'password':        false,
    'authenticated':   false,
    'nosalt':          false,
    'markup':          false,
    'deterministic':   false,
    'rulesets':        null,
    'ruleset-file':    null,
    'keyboard-file':   null,
    'dictionary-file': null,
    'query':           null,
  };

  var shortcuts = {
    '-V': '--version',
    '-h': '--help',
    '-?': '--help',
    '-v': '--verbose',
    '-d': '--decode',
    '-f': '--file=',
    '-o': '--output-file=',
    '-g': '--original-file=',
    '-P': '--password',
    '-a': '--authenticated',
    '-q': '--query=',
  };

  var options = parseArgs(process.argv.slice(2), defaultOptions, shortcuts);

  var seeHelp = os.EOL + os.EOL + "See '" + _name + " --help'."
      + os.EOL;

  if (options['!?'].length > 0) {
    var unknown = options['!?'][0];

    console.error("Unknown option '" + unknown + "'." + seeHelp);

    if (unknown.slice(0, 2) === '--') {
      // Find and display close matches using Levenshtein distance.
      printCloseMatches(unknown.slice(2), Object.keys(defaultOptions));
    }

    die();
  }

  if ((options.help || options.version || options.license
        || options['view-source'])
      && Object.keys(options).length > 1) {
    // '--help', '--version', and '--license' do not take any arguments.
    dieOnExit();
    printUsage();
    return;
  }

  if (options.help) {
    printHelp();
    return;
  }

  if (options.version) {
    printVersion();
    return;
  }

  if (options.license) {
    printLicense();
    return;
  }

  if (options['view-source']) {
    printSource();
    return;
  }

  var optKeys = Object.keys(options);

  // There are three 'modes' broadly: encode (default), decode, and query.
  var decodeMode = options.decode;
  var queryMode  = options.hasOwnProperty('query');

  var encodeMode = !decodeMode && !queryMode;

  var validOpts = null;

  // Valid options for each mode.
  if (encodeMode) {
    validOpts = 'highlight verbose secret file output-file format password'
      + ' authenticated nosalt markup deterministic rulesets ruleset-file'
      + ' keyboard-file dictionary-file';
  } else if (decodeMode) {
    validOpts = 'highlight verbose decode file original-file format password'
      + ' authenticated nosalt markup';
  } else if (queryMode) {
    validOpts = 'highlight verbose query rulesets ruleset-file'
      + ' keyboard-file dictionary-file';
  }

  validOpts = validOpts && validOpts.split(' ') || [];

  if (encodeMode + decodeMode + queryMode !== 1
      || !optKeys.every(function (k) { return validOpts.indexOf(k) !== -1 })) {
    dieOnExit();
    printUsage();
    return;
  }

  // If any boolean options have non-boolean (string) values, print usage and
  // exit.
  if (!typeMatch(defaultOptions, options, 'boolean', [ 'password' ])) {
    dieOnExit();
    printUsage();
    return;
  }

  // Positional arguments.
  mapOptions(options, encodeMode ? [ 'secret', 'file' ] : [ 'file' ],
      options['...']);

  if (encodeMode && typeof options.secret !== 'string') {
    dieOnExit();
    printUsage();
    return;
  }

  optKeys.forEach(function (name) {
    if ((name === 'file' || name.slice(-5) === '-file')
        && options[name] === '') {
      die('Filename cannot be blank.' + seeHelp);
    }
  });

  if (decodeMode && !options['original-file'] && !options.markup) {
    die("Required '--original-file' or '--markup' argument." + seeHelp);
  }

  if (options.format != null && options.format !== 'hex'
      && options.format !== 'base64') {
    die("Format must be 'hex' or 'base64'." + seeHelp);
  }

  if (options.verbose) {
    say = sayImpl(function () {
      return '[' + process.uptime().toFixed(2) + ']';
    });
  }

  var isTerminal = function () {
    return !options['output-file'] && process.stdout.isTTY;
  };

  say('Hi!');

  chain([
      function (callback) {
        readPassword(options.password, callback);
      },

      function (password, callback) {
        if (encodeMode || decodeMode) {
          readInput(options.file, function (error, message) {
            callback(error, password, message);
          });

        } else {
          callback(null, password, null);
        }
      },

      function (password, message, callback) {
        if (decodeMode && !options.markup) {
          // Read the original file.
          say('Reading original file ' + options['original-file']);

          slurpFile(options['original-file'], function (error, original) {
            callback(error, password, message, original);
          });

        } else {
          callback(null, password, message, null);
        }
      },

      function (password, message, original, callback) {
        if (encodeMode || queryMode) {
          loadDictionary(options['dictionary-file']);

          loadKeyboard(options['keyboard-file']);

          loadRulesets(options.rulesets, options['ruleset-file']);

          if (encodeMode && !options.deterministic) {
            say('Shuffling rules');

            rulesetOrder.forEach(shuffleRules);
          }
        }

        if (encodeMode) {
          say('Secret: ' + options.secret);

          say('Encoding');

          var encodeOptions = Object.create(options);

          if (!isTerminal()) {
            encodeOptions.highlight = null;
          }

          var output = encode(message, options.secret, password,
              encodeOptions);

          if (!output) {
            throw '';
          }

          callback(null, output);

        } else if (decodeMode) {
          say('Decoding');

          var secret = decode(message, password,
              Object.create(options, { original: { value: original } })
              );

          say('Secret: ' + secret);

          // Note: secret can be an empty string! It's an error only if it's
          // null or undefined.
          if (secret == null) {
            // Throw an empty string to exit quietly with a nonzero exit code.
            throw '';
          }

          callback(null, secret);

        } else if (queryMode) {
          say('Query: ' + options.query);

          callback(null, query(options.query, options));
        }
      }
    ],

    function (error) {
      logError(error);

      say('Sorry, we failed');

      die();
    },

    function (finalResult) {
      say('Almost done!');

      if (!encodeMode || isTerminal()) {
        if (finalResult) {
          console.log(finalResult);
        }
      } else {
        writeOutput(finalResult, options['output-file']);
      }

      say('Goodbye');
    }
  );
}

function main() {
  run();
}

if (require.main === module) {
  main();
}

exports.run = run;

// vim: et ts=2 sw=2


<|start_filename|>package.json<|end_filename|>
{
  "name": "typo-steganography",
  "version": "0.4.12",
  "description": "Hide secret information in typographical errors",
  "author": {
    "name": "<NAME>",
    "email": "<EMAIL>",
    "url": "http://manishjethani.com/"
  },
  "homepage": "http://mjethani.github.io/typo",
  "bin": {
    "typo": "./typo"
  },
  "scripts": {
    "build": "bash build.sh",
    "sign":  "keybase sign typo.js && keybase dir sign"
  },
  "keywords": [
    "steganography",
    "encryption",
    "text"
  ],
  "preferGlobal": true,
  "repository": {
    "type": "git",
    "url": "https://github.com/mjethani/typo.git"
  },
  "engines": { "node": ">=0.12.0" },
  "dependencies": {},
  "bugs": {
    "url": "https://github.com/mjethani/typo/issues",
    "email": "<EMAIL>"
  },
  "license": "ISC"
}

