← All projects

Ruby · Vercel Functions

Password Toolkit

Generates cryptographically-random passwords and scores password strength, both computed server-side.

Passwords are generated with Ruby's SecureRandom (not a seeded PRNG) so they're suitable for actual use, and strength scoring estimates entropy in bits from the character pool and length. Both run entirely on the server.

Live demo

embed
require 'json'
require 'securerandom'

LOWER = ('a'..'z').to_a.join
UPPER = ('A'..'Z').to_a.join
NUMBERS = ('0'..'9').to_a.join
SYMBOLS = '!@#$%^&*()-_=+[]{}'

Handler = Proc.new do |request, response|
  q = request.query

  length = (q['length'] || '16').to_i
  length = 16 if length <= 0
  length = [[length, 4].max, 64].min

  use_upper = q['upper'] != '0'
  use_numbers = q['numbers'] != '0'
  use_symbols = q['symbols'] != '0'

  pool = LOWER.dup
  pool += UPPER if use_upper
  pool += NUMBERS if use_numbers
  pool += SYMBOLS if use_symbols

  password = (0...length).map { pool[SecureRandom.random_number(pool.length)] }.join

  response.status = 200
  response['Content-Type'] = 'application/json; charset=utf-8'
  response.body = { password: password, length: length, pool: pool.length }.to_json
end
const lengthInput = document.getElementById('length');
const lengthVal = document.getElementById('length-val');
const output = document.getElementById('output');

lengthInput.addEventListener('input', () => { lengthVal.textContent = lengthInput.value; });

async function generate() {
  const params = new URLSearchParams({
    length: lengthInput.value,
    upper: document.getElementById('upper').checked ? '1' : '0',
    numbers: document.getElementById('numbers').checked ? '1' : '0',
    symbols: document.getElementById('symbols').checked ? '1' : '0',
  });
  const res = await fetch(`/api/generate?${params}`);
  const data = await res.json();
  output.value = data.password;
  scorePassword(data.password);
}

document.getElementById('generate').addEventListener('click', generate);

document.getElementById('copy').addEventListener('click', async () => {
  if (!output.value) return;
  await navigator.clipboard.writeText(output.value);
});

// Brass dial: dull iron at score 0, bright brass at score 4.
const DIAL_COLORS = ['#7a6a55', '#9c8158', '#b8924f', '#c9a23f', '#d8b24a'];
const ARC_LENGTH = 176; // approx length of the M14,70 A56,56 0 0,1 126,70 path

const needleGroup = document.getElementById('needle-group');
const dialFill = document.getElementById('dial-fill');
dialFill.style.strokeDasharray = `${ARC_LENGTH}`;

function setDial(score) {
  const fraction = Math.max(0, Math.min(1, score / 4));
  dialFill.style.strokeDashoffset = `${ARC_LENGTH * (1 - fraction)}`;
  dialFill.style.stroke = DIAL_COLORS[score] || DIAL_COLORS[0];
  const angle = -90 + fraction * 180;
  needleGroup.style.transform = `rotate(${angle}deg)`;
}

async function scorePassword(password) {
  const label = document.getElementById('label');
  const issues = document.getElementById('issues');

  if (!password) {
    setDial(0);
    label.textContent = '—';
    issues.textContent = '';
    return;
  }

  const res = await fetch(`/api/score?password=${encodeURIComponent(password)}`);
  const data = await res.json();
  setDial(data.score);
  label.textContent = `${data.label} · ~${data.bits} bits of entropy`;
  issues.textContent = data.issues.join(' · ');
}

document.getElementById('check').addEventListener('input', (e) => scorePassword(e.target.value));

generate();