Implementing a passphrase generator in the browser
The quest for a useful password generator
I had a problem. I wanted to make my passwords in my native language, finnish. This meant coming up with words and phrases every time I wanted a memorable and secure password. That was way too tedious for way too long, so I wanted to come up with a way to make finnish passwords automatically, no thinking needed.
I went on a quest to look for a solution, which as it turned out, did not exist. So, I decided to build it myself. It is a PWA, which makes it really nice to use even on a bad connection or no connection at all.
What does it do?
As of now, it generates a password or a passphrase. Mainly, it generates either a random string of characters and numbers, or a random string of words in either finnish or english. That can be further enforced by adding special characters to the final password.
All of the randomness is made using the Web Crypto Api, and the frontend is built with with plain old React, using Vite.
Interactions
In the video above you can see the strength indicator paired with additional info in the dynamic island-inspired view.
I set out to make using the application as smooth as possible. With some thought out animations, I tried to make the interactions feel more alive.
Built using Radix UI and Framer Motion
This project benefitted heavily from shadcn UI components that are built with Radix Primitives of course. The components are a delight to work with, and they are built with great thought put in to them.
Generating a password using typescript
So how to create a secure password using typescript/javascript?
There are two ways, from which only one should really be used, but I'll go over both to cover the reasons why one is better than the other.
Password using Math.random()
A naive and unsafe way to generate a random string of characters would be to do something like this:
const CHAR_LIST = "abcdefghijklmnopqrstuyxz0123456789";
function createUnsafeNonRandomPwd(len: number): string {
let badPassword = "";
for (let i = 0; i < len; i++) {
badPassword += CHAR_LIST[Math.floor(Math.random() * CHAR_LIST.length)];
}
return badPassword;
}
This, of course is a horrible idea, since Math.random()
is not truly random, about which there is a lot of existing writing,
so I won't go into it now. Due to the fact that some random number generators, like Math.random()
derive the randomness from some internal state, it can be exploited. This guy even
cracked a password by switching the programs clock to the time when the password was generated.
Anyway, let's look at how I did the 'right' way for salasanakone.com
Generate password using window.crypto
This method quite a bit more involved, so let's walk through it step by step.
We start from the humble need to generate a truly random number.
Here, we take the range of the values, calculate how many bytes that is, and pass
that to the generateRandomValueFromBytes
, which we will implement next!
function generateRandomNumberInRange(min, max) {
if (min >= max) {
throw new Error("Range is invalid");
}
const range = max - min;
// How many bytes the number is
const requestBytes = Math.ceil(Math.log2(range) / 8);
if (requestBytes === 0) {
return 0;
}
const randomValue = generateRandomValueFromBytes(requestBytes);
// Make sure the number is between min and max by taking the remainder from randomValue
return min + (randomValue % range);
}
Here, we create a random number, which is requestBytes
bytes large, and return that.
/** This function takes in the amount of bytes, and generates that many numbers */
function generateRandomValueFromBytes(requestBytes) {
const maxNum = 256 ** requestBytes; // the largest the number can be
const arr = new Uint8Array(requestBytes);
let val = 0;
do {
// Fill the array with truly random bytes
crypto.getRandomValues(arr);
val = 0;
for (let i = 0; i < requestBytes; i++) {
// Convert to a single number by left-shifting and adding each byte
val = (val << 8) + arr[i];
}
} while (val >= maxNum - (maxNum % requestBytes));
return val;
}
To tie it all together, fill an array of desired length with random numbers created by generateRandomNumberInRange
, and map those numbers to the list of characters you want to generate from:
function createPwdFromString(stringToUse: string, len: number): string {
const numArr = generateRandomArray(len, 0, stringToUse.length - 1);
let str = "";
for (let i = 0; i < numArr.length; i++) {
str += stringToUse[numArr[i]];
}
return str;
}
/** generates an array of random numbers between min and max, with length `len` */
function generateRandomArray(len: number, min: number, max: number): number[] {
const arr = new Array<number>(len);
for (let i = 0; i < len; i++) {
arr[i] = generateRandomNumberInRange(min, max);
}
return arr;
}
/** Usage */
const pwd = createPwdFromString(CHAR_LIST, 24);
// ... a 24 character random string created from characters in CHAR_LIST
So, there you have it! That is how to create a random password or string in js.
Now, the good thing about those functions, is that they are generic enough to allow for easy creation of passphrases as well. Let's take a look at that:
This function just takes in a dataset, which is nothing more than an array of strings. It then picks words from the dataset at random places! Nice and easy.
/** generates an array of length `length` from randomly selected strings in dataset */
function getRandomWordsFromDataset(length: number, dataset: string[]): string[] {
const maxCount = dataset.length - 1; // the max word count in the dataset
const randomNumsArray = generateRandomArray(length, 0, maxCount);
const wordArray: string[] = new Array(length);
for (let i = 0; i < length; i++) {
wordArray[i] = dataset[randomNumsArray[i]];
}
return wordArray;
}
You can then join the array with whatever separator you like, in the case of salasanakone.com, it's joined with whatever you input in the UI, and a random number is placed in the passphrase to further strengthen the passphrase.
Challenges
This project, which initially was just a random string generator, turned out to become quite a challenge to make "production ready". Challenges started when I added the passphrase functionality.
1. Dictionaries
Loading the custom dictionaries, which are just massive arrays of words, turned out to be challenging to get just right. Loading the datasets had to be done asynchronously over the network from a CDN, and it shouldn't block the UI. I spun up a quick cloudflare worker to create an API for getting the datasets, as I had already experimented with making a passphrase generating API.
For the zxcvbn default dictionaries, I loaded them using dynamic imports.
async function getDic(lang: Language): Promise<OptionsType | undefined> {
switch (lang) {
case Language.fi: {
return await import("https://cdn.jsdelivr.net/npm/@zxcvbn-ts/[email protected]/+esm");
}
case Language.en: {
return await import("https://cdn.jsdelivr.net/npm/@zxcvbn-ts/[email protected]/+esm");
}
default:
return undefined;
}
}
2. Measuring the strength
Strength is measured using the zxcvbn-ts library. On load, the application spins up a web worker to load and setup the zxcvbn with the correct dictionaries based on the language.
When it comes time to score the password, a debounced call is made to the same web worker to calculate the score. Then the worker send back a message with the scoring. Very neat!
function checkStrength(password: string): ZxcvbnResult {
return zxcvbn(password);
}
let prev: ZxcvbnResult | undefined; // in-memory cache for the result
self.onmessage = async function handleOnMessage(
e: MessageEvent<CheckerWorkerPostMessageData>,
): Promise<void> {
// first check if this event is a language change, eg. a "en" or "fi"
if (typeof e.data === "string") {
return await handleLanguageChange(e.data);
}
if (prev && prev.password === e.data.strValue) {
// Previous calculation was done on same string as supplied
return self.postMessage(prev);
}
const result = checkStrength(e.data.strValue);
prev = result;
return self.postMessage(result);
};
Using web workers allowed me to get rid of a massive amount of jank on the site, while still keeping the implementation fairly simple.
3. State management
I didn't want to bring in an external library for managing the forms state, which meant figuring out how to efficiently manage state inside the form. Keeping in mind that almost all major components needed to have access to shared state, I opted to use React Context for the majority of the state. The issue here of course is that context makes the whole subtree of components re-render on change.
Links to project
Check it out here: salasanakone.com
Check the repo on GitHub
Updates
4.4.2023 - UI reworking and passphrase support
Thanks to some great critisism, I added passphrases to the site. Now anyone can actually remember these passwords.
Updated the old zxcvbn library to the newer, more actively maintained zxcvbn-ts. This helped quite a lot in measuring the strength, since it provides a Finnish dictionary, and also additional performance improvements.
Speed Index is back to 2,4s, since I've added more functionality, but it actually loads to interactive faster!
20.3.2023 - PWA and some added functionality
It's now a PWA! Try it out, and add to your devices home screen.
Added numbers in the password, since most sites require numbers too in their passwords. Makes it quite a lot harder to read, but worth the trouble IMO.
Performance increases! Less blocking scripts, quicker time to interactive. On mobile, I got PageSpeed Insights speed index down to 1,8s - 2,0s from nearly 4 seconds.