Last active
January 17, 2024 03:15
-
-
Save jamesdiacono/bcc12815b9c83104c34261269151e517 to your computer and use it in GitHub Desktop.
Generates a password from a supply of (presumably random) bytes.
This file contains 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
// Generates a password from a supply of (presumably random) bytes. | |
// The makeup of the password is described by the 'constraints' parameter, | |
// which is an array of constraint objects. The ordering of constraints does | |
// not matter. | |
// Each constraint has an "alphabet" property containing a string of allowed | |
// characters to choose from. | |
// A constraint's "position" property controls placement of a single character. | |
// A constraint's "count" property controls how many characters to distribute. | |
// A constraint should not have both a "count" and "position" property. | |
// For example, a constraint array like | |
// [ | |
// {alphabet: "ABCDE", position: 0}, | |
// {alphabet: "abcde", count: 3}, | |
// {alphabet: "12345", position: 4} | |
// ] | |
// might produce passwords like | |
// "Aaec2" | |
// "Dace3" | |
// "Edbe3" | |
// "Ebaa2" | |
// "Dadb5" | |
// The password itself is derived from the 'bytes' parameter, an array of | |
// integers 0 thru 255. | |
/*jslint bitwise, browser */ | |
function unoccupied(array) { | |
const positions = []; | |
array.forEach(function (element, element_nr) { | |
if (element === undefined) { | |
positions.push(element_nr); | |
} | |
}); | |
return positions; | |
} | |
function generate_password(bytes, constraints) { | |
function integer(magnitude) { | |
// Pluck an integer n, where 0 ≤ n < magnitude, from the remaining bytes. | |
const nr_bits = Math.ceil(Math.log2(magnitude)); | |
let position = 0; | |
let n = 0; | |
while (position < nr_bits) { | |
if (bytes.length === 0) { | |
throw new Error("Bytes exhausted."); | |
} | |
const remaining = nr_bits - position; | |
const mask = 2 ** remaining - 1; | |
const bits = bytes[0] & mask; | |
n |= bits << position; | |
bytes = bytes.slice(1); | |
position += 8; | |
} | |
// If n exceeds the magnitude, reject this sample and try again. Otherwise | |
// return n. | |
return ( | |
n >= magnitude | |
? integer(magnitude) | |
: n | |
); | |
} | |
function choose(array) { | |
return array[integer(array.length)]; | |
} | |
// Infer the length of the password from the constraints. | |
const length = constraints.reduce(function (length, constraint) { | |
return length + (constraint.count ?? 1); | |
}, 0); | |
let password = new Array(length).fill(); | |
// Firstly, fill in those characters whose position is known. | |
constraints.filter(function (constraint) { | |
return Number.isSafeInteger(constraint.position); | |
}).forEach(function (constraint) { | |
if (constraint.position < 0 || constraint.position >= password.length) { | |
throw new Error("Out of range."); | |
} | |
if (password[constraint.position] !== undefined) { | |
throw new Error("Occupied."); | |
} | |
const glyphs = Array.from(constraint.alphabet); | |
password[constraint.position] = choose(glyphs); | |
}); | |
// Now fill in the remaining characters. | |
constraints.filter(function (constraint) { | |
return Number.isSafeInteger(constraint.count); | |
}).forEach(function (constraint) { | |
new Array(constraint.count).fill().forEach(function () { | |
const positions = unoccupied(password); | |
if (positions.length === 0) { | |
throw new Error("Occupied."); | |
} | |
const glyphs = Array.from(constraint.alphabet); | |
password[choose(positions)] = choose(glyphs); | |
}); | |
}); | |
return password.join(""); | |
} | |
// This demo generates a 13 character password that: | |
// a) contains at least two digits | |
// b) starts with a letter | |
// c) contains no special characters. | |
const digit = "0123456789"; | |
const lower = "abcdefghijklmnopqrstuvwxyz"; | |
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
let bytes = new Uint8Array(50); | |
crypto.getRandomValues(bytes); | |
const password = generate_password(bytes, [ | |
{count: 2, alphabet: digit}, | |
{count: 10, alphabet: digit + lower + upper}, | |
{position: 0, alphabet: lower + upper} | |
]); | |
console.log(password); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment