Skip to content

Instantly share code, notes, and snippets.

@jamesdiacono
Last active January 17, 2024 03:15
Show Gist options
  • Save jamesdiacono/bcc12815b9c83104c34261269151e517 to your computer and use it in GitHub Desktop.
Save jamesdiacono/bcc12815b9c83104c34261269151e517 to your computer and use it in GitHub Desktop.
Generates a password from a supply of (presumably random) bytes.
// 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