feat: add passphrases, uuid4

This commit is contained in:
Aetherinox 2024-04-21 03:31:06 -07:00
parent c1fda05c77
commit fab0b1ef79
No known key found for this signature in database
GPG Key ID: CB5C4C30CD0D4028
6 changed files with 8109 additions and 62 deletions

View File

@ -1,6 +1,7 @@
import * as kdbxweb from 'kdbxweb';
import { phonetic } from 'util/generators/phonetic';
import { shuffle } from 'util/fn';
import {} from 'crypto';
const CharRanges = {
upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
@ -8,9 +9,9 @@ const CharRanges = {
digits: '123456789',
special: '!@#$%^&*_+-=,./?;:`"~\'\\',
brackets: '(){}[]<>',
high:
'¡¢£¤¥¦§©ª«¬®¯°±²³´µ¶¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ',
ambiguous: 'O0oIl'
high: '¡¢£¤¥¦§©ª«¬®¯°±²³´µ¶¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ',
ambiguous: 'O0oIl',
spaces: ' '
};
const DefaultCharRangesByPattern = {
@ -20,30 +21,80 @@ const DefaultCharRangesByPattern = {
'*': CharRanges.special,
'[': CharRanges.brackets,
'Ä': CharRanges.high,
'0': CharRanges.ambiguous
'0': CharRanges.ambiguous,
'S': CharRanges.spaces
};
const PasswordGenerator = {
generate(opts) {
const bUseSpaces = opts.spaces;
const bUpperInc = opts.include && opts.include.length && opts.upper; // allows for using uppercase checkboxes for presets
const bLowerInc = opts.include && opts.include.length && opts.lower; // allows for using lowercase checkboxes for presets
if (!opts || typeof opts.length !== 'number' || opts.length < 0) {
return '';
}
if (opts.name === 'Pronounceable') {
if (opts.name.toLowerCase() === 'uuid') {
return this.generateUUIDv4(opts);
}
if (opts.name.toLowerCase() === 'passphrase') {
return this.generatePassphrase(opts);
}
if (opts.name.toLowerCase() === 'pronounceable') {
return this.generatePronounceable(opts);
}
const ranges = Object.keys(CharRanges)
.filter((r) => opts[r])
.map((r) => CharRanges[r]);
// used if action specifies custom opts.include
// delete all CharRanges
// used for presets such as hashes since we need to keep the list of opts.include characters
// but also allows for selecting upper and lowercase checkboxes
if (bUpperInc || bLowerInc) {
if (bUpperInc && opts.include) {
opts.include = opts.include.toUpperCase();
} else if (bLowerInc && opts.include) {
opts.include = opts.include.toLowerCase();
}
}
// returns string of characters to choose from
let ranges = Object.keys(CharRanges)
.filter((r) => {
return opts[r];
})
.map((r) => {
return CharRanges[r];
});
// delete ranges if opts.include specified, use the include characters instead
if (bUpperInc || bLowerInc) {
ranges = [];
}
Object.keys(ranges).forEach((r) => {
const itemKey = r;
const item = ranges[itemKey];
if (item === String.fromCharCode(32)) {
ranges.pop(r);
}
});
if (opts.include && opts.include.length) {
ranges.push(opts.include);
}
if (!ranges.length) {
return '';
}
const rangesByPatternChar = {
...DefaultCharRangesByPattern,
'I': opts.include || ''
};
const pattern = opts.pattern || 'X';
let countDefaultChars = 0;
@ -56,6 +107,7 @@ const PasswordGenerator = {
const rangeIxRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const rangeCharRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const randomBytes = kdbxweb.CryptoEngine.random(opts.length);
const defaultRangeGeneratedChars = [];
for (let i = 0; i < countDefaultChars; i++) {
const rangeIx = i < ranges.length ? i : rangeIxRandomBytes[i] % ranges.length;
@ -65,19 +117,44 @@ const PasswordGenerator = {
}
shuffle(defaultRangeGeneratedChars);
const randomBytes = kdbxweb.CryptoEngine.random(opts.length);
const chars = [];
for (let i = 0; i < opts.length; i++) {
const pwdLenReq = opts.length;
let pwdLenNow = 1;
let pwdCharLast = null;
let pwdMarginSp = Math.round(pwdLenReq * Math.random());
if (pwdMarginSp === pwdLenReq) {
pwdMarginSp -= 1;
}
for (let i = 0; i < pwdLenReq; i++) {
const rand = Math.round(Math.random() * 1000) + randomBytes[i];
const patternChar = pattern[i % pattern.length];
if (patternChar === 'X') {
chars.push(defaultRangeGeneratedChars.pop());
const bAlreadyHasSpace =
chars.find((element) => element === String.fromCharCode(32)) !== undefined;
let char = defaultRangeGeneratedChars.pop();
if (
bUseSpaces === true &&
pwdLenNow > 1 &&
pwdLenNow !== pwdLenReq &&
pwdCharLast !== String.fromCharCode(32)
) {
if (Math.random() < 0.1 || (pwdLenNow >= pwdMarginSp && !bAlreadyHasSpace)) {
char = String.fromCharCode(32);
}
chars.push(char);
} else {
chars.push(char);
}
pwdLenNow += 1;
pwdCharLast = char;
} else {
const range = rangesByPatternChar[patternChar];
const char = range ? range[rand % range.length] : patternChar;
chars.push(char);
}
}
return chars.join('');
},
@ -100,7 +177,80 @@ const PasswordGenerator = {
}
result += ch;
}
return result.substr(0, opts.length);
// return result.substr(0, opts.length);
return result.slice(0, opts.length);
},
// UUID v4 based on pure randomness, whereas v5 is derived from a string.
// ASCII string with 8 hexadecimal digits followed by a hyphen
// then three groups of 4 hexadecimal digits each followed by a hyphen
// then 12 hexadecimal digits.
// possibilities: 2^128
generateUUIDv4(opts) {
let p = '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
(+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16)
);
if (opts.lower && !opts.upper) {
p = p.toLowerCase(); // lowercase
} else if (opts.upper && !opts.lower) {
p = p.toUpperCase(); // uppercase
} else if (opts.upper && opts.lower) {
p = p.charAt(0).toUpperCase() + p.slice(1).toLowerCase(); // Mixed
}
return p;
},
generatePassphrase(opts) {
const pass = phonetic.generate({
length: opts.length
});
const passphraseList = require('../../wordlist/default.json');
shuffle(passphraseList);
let i;
const spacer = '';
const chars = [];
for (i = 0; i < pass.length; i++) {
const id = Math.floor(Math.random() * passphraseList.length);
let p = passphraseList[id];
passphraseList.splice(id, 1);
if (opts.lower && !opts.upper) {
p = p.toLowerCase(); // word1 word2
} else if (opts.upper && !opts.lower) {
p = p.toUpperCase(); // WORD1 WORD2
} else if (opts.upper && opts.lower) {
p = p.charAt(0).toUpperCase() + p.slice(1).toLowerCase(); // Word1 Word2
}
if (opts.digits) {
p = p + Math.floor(Math.random() * 10);
}
if (i < pass.length - 1) {
if (opts.special) {
p = p + ',';
} else if (opts.high) {
if (opts.spaces) {
p = p + ' -';
} else {
p = p + '-';
}
}
}
if (i < pass.length - 1 && opts.spaces) {
p = p + ' ';
}
chars.push(p);
}
return chars.join(spacer);
},
deriveOpts(password) {

View File

@ -60,17 +60,24 @@ class GeneratorPresetsView extends View {
const rangeOverride = {
high: '¡¢£¤¥¦§©ª«¬®¯°±¹²´µ¶»¼÷¿ÀÖîü...'
};
return ['Upper', 'Lower', 'Digits', 'Special', 'Brackets', 'High', 'Ambiguous'].map(
(name) => {
const nameLower = name.toLowerCase();
return {
name: nameLower,
title: Locale['genPs' + name],
enabled: sel[nameLower],
sample: rangeOverride[nameLower] || CharRanges[nameLower]
};
}
);
return [
'Upper',
'Lower',
'Digits',
'Special',
'Brackets',
'High',
'Ambiguous',
'Spaces'
].map((name) => {
const nameLower = name.toLowerCase();
return {
name: nameLower,
title: Locale['genPs' + name],
enabled: sel[nameLower],
sample: rangeOverride[nameLower] || CharRanges[nameLower]
};
});
}
getPreset(name) {
@ -109,6 +116,7 @@ class GeneratorPresetsView extends View {
special: selected.special,
brackets: selected.brackets,
ambiguous: selected.ambiguous,
spaces: selected.spaces,
include: selected.include
};
GeneratorPresets.add(preset);

View File

@ -11,8 +11,8 @@ import template from 'templates/generator.hbs';
class GeneratorView extends View {
parent = 'body';
template = template;
spacesLenMin = 5;
events = {
'click': 'click',
@ -27,32 +27,8 @@ class GeneratorView extends View {
};
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
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, 128
];
presets = null;
@ -80,8 +56,10 @@ class GeneratorView extends View {
showToggleButton: this.model.copy,
opt: this.gen,
hide: this.hide,
spaces: false,
presets: this.presets,
preset: this.preset,
presetLC: this.preset.toLowerCase(),
showTemplateEditor: !this.model.noTemplateEditor
});
this.resultEl = this.$el.find('.gen__result');
@ -103,6 +81,7 @@ class GeneratorView extends View {
const defaultPreset = this.presets.filter((p) => p.default)[0] || this.presets[0];
this.preset = defaultPreset.name;
}
this.presetLC = this.preset.toLowerCase();
this.presets.forEach((pr) => {
pr.pseudoLength = this.lengthToPseudoValue(pr.length);
});
@ -120,8 +99,21 @@ class GeneratorView extends View {
showPassword() {
if (this.hide && !this.model.copy) {
this.resultEl.text(PasswordPresenter.present(this.password.length));
// this.resultEl.prop('value', PasswordPresenter.present(this.password.length));
} else {
this.resultEl.text(this.password);
// this.resultEl.prop('value', this.password);
}
}
setDisabledElements() {
// disabled elements / checkboxes greyed out
if (this.gen.disabledElements && this.gen.disabledElements.length) {
for (let i = 0; i < this.gen.disabledElements.length; i++) {
const elementName = this.gen.disabledElements[i];
this.gen[elementName] = false;
this.$el.find('.checkbox-' + elementName).attr('disabled', 'disabled');
}
}
}
@ -135,6 +127,20 @@ class GeneratorView extends View {
this.gen.length = val;
this.$el.find('.gen__length-range-val').text(val);
this.optionChanged('length');
// dont allow spaces unless password is a certain length
if (this.presetLC !== 'passphrase') {
const cboxSpaces = document.getElementsByClassName('checkbox-spaces');
if (this.gen.length > this.spacesLenMin) {
this.$el.find('.checkbox-spaces').removeAttr('disabled');
if (cboxSpaces.item(0).checked) {
this.gen.spaces = true; // force spaces to be enabled
}
} else {
this.$el.find('.checkbox-spaces').attr('disabled', 'disabled');
cboxSpaces.item(0).checked = false;
this.gen.spaces = false; // force spaces to be disabled
}
}
this.generate();
}
}
@ -144,26 +150,78 @@ class GeneratorView extends View {
if (id) {
this.gen[id] = e.target.checked;
}
if (this.presetLC === 'passphrase') {
const cbSpecial = document.getElementsByClassName('checkbox-special');
const cbHigh = document.getElementsByClassName('checkbox-high');
if (id === 'special') {
// upper checked -> uncheck and re-enable lower
if (cbSpecial.item(0).checked) {
this.$el.find('.checkbox-high').attr('disabled', 'disabled');
cbHigh.item(0).checked = false;
this.gen.high = false;
} else {
this.$el.find('.checkbox-high').removeAttr('disabled');
}
} else if (id === 'high') {
// upper checked -> uncheck and re-enable lower
if (cbHigh.item(0).checked) {
this.$el.find('.checkbox-special').attr('disabled', 'disabled');
cbSpecial.item(0).checked = false;
this.gen.special = false;
} else {
this.$el.find('.checkbox-special').removeAttr('disabled');
}
}
}
this.optionChanged(id);
this.generate();
}
optionChanged(option) {
// certain presets should allow users to check custom options.
// if a preset is not in the list, checking any checkbox will set preset to 'custom'
if (
this.preset === 'Custom' ||
(this.preset === 'Pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0)
this.presetLC === 'custom' ||
(this.presetLC === 'passphrase' &&
['length', 'lower', 'upper', 'digits', 'high', 'special', 'spaces'].indexOf(
option
) >= 0) ||
((this.presetLC === 'hash128' ||
this.presetLC === 'hash256' ||
this.presetLC === 'hash512') &&
['lower', 'upper'].indexOf(option) >= 0) ||
(this.presetLC === 'uuid' && ['lower', 'upper', 'digits'].indexOf(option) >= 0) ||
(this.presetLC === 'pronounceable' && ['length', 'lower', 'upper'].indexOf(option) >= 0)
) {
return;
}
this.preset = this.gen.name = 'Custom';
this.$el.find('.gen__sel-tpl').val('');
}
generate() {
this.password = PasswordGenerator.generate(this.gen);
this.setDisabledElements();
this.showPassword();
const isLong = this.password.length > 32;
this.resultEl.toggleClass('gen__result--long-pass', isLong);
// disable spaces checkif if password is below minimum
if (this.gen.length < this.spacesLenMin && this.presetLC !== 'passphrase') {
this.$el.find('.checkbox-spaces').attr('disabled', 'disabled');
const cboxSpaces = document.getElementsByClassName('checkbox-spaces');
cboxSpaces.item(0).checked = false;
this.gen.spaces = false; // force spaces to be disabled
}
this.resultEl.removeClass('__wrap');
this.resultEl.removeClass('__nowrap');
this.setInputHeight();
// password vs passphrase word wrapping
this.resultEl.toggleClass(this.presetLC === 'passphrase' ? '__nowrap' : '__wrap');
}
hideChange(e) {
@ -193,11 +251,27 @@ class GeneratorView extends View {
return;
}
this.preset = name;
this.presetLC = this.preset.toLowerCase();
const preset = this.presets.find((t) => t.name === name);
this.gen = { ...preset };
this.render();
}
setInputHeight() {
const MinHeight = 10;
this.resultEl.height(MinHeight);
let newHeight = this.resultEl[0].scrollHeight;
if (newHeight <= MinHeight) {
newHeight = MinHeight;
}
if (newHeight > 130) {
newHeight = 130;
}
this.resultEl.height(newHeight);
}
newPass() {
this.generate();
}

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,7 @@
<code>[</code> {{res 'genPsBrackets'}}<br/>
<code>Ä</code> {{res 'genPsHigh'}}<br/>
<code>0</code> {{res 'genPsAmbiguous'}}<br/>
<code>S</code> {{res 'genPsSpaces'}}<br/>
<code>I</code> {{res 'genPsIncluded'}}
</p>
</div>

View File

@ -20,22 +20,24 @@
<option value="...">...</option>
{{/if}}
</select>
<input type="range" class="gen__length-range" value="{{opt.pseudoLength}}" min="0" max="25" />
<input type="range" class="gen__length-range" value="{{opt.pseudoLength}}" min="0" max="26" />
<div>
<div class="gen__check"><input type="checkbox" id="gen__check-upper"
<div class="gen__check"><input class="checkbox-upper" type="checkbox" id="gen__check-upper"
data-id="upper" {{#if opt.upper}}checked{{/if}}><label for="gen__check-upper">ABC</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-lower"
<div class="gen__check"><input class="checkbox-lower" type="checkbox" id="gen__check-lower"
data-id="lower" {{#if opt.lower}}checked{{/if}}><label for="gen__check-lower">abc</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-digits"
<div class="gen__check"><input class="checkbox-digits" type="checkbox" id="gen__check-digits"
data-id="digits" {{#if opt.digits}}checked{{/if}}><label for="gen__check-digits">123</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-special"
<div class="gen__check"><input class="checkbox-special" type="checkbox" id="gen__check-special"
data-id="special" {{#if opt.special}}checked{{/if}}><label for="gen__check-special">!@#</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-brackets"
<div class="gen__check"><input class="checkbox-brackets" type="checkbox" id="gen__check-brackets"
data-id="brackets" {{#if opt.brackets}}checked{{/if}}><label for="gen__check-brackets">({&lt;</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-high"
<div class="gen__check"><input class="checkbox-high" type="checkbox" id="gen__check-high"
data-id="high" {{#if opt.high}}checked{{/if}}><label for="gen__check-high">äæ±</label></div>
<div class="gen__check"><input type="checkbox" id="gen__check-ambiguous"
<div class="gen__check"><input class="checkbox-ambiguous" type="checkbox" id="gen__check-ambiguous"
data-id="ambiguous" {{#if opt.ambiguous}}checked{{/if}}><label for="gen__check-ambiguous">0Oo</label></div>
<div class="gen__check spaces"><input class="checkbox-spaces" type="checkbox" id="gen__check-spaces"
data-id="spaces" {{#if opt.spaces}}checked{{/if}}><label for="gen__check-spaces">Spaces</label></div>
</div>
<div class="gen__result"></div>
<div class="gen__btn-wrap"><button class="gen__btn-ok">{{btnTitle}}</button></div>