Last active
October 31, 2020 11:07
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Benchmarked for good performance (without async / Promises).
t = performance.now(); Array(100_000).fill().map(() => cryptoRandomMixedString()); performance.now() - t;