Skip to content

Instantly share code, notes, and snippets.

@heri16
Last active October 31, 2020 11:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heri16/e1403b2d6bd360b983b2fadaa8074efd to your computer and use it in GitHub Desktop.
Save heri16/e1403b2d6bd360b983b2fadaa8074efd to your computer and use it in GitHub Desktop.
Shortest secure mixed-password generator - Easy to audit CSPRNG crypto.getRandomValues() with no bias
// Special chars from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html
const validUppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const validLowercase = 'abcdefghijklmnopqrstuvwxyz';
const validNumber = '0123456789';
const validSpecial = '^$*.[]{}()?"!@#%&/\\,><\':;|_~\`';
const validChars = validSpecial + validUppercase + validLowercase + validNumber;
// See: https://javascript.info/regular-expressions
const regexpUppercase = new RegExp(`[${validUppercase}]`, 'g');
const regexpLowercase = new RegExp(`[${validLowercase}]`, 'g');
const regexpNumber = new RegExp(`[${validNumber}]`, 'g');
const regexpSpecial = new RegExp(`[${validSpecial.split('').map(c => '\\' + c).join('')}]`, 'g');
const requiredCharSets = [
[validSpecial, regexpSpecial],
[validUppercase, regexpUppercase],
[validLowercase, regexpLowercase],
[validNumber, regexpNumber],
];
function countMatch(str, regexp) {
return ((str || '').match(regexp) || []).length
}
function cryptoRandomArray(len, max) {
const UintArray = (max <= 255 ? Uint8Array : max <= 65535 ? Uint16Array : max <= 4294967295 ? Uint32Array : BigUint64Array);
const randMax = Math.pow(2, UintArray.BYTES_PER_ELEMENT * 8) - 1;
const bytearray = new UintArray(len);
window.crypto.getRandomValues(bytearray);
// See: https://gist.github.com/joepie91/7105003c3b26e65efcea63f3db82dfba
const upper = max + 1;
if (upper > randMax) return bytearray;
const unbiasedMax = randMax - (randMax % upper) - 1;
return bytearray.map((x) => {
if (x > unbiasedMax) {
const b = new UintArray(1);
do { window.crypto.getRandomValues(b); } while (b[0] > unbiasedMax)
return b[0] % upper;
}
return x % upper;
});
}
function cryptoRandomString(len = 40, charSet = validChars) {
const charPositions = cryptoRandomArray(len, charSet.length - 1);
// Convert to normal array as charCodeAt() may overflow
const charCodes = Array.prototype.slice.call(charPositions).map(pos => charSet.charCodeAt(pos));
return String.fromCharCode.apply(null, charCodes);
}
function cryptoRandomMixedString(len = 40, minChars = 1, charSets = requiredCharSets) {
const randomString = cryptoRandomString(len);
const addMissing = charSets.map(([ charSet, regexp, minCount = minChars ]) => {
const count = countMatch(randomString, regexp);
if (count < minCount) return cryptoRandomString(minCount - count, charSet);
}).join('');
return randomString + addMissing;
}
// Special chars from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html
const validUppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const validLowercase = 'abcdefghijklmnopqrstuvwxyz';
const validNumber = '0123456789';
const validSpecial = '^$*.[]{}()?"!@#%&/\\,><\':;|_~\`';
// See: https://javascript.info/regular-expressions
const regexpUppercase = new RegExp(`[${validUppercase}]`, 'g');
const regexpLowercase = new RegExp(`[${validLowercase}]`, 'g');
const regexpNumber = new RegExp(`[${validNumber}]`, 'g');
const regexpSpecial = new RegExp(`[${validSpecial.split('').map(c => '\\' + c).join('')}]`, 'g');
// See: https://javascript.info/property-descriptors#sealing-an-object-globally
const asciiChars = validSpecial + validUppercase + validLowercase + validNumber;
const asciiCharSets = Object.freeze([
Object.freeze([validSpecial, regexpSpecial]),
Object.freeze([validUppercase, regexpUppercase]),
Object.freeze([validLowercase, regexpLowercase]),
Object.freeze([validNumber, regexpNumber])
]);
// Sequence generator function (commonly referred to as "range", e.g. Clojure, PHP etc)
const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + (i * step));
// Sample Unicode from https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=\p{Emoji}&abb=on
const validUnicode = String.fromCodePoint.apply(null, range('🏷'.codePointAt(0), '📽'.codePointAt(0), 1));
// See: https://javascript.info/regexp-unicode
const regexpUnicode = /[^\p{ASCII}]/gu; // new RegExp(`[${validUnicode}]`, 'gu')
// See: https://javascript.info/property-descriptors#sealing-an-object-globally
const unicodeChars = validUnicode + asciiChars;
const unicodeCharSets = Object.freeze([Object.freeze([validUnicode, regexpUnicode])].concat(asciiCharSets));
function countMatch(str, regexp) {
return ((str || '').match(regexp) || []).length
}
function cryptoRandomArray(len, max) {
const UintArray = (max <= 255 ? Uint8Array : max <= 65535 ? Uint16Array : max <= 4294967295 ? Uint32Array : BigUint64Array);
const randMax = Math.pow(2, UintArray.BYTES_PER_ELEMENT * 8) - 1;
const bytearray = new UintArray(len);
window.crypto.getRandomValues(bytearray);
// See: https://gist.github.com/joepie91/7105003c3b26e65efcea63f3db82dfba
const upper = max + 1;
if (upper > randMax) return bytearray;
const unbiasedMax = randMax - (randMax % upper) - 1;
return bytearray.map((x) => {
if (x > unbiasedMax) {
const b = new UintArray(1);
do { window.crypto.getRandomValues(b); } while (b[0] > unbiasedMax)
return b[0] % upper;
}
return x % upper;
});
}
function cryptoRandomString(len = 40, charSet = asciiChars) {
// See: https://javascript.info/string#surrogate-pairs
// See: https://javascript.info/iterable#array-from
const charSetSplit = Array.from(charSet); // can polyfill unlike spread-syntax
const charPositions = cryptoRandomArray(len, charSetSplit.length - 1);
// See: https://javascript.info/arraybuffer-binary-arrays
return Array.from(charPositions).map(pos => charSetSplit[pos]).join('');
}
function cryptoRandomMixedString(len = 40, minChars = 1, charSets = asciiCharSets) {
const charSet = charSets.reduce((acc, el) => acc + el[0], '');
const randomString = cryptoRandomString(len, charSet);
const addMissing = charSets.map(([ charSet, regexp, minCount = minChars ]) => {
const count = countMatch(randomString, regexp);
if (count < minCount) return cryptoRandomString(minCount - count, charSet);
}).join('');
return randomString + addMissing;
}
@heri16
Copy link
Author

heri16 commented Oct 16, 2020

CSPRNG: A Cryptographically Secure Pseudo-Random Number Generator

https://gist.github.com/joepie91/7105003c3b26e65efcea63f3db82dfba

@heri16
Copy link
Author

heri16 commented Oct 16, 2020

Benchmarked for good performance (without async / Promises).

t = performance.now(); Array(100_000).fill().map(() => cryptoRandomMixedString()); performance.now() - t;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment