generator presets

This commit is contained in:
Antelle 2016-02-16 00:06:11 +03:00
parent ef23e91d34
commit ab76e3f90d
9 changed files with 421 additions and 12 deletions

View File

@ -51,6 +51,16 @@ var Locale = {
footerTitleLock: 'Lock',
genLen: 'Length',
genPresetDefault: 'default preset',
genPresetDerived: 'like old password',
genPresetPronounceable: 'pronounceable',
genPresetMed: 'medium length',
genPresetLong: 'long',
genPresetPin4: '4-digit PIN',
genPresetMac: 'MAC address',
genPresetHash128: '128-bit hash',
genPresetHash256: '256-bit hash',
grpTitle: 'Group',
grpSearch: 'Enable searching entries in this group',
@ -243,7 +253,7 @@ var Locale = {
setFileHistory: 'History',
setFileEnableTrash: 'Enable trash',
setFileHistLen: 'History length, keep last records per entry',
resFileHistSize: 'History size, total MB per file',
setFileHistSize: 'History size, total MB per file',
setFileAdvanced: 'Advanced',
setFileRounds: 'Key encryption rounds',
setFileUseKeyFile: 'Use key file',

View File

@ -1,6 +1,7 @@
'use strict';
var kdbxweb = require('kdbxweb');
var kdbxweb = require('kdbxweb'),
phonetic = require('./phonetic');
var PasswordGenerator = {
charRanges: {
@ -12,10 +13,21 @@ var PasswordGenerator = {
high: '¡¢£¤¥¦§©ª«¬®¯°±²³´µ¶¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ',
ambiguous: 'O0oIl'
},
generate: function(opts) {
if (!opts || typeof opts.length !== 'number' || opts.length < 0) {
return '';
}
switch (opts.name) {
case 'Pronounceable':
return this.generatePronounceable(opts);
case 'Hash128':
return this.generateHash(32);
case 'Hash256':
return this.generateHash(64);
case 'Mac':
return this.generateMac();
}
var ranges = Object.keys(this.charRanges)
.filter(function(r) { return opts[r]; })
.map(function(r) { return this.charRanges[r]; }, this);
@ -31,6 +43,69 @@ var PasswordGenerator = {
}
return _.shuffle(chars).join('');
},
generateMac: function() {
var segmentsCount = 6;
var randomBytes = kdbxweb.Random.getBytes(segmentsCount);
var result = '';
for (var i = 0; i < segmentsCount; i++) {
var segment = randomBytes[i].toString(16).toUpperCase();
if (segment.length < 2) {
segment = '0' + segment;
}
result += (result ? '-' : '') + segment;
}
return result;
},
generateHash: function(length) {
var randomBytes = kdbxweb.Random.getBytes(length);
var result = '';
for (var i = 0; i < length; i++) {
result += randomBytes[i].toString(16)[0];
}
return result;
},
generatePronounceable: function(opts) {
var pass = phonetic.generate({ length: opts.length });
var result = '';
var upper = [];
var i;
if (opts.upper) {
for (i = 0; i < pass.length; i += 8) {
upper.push(Math.floor(Math.random() * opts.length));
}
}
for (i = 0; i < pass.length; i++) {
var ch = pass[i];
if (upper.indexOf(i) >= 0) {
ch = ch.toUpperCase();
}
result += ch;
}
return result.substr(0, opts.length);
},
deriveOpts: function(password) {
var opts = {};
var length = 0;
if (password) {
var charRanges = this.charRanges;
password.forEachChar(function(ch) {
length++;
ch = String.fromCharCode(ch);
_.forEach(charRanges, function(chars, range) {
if (chars.indexOf(ch) >= 0) {
opts[range] = true;
}
});
});
}
opts.length = length;
return opts;
},
present: function(length) {
return new Array(length + 1).join('•');
}

View File

@ -0,0 +1,258 @@
'use strict';
/*
* Phonetic
* Copyright 2013 Tom Frost
*/
// removed node.js deps, making it available to load in browser
/**
* Phonetics that sound best before a vowel.
* @type {Array}
*/
var PHONETIC_PRE = [
// Simple phonetics
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p',
'qu', 'r', 's', 't',
// Complex phonetics
'bl',
'ch', 'cl', 'cr',
'dr',
'fl', 'fr',
'gl', 'gr',
'kl', 'kr',
'ph', 'pr', 'pl',
'sc', 'sh', 'sl', 'sn', 'sr', 'st', 'str', 'sw',
'th', 'tr',
'br',
'v', 'w', 'y', 'z'
];
/**
* The number of simple phonetics within the 'pre' set.
* @type {number}
*/
var PHONETIC_PRE_SIMPLE_LENGTH = 16;
/**
* Vowel sound phonetics.
* @type {Array}
*/
var PHONETIC_MID = [
// Simple phonetics
'a', 'e', 'i', 'o', 'u',
// Complex phonetics
'ee', 'ie', 'oo', 'ou', 'ue'
];
/**
* The number of simple phonetics within the 'mid' set.
* @type {number}
*/
var PHONETIC_MID_SIMPLE_LENGTH = 5;
/**
* Phonetics that sound best after a vowel.
* @type {Array}
*/
var PHONETIC_POST = [
// Simple phonetics
'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'y',
// Complex phonetics
'ch', 'ck',
'ln',
'nk', 'ng',
'rn',
'sh', 'sk', 'st',
'th',
'x', 'z'
];
/**
* The number of simple phonetics within the 'post' set.
* @type {number}
*/
var PHONETIC_POST_SIMPLE_LENGTH = 13;
/**
* A mapping of regular expressions to replacements, which will be run on the
* resulting word before it gets returned. The purpose of replacements is to
* address language subtleties that the phonetic builder is incapable of
* understanding, such as 've' more pronounceable than just 'v' at the end of
* a word, 'ey' more pronounceable than 'iy', etc.
* @type {{}}
*/
var REPLACEMENTS = {
'quu': 'que',
'qu([aeiou]){2}': 'qu$1',
'[iu]y': 'ey',
'eye': 'ye',
'(.)ye$': '$1y',
'(^|e)cie(?!$)': '$1cei',
'([vz])$': '$1e',
'[iu]w': 'ow'
};
/**
* Adds a single syllable to the word contained in the wordObj. A syllable
* contains, at maximum, a phonetic from each the PRE, MID, and POST phonetic
* sets. Some syllables will omit pre or post based on the
* options.compoundSimplicity.
*
* @param {{word, numeric, lastSkippedPre, lastSkippedPost, opts}} wordObj The
* word object on which to operate.
*/
function addSyllable(wordObj) {
var deriv = getDerivative(wordObj.numeric),
compound = deriv % wordObj.opts.compoundSimplicity === 0,
first = wordObj.word === '',
preOnFirst = deriv % 6 > 0;
if ((first && preOnFirst) || wordObj.lastSkippedPost || compound) {
wordObj.word += getNextPhonetic(PHONETIC_PRE,
PHONETIC_PRE_SIMPLE_LENGTH, wordObj);
wordObj.lastSkippedPre = false;
} else {
wordObj.lastSkippedPre = true;
}
wordObj.word += getNextPhonetic(PHONETIC_MID, PHONETIC_MID_SIMPLE_LENGTH,
wordObj, first && wordObj.lastSkippedPre);
if (wordObj.lastSkippedPre || compound) {
wordObj.word += getNextPhonetic(PHONETIC_POST,
PHONETIC_POST_SIMPLE_LENGTH, wordObj);
wordObj.lastSkippedPost = false;
} else {
wordObj.lastSkippedPost = true;
}
}
/**
* Gets a derivative of a number by repeatedly dividing it by 7 and adding the
* remainders together. It's useful to base decisions on a derivative rather
* than the wordObj's current numeric, as it avoids making the same decisions
* around the same phonetics.
*
* @param {number} num A number from which a derivative should be calculated
* @returns {number} The derivative.
*/
function getDerivative(num) {
var derivative = 1;
while (num) {
derivative += num % 7;
num = Math.floor(num / 7);
}
return derivative;
}
/**
* Combines the option defaults with the provided overrides. Available
* options are:
* - seed: A string or number with which to seed the generator. Using the
* same seed (with the same other options) will coerce the generator
* into producing the same word. Default is random.
* - phoneticSimplicity: The greater this number, the simpler the phonetics.
* For example, 1 might produce 'str' while 5 might produce 's' for
* the same syllable. Minimum is 1, default is 5.
* - compoundSimplicity: The greater this number, the less likely the
* resulting word will sound "compound", such as "ripkuth" instead of
* "riputh". Minimum is 1, default is 5.
*
* @param {{}} overrides A set of options and values with which to override
* the defaults.
* @returns {{seed, phoneticSimplicity, compoundSimplicity}}
* An options object.
*/
function getOptions(overrides) {
var options = {};
overrides = overrides || {};
options.length = overrides.length || 16;
options.seed = overrides.seed || Math.random();
options.phoneticSimplicity = overrides.phoneticSimplicity ?
Math.max(overrides.phoneticSimplicity, 1) : 5;
options.compoundSimplicity = overrides.compoundSimplicity ?
Math.max(overrides.compoundSimplicity, 1) : 5;
return options;
}
/**
* Gets the next pseudo-random phonetic from a given phonetic set,
* intelligently determining whether to include "complex" phonetics in that
* set based on the options.phoneticSimplicity.
*
* @param {Array} phoneticSet The array of phonetics from which to choose
* @param {number} simpleCap The number of 'simple' phonetics at the beginning
* of the phoneticSet
* @param {{word, numeric, lastSkippedPre, lastSkippedPost, opts}} wordObj The
* wordObj for which the phonetic is being chosen
* @param {boolean} [forceSimple] true to force a simple phonetic to be
* chosen; otherwise, the function will choose whether to include complex
* phonetics based on the derivative of wordObj.numeric.
* @returns {string} The chosen phonetic.
*/
function getNextPhonetic(phoneticSet, simpleCap, wordObj, forceSimple) {
var deriv = getDerivative(wordObj.numeric),
simple = (wordObj.numeric + deriv) % wordObj.opts.phoneticSimplicity > 0,
cap = simple || forceSimple ? simpleCap : phoneticSet.length,
phonetic = phoneticSet[wordObj.numeric % cap];
wordObj.numeric = getNumericHash(wordObj.numeric + wordObj.word);
return phonetic;
}
/**
* Generates a numeric hash based on the input data. The hash is an md5, with
* each block of 32 bits converted to an integer and added together.
*
* @param {string|number} data The string or number to be hashed.
* @returns {number}
*/
function getNumericHash(data) {
var numeric = 0;
data += '-Phonetic';
for (var i = 0; i < data.length; i++) {
numeric += data.charCodeAt(i);
}
return numeric;
}
/**
* Applies post-processing to a word after it has already been generated. In
* this phase, the REPLACEMENTS are executed, applying language intelligence
* that can make generated words more pronounceable. The first letter is
* also capitalized.
*
* @param {{word, numeric, lastSkippedPre, lastSkippedPost, opts}} wordObj The
* word object to be processed.
* @returns {string} The processed word.
*/
function postProcess(wordObj) {
var regex;
for (var i in REPLACEMENTS) {
if (REPLACEMENTS.hasOwnProperty(i)) {
regex = new RegExp(i);
wordObj.word = wordObj.word.replace(regex, REPLACEMENTS[i]);
}
}
return wordObj.word;
}
/**
* Generates a new word based on the given options. For available options,
* see getOptions.
*
* @param {*} [options] A collection of options to control the word generator.
* @returns {string} A generated word.
*/
module.exports.generate = function(options) {
options = getOptions(options);
var length = options.length,
wordObj = {
numeric: getNumericHash(options.seed),
lastSkippedPost: false,
word: '',
opts: options
};
while (wordObj.word.length < length) {
addSyllable(wordObj);
}
return postProcess(wordObj).substr(0, length);
};

View File

@ -56,7 +56,7 @@ var FieldViewText = FieldView.extend({
this.hideGenerator();
} else {
var fieldRect = this.input[0].getBoundingClientRect();
this.gen = new GeneratorView({model: {pos: {left: fieldRect.left, top: fieldRect.bottom}}}).render();
this.gen = new GeneratorView({model: {pos: {left: fieldRect.left, top: fieldRect.bottom}, password: this.value}}).render();
this.gen.once('remove', this.generatorClosed.bind(this));
this.gen.once('result', this.generatorResult.bind(this));
}

View File

@ -5,10 +5,6 @@ var Backbone = require('backbone'),
CopyPaste = require('../comp/copy-paste'),
Locale = require('../util/locale');
var DefaultGenOpts = {
length: 16, upper: true, lower: true, digits: true, special: false, brackets: false, high: false, ambiguous: false
};
var GeneratorView = Backbone.View.extend({
el: 'body',
@ -20,26 +16,67 @@ var GeneratorView = Backbone.View.extend({
'mousemove .gen__length-range': 'lengthChange',
'change .gen__length-range': 'lengthChange',
'change .gen__check input[type=checkbox]': 'checkChange',
'click .gen__btn-ok': 'btnOkClick'
'click .gen__btn-ok': 'btnOkClick',
'change .gen__sel-tpl': 'templateChange'
},
valuesMap: [3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,24,26,28,30,32,48,64],
presets: null,
preset: null,
initialize: function () {
this.createPresets();
var preset = this.preset;
this.gen = _.clone(_.find(this.presets, function(pr) { return pr.name === preset; }));
this.gen = _.clone(this.presets[1]);
$('body').one('click', this.remove.bind(this));
this.gen = _.clone(DefaultGenOpts);
},
render: function() {
var canCopy = document.queryCommandSupported('copy');
var btnTitle = this.model.copy ? canCopy ? Locale.alertCopy : Locale.alertClose : Locale.alertOk;
this.renderTemplate({ btnTitle: btnTitle, opt: this.gen });
this.renderTemplate({ btnTitle: btnTitle, opt: this.gen, presets: this.presets, preset: this.preset });
this.resultEl = this.$el.find('.gen__result');
this.$el.css(this.model.pos);
this.generate();
return this;
},
createPresets: function() {
this.presets = [
{ name: 'Default', length: 16, upper: true, lower: true, digits: true },
{ name: 'Pronounceable', length: 10, lower: true, upper: true },
{ name: 'Med', length: 16, upper: true, lower: true, digits: true, special: true, brackets: true, ambiguous: true },
{ name: 'Long', length: 32, upper: true, lower: true, digits: true },
{ name: 'Pin4', length: 4, digits: true },
{ name: 'Mac', length: 17, upper: true, digits: true, special: true },
{ name: 'Hash128', length: 32, lower: true, digits: true },
{ name: 'Hash256', length: 64, lower: true, digits: true }
];
if (this.model.password) {
var derivedPreset = { name: 'Derived' };
_.extend(derivedPreset, PasswordGenerator.deriveOpts(this.model.password));
for (var i = 0; i < this.valuesMap.length; i++) {
if (this.valuesMap[i] >= derivedPreset.length) {
derivedPreset.length = this.valuesMap[i];
break;
}
}
if (derivedPreset.length > this.valuesMap[this.valuesMap.length - 1]) {
derivedPreset.length = this.valuesMap[this.valuesMap.length - 1];
}
this.presets.splice(1, 0, derivedPreset);
this.preset = 'Derived';
} else {
this.preset = 'Default';
}
this.presets.forEach(function(pr) {
pr.pseudoLength = this.valuesMap.indexOf(pr.length);
pr.title = Locale['genPreset' + pr.name];
}, this);
},
click: function(e) {
e.stopPropagation();
},
@ -49,6 +86,7 @@ var GeneratorView = Backbone.View.extend({
if (val !== this.gen.length) {
this.gen.length = val;
this.$el.find('.gen__length-range-val').html(val);
this.optionChanged('length');
this.generate();
}
},
@ -58,9 +96,19 @@ var GeneratorView = Backbone.View.extend({
if (id) {
this.gen[id] = e.target.checked;
}
this.optionChanged(id);
this.generate();
},
optionChanged: function(option) {
if (this.preset === 'Custom' ||
this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0) {
return;
}
this.preset = this.gen.name = 'Custom';
this.$el.find('.gen__sel-tpl').val('');
},
generate: function() {
this.password = PasswordGenerator.generate(this.gen);
this.resultEl.text(this.password);
@ -77,6 +125,14 @@ var GeneratorView = Backbone.View.extend({
CopyPaste.copy(this.password);
this.trigger('result', this.password);
this.remove();
},
templateChange: function(e) {
var name = e.target.value;
this.preset = name;
var preset = _.find(this.presets, function(t) { return t.name === name; });
this.gen = _.clone(preset);
this.render();
}
});

View File

@ -5,6 +5,10 @@
width: 11em;
&__length-range {
}
&__sel-tpl {
width: 100%;
margin-top: $base-padding-v;
}
&__check {
width: 40%;
display: inline-block;

View File

@ -1,6 +1,11 @@
<div class="gen">
<div>{{res 'genLen'}}: <span class="gen__length-range-val">{{opt.length}}</span></div>
<input type="range" class="gen__length-range" value="13" min="0" max="25" />
<select class="gen__sel-tpl input-base">
{{#each presets as |ps|}}
<option value="{{ps.name}}" {{#ifeq ps.name ../preset}}selected{{/ifeq}}>{{ps.title}}</option>
{{/each}}
</select>
<input type="range" class="gen__length-range" value="{{opt.pseudoLength}}" min="0" max="25" />
<div>
<div class="gen__check"><input type="checkbox" id="gen__check-upper"
data-id="upper" {{#if opt.upper}}checked{{/if}}><label for="gen__check-upper">ABC</label></div>

View File

@ -30,6 +30,7 @@
<li><a href="https://github.com/Diokuz/baron" target="_blank">baron</a><span class="muted-color">, native scroll with custom scrollbar</span></li>
<li><a href="http://dbushell.github.io/Pikaday/" target="_blank">pikaday</a><span class="muted-color">, a refreshing JavaScript datepicker</span></li>
<li><a href="https://github.com/eligrey/FileSaver.js" target="_blank">filesaver.js</a><span class="muted-color">, HTML5 saveAs FileSaver implementation</span></li>
<li><a href="https://github.com/TomFrost/node-phonetic" target="_blank">node-phonetic</a><span class="muted-color">, generates unique, pronounceable names</span></li>
</ul>
<h3>Desktop modules</h3>

View File

@ -50,7 +50,7 @@
</div>
<label for="settings__file-hist-len">{{res 'setFileHistLen'}}:</label>
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-hist-len" value="{{historyMaxItems}}" />
<label for="settings__file-hist-size">{{res 'resFileHistSize'}}:</label>
<label for="settings__file-hist-size">{{res 'setFileHistSize'}}:</label>
<input type="text" pattern="\d+" required class="settings__input input-base" id="settings__file-hist-size" value="{{historyMaxSize}}" />
<h2>{{res 'setFileAdvanced'}}</h2>