From 58f585b6915fb954ab899a868e71a2bbfc79083d Mon Sep 17 00:00:00 2001 From: reze Date: Wed, 21 Oct 2026 09:01:42 +0000 Subject: [PATCH] reze: Add Node.js password generator with memorable and random modes Public password generator with no auth and no npm dependencies. - Server-side generation using crypto.randomInt; two levels: diceware-style memorable passphrases (42abacus-91abdomen) and fully random characters. - Configurable word list (EFF Large / EFF Short), digits per word, separators, capitalization, length, character class toggles, custom symbols and ambiguous-character exclusion. - Light/dark theme matching druid520/site, entropy estimate and strength bar, clipboard copy, options persisted in localStorage. - Dockerfile for Dokploy plus a Woodpecker deploy hook on trunk. --- .dockerignore | 6 + .gitignore | 4 + .woodpecker.yml | 15 + Dockerfile | 20 + README.md | 185 + docker-compose.yml | 9 + package.json | 15 + public/404.html | 19 + public/app.js | 379 ++ public/index.html | 117 + public/style.css | 240 ++ public/theme.js | 32 + public/words/eff-large.txt | 7776 ++++++++++++++++++++++++++++++++++++ public/words/eff-short.txt | 1296 ++++++ scripts/run.sh | 16 + server.js | 289 ++ src/generator.js | 249 ++ src/words.js | 57 + test/generator.test.js | 250 ++ 19 files changed, 10974 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .woodpecker.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 public/404.html create mode 100644 public/app.js create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 public/theme.js create mode 100644 public/words/eff-large.txt create mode 100644 public/words/eff-short.txt create mode 100755 scripts/run.sh create mode 100644 server.js create mode 100644 src/generator.js create mode 100644 src/words.js create mode 100644 test/generator.test.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27a4f19 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.git +npm-debug.log +test +*.md +.woodpecker.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c35ee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log* +.DS_Store +*.log diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..401b497 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,15 @@ +when: + - event: push + branch: trunk + +steps: + deploy: + image: curlimages/curl:latest + # Needs Dokploy on the internal network; allowed because an admin marked this repo "trusted network". + # Pipelines otherwise run on the isolated ci-net (internet + git.kg only). + network_mode: dokploy-network + environment: + DEPLOY_URL: + from_secret: DEPLOY_URL + commands: + - 'curl -fsS -X POST "$${DEPLOY_URL}" -H "Content-Length: 0"' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2db0112 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json ./ +COPY server.js ./ +COPY src ./src +COPY public ./public + +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +USER node + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1 + +CMD ["node","server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8fd392 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# reze-test + +A small, dependency-free password generator. It has two modes: + +- **Memorable** — diceware-style passphrases shaped like `[num][word]-[num][word]-…`, e.g. `42abacus-91abdomen`. +- **Random** — fully random characters from a configurable character set. + +Everything is generated server-side with Node's CSPRNG (`crypto.randomInt`). There is no +account, no login, no database, and nothing you generate is stored or logged. + +## Features + +- Two randomization levels: memorable passphrases and fully random passwords. +- EFF word lists (7776-word and 1296-word) selectable per request. +- Configurable digits per word, digit separator, group separator and capitalization. +- Configurable length, character classes, custom symbol set, ambiguous-character exclusion + and "at least one of each selected set". +- Generate up to 10 passwords at once, each with an entropy estimate and a strength bar. +- One-click copy with a clipboard fallback. +- Light/dark theme with a system-theme fallback, persisted in `localStorage`. +- Options persisted in `localStorage`; generated passwords are never persisted. +- No npm dependencies at all — Node.js built-ins only. + +## Requirements + +- Node.js 22 or newer (ESM, `"type": "module"`). + +## Run locally + +```sh +node server.js +``` + +The server listens on `0.0.0.0:3000` by default. Override the port with `PORT`: + +```sh +PORT=8080 node server.js +``` + +Open . + +Or via the POSIX sh helper, which defaults `PORT` to 3000: + +```sh +sh scripts/run.sh +PORT=8080 sh scripts/run.sh +``` + +## Tests + +```sh +node --test +``` + +## API + +All API responses are JSON. Every response carries the security headers +`X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, +`X-Frame-Options: DENY` and a strict `Content-Security-Policy`. + +### `GET /healthz` + +Liveness probe used by the Docker `HEALTHCHECK`. + +```sh +curl -s localhost:3000/healthz +``` + +```json +{"status":"ok"} +``` + +### `GET /api/meta` + +Describes the available word lists. + +```sh +curl -s localhost:3000/api/meta +``` + +```json +{"lists":[{"id":"eff-large","name":"EFF Large Word List","size":7776},{"id":"eff-short","name":"EFF Short Word List (2.0)","size":1296}]} +``` + +### `POST /api/generate` + +Body is a JSON object. `mode` selects the algorithm; `count` (1–10, default 1) is the +number of passwords to return. Unknown modes, malformed JSON and out-of-range options +are rejected with `400` and a JSON `{"error": "…"}` body. Request bodies are limited to +64 KB. + +Memorable options: `words` (2–10), `digits` (0–4), `digitSeparator` (`''` or `'-'`), +`separator` (`'-'`, `'_'`, `'.'`, `' '` or `''`), `capitalization` +(`'none'`, `'title'`, `'upper'`), `list` (`'eff-large'`, `'eff-short'`). + +```sh +curl -s -X POST localhost:3000/api/generate \ + -H 'Content-Type: application/json' \ + -d '{"mode":"memorable","words":2,"digits":2,"separator":"-","digitSeparator":"","capitalization":"title","list":"eff-large","count":2}' +``` + +```json +{"mode":"memorable","count":2,"passwords":[{"value":"07Sample-29Breach","mode":"memorable","entropy":39.1},{"value":"69Reassign-85Probation","mode":"memorable","entropy":39.1}],"entropy":39.1} +``` + +Random options: `length` (8–64), `useLower`, `useUpper`, `useDigits`, `useSymbols`, +`customSymbols` (replaces the default symbol set when non-empty), `excludeAmbiguous` +(removes `0 O 1 l I |`) and `requireEach`. + +```sh +curl -s -X POST localhost:3000/api/generate \ + -H 'Content-Type: application/json' \ + -d '{"mode":"random","length":24,"useLower":true,"useUpper":true,"useDigits":true,"useSymbols":true,"requireEach":true,"count":1}' +``` + +```json +{"mode":"random","count":1,"passwords":[{"value":"3V#P@bQ2C4c3;=igw}E(EpnG","mode":"random","entropy":154.6}],"entropy":154.6} +``` + +### Entropy + +- Random mode: `length * log2(poolSize)`, where `poolSize` is the size of the union of all + enabled character classes. +- Memorable mode: `words * log2(listSize) + words * digits * log2(10)`. Separators and the + capitalization scheme add no entropy. + +### Other paths + +Unknown `/api/*` paths return a JSON `404`. Unknown page paths return +`public/404.html` with HTTP `404`. Only `GET` and `HEAD` are accepted for static files. + +## Word lists + +| id | list | size | +| --- | --- | --- | +| `eff-large` | EFF Large Word List | 7776 | +| `eff-short` | EFF Short Word List (2.0) | 1296 | + +Both files live in `public/words/` and are loaded once at startup. + +- Source: [Electronic Frontier Foundation](https://www.eff.org/dice) — the + [EFF Diceware wordlists](https://www.eff.org/dice-roll-the-dice). +- Licence: Creative Commons Attribution 3.0 Unported (CC BY 3.0), + . + +The lists contain a few hyphenated entries (`drop-down`, `felt-tip`, `t-shirt`, `yo-yo`), +which are used verbatim. + +## Environment variables + +| variable | default | description | +| --- | --- | --- | +| `PORT` | `3000` | TCP port the HTTP server binds to on `0.0.0.0`. | +| `NODE_ENV` | — | Set to `production` in the image. | + +## Docker + +```sh +docker build -t reze-test . +docker run --rm -p 3000:3000 reze-test +``` + +Or with compose: + +```sh +docker compose up --build -d +``` + +The image is a single stage based on `node:22-alpine`, runs as the non-root `node` user +and ships a `HEALTHCHECK` that polls `http://127.0.0.1:3000/healthz` with `wget`. + +## Deployment (Dokploy) + +- Repository: `druid520/reze-test` on Gitea (git.kg.boson.party). +- Branch: `trunk`. +- Build type: Dockerfile (or "Nixpacks" — the app needs nothing but Node). +- Environment variable: `PORT=3000`. +- Exposed port: `3000`. +- CI hook: add the repository secret `DEPLOY_URL` (the Dokploy deploy webhook URL). + `.woodpecker.yml` posts to it on every push to `trunk` so Dokploy rebuilds and + redeploys the service. + +## License + +MIT for the code. The bundled word lists are CC BY 3.0, © Electronic Frontier Foundation. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d3289b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: . + image: reze-test:latest + ports: + - "3000:3000" + environment: + PORT: 3000 + restart: unless-stopped diff --git a/package.json b/package.json new file mode 100644 index 0000000..ecccf5e --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "reze-test", + "version": "1.0.0", + "private": true, + "description": "Password generator web app: diceware-style passphrases and fully random passwords.", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "start": "node server.js", + "test": "node --test" + }, + "license": "MIT" +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..8e64b8d --- /dev/null +++ b/public/404.html @@ -0,0 +1,19 @@ + + + + + +Not found · reze-test + + + + +
+
+

Error 404

+

Not found

+

There's nothing at this address.

+ Back to reze-test +
+ + diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..c9760e0 --- /dev/null +++ b/public/app.js @@ -0,0 +1,379 @@ +(function () { + 'use strict'; + + var STORAGE_KEY = 'opts'; + + var DEFAULTS = { + mode: 'memorable', + words: 4, + digits: 2, + digitSeparator: '', + separator: '-', + capitalization: 'none', + list: 'eff-large', + length: 20, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: false, + customSymbols: '', + excludeAmbiguous: false, + requireEach: true, + count: 1 + }; + + var FIELDS = [ + { key: 'words', id: 'words', type: 'int', min: 2, max: 10 }, + { key: 'digits', id: 'digits', type: 'int', min: 0, max: 4 }, + { key: 'digitSeparator', id: 'digitSeparator', type: 'str' }, + { key: 'separator', id: 'separator', type: 'str' }, + { key: 'capitalization', id: 'capitalization', type: 'str' }, + { key: 'list', id: 'list', type: 'str' }, + { key: 'length', id: 'length', type: 'int', min: 8, max: 64 }, + { key: 'count', id: 'count', type: 'int', min: 1, max: 10 }, + { key: 'customSymbols', id: 'customSymbols', type: 'str' }, + { key: 'useLower', id: 'useLower', type: 'bool' }, + { key: 'useUpper', id: 'useUpper', type: 'bool' }, + { key: 'useDigits', id: 'useDigits', type: 'bool' }, + { key: 'useSymbols', id: 'useSymbols', type: 'bool' }, + { key: 'excludeAmbiguous', id: 'excludeAmbiguous', type: 'bool' }, + { key: 'requireEach', id: 'requireEach', type: 'bool' } + ]; + + var STRENGTH_LABELS = ['very weak', 'weak', 'fair', 'strong', 'very strong']; + var STRENGTH_THRESHOLDS = [28, 40, 60, 80]; + + function byId(id) { + return document.getElementById(id); + } + + function clampInt(value, fallback, min, max) { + var n = parseInt(value, 10); + if (isNaN(n)) return fallback; + if (n < min) return min; + if (n > max) return max; + return n; + } + + function loadState() { + var state = {}; + var key; + for (key in DEFAULTS) { + if (Object.prototype.hasOwnProperty.call(DEFAULTS, key)) state[key] = DEFAULTS[key]; + } + try { + var raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + var saved = JSON.parse(raw); + if (saved && typeof saved === 'object') { + for (key in saved) { + if (Object.prototype.hasOwnProperty.call(state, key)) state[key] = saved[key]; + } + } + } + } catch (e) {} + return state; + } + + function saveState(state) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch (e) {} + } + + function readState() { + var state = {}; + var i; + var field; + var el; + + for (i = 0; i < FIELDS.length; i += 1) { + field = FIELDS[i]; + el = byId(field.id); + if (!el) continue; + if (field.type === 'bool') { + state[field.key] = el.checked; + } else if (field.type === 'int') { + state[field.key] = clampInt(el.value, DEFAULTS[field.key], field.min, field.max); + } else { + state[field.key] = el.value; + } + } + + var checked = document.querySelector('input[name="mode"]:checked'); + state.mode = checked ? checked.value : DEFAULTS.mode; + + return state; + } + + function applyState(state) { + var i; + var field; + var el; + + for (i = 0; i < FIELDS.length; i += 1) { + field = FIELDS[i]; + el = byId(field.id); + if (!el) continue; + if (field.type === 'bool') { + el.checked = Boolean(state[field.key]); + } else if (field.type === 'int') { + el.value = clampInt(state[field.key], DEFAULTS[field.key], field.min, field.max); + } else { + el.value = state[field.key] === undefined || state[field.key] === null ? '' : state[field.key]; + } + } + + var radios = document.querySelectorAll('input[name="mode"]'); + for (i = 0; i < radios.length; i += 1) { + radios[i].checked = radios[i].value === state.mode; + } + } + + function updatePanels(state) { + var memorable = byId('panel-memorable'); + var random = byId('panel-random'); + if (memorable) memorable.hidden = state.mode !== 'memorable'; + if (random) random.hidden = state.mode !== 'random'; + } + + function updateLengthOut() { + var length = byId('length'); + var out = byId('length-out'); + if (length && out) out.textContent = length.value; + } + + function strengthLevel(bits) { + var i; + for (i = 0; i < STRENGTH_THRESHOLDS.length; i += 1) { + if (bits < STRENGTH_THRESHOLDS[i]) return i; + } + return STRENGTH_LABELS.length - 1; + } + + function buildStrength(bits) { + var level = strengthLevel(bits); + var wrap = document.createElement('span'); + wrap.className = 'strength'; + wrap.setAttribute('role', 'img'); + wrap.setAttribute('aria-label', 'strength: ' + STRENGTH_LABELS[level]); + var i; + for (i = 0; i < STRENGTH_LABELS.length; i += 1) { + var seg = document.createElement('i'); + if (i <= level) seg.className = 'on'; + wrap.appendChild(seg); + } + return wrap; + } + + function showError(message) { + var box = byId('error'); + if (!box) return; + if (message) { + box.textContent = message; + box.hidden = false; + } else { + box.textContent = ''; + box.hidden = true; + } + } + + function copyWithFallback(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + var area = document.createElement('textarea'); + area.value = text; + area.setAttribute('readonly', 'readonly'); + area.style.position = 'absolute'; + area.style.left = '-9999px'; + document.body.appendChild(area); + area.select(); + var ok = false; + try { + ok = document.execCommand('copy'); + } catch (e) { + ok = false; + } + document.body.removeChild(area); + if (ok) { + resolve(); + } else { + reject(new Error('copy failed')); + } + }); + } + + function copyPassword(value, button) { + var original = button.getAttribute('data-label') || 'Copy'; + copyWithFallback(value).then(function () { + button.textContent = 'Copied'; + }, function () { + button.textContent = 'Failed'; + }).then(function () { + window.setTimeout(function () { + button.textContent = original; + }, 1200); + }); + } + + function renderPasswords(data) { + var container = byId('results'); + if (!container) return; + container.textContent = ''; + + var list = (data && data.passwords) || []; + for (let i = 0; i < list.length; i += 1) { + const item = list[i]; + + const row = document.createElement('div'); + row.className = 'pw'; + + const value = document.createElement('pre'); + value.className = 'pw-value'; + value.textContent = item.value; + + const meta = document.createElement('div'); + meta.className = 'pw-meta'; + + const entropy = document.createElement('span'); + entropy.className = 'pw-entropy'; + entropy.textContent = item.entropy + ' bits'; + + const level = strengthLevel(item.entropy); + const label = document.createElement('span'); + label.className = 'pw-strength-label'; + label.textContent = STRENGTH_LABELS[level]; + + const copy = document.createElement('button'); + copy.type = 'button'; + copy.className = 'button copy'; + copy.textContent = 'Copy'; + copy.setAttribute('data-label', 'Copy'); + copy.setAttribute('aria-label', 'Copy password'); + copy.addEventListener('click', function () { + copyPassword(item.value, copy); + }); + + meta.appendChild(entropy); + meta.appendChild(buildStrength(item.entropy)); + meta.appendChild(label); + meta.appendChild(copy); + + row.appendChild(value); + row.appendChild(meta); + container.appendChild(row); + } + } + + function payloadFor(state) { + if (state.mode === 'random') { + return { + mode: 'random', + length: state.length, + useLower: state.useLower, + useUpper: state.useUpper, + useDigits: state.useDigits, + useSymbols: state.useSymbols, + customSymbols: state.customSymbols, + excludeAmbiguous: state.excludeAmbiguous, + requireEach: state.requireEach, + count: state.count + }; + } + return { + mode: 'memorable', + words: state.words, + digits: state.digits, + digitSeparator: state.digitSeparator, + separator: state.separator, + capitalization: state.capitalization, + list: state.list, + count: state.count + }; + } + + function generate() { + var state = readState(); + saveState(state); + showError(''); + + fetch('/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payloadFor(state)) + }).then(function (response) { + return response.json().then(function (data) { + if (!response.ok) { + throw new Error(data && data.error ? data.error : 'request failed with status ' + response.status); + } + return data; + }); + }).then(function (data) { + renderPasswords(data); + }, function (err) { + showError(err && err.message ? err.message : 'request failed'); + }); + } + + function loadMeta() { + fetch('/api/meta', { headers: { Accept: 'application/json' } }).then(function (response) { + return response.json(); + }).then(function (data) { + var lists = (data && data.lists) || []; + var i; + for (i = 0; i < lists.length; i += 1) { + var option = document.querySelector('#list option[value="' + lists[i].id + '"]'); + if (option) option.textContent = lists[i].name + ' (' + lists[i].size + ' words)'; + } + }, function () {}); + } + + function init() { + var state = loadState(); + applyState(state); + updatePanels(state); + updateLengthOut(); + + var i; + var field; + for (i = 0; i < FIELDS.length; i += 1) { + field = FIELDS[i]; + (function (f) { + var el = byId(f.id); + if (!el) return; + var handler = function () { + var current = readState(); + saveState(current); + updatePanels(current); + updateLengthOut(); + }; + el.addEventListener('change', handler); + el.addEventListener('input', handler); + })(field); + } + + var radios = document.querySelectorAll('input[name="mode"]'); + for (i = 0; i < radios.length; i += 1) { + radios[i].addEventListener('change', function () { + var current = readState(); + saveState(current); + updatePanels(current); + }); + } + + var generateBtn = byId('generate'); + if (generateBtn) generateBtn.addEventListener('click', generate); + var regenerateBtn = byId('regenerate'); + if (regenerateBtn) regenerateBtn.addEventListener('click', generate); + + loadMeta(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..681c712 --- /dev/null +++ b/public/index.html @@ -0,0 +1,117 @@ + + + + + +reze-test + + + + + +
+
+

reze-test

+

Password generator. Diceware-style passphrases or fully random characters — generated locally on the server with a CSPRNG. Nothing you generate is stored.

+ +

Mode

+
+ + +
+ +
+

Passphrase

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + +

Output

+ +
+ + +
+ +
+ + +
+ + + +
+
+ + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..77dbebe --- /dev/null +++ b/public/style.css @@ -0,0 +1,240 @@ +:root { + --bg: #fafafa; + --fg: #1a1a1a; + --muted: #666; + --line: #ddd; + --code-bg: #f0f0f0; + --link: #0b5cad; + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #111; + --fg: #e6e6e6; + --muted: #999; + --line: #333; + --code-bg: #1c1c1c; + --link: #c4a7ff; + } +} + +:root[data-theme="dark"] { + --bg: #111; + --fg: #e6e6e6; + --muted: #999; + --line: #333; + --code-bg: #1c1c1c; + --link: #c4a7ff; + color-scheme: dark; +} + +:root[data-theme="light"] { color-scheme: light; } + +[hidden] { display: none !important; } + +* { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +main { + max-width: 42rem; + margin: 0 auto; + padding: 3rem 1.25rem; +} + +h1 { font-size: 1.75rem; margin: 0 0 2rem; } + +h2 { + font-size: .8rem; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--muted); + margin: 2.5rem 0 .75rem; +} + +a { color: var(--link); text-decoration: none; } +a:hover { text-decoration: underline; } + +.topbar { display: flex; justify-content: flex-end; margin: -1.5rem 0 1.5rem; } +.theme-toggle { + padding: .25rem .7rem; + background: transparent; + color: var(--fg); + border: 1px solid var(--line); + border-radius: 6px; + font-family: inherit; + font-size: .85rem; + line-height: 1.4; + cursor: pointer; +} +.theme-toggle:hover { border-color: var(--muted); } +.theme-toggle:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; } + +dl { + display: grid; + grid-template-columns: max-content 1fr; + gap: .5rem 1.5rem; + margin: 0; +} +dt { color: var(--muted); } +dd { margin: 0; overflow-wrap: anywhere; } + +.key { margin: 0 0 1.25rem; } +.key p { margin: 0 0 .35rem; color: var(--muted); font-size: .9rem; } + +pre { + margin: 0; + padding: .75rem 1rem; + background: var(--code-bg); + border: 1px solid var(--line); + border-radius: 6px; + overflow-x: auto; + font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +main.error h2 { margin-top: 0; } +main.error h1 { margin-bottom: .75rem; } +main.error p { color: var(--muted); margin: 0 0 2rem; } + +.lead { margin: -1.5rem 0 0; color: var(--muted); } + +.radios { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; } + +.radio, +.check { + display: flex; + align-items: center; + gap: .5rem; + font-size: .95rem; +} + +.radio input, +.check input { + width: 1rem; + height: 1rem; + margin: 0; + accent-color: var(--link); +} + +.checks { display: grid; gap: .4rem; margin-bottom: 1rem; } + +.field { margin-bottom: 1rem; } +.field > label { + display: block; + margin-bottom: .3rem; + color: var(--muted); + font-size: .9rem; +} + +input[type="text"], +input[type="number"], +select { + width: 100%; + max-width: 24rem; + padding: .4rem .55rem; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--line); + border-radius: 6px; + font: inherit; + font-size: .95rem; +} + +select { cursor: pointer; } + +input[type="range"] { + width: 100%; + max-width: 24rem; + accent-color: var(--link); +} + +.range-row { display: flex; align-items: center; gap: .75rem; } +.range-row output { + min-width: 2.5rem; + color: var(--muted); + font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +input:focus-visible, +select:focus-visible, +button:focus-visible { + outline: 2px solid var(--link); + outline-offset: 2px; +} + +.button { + padding: .45rem .9rem; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--line); + border-radius: 6px; + font-family: inherit; + font-size: .9rem; + line-height: 1.4; + cursor: pointer; +} +.button:hover { border-color: var(--muted); } + +.button.primary { + background: var(--link); + color: #fff; + border-color: var(--link); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .button.primary { color: #111; } +} +:root[data-theme="dark"] .button.primary { color: #111; } + +.actions { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1.25rem 0; } + +.error-line { + margin: 0 0 1.25rem; + padding-left: .6rem; + border-left: 2px solid var(--link); + color: var(--fg); + font-size: .9rem; +} + +.pw { margin-bottom: 1.5rem; } +.pw:last-child { margin-bottom: 0; } + +.pw-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: .5rem .9rem; + margin-top: .5rem; + color: var(--muted); + font-size: .85rem; +} + +.pw-entropy { font: .85rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } + +.strength { display: inline-flex; gap: 3px; } +.strength i { + display: inline-block; + width: 14px; + height: 6px; + border-radius: 3px; + background: var(--line); +} +.strength i.on { background: var(--link); } + +.pw-strength-label { text-transform: capitalize; } + +@media (max-width: 30rem) { + dl { grid-template-columns: 1fr; gap: 0; } + dd { margin-bottom: .75rem; } + input[type="text"], + input[type="number"], + select, + input[type="range"] { max-width: 100%; } + .actions .button { flex: 1 1 auto; } +} diff --git a/public/theme.js b/public/theme.js new file mode 100644 index 0000000..8144ffe --- /dev/null +++ b/public/theme.js @@ -0,0 +1,32 @@ +(function () { + var root = document.documentElement; + var media = window.matchMedia('(prefers-color-scheme: dark)'); + + try { + var saved = localStorage.getItem('theme'); + if (saved === 'light' || saved === 'dark') root.dataset.theme = saved; + } catch (e) {} + + function current() { + return root.dataset.theme || (media.matches ? 'dark' : 'light'); + } + + function label(btn) { + var next = current() === 'dark' ? 'light' : 'dark'; + btn.textContent = next === 'dark' ? 'Dark mode' : 'Light mode'; + btn.setAttribute('aria-label', 'Switch to ' + next + ' mode'); + } + + document.addEventListener('DOMContentLoaded', function () { + var btn = document.getElementById('theme-toggle'); + if (!btn) return; + label(btn); + btn.hidden = false; + btn.addEventListener('click', function () { + root.dataset.theme = current() === 'dark' ? 'light' : 'dark'; + try { localStorage.setItem('theme', root.dataset.theme); } catch (e) {} + label(btn); + }); + media.addEventListener('change', function () { label(btn); }); + }); +})(); diff --git a/public/words/eff-large.txt b/public/words/eff-large.txt new file mode 100644 index 0000000..caf71f5 --- /dev/null +++ b/public/words/eff-large.txt @@ -0,0 +1,7776 @@ +abacus +abdomen +abdominal +abide +abiding +ability +ablaze +able +abnormal +abrasion +abrasive +abreast +abridge +abroad +abruptly +absence +absentee +absently +absinthe +absolute +absolve +abstain +abstract +absurd +accent +acclaim +acclimate +accompany +account +accuracy +accurate +accustom +acetone +achiness +aching +acid +acorn +acquaint +acquire +acre +acrobat +acronym +acting +action +activate +activator +active +activism +activist +activity +actress +acts +acutely +acuteness +aeration +aerobics +aerosol +aerospace +afar +affair +affected +affecting +affection +affidavit +affiliate +affirm +affix +afflicted +affluent +afford +affront +aflame +afloat +aflutter +afoot +afraid +afterglow +afterlife +aftermath +aftermost +afternoon +aged +ageless +agency +agenda +agent +aggregate +aghast +agile +agility +aging +agnostic +agonize +agonizing +agony +agreeable +agreeably +agreed +agreeing +agreement +aground +ahead +ahoy +aide +aids +aim +ajar +alabaster +alarm +albatross +album +alfalfa +algebra +algorithm +alias +alibi +alienable +alienate +aliens +alike +alive +alkaline +alkalize +almanac +almighty +almost +aloe +aloft +aloha +alone +alongside +aloof +alphabet +alright +although +altitude +alto +aluminum +alumni +always +amaretto +amaze +amazingly +amber +ambiance +ambiguity +ambiguous +ambition +ambitious +ambulance +ambush +amendable +amendment +amends +amenity +amiable +amicably +amid +amigo +amino +amiss +ammonia +ammonium +amnesty +amniotic +among +amount +amperage +ample +amplifier +amplify +amply +amuck +amulet +amusable +amused +amusement +amuser +amusing +anaconda +anaerobic +anagram +anatomist +anatomy +anchor +anchovy +ancient +android +anemia +anemic +aneurism +anew +angelfish +angelic +anger +angled +angler +angles +angling +angrily +angriness +anguished +angular +animal +animate +animating +animation +animator +anime +animosity +ankle +annex +annotate +announcer +annoying +annually +annuity +anointer +another +answering +antacid +antarctic +anteater +antelope +antennae +anthem +anthill +anthology +antibody +antics +antidote +antihero +antiquely +antiques +antiquity +antirust +antitoxic +antitrust +antiviral +antivirus +antler +antonym +antsy +anvil +anybody +anyhow +anymore +anyone +anyplace +anything +anytime +anyway +anywhere +aorta +apache +apostle +appealing +appear +appease +appeasing +appendage +appendix +appetite +appetizer +applaud +applause +apple +appliance +applicant +applied +apply +appointee +appraisal +appraiser +apprehend +approach +approval +approve +apricot +april +apron +aptitude +aptly +aqua +aqueduct +arbitrary +arbitrate +ardently +area +arena +arguable +arguably +argue +arise +armadillo +armband +armchair +armed +armful +armhole +arming +armless +armoire +armored +armory +armrest +army +aroma +arose +around +arousal +arrange +array +arrest +arrival +arrive +arrogance +arrogant +arson +art +ascend +ascension +ascent +ascertain +ashamed +ashen +ashes +ashy +aside +askew +asleep +asparagus +aspect +aspirate +aspire +aspirin +astonish +astound +astride +astrology +astronaut +astronomy +astute +atlantic +atlas +atom +atonable +atop +atrium +atrocious +atrophy +attach +attain +attempt +attendant +attendee +attention +attentive +attest +attic +attire +attitude +attractor +attribute +atypical +auction +audacious +audacity +audible +audibly +audience +audio +audition +augmented +august +authentic +author +autism +autistic +autograph +automaker +automated +automatic +autopilot +available +avalanche +avatar +avenge +avenging +avenue +average +aversion +avert +aviation +aviator +avid +avoid +await +awaken +award +aware +awhile +awkward +awning +awoke +awry +axis +babble +babbling +babied +baboon +backache +backboard +backboned +backdrop +backed +backer +backfield +backfire +backhand +backing +backlands +backlash +backless +backlight +backlit +backlog +backpack +backpedal +backrest +backroom +backshift +backside +backslid +backspace +backspin +backstab +backstage +backtalk +backtrack +backup +backward +backwash +backwater +backyard +bacon +bacteria +bacterium +badass +badge +badland +badly +badness +baffle +baffling +bagel +bagful +baggage +bagged +baggie +bagginess +bagging +baggy +bagpipe +baguette +baked +bakery +bakeshop +baking +balance +balancing +balcony +balmy +balsamic +bamboo +banana +banish +banister +banjo +bankable +bankbook +banked +banker +banking +banknote +bankroll +banner +bannister +banshee +banter +barbecue +barbed +barbell +barber +barcode +barge +bargraph +barista +baritone +barley +barmaid +barman +barn +barometer +barrack +barracuda +barrel +barrette +barricade +barrier +barstool +bartender +barterer +bash +basically +basics +basil +basin +basis +basket +batboy +batch +bath +baton +bats +battalion +battered +battering +battery +batting +battle +bauble +bazooka +blabber +bladder +blade +blah +blame +blaming +blanching +blandness +blank +blaspheme +blasphemy +blast +blatancy +blatantly +blazer +blazing +bleach +bleak +bleep +blemish +blend +bless +blighted +blimp +bling +blinked +blinker +blinking +blinks +blip +blissful +blitz +blizzard +bloated +bloating +blob +blog +bloomers +blooming +blooper +blot +blouse +blubber +bluff +bluish +blunderer +blunt +blurb +blurred +blurry +blurt +blush +blustery +boaster +boastful +boasting +boat +bobbed +bobbing +bobble +bobcat +bobsled +bobtail +bodacious +body +bogged +boggle +bogus +boil +bok +bolster +bolt +bonanza +bonded +bonding +bondless +boned +bonehead +boneless +bonelike +boney +bonfire +bonnet +bonsai +bonus +bony +boogeyman +boogieman +book +boondocks +booted +booth +bootie +booting +bootlace +bootleg +boots +boozy +borax +boring +borough +borrower +borrowing +boss +botanical +botanist +botany +botch +both +bottle +bottling +bottom +bounce +bouncing +bouncy +bounding +boundless +bountiful +bovine +boxcar +boxer +boxing +boxlike +boxy +breach +breath +breeches +breeching +breeder +breeding +breeze +breezy +brethren +brewery +brewing +briar +bribe +brick +bride +bridged +brigade +bright +brilliant +brim +bring +brink +brisket +briskly +briskness +bristle +brittle +broadband +broadcast +broaden +broadly +broadness +broadside +broadways +broiler +broiling +broken +broker +bronchial +bronco +bronze +bronzing +brook +broom +brought +browbeat +brownnose +browse +browsing +bruising +brunch +brunette +brunt +brush +brussels +brute +brutishly +bubble +bubbling +bubbly +buccaneer +bucked +bucket +buckle +buckshot +buckskin +bucktooth +buckwheat +buddhism +buddhist +budding +buddy +budget +buffalo +buffed +buffer +buffing +buffoon +buggy +bulb +bulge +bulginess +bulgur +bulk +bulldog +bulldozer +bullfight +bullfrog +bullhorn +bullion +bullish +bullpen +bullring +bullseye +bullwhip +bully +bunch +bundle +bungee +bunion +bunkbed +bunkhouse +bunkmate +bunny +bunt +busboy +bush +busily +busload +bust +busybody +buzz +cabana +cabbage +cabbie +cabdriver +cable +caboose +cache +cackle +cacti +cactus +caddie +caddy +cadet +cadillac +cadmium +cage +cahoots +cake +calamari +calamity +calcium +calculate +calculus +caliber +calibrate +calm +caloric +calorie +calzone +camcorder +cameo +camera +camisole +camper +campfire +camping +campsite +campus +canal +canary +cancel +candied +candle +candy +cane +canine +canister +cannabis +canned +canning +cannon +cannot +canola +canon +canopener +canopy +canteen +canyon +capable +capably +capacity +cape +capillary +capital +capitol +capped +capricorn +capsize +capsule +caption +captivate +captive +captivity +capture +caramel +carat +caravan +carbon +cardboard +carded +cardiac +cardigan +cardinal +cardstock +carefully +caregiver +careless +caress +caretaker +cargo +caring +carless +carload +carmaker +carnage +carnation +carnival +carnivore +carol +carpenter +carpentry +carpool +carport +carried +carrot +carrousel +carry +cartel +cartload +carton +cartoon +cartridge +cartwheel +carve +carving +carwash +cascade +case +cash +casing +casino +casket +cassette +casually +casualty +catacomb +catalog +catalyst +catalyze +catapult +cataract +catatonic +catcall +catchable +catcher +catching +catchy +caterer +catering +catfight +catfish +cathedral +cathouse +catlike +catnap +catnip +catsup +cattail +cattishly +cattle +catty +catwalk +caucasian +caucus +causal +causation +cause +causing +cauterize +caution +cautious +cavalier +cavalry +caviar +cavity +cedar +celery +celestial +celibacy +celibate +celtic +cement +census +ceramics +ceremony +certainly +certainty +certified +certify +cesarean +cesspool +chafe +chaffing +chain +chair +chalice +challenge +chamber +chamomile +champion +chance +change +channel +chant +chaos +chaperone +chaplain +chapped +chaps +chapter +character +charbroil +charcoal +charger +charging +chariot +charity +charm +charred +charter +charting +chase +chasing +chaste +chastise +chastity +chatroom +chatter +chatting +chatty +cheating +cheddar +cheek +cheer +cheese +cheesy +chef +chemicals +chemist +chemo +cherisher +cherub +chess +chest +chevron +chevy +chewable +chewer +chewing +chewy +chief +chihuahua +childcare +childhood +childish +childless +childlike +chili +chill +chimp +chip +chirping +chirpy +chitchat +chivalry +chive +chloride +chlorine +choice +chokehold +choking +chomp +chooser +choosing +choosy +chop +chosen +chowder +chowtime +chrome +chubby +chuck +chug +chummy +chump +chunk +churn +chute +cider +cilantro +cinch +cinema +cinnamon +circle +circling +circular +circulate +circus +citable +citadel +citation +citizen +citric +citrus +city +civic +civil +clad +claim +clambake +clammy +clamor +clamp +clamshell +clang +clanking +clapped +clapper +clapping +clarify +clarinet +clarity +clash +clasp +class +clatter +clause +clavicle +claw +clay +clean +clear +cleat +cleaver +cleft +clench +clergyman +clerical +clerk +clever +clicker +client +climate +climatic +cling +clinic +clinking +clip +clique +cloak +clobber +clock +clone +cloning +closable +closure +clothes +clothing +cloud +clover +clubbed +clubbing +clubhouse +clump +clumsily +clumsy +clunky +clustered +clutch +clutter +coach +coagulant +coastal +coaster +coasting +coastland +coastline +coat +coauthor +cobalt +cobbler +cobweb +cocoa +coconut +cod +coeditor +coerce +coexist +coffee +cofounder +cognition +cognitive +cogwheel +coherence +coherent +cohesive +coil +coke +cola +cold +coleslaw +coliseum +collage +collapse +collar +collected +collector +collide +collie +collision +colonial +colonist +colonize +colony +colossal +colt +coma +come +comfort +comfy +comic +coming +comma +commence +commend +comment +commerce +commode +commodity +commodore +common +commotion +commute +commuting +compacted +compacter +compactly +compactor +companion +company +compare +compel +compile +comply +component +composed +composer +composite +compost +composure +compound +compress +comprised +computer +computing +comrade +concave +conceal +conceded +concept +concerned +concert +conch +concierge +concise +conclude +concrete +concur +condense +condiment +condition +condone +conducive +conductor +conduit +cone +confess +confetti +confidant +confident +confider +confiding +configure +confined +confining +confirm +conflict +conform +confound +confront +confused +confusing +confusion +congenial +congested +congrats +congress +conical +conjoined +conjure +conjuror +connected +connector +consensus +consent +console +consoling +consonant +constable +constant +constrain +constrict +construct +consult +consumer +consuming +contact +container +contempt +contend +contented +contently +contents +contest +context +contort +contour +contrite +control +contusion +convene +convent +copartner +cope +copied +copier +copilot +coping +copious +copper +copy +coral +cork +cornball +cornbread +corncob +cornea +corned +corner +cornfield +cornflake +cornhusk +cornmeal +cornstalk +corny +coronary +coroner +corporal +corporate +corral +correct +corridor +corrode +corroding +corrosive +corsage +corset +cortex +cosigner +cosmetics +cosmic +cosmos +cosponsor +cost +cottage +cotton +couch +cough +could +countable +countdown +counting +countless +country +county +courier +covenant +cover +coveted +coveting +coyness +cozily +coziness +cozy +crabbing +crabgrass +crablike +crabmeat +cradle +cradling +crafter +craftily +craftsman +craftwork +crafty +cramp +cranberry +crane +cranial +cranium +crank +crate +crave +craving +crawfish +crawlers +crawling +crayfish +crayon +crazed +crazily +craziness +crazy +creamed +creamer +creamlike +crease +creasing +creatable +create +creation +creative +creature +credible +credibly +credit +creed +creme +creole +crepe +crept +crescent +crested +cresting +crestless +crevice +crewless +crewman +crewmate +crib +cricket +cried +crier +crimp +crimson +cringe +cringing +crinkle +crinkly +crisped +crisping +crisply +crispness +crispy +criteria +critter +croak +crock +crook +croon +crop +cross +crouch +crouton +crowbar +crowd +crown +crucial +crudely +crudeness +cruelly +cruelness +cruelty +crumb +crummiest +crummy +crumpet +crumpled +cruncher +crunching +crunchy +crusader +crushable +crushed +crusher +crushing +crust +crux +crying +cryptic +crystal +cubbyhole +cube +cubical +cubicle +cucumber +cuddle +cuddly +cufflink +culinary +culminate +culpable +culprit +cultivate +cultural +culture +cupbearer +cupcake +cupid +cupped +cupping +curable +curator +curdle +cure +curfew +curing +curled +curler +curliness +curling +curly +curry +curse +cursive +cursor +curtain +curtly +curtsy +curvature +curve +curvy +cushy +cusp +cussed +custard +custodian +custody +customary +customer +customize +customs +cut +cycle +cyclic +cycling +cyclist +cylinder +cymbal +cytoplasm +cytoplast +dab +dad +daffodil +dagger +daily +daintily +dainty +dairy +daisy +dallying +dance +dancing +dandelion +dander +dandruff +dandy +danger +dangle +dangling +daredevil +dares +daringly +darkened +darkening +darkish +darkness +darkroom +darling +darn +dart +darwinism +dash +dastardly +data +datebook +dating +daughter +daunting +dawdler +dawn +daybed +daybreak +daycare +daydream +daylight +daylong +dayroom +daytime +dazzler +dazzling +deacon +deafening +deafness +dealer +dealing +dealmaker +dealt +dean +debatable +debate +debating +debit +debrief +debtless +debtor +debug +debunk +decade +decaf +decal +decathlon +decay +deceased +deceit +deceiver +deceiving +december +decency +decent +deception +deceptive +decibel +decidable +decimal +decimeter +decipher +deck +declared +decline +decode +decompose +decorated +decorator +decoy +decrease +decree +dedicate +dedicator +deduce +deduct +deed +deem +deepen +deeply +deepness +deface +defacing +defame +default +defeat +defection +defective +defendant +defender +defense +defensive +deferral +deferred +defiance +defiant +defile +defiling +define +definite +deflate +deflation +deflator +deflected +deflector +defog +deforest +defraud +defrost +deftly +defuse +defy +degraded +degrading +degrease +degree +dehydrate +deity +dejected +delay +delegate +delegator +delete +deletion +delicacy +delicate +delicious +delighted +delirious +delirium +deliverer +delivery +delouse +delta +deluge +delusion +deluxe +demanding +demeaning +demeanor +demise +democracy +democrat +demote +demotion +demystify +denatured +deniable +denial +denim +denote +dense +density +dental +dentist +denture +deny +deodorant +deodorize +departed +departure +depict +deplete +depletion +deplored +deploy +deport +depose +depraved +depravity +deprecate +depress +deprive +depth +deputize +deputy +derail +deranged +derby +derived +desecrate +deserve +deserving +designate +designed +designer +designing +deskbound +desktop +deskwork +desolate +despair +despise +despite +destiny +destitute +destruct +detached +detail +detection +detective +detector +detention +detergent +detest +detonate +detonator +detoxify +detract +deuce +devalue +deviancy +deviant +deviate +deviation +deviator +device +devious +devotedly +devotee +devotion +devourer +devouring +devoutly +dexterity +dexterous +diabetes +diabetic +diabolic +diagnoses +diagnosis +diagram +dial +diameter +diaper +diaphragm +diary +dice +dicing +dictate +dictation +dictator +difficult +diffused +diffuser +diffusion +diffusive +dig +dilation +diligence +diligent +dill +dilute +dime +diminish +dimly +dimmed +dimmer +dimness +dimple +diner +dingbat +dinghy +dinginess +dingo +dingy +dining +dinner +diocese +dioxide +diploma +dipped +dipper +dipping +directed +direction +directive +directly +directory +direness +dirtiness +disabled +disagree +disallow +disarm +disarray +disaster +disband +disbelief +disburse +discard +discern +discharge +disclose +discolor +discount +discourse +discover +discuss +disdain +disengage +disfigure +disgrace +dish +disinfect +disjoin +disk +dislike +disliking +dislocate +dislodge +disloyal +dismantle +dismay +dismiss +dismount +disobey +disorder +disown +disparate +disparity +dispatch +dispense +dispersal +dispersed +disperser +displace +display +displease +disposal +dispose +disprove +dispute +disregard +disrupt +dissuade +distance +distant +distaste +distill +distinct +distort +distract +distress +district +distrust +ditch +ditto +ditzy +dividable +divided +dividend +dividers +dividing +divinely +diving +divinity +divisible +divisibly +division +divisive +divorcee +dizziness +dizzy +doable +docile +dock +doctrine +document +dodge +dodgy +doily +doing +dole +dollar +dollhouse +dollop +dolly +dolphin +domain +domelike +domestic +dominion +dominoes +donated +donation +donator +donor +donut +doodle +doorbell +doorframe +doorknob +doorman +doormat +doornail +doorpost +doorstep +doorstop +doorway +doozy +dork +dormitory +dorsal +dosage +dose +dotted +doubling +douche +dove +down +dowry +doze +drab +dragging +dragonfly +dragonish +dragster +drainable +drainage +drained +drainer +drainpipe +dramatic +dramatize +drank +drapery +drastic +draw +dreaded +dreadful +dreadlock +dreamboat +dreamily +dreamland +dreamless +dreamlike +dreamt +dreamy +drearily +dreary +drench +dress +drew +dribble +dried +drier +drift +driller +drilling +drinkable +drinking +dripping +drippy +drivable +driven +driver +driveway +driving +drizzle +drizzly +drone +drool +droop +drop-down +dropbox +dropkick +droplet +dropout +dropper +drove +drown +drowsily +drudge +drum +dry +dubbed +dubiously +duchess +duckbill +ducking +duckling +ducktail +ducky +duct +dude +duffel +dugout +duh +duke +duller +dullness +duly +dumping +dumpling +dumpster +duo +dupe +duplex +duplicate +duplicity +durable +durably +duration +duress +during +dusk +dust +dutiful +duty +duvet +dwarf +dweeb +dwelled +dweller +dwelling +dwindle +dwindling +dynamic +dynamite +dynasty +dyslexia +dyslexic +each +eagle +earache +eardrum +earflap +earful +earlobe +early +earmark +earmuff +earphone +earpiece +earplugs +earring +earshot +earthen +earthlike +earthling +earthly +earthworm +earthy +earwig +easeful +easel +easiest +easily +easiness +easing +eastbound +eastcoast +easter +eastward +eatable +eaten +eatery +eating +eats +ebay +ebony +ebook +ecard +eccentric +echo +eclair +eclipse +ecologist +ecology +economic +economist +economy +ecosphere +ecosystem +edge +edginess +edging +edgy +edition +editor +educated +education +educator +eel +effective +effects +efficient +effort +eggbeater +egging +eggnog +eggplant +eggshell +egomaniac +egotism +egotistic +either +eject +elaborate +elastic +elated +elbow +eldercare +elderly +eldest +electable +election +elective +elephant +elevate +elevating +elevation +elevator +eleven +elf +eligible +eligibly +eliminate +elite +elitism +elixir +elk +ellipse +elliptic +elm +elongated +elope +eloquence +eloquent +elsewhere +elude +elusive +elves +email +embargo +embark +embassy +embattled +embellish +ember +embezzle +emblaze +emblem +embody +embolism +emboss +embroider +emcee +emerald +emergency +emission +emit +emote +emoticon +emotion +empathic +empathy +emperor +emphases +emphasis +emphasize +emphatic +empirical +employed +employee +employer +emporium +empower +emptier +emptiness +empty +emu +enable +enactment +enamel +enchanted +enchilada +encircle +enclose +enclosure +encode +encore +encounter +encourage +encroach +encrust +encrypt +endanger +endeared +endearing +ended +ending +endless +endnote +endocrine +endorphin +endorse +endowment +endpoint +endurable +endurance +enduring +energetic +energize +energy +enforced +enforcer +engaged +engaging +engine +engorge +engraved +engraver +engraving +engross +engulf +enhance +enigmatic +enjoyable +enjoyably +enjoyer +enjoying +enjoyment +enlarged +enlarging +enlighten +enlisted +enquirer +enrage +enrich +enroll +enslave +ensnare +ensure +entail +entangled +entering +entertain +enticing +entire +entitle +entity +entomb +entourage +entrap +entree +entrench +entrust +entryway +entwine +enunciate +envelope +enviable +enviably +envious +envision +envoy +envy +enzyme +epic +epidemic +epidermal +epidermis +epidural +epilepsy +epileptic +epilogue +epiphany +episode +equal +equate +equation +equator +equinox +equipment +equity +equivocal +eradicate +erasable +erased +eraser +erasure +ergonomic +errand +errant +erratic +error +erupt +escalate +escalator +escapable +escapade +escapist +escargot +eskimo +esophagus +espionage +espresso +esquire +essay +essence +essential +establish +estate +esteemed +estimate +estimator +estranged +estrogen +etching +eternal +eternity +ethanol +ether +ethically +ethics +euphemism +evacuate +evacuee +evade +evaluate +evaluator +evaporate +evasion +evasive +even +everglade +evergreen +everybody +everyday +everyone +evict +evidence +evident +evil +evoke +evolution +evolve +exact +exalted +example +excavate +excavator +exceeding +exception +excess +exchange +excitable +exciting +exclaim +exclude +excluding +exclusion +exclusive +excretion +excretory +excursion +excusable +excusably +excuse +exemplary +exemplify +exemption +exerciser +exert +exes +exfoliate +exhale +exhaust +exhume +exile +existing +exit +exodus +exonerate +exorcism +exorcist +expand +expanse +expansion +expansive +expectant +expedited +expediter +expel +expend +expenses +expensive +expert +expire +expiring +explain +expletive +explicit +explode +exploit +explore +exploring +exponent +exporter +exposable +expose +exposure +express +expulsion +exquisite +extended +extending +extent +extenuate +exterior +external +extinct +extortion +extradite +extras +extrovert +extrude +extruding +exuberant +fable +fabric +fabulous +facebook +facecloth +facedown +faceless +facelift +faceplate +faceted +facial +facility +facing +facsimile +faction +factoid +factor +factsheet +factual +faculty +fade +fading +failing +falcon +fall +false +falsify +fame +familiar +family +famine +famished +fanatic +fancied +fanciness +fancy +fanfare +fang +fanning +fantasize +fantastic +fantasy +fascism +fastball +faster +fasting +fastness +faucet +favorable +favorably +favored +favoring +favorite +fax +feast +federal +fedora +feeble +feed +feel +feisty +feline +felt-tip +feminine +feminism +feminist +feminize +femur +fence +fencing +fender +ferment +fernlike +ferocious +ferocity +ferret +ferris +ferry +fervor +fester +festival +festive +festivity +fetal +fetch +fever +fiber +fiction +fiddle +fiddling +fidelity +fidgeting +fidgety +fifteen +fifth +fiftieth +fifty +figment +figure +figurine +filing +filled +filler +filling +film +filter +filth +filtrate +finale +finalist +finalize +finally +finance +financial +finch +fineness +finer +finicky +finished +finisher +finishing +finite +finless +finlike +fiscally +fit +five +flaccid +flagman +flagpole +flagship +flagstick +flagstone +flail +flakily +flaky +flame +flammable +flanked +flanking +flannels +flap +flaring +flashback +flashbulb +flashcard +flashily +flashing +flashy +flask +flatbed +flatfoot +flatly +flatness +flatten +flattered +flatterer +flattery +flattop +flatware +flatworm +flavored +flavorful +flavoring +flaxseed +fled +fleshed +fleshy +flick +flier +flight +flinch +fling +flint +flip +flirt +float +flock +flogging +flop +floral +florist +floss +flounder +flyable +flyaway +flyer +flying +flyover +flypaper +foam +foe +fog +foil +folic +folk +follicle +follow +fondling +fondly +fondness +fondue +font +food +fool +footage +football +footbath +footboard +footer +footgear +foothill +foothold +footing +footless +footman +footnote +footpad +footpath +footprint +footrest +footsie +footsore +footwear +footwork +fossil +foster +founder +founding +fountain +fox +foyer +fraction +fracture +fragile +fragility +fragment +fragrance +fragrant +frail +frame +framing +frantic +fraternal +frayed +fraying +frays +freckled +freckles +freebase +freebee +freebie +freedom +freefall +freehand +freeing +freeload +freely +freemason +freeness +freestyle +freeware +freeway +freewill +freezable +freezing +freight +french +frenzied +frenzy +frequency +frequent +fresh +fretful +fretted +friction +friday +fridge +fried +friend +frighten +frightful +frigidity +frigidly +frill +fringe +frisbee +frisk +fritter +frivolous +frolic +from +front +frostbite +frosted +frostily +frosting +frostlike +frosty +froth +frown +frozen +fructose +frugality +frugally +fruit +frustrate +frying +gab +gaffe +gag +gainfully +gaining +gains +gala +gallantly +galleria +gallery +galley +gallon +gallows +gallstone +galore +galvanize +gambling +game +gaming +gamma +gander +gangly +gangrene +gangway +gap +garage +garbage +garden +gargle +garland +garlic +garment +garnet +garnish +garter +gas +gatherer +gathering +gating +gauging +gauntlet +gauze +gave +gawk +gazing +gear +gecko +geek +geiger +gem +gender +generic +generous +genetics +genre +gentile +gentleman +gently +gents +geography +geologic +geologist +geology +geometric +geometry +geranium +gerbil +geriatric +germicide +germinate +germless +germproof +gestate +gestation +gesture +getaway +getting +getup +giant +gibberish +giblet +giddily +giddiness +giddy +gift +gigabyte +gigahertz +gigantic +giggle +giggling +giggly +gigolo +gilled +gills +gimmick +girdle +giveaway +given +giver +giving +gizmo +gizzard +glacial +glacier +glade +gladiator +gladly +glamorous +glamour +glance +glancing +glandular +glare +glaring +glass +glaucoma +glazing +gleaming +gleeful +glider +gliding +glimmer +glimpse +glisten +glitch +glitter +glitzy +gloater +gloating +gloomily +gloomy +glorified +glorifier +glorify +glorious +glory +gloss +glove +glowing +glowworm +glucose +glue +gluten +glutinous +glutton +gnarly +gnat +goal +goatskin +goes +goggles +going +goldfish +goldmine +goldsmith +golf +goliath +gonad +gondola +gone +gong +good +gooey +goofball +goofiness +goofy +google +goon +gopher +gore +gorged +gorgeous +gory +gosling +gossip +gothic +gotten +gout +gown +grab +graceful +graceless +gracious +gradation +graded +grader +gradient +grading +gradually +graduate +graffiti +grafted +grafting +grain +granddad +grandkid +grandly +grandma +grandpa +grandson +granite +granny +granola +grant +granular +grape +graph +grapple +grappling +grasp +grass +gratified +gratify +grating +gratitude +gratuity +gravel +graveness +graves +graveyard +gravitate +gravity +gravy +gray +grazing +greasily +greedily +greedless +greedy +green +greeter +greeting +grew +greyhound +grid +grief +grievance +grieving +grievous +grill +grimace +grimacing +grime +griminess +grimy +grinch +grinning +grip +gristle +grit +groggily +groggy +groin +groom +groove +grooving +groovy +grope +ground +grouped +grout +grove +grower +growing +growl +grub +grudge +grudging +grueling +gruffly +grumble +grumbling +grumbly +grumpily +grunge +grunt +guacamole +guidable +guidance +guide +guiding +guileless +guise +gulf +gullible +gully +gulp +gumball +gumdrop +gumminess +gumming +gummy +gurgle +gurgling +guru +gush +gusto +gusty +gutless +guts +gutter +guy +guzzler +gyration +habitable +habitant +habitat +habitual +hacked +hacker +hacking +hacksaw +had +haggler +haiku +half +halogen +halt +halved +halves +hamburger +hamlet +hammock +hamper +hamster +hamstring +handbag +handball +handbook +handbrake +handcart +handclap +handclasp +handcraft +handcuff +handed +handful +handgrip +handgun +handheld +handiness +handiwork +handlebar +handled +handler +handling +handmade +handoff +handpick +handprint +handrail +handsaw +handset +handsfree +handshake +handstand +handwash +handwork +handwoven +handwrite +handyman +hangnail +hangout +hangover +hangup +hankering +hankie +hanky +haphazard +happening +happier +happiest +happily +happiness +happy +harbor +hardcopy +hardcore +hardcover +harddisk +hardened +hardener +hardening +hardhat +hardhead +hardiness +hardly +hardness +hardship +hardware +hardwired +hardwood +hardy +harmful +harmless +harmonica +harmonics +harmonize +harmony +harness +harpist +harsh +harvest +hash +hassle +haste +hastily +hastiness +hasty +hatbox +hatchback +hatchery +hatchet +hatching +hatchling +hate +hatless +hatred +haunt +haven +hazard +hazelnut +hazily +haziness +hazing +hazy +headache +headband +headboard +headcount +headdress +headed +header +headfirst +headgear +heading +headlamp +headless +headlock +headphone +headpiece +headrest +headroom +headscarf +headset +headsman +headstand +headstone +headway +headwear +heap +heat +heave +heavily +heaviness +heaving +hedge +hedging +heftiness +hefty +helium +helmet +helper +helpful +helping +helpless +helpline +hemlock +hemstitch +hence +henchman +henna +herald +herbal +herbicide +herbs +heritage +hermit +heroics +heroism +herring +herself +hertz +hesitancy +hesitant +hesitate +hexagon +hexagram +hubcap +huddle +huddling +huff +hug +hula +hulk +hull +human +humble +humbling +humbly +humid +humiliate +humility +humming +hummus +humongous +humorist +humorless +humorous +humpback +humped +humvee +hunchback +hundredth +hunger +hungrily +hungry +hunk +hunter +hunting +huntress +huntsman +hurdle +hurled +hurler +hurling +hurray +hurricane +hurried +hurry +hurt +husband +hush +husked +huskiness +hut +hybrid +hydrant +hydrated +hydration +hydrogen +hydroxide +hyperlink +hypertext +hyphen +hypnoses +hypnosis +hypnotic +hypnotism +hypnotist +hypnotize +hypocrisy +hypocrite +ibuprofen +ice +iciness +icing +icky +icon +icy +idealism +idealist +idealize +ideally +idealness +identical +identify +identity +ideology +idiocy +idiom +idly +igloo +ignition +ignore +iguana +illicitly +illusion +illusive +image +imaginary +imagines +imaging +imbecile +imitate +imitation +immature +immerse +immersion +imminent +immobile +immodest +immorally +immortal +immovable +immovably +immunity +immunize +impaired +impale +impart +impatient +impeach +impeding +impending +imperfect +imperial +impish +implant +implement +implicate +implicit +implode +implosion +implosive +imply +impolite +important +importer +impose +imposing +impotence +impotency +impotent +impound +imprecise +imprint +imprison +impromptu +improper +improve +improving +improvise +imprudent +impulse +impulsive +impure +impurity +iodine +iodize +ion +ipad +iphone +ipod +irate +irk +iron +irregular +irrigate +irritable +irritably +irritant +irritate +islamic +islamist +isolated +isolating +isolation +isotope +issue +issuing +italicize +italics +item +itinerary +itunes +ivory +ivy +jab +jackal +jacket +jackknife +jackpot +jailbird +jailbreak +jailer +jailhouse +jalapeno +jam +janitor +january +jargon +jarring +jasmine +jaundice +jaunt +java +jawed +jawless +jawline +jaws +jaybird +jaywalker +jazz +jeep +jeeringly +jellied +jelly +jersey +jester +jet +jiffy +jigsaw +jimmy +jingle +jingling +jinx +jitters +jittery +job +jockey +jockstrap +jogger +jogging +john +joining +jokester +jokingly +jolliness +jolly +jolt +jot +jovial +joyfully +joylessly +joyous +joyride +joystick +jubilance +jubilant +judge +judgingly +judicial +judiciary +judo +juggle +juggling +jugular +juice +juiciness +juicy +jujitsu +jukebox +july +jumble +jumbo +jump +junction +juncture +june +junior +juniper +junkie +junkman +junkyard +jurist +juror +jury +justice +justifier +justify +justly +justness +juvenile +kabob +kangaroo +karaoke +karate +karma +kebab +keenly +keenness +keep +keg +kelp +kennel +kept +kerchief +kerosene +kettle +kick +kiln +kilobyte +kilogram +kilometer +kilowatt +kilt +kimono +kindle +kindling +kindly +kindness +kindred +kinetic +kinfolk +king +kinship +kinsman +kinswoman +kissable +kisser +kissing +kitchen +kite +kitten +kitty +kiwi +kleenex +knapsack +knee +knelt +knickers +knoll +koala +kooky +kosher +krypton +kudos +kung +labored +laborer +laboring +laborious +labrador +ladder +ladies +ladle +ladybug +ladylike +lagged +lagging +lagoon +lair +lake +lance +landed +landfall +landfill +landing +landlady +landless +landline +landlord +landmark +landmass +landmine +landowner +landscape +landside +landslide +language +lankiness +lanky +lantern +lapdog +lapel +lapped +lapping +laptop +lard +large +lark +lash +lasso +last +latch +late +lather +latitude +latrine +latter +latticed +launch +launder +laundry +laurel +lavender +lavish +laxative +lazily +laziness +lazy +lecturer +left +legacy +legal +legend +legged +leggings +legible +legibly +legislate +lego +legroom +legume +legwarmer +legwork +lemon +lend +length +lens +lent +leotard +lesser +letdown +lethargic +lethargy +letter +lettuce +level +leverage +levers +levitate +levitator +liability +liable +liberty +librarian +library +licking +licorice +lid +life +lifter +lifting +liftoff +ligament +likely +likeness +likewise +liking +lilac +lilly +lily +limb +limeade +limelight +limes +limit +limping +limpness +line +lingo +linguini +linguist +lining +linked +linoleum +linseed +lint +lion +lip +liquefy +liqueur +liquid +lisp +list +litigate +litigator +litmus +litter +little +livable +lived +lively +liver +livestock +lividly +living +lizard +lubricant +lubricate +lucid +luckily +luckiness +luckless +lucrative +ludicrous +lugged +lukewarm +lullaby +lumber +luminance +luminous +lumpiness +lumping +lumpish +lunacy +lunar +lunchbox +luncheon +lunchroom +lunchtime +lung +lurch +lure +luridness +lurk +lushly +lushness +luster +lustfully +lustily +lustiness +lustrous +lusty +luxurious +luxury +lying +lyrically +lyricism +lyricist +lyrics +macarena +macaroni +macaw +mace +machine +machinist +magazine +magenta +maggot +magical +magician +magma +magnesium +magnetic +magnetism +magnetize +magnifier +magnify +magnitude +magnolia +mahogany +maimed +majestic +majesty +majorette +majority +makeover +maker +makeshift +making +malformed +malt +mama +mammal +mammary +mammogram +manager +managing +manatee +mandarin +mandate +mandatory +mandolin +manger +mangle +mango +mangy +manhandle +manhole +manhood +manhunt +manicotti +manicure +manifesto +manila +mankind +manlike +manliness +manly +manmade +manned +mannish +manor +manpower +mantis +mantra +manual +many +map +marathon +marauding +marbled +marbles +marbling +march +mardi +margarine +margarita +margin +marigold +marina +marine +marital +maritime +marlin +marmalade +maroon +married +marrow +marry +marshland +marshy +marsupial +marvelous +marxism +mascot +masculine +mashed +mashing +massager +masses +massive +mastiff +matador +matchbook +matchbox +matcher +matching +matchless +material +maternal +maternity +math +mating +matriarch +matrimony +matrix +matron +matted +matter +maturely +maturing +maturity +mauve +maverick +maximize +maximum +maybe +mayday +mayflower +moaner +moaning +mobile +mobility +mobilize +mobster +mocha +mocker +mockup +modified +modify +modular +modulator +module +moisten +moistness +moisture +molar +molasses +mold +molecular +molecule +molehill +mollusk +mom +monastery +monday +monetary +monetize +moneybags +moneyless +moneywise +mongoose +mongrel +monitor +monkhood +monogamy +monogram +monologue +monopoly +monorail +monotone +monotype +monoxide +monsieur +monsoon +monstrous +monthly +monument +moocher +moodiness +moody +mooing +moonbeam +mooned +moonlight +moonlike +moonlit +moonrise +moonscape +moonshine +moonstone +moonwalk +mop +morale +morality +morally +morbidity +morbidly +morphine +morphing +morse +mortality +mortally +mortician +mortified +mortify +mortuary +mosaic +mossy +most +mothball +mothproof +motion +motivate +motivator +motive +motocross +motor +motto +mountable +mountain +mounted +mounting +mourner +mournful +mouse +mousiness +moustache +mousy +mouth +movable +move +movie +moving +mower +mowing +much +muck +mud +mug +mulberry +mulch +mule +mulled +mullets +multiple +multiply +multitask +multitude +mumble +mumbling +mumbo +mummified +mummify +mummy +mumps +munchkin +mundane +municipal +muppet +mural +murkiness +murky +murmuring +muscular +museum +mushily +mushiness +mushroom +mushy +music +musket +muskiness +musky +mustang +mustard +muster +mustiness +musty +mutable +mutate +mutation +mute +mutilated +mutilator +mutiny +mutt +mutual +muzzle +myself +myspace +mystified +mystify +myth +nacho +nag +nail +name +naming +nanny +nanometer +nape +napkin +napped +napping +nappy +narrow +nastily +nastiness +national +native +nativity +natural +nature +naturist +nautical +navigate +navigator +navy +nearby +nearest +nearly +nearness +neatly +neatness +nebula +nebulizer +nectar +negate +negation +negative +neglector +negligee +negligent +negotiate +nemeses +nemesis +neon +nephew +nerd +nervous +nervy +nest +net +neurology +neuron +neurosis +neurotic +neuter +neutron +never +next +nibble +nickname +nicotine +niece +nifty +nimble +nimbly +nineteen +ninetieth +ninja +nintendo +ninth +nuclear +nuclei +nucleus +nugget +nullify +number +numbing +numbly +numbness +numeral +numerate +numerator +numeric +numerous +nuptials +nursery +nursing +nurture +nutcase +nutlike +nutmeg +nutrient +nutshell +nuttiness +nutty +nuzzle +nylon +oaf +oak +oasis +oat +obedience +obedient +obituary +object +obligate +obliged +oblivion +oblivious +oblong +obnoxious +oboe +obscure +obscurity +observant +observer +observing +obsessed +obsession +obsessive +obsolete +obstacle +obstinate +obstruct +obtain +obtrusive +obtuse +obvious +occultist +occupancy +occupant +occupier +occupy +ocean +ocelot +octagon +octane +october +octopus +ogle +oil +oink +ointment +okay +old +olive +olympics +omega +omen +ominous +omission +omit +omnivore +onboard +oncoming +ongoing +onion +online +onlooker +only +onscreen +onset +onshore +onslaught +onstage +onto +onward +onyx +oops +ooze +oozy +opacity +opal +open +operable +operate +operating +operation +operative +operator +opium +opossum +opponent +oppose +opposing +opposite +oppressed +oppressor +opt +opulently +osmosis +other +otter +ouch +ought +ounce +outage +outback +outbid +outboard +outbound +outbreak +outburst +outcast +outclass +outcome +outdated +outdoors +outer +outfield +outfit +outflank +outgoing +outgrow +outhouse +outing +outlast +outlet +outline +outlook +outlying +outmatch +outmost +outnumber +outplayed +outpost +outpour +output +outrage +outrank +outreach +outright +outscore +outsell +outshine +outshoot +outsider +outskirts +outsmart +outsource +outspoken +outtakes +outthink +outward +outweigh +outwit +oval +ovary +oven +overact +overall +overarch +overbid +overbill +overbite +overblown +overboard +overbook +overbuilt +overcast +overcoat +overcome +overcook +overcrowd +overdraft +overdrawn +overdress +overdrive +overdue +overeager +overeater +overexert +overfed +overfeed +overfill +overflow +overfull +overgrown +overhand +overhang +overhaul +overhead +overhear +overheat +overhung +overjoyed +overkill +overlabor +overlaid +overlap +overlay +overload +overlook +overlord +overlying +overnight +overpass +overpay +overplant +overplay +overpower +overprice +overrate +overreach +overreact +override +overripe +overrule +overrun +overshoot +overshot +oversight +oversized +oversleep +oversold +overspend +overstate +overstay +overstep +overstock +overstuff +oversweet +overtake +overthrow +overtime +overtly +overtone +overture +overturn +overuse +overvalue +overview +overwrite +owl +oxford +oxidant +oxidation +oxidize +oxidizing +oxygen +oxymoron +oyster +ozone +paced +pacemaker +pacific +pacifier +pacifism +pacifist +pacify +padded +padding +paddle +paddling +padlock +pagan +pager +paging +pajamas +palace +palatable +palm +palpable +palpitate +paltry +pampered +pamperer +pampers +pamphlet +panama +pancake +pancreas +panda +pandemic +pang +panhandle +panic +panning +panorama +panoramic +panther +pantomime +pantry +pants +pantyhose +paparazzi +papaya +paper +paprika +papyrus +parabola +parachute +parade +paradox +paragraph +parakeet +paralegal +paralyses +paralysis +paralyze +paramedic +parameter +paramount +parasail +parasite +parasitic +parcel +parched +parchment +pardon +parish +parka +parking +parkway +parlor +parmesan +parole +parrot +parsley +parsnip +partake +parted +parting +partition +partly +partner +partridge +party +passable +passably +passage +passcode +passenger +passerby +passing +passion +passive +passivism +passover +passport +password +pasta +pasted +pastel +pastime +pastor +pastrami +pasture +pasty +patchwork +patchy +paternal +paternity +path +patience +patient +patio +patriarch +patriot +patrol +patronage +patronize +pauper +pavement +paver +pavestone +pavilion +paving +pawing +payable +payback +paycheck +payday +payee +payer +paying +payment +payphone +payroll +pebble +pebbly +pecan +pectin +peculiar +peddling +pediatric +pedicure +pedigree +pedometer +pegboard +pelican +pellet +pelt +pelvis +penalize +penalty +pencil +pendant +pending +penholder +penknife +pennant +penniless +penny +penpal +pension +pentagon +pentagram +pep +perceive +percent +perch +percolate +perennial +perfected +perfectly +perfume +periscope +perish +perjurer +perjury +perkiness +perky +perm +peroxide +perpetual +perplexed +persecute +persevere +persuaded +persuader +pesky +peso +pessimism +pessimist +pester +pesticide +petal +petite +petition +petri +petroleum +petted +petticoat +pettiness +petty +petunia +phantom +phobia +phoenix +phonebook +phoney +phonics +phoniness +phony +phosphate +photo +phrase +phrasing +placard +placate +placidly +plank +planner +plant +plasma +plaster +plastic +plated +platform +plating +platinum +platonic +platter +platypus +plausible +plausibly +playable +playback +player +playful +playgroup +playhouse +playing +playlist +playmaker +playmate +playoff +playpen +playroom +playset +plaything +playtime +plaza +pleading +pleat +pledge +plentiful +plenty +plethora +plexiglas +pliable +plod +plop +plot +plow +ploy +pluck +plug +plunder +plunging +plural +plus +plutonium +plywood +poach +pod +poem +poet +pogo +pointed +pointer +pointing +pointless +pointy +poise +poison +poker +poking +polar +police +policy +polio +polish +politely +polka +polo +polyester +polygon +polygraph +polymer +poncho +pond +pony +popcorn +pope +poplar +popper +poppy +popsicle +populace +popular +populate +porcupine +pork +porous +porridge +portable +portal +portfolio +porthole +portion +portly +portside +poser +posh +posing +possible +possibly +possum +postage +postal +postbox +postcard +posted +poster +posting +postnasal +posture +postwar +pouch +pounce +pouncing +pound +pouring +pout +powdered +powdering +powdery +power +powwow +pox +praising +prance +prancing +pranker +prankish +prankster +prayer +praying +preacher +preaching +preachy +preamble +precinct +precise +precision +precook +precut +predator +predefine +predict +preface +prefix +preflight +preformed +pregame +pregnancy +pregnant +preheated +prelaunch +prelaw +prelude +premiere +premises +premium +prenatal +preoccupy +preorder +prepaid +prepay +preplan +preppy +preschool +prescribe +preseason +preset +preshow +president +presoak +press +presume +presuming +preteen +pretended +pretender +pretense +pretext +pretty +pretzel +prevail +prevalent +prevent +preview +previous +prewar +prewashed +prideful +pried +primal +primarily +primary +primate +primer +primp +princess +print +prior +prism +prison +prissy +pristine +privacy +private +privatize +prize +proactive +probable +probably +probation +probe +probing +probiotic +problem +procedure +process +proclaim +procreate +procurer +prodigal +prodigy +produce +product +profane +profanity +professed +professor +profile +profound +profusely +progeny +prognosis +program +progress +projector +prologue +prolonged +promenade +prominent +promoter +promotion +prompter +promptly +prone +prong +pronounce +pronto +proofing +proofread +proofs +propeller +properly +property +proponent +proposal +propose +props +prorate +protector +protegee +proton +prototype +protozoan +protract +protrude +proud +provable +proved +proven +provided +provider +providing +province +proving +provoke +provoking +provolone +prowess +prowler +prowling +proximity +proxy +prozac +prude +prudishly +prune +pruning +pry +psychic +public +publisher +pucker +pueblo +pug +pull +pulmonary +pulp +pulsate +pulse +pulverize +puma +pumice +pummel +punch +punctual +punctuate +punctured +pungent +punisher +punk +pupil +puppet +puppy +purchase +pureblood +purebred +purely +pureness +purgatory +purge +purging +purifier +purify +purist +puritan +purity +purple +purplish +purposely +purr +purse +pursuable +pursuant +pursuit +purveyor +pushcart +pushchair +pusher +pushiness +pushing +pushover +pushpin +pushup +pushy +putdown +putt +puzzle +puzzling +pyramid +pyromania +python +quack +quadrant +quail +quaintly +quake +quaking +qualified +qualifier +qualify +quality +qualm +quantum +quarrel +quarry +quartered +quarterly +quarters +quartet +quench +query +quicken +quickly +quickness +quicksand +quickstep +quiet +quill +quilt +quintet +quintuple +quirk +quit +quiver +quizzical +quotable +quotation +quote +rabid +race +racing +racism +rack +racoon +radar +radial +radiance +radiantly +radiated +radiation +radiator +radio +radish +raffle +raft +rage +ragged +raging +ragweed +raider +railcar +railing +railroad +railway +raisin +rake +raking +rally +ramble +rambling +ramp +ramrod +ranch +rancidity +random +ranged +ranger +ranging +ranked +ranking +ransack +ranting +rants +rare +rarity +rascal +rash +rasping +ravage +raven +ravine +raving +ravioli +ravishing +reabsorb +reach +reacquire +reaction +reactive +reactor +reaffirm +ream +reanalyze +reappear +reapply +reappoint +reapprove +rearrange +rearview +reason +reassign +reassure +reattach +reawake +rebalance +rebate +rebel +rebirth +reboot +reborn +rebound +rebuff +rebuild +rebuilt +reburial +rebuttal +recall +recant +recapture +recast +recede +recent +recess +recharger +recipient +recital +recite +reckless +reclaim +recliner +reclining +recluse +reclusive +recognize +recoil +recollect +recolor +reconcile +reconfirm +reconvene +recopy +record +recount +recoup +recovery +recreate +rectal +rectangle +rectified +rectify +recycled +recycler +recycling +reemerge +reenact +reenter +reentry +reexamine +referable +referee +reference +refill +refinance +refined +refinery +refining +refinish +reflected +reflector +reflex +reflux +refocus +refold +reforest +reformat +reformed +reformer +reformist +refract +refrain +refreeze +refresh +refried +refueling +refund +refurbish +refurnish +refusal +refuse +refusing +refutable +refute +regain +regalia +regally +reggae +regime +region +register +registrar +registry +regress +regretful +regroup +regular +regulate +regulator +rehab +reheat +rehire +rehydrate +reimburse +reissue +reiterate +rejoice +rejoicing +rejoin +rekindle +relapse +relapsing +relatable +related +relation +relative +relax +relay +relearn +release +relenting +reliable +reliably +reliance +reliant +relic +relieve +relieving +relight +relish +relive +reload +relocate +relock +reluctant +rely +remake +remark +remarry +rematch +remedial +remedy +remember +reminder +remindful +remission +remix +remnant +remodeler +remold +remorse +remote +removable +removal +removed +remover +removing +rename +renderer +rendering +rendition +renegade +renewable +renewably +renewal +renewed +renounce +renovate +renovator +rentable +rental +rented +renter +reoccupy +reoccur +reopen +reorder +repackage +repacking +repaint +repair +repave +repaying +repayment +repeal +repeated +repeater +repent +rephrase +replace +replay +replica +reply +reporter +repose +repossess +repost +repressed +reprimand +reprint +reprise +reproach +reprocess +reproduce +reprogram +reps +reptile +reptilian +repugnant +repulsion +repulsive +repurpose +reputable +reputably +request +require +requisite +reroute +rerun +resale +resample +rescuer +reseal +research +reselect +reseller +resemble +resend +resent +reset +reshape +reshoot +reshuffle +residence +residency +resident +residual +residue +resigned +resilient +resistant +resisting +resize +resolute +resolved +resonant +resonate +resort +resource +respect +resubmit +result +resume +resupply +resurface +resurrect +retail +retainer +retaining +retake +retaliate +retention +rethink +retinal +retired +retiree +retiring +retold +retool +retorted +retouch +retrace +retract +retrain +retread +retreat +retrial +retrieval +retriever +retry +return +retying +retype +reunion +reunite +reusable +reuse +reveal +reveler +revenge +revenue +reverb +revered +reverence +reverend +reversal +reverse +reversing +reversion +revert +revisable +revise +revision +revisit +revivable +revival +reviver +reviving +revocable +revoke +revolt +revolver +revolving +reward +rewash +rewind +rewire +reword +rework +rewrap +rewrite +rhyme +ribbon +ribcage +rice +riches +richly +richness +rickety +ricotta +riddance +ridden +ride +riding +rifling +rift +rigging +rigid +rigor +rimless +rimmed +rind +rink +rinse +rinsing +riot +ripcord +ripeness +ripening +ripping +ripple +rippling +riptide +rise +rising +risk +risotto +ritalin +ritzy +rival +riverbank +riverbed +riverboat +riverside +riveter +riveting +roamer +roaming +roast +robbing +robe +robin +robotics +robust +rockband +rocker +rocket +rockfish +rockiness +rocking +rocklike +rockslide +rockstar +rocky +rogue +roman +romp +rope +roping +roster +rosy +rotten +rotting +rotunda +roulette +rounding +roundish +roundness +roundup +roundworm +routine +routing +rover +roving +royal +rubbed +rubber +rubbing +rubble +rubdown +ruby +ruckus +rudder +rug +ruined +rule +rumble +rumbling +rummage +rumor +runaround +rundown +runner +running +runny +runt +runway +rupture +rural +ruse +rush +rust +rut +sabbath +sabotage +sacrament +sacred +sacrifice +sadden +saddlebag +saddled +saddling +sadly +sadness +safari +safeguard +safehouse +safely +safeness +saffron +saga +sage +sagging +saggy +said +saint +sake +salad +salami +salaried +salary +saline +salon +saloon +salsa +salt +salutary +salute +salvage +salvaging +salvation +same +sample +sampling +sanction +sanctity +sanctuary +sandal +sandbag +sandbank +sandbar +sandblast +sandbox +sanded +sandfish +sanding +sandlot +sandpaper +sandpit +sandstone +sandstorm +sandworm +sandy +sanitary +sanitizer +sank +santa +sapling +sappiness +sappy +sarcasm +sarcastic +sardine +sash +sasquatch +sassy +satchel +satiable +satin +satirical +satisfied +satisfy +saturate +saturday +sauciness +saucy +sauna +savage +savanna +saved +savings +savior +savor +saxophone +say +scabbed +scabby +scalded +scalding +scale +scaling +scallion +scallop +scalping +scam +scandal +scanner +scanning +scant +scapegoat +scarce +scarcity +scarecrow +scared +scarf +scarily +scariness +scarring +scary +scavenger +scenic +schedule +schematic +scheme +scheming +schilling +schnapps +scholar +science +scientist +scion +scoff +scolding +scone +scoop +scooter +scope +scorch +scorebook +scorecard +scored +scoreless +scorer +scoring +scorn +scorpion +scotch +scoundrel +scoured +scouring +scouting +scouts +scowling +scrabble +scraggly +scrambled +scrambler +scrap +scratch +scrawny +screen +scribble +scribe +scribing +scrimmage +script +scroll +scrooge +scrounger +scrubbed +scrubber +scruffy +scrunch +scrutiny +scuba +scuff +sculptor +sculpture +scurvy +scuttle +secluded +secluding +seclusion +second +secrecy +secret +sectional +sector +secular +securely +security +sedan +sedate +sedation +sedative +sediment +seduce +seducing +segment +seismic +seizing +seldom +selected +selection +selective +selector +self +seltzer +semantic +semester +semicolon +semifinal +seminar +semisoft +semisweet +senate +senator +send +senior +senorita +sensation +sensitive +sensitize +sensually +sensuous +sepia +september +septic +septum +sequel +sequence +sequester +series +sermon +serotonin +serpent +serrated +serve +service +serving +sesame +sessions +setback +setting +settle +settling +setup +sevenfold +seventeen +seventh +seventy +severity +shabby +shack +shaded +shadily +shadiness +shading +shadow +shady +shaft +shakable +shakily +shakiness +shaking +shaky +shale +shallot +shallow +shame +shampoo +shamrock +shank +shanty +shape +shaping +share +sharpener +sharper +sharpie +sharply +sharpness +shawl +sheath +shed +sheep +sheet +shelf +shell +shelter +shelve +shelving +sherry +shield +shifter +shifting +shiftless +shifty +shimmer +shimmy +shindig +shine +shingle +shininess +shining +shiny +ship +shirt +shivering +shock +shone +shoplift +shopper +shopping +shoptalk +shore +shortage +shortcake +shortcut +shorten +shorter +shorthand +shortlist +shortly +shortness +shorts +shortwave +shorty +shout +shove +showbiz +showcase +showdown +shower +showgirl +showing +showman +shown +showoff +showpiece +showplace +showroom +showy +shrank +shrapnel +shredder +shredding +shrewdly +shriek +shrill +shrimp +shrine +shrink +shrivel +shrouded +shrubbery +shrubs +shrug +shrunk +shucking +shudder +shuffle +shuffling +shun +shush +shut +shy +siamese +siberian +sibling +siding +sierra +siesta +sift +sighing +silenced +silencer +silent +silica +silicon +silk +silliness +silly +silo +silt +silver +similarly +simile +simmering +simple +simplify +simply +sincere +sincerity +singer +singing +single +singular +sinister +sinless +sinner +sinuous +sip +siren +sister +sitcom +sitter +sitting +situated +situation +sixfold +sixteen +sixth +sixties +sixtieth +sixtyfold +sizable +sizably +size +sizing +sizzle +sizzling +skater +skating +skedaddle +skeletal +skeleton +skeptic +sketch +skewed +skewer +skid +skied +skier +skies +skiing +skilled +skillet +skillful +skimmed +skimmer +skimming +skimpily +skincare +skinhead +skinless +skinning +skinny +skintight +skipper +skipping +skirmish +skirt +skittle +skydiver +skylight +skyline +skype +skyrocket +skyward +slab +slacked +slacker +slacking +slackness +slacks +slain +slam +slander +slang +slapping +slapstick +slashed +slashing +slate +slather +slaw +sled +sleek +sleep +sleet +sleeve +slept +sliceable +sliced +slicer +slicing +slick +slider +slideshow +sliding +slighted +slighting +slightly +slimness +slimy +slinging +slingshot +slinky +slip +slit +sliver +slobbery +slogan +sloped +sloping +sloppily +sloppy +slot +slouching +slouchy +sludge +slug +slum +slurp +slush +sly +small +smartly +smartness +smasher +smashing +smashup +smell +smelting +smile +smilingly +smirk +smite +smith +smitten +smock +smog +smoked +smokeless +smokiness +smoking +smoky +smolder +smooth +smother +smudge +smudgy +smuggler +smuggling +smugly +smugness +snack +snagged +snaking +snap +snare +snarl +snazzy +sneak +sneer +sneeze +sneezing +snide +sniff +snippet +snipping +snitch +snooper +snooze +snore +snoring +snorkel +snort +snout +snowbird +snowboard +snowbound +snowcap +snowdrift +snowdrop +snowfall +snowfield +snowflake +snowiness +snowless +snowman +snowplow +snowshoe +snowstorm +snowsuit +snowy +snub +snuff +snuggle +snugly +snugness +speak +spearfish +spearhead +spearman +spearmint +species +specimen +specked +speckled +specks +spectacle +spectator +spectrum +speculate +speech +speed +spellbind +speller +spelling +spendable +spender +spending +spent +spew +sphere +spherical +sphinx +spider +spied +spiffy +spill +spilt +spinach +spinal +spindle +spinner +spinning +spinout +spinster +spiny +spiral +spirited +spiritism +spirits +spiritual +splashed +splashing +splashy +splatter +spleen +splendid +splendor +splice +splicing +splinter +splotchy +splurge +spoilage +spoiled +spoiler +spoiling +spoils +spoken +spokesman +sponge +spongy +sponsor +spoof +spookily +spooky +spool +spoon +spore +sporting +sports +sporty +spotless +spotlight +spotted +spotter +spotting +spotty +spousal +spouse +spout +sprain +sprang +sprawl +spray +spree +sprig +spring +sprinkled +sprinkler +sprint +sprite +sprout +spruce +sprung +spry +spud +spur +sputter +spyglass +squabble +squad +squall +squander +squash +squatted +squatter +squatting +squeak +squealer +squealing +squeamish +squeegee +squeeze +squeezing +squid +squiggle +squiggly +squint +squire +squirt +squishier +squishy +stability +stabilize +stable +stack +stadium +staff +stage +staging +stagnant +stagnate +stainable +stained +staining +stainless +stalemate +staleness +stalling +stallion +stamina +stammer +stamp +stand +stank +staple +stapling +starboard +starch +stardom +stardust +starfish +stargazer +staring +stark +starless +starlet +starlight +starlit +starring +starry +starship +starter +starting +startle +startling +startup +starved +starving +stash +state +static +statistic +statue +stature +status +statute +statutory +staunch +stays +steadfast +steadier +steadily +steadying +steam +steed +steep +steerable +steering +steersman +stegosaur +stellar +stem +stench +stencil +step +stereo +sterile +sterility +sterilize +sterling +sternness +sternum +stew +stick +stiffen +stiffly +stiffness +stifle +stifling +stillness +stilt +stimulant +stimulate +stimuli +stimulus +stinger +stingily +stinging +stingray +stingy +stinking +stinky +stipend +stipulate +stir +stitch +stock +stoic +stoke +stole +stomp +stonewall +stoneware +stonework +stoning +stony +stood +stooge +stool +stoop +stoplight +stoppable +stoppage +stopped +stopper +stopping +stopwatch +storable +storage +storeroom +storewide +storm +stout +stove +stowaway +stowing +straddle +straggler +strained +strainer +straining +strangely +stranger +strangle +strategic +strategy +stratus +straw +stray +streak +stream +street +strength +strenuous +strep +stress +stretch +strewn +stricken +strict +stride +strife +strike +striking +strive +striving +strobe +strode +stroller +strongbox +strongly +strongman +struck +structure +strudel +struggle +strum +strung +strut +stubbed +stubble +stubbly +stubborn +stucco +stuck +student +studied +studio +study +stuffed +stuffing +stuffy +stumble +stumbling +stump +stung +stunned +stunner +stunning +stunt +stupor +sturdily +sturdy +styling +stylishly +stylist +stylized +stylus +suave +subarctic +subatomic +subdivide +subdued +subduing +subfloor +subgroup +subheader +subject +sublease +sublet +sublevel +sublime +submarine +submerge +submersed +submitter +subpanel +subpar +subplot +subprime +subscribe +subscript +subsector +subside +subsiding +subsidize +subsidy +subsoil +subsonic +substance +subsystem +subtext +subtitle +subtly +subtotal +subtract +subtype +suburb +subway +subwoofer +subzero +succulent +such +suction +sudden +sudoku +suds +sufferer +suffering +suffice +suffix +suffocate +suffrage +sugar +suggest +suing +suitable +suitably +suitcase +suitor +sulfate +sulfide +sulfite +sulfur +sulk +sullen +sulphate +sulphuric +sultry +superbowl +superglue +superhero +superior +superjet +superman +supermom +supernova +supervise +supper +supplier +supply +support +supremacy +supreme +surcharge +surely +sureness +surface +surfacing +surfboard +surfer +surgery +surgical +surging +surname +surpass +surplus +surprise +surreal +surrender +surrogate +surround +survey +survival +survive +surviving +survivor +sushi +suspect +suspend +suspense +sustained +sustainer +swab +swaddling +swagger +swampland +swan +swapping +swarm +sway +swear +sweat +sweep +swell +swept +swerve +swifter +swiftly +swiftness +swimmable +swimmer +swimming +swimsuit +swimwear +swinger +swinging +swipe +swirl +switch +swivel +swizzle +swooned +swoop +swoosh +swore +sworn +swung +sycamore +sympathy +symphonic +symphony +symptom +synapse +syndrome +synergy +synopses +synopsis +synthesis +synthetic +syrup +system +t-shirt +tabasco +tabby +tableful +tables +tablet +tableware +tabloid +tackiness +tacking +tackle +tackling +tacky +taco +tactful +tactical +tactics +tactile +tactless +tadpole +taekwondo +tag +tainted +take +taking +talcum +talisman +tall +talon +tamale +tameness +tamer +tamper +tank +tanned +tannery +tanning +tantrum +tapeless +tapered +tapering +tapestry +tapioca +tapping +taps +tarantula +target +tarmac +tarnish +tarot +tartar +tartly +tartness +task +tassel +taste +tastiness +tasting +tasty +tattered +tattle +tattling +tattoo +taunt +tavern +thank +that +thaw +theater +theatrics +thee +theft +theme +theology +theorize +thermal +thermos +thesaurus +these +thesis +thespian +thicken +thicket +thickness +thieving +thievish +thigh +thimble +thing +think +thinly +thinner +thinness +thinning +thirstily +thirsting +thirsty +thirteen +thirty +thong +thorn +those +thousand +thrash +thread +threaten +threefold +thrift +thrill +thrive +thriving +throat +throbbing +throng +throttle +throwaway +throwback +thrower +throwing +thud +thumb +thumping +thursday +thus +thwarting +thyself +tiara +tibia +tidal +tidbit +tidiness +tidings +tidy +tiger +tighten +tightly +tightness +tightrope +tightwad +tigress +tile +tiling +till +tilt +timid +timing +timothy +tinderbox +tinfoil +tingle +tingling +tingly +tinker +tinkling +tinsel +tinsmith +tint +tinwork +tiny +tipoff +tipped +tipper +tipping +tiptoeing +tiptop +tiring +tissue +trace +tracing +track +traction +tractor +trade +trading +tradition +traffic +tragedy +trailing +trailside +train +traitor +trance +tranquil +transfer +transform +translate +transpire +transport +transpose +trapdoor +trapeze +trapezoid +trapped +trapper +trapping +traps +trash +travel +traverse +travesty +tray +treachery +treading +treadmill +treason +treat +treble +tree +trekker +tremble +trembling +tremor +trench +trend +trespass +triage +trial +triangle +tribesman +tribunal +tribune +tributary +tribute +triceps +trickery +trickily +tricking +trickle +trickster +tricky +tricolor +tricycle +trident +tried +trifle +trifocals +trillion +trilogy +trimester +trimmer +trimming +trimness +trinity +trio +tripod +tripping +triumph +trivial +trodden +trolling +trombone +trophy +tropical +tropics +trouble +troubling +trough +trousers +trout +trowel +truce +truck +truffle +trump +trunks +trustable +trustee +trustful +trusting +trustless +truth +try +tubby +tubeless +tubular +tucking +tuesday +tug +tuition +tulip +tumble +tumbling +tummy +turban +turbine +turbofan +turbojet +turbulent +turf +turkey +turmoil +turret +turtle +tusk +tutor +tutu +tux +tweak +tweed +tweet +tweezers +twelve +twentieth +twenty +twerp +twice +twiddle +twiddling +twig +twilight +twine +twins +twirl +twistable +twisted +twister +twisting +twisty +twitch +twitter +tycoon +tying +tyke +udder +ultimate +ultimatum +ultra +umbilical +umbrella +umpire +unabashed +unable +unadorned +unadvised +unafraid +unaired +unaligned +unaltered +unarmored +unashamed +unaudited +unawake +unaware +unbaked +unbalance +unbeaten +unbend +unbent +unbiased +unbitten +unblended +unblessed +unblock +unbolted +unbounded +unboxed +unbraided +unbridle +unbroken +unbuckled +unbundle +unburned +unbutton +uncanny +uncapped +uncaring +uncertain +unchain +unchanged +uncharted +uncheck +uncivil +unclad +unclaimed +unclamped +unclasp +uncle +unclip +uncloak +unclog +unclothed +uncoated +uncoiled +uncolored +uncombed +uncommon +uncooked +uncork +uncorrupt +uncounted +uncouple +uncouth +uncover +uncross +uncrown +uncrushed +uncured +uncurious +uncurled +uncut +undamaged +undated +undaunted +undead +undecided +undefined +underage +underarm +undercoat +undercook +undercut +underdog +underdone +underfed +underfeed +underfoot +undergo +undergrad +underhand +underline +underling +undermine +undermost +underpaid +underpass +underpay +underrate +undertake +undertone +undertook +undertow +underuse +underwear +underwent +underwire +undesired +undiluted +undivided +undocked +undoing +undone +undrafted +undress +undrilled +undusted +undying +unearned +unearth +unease +uneasily +uneasy +uneatable +uneaten +unedited +unelected +unending +unengaged +unenvied +unequal +unethical +uneven +unexpired +unexposed +unfailing +unfair +unfasten +unfazed +unfeeling +unfiled +unfilled +unfitted +unfitting +unfixable +unfixed +unflawed +unfocused +unfold +unfounded +unframed +unfreeze +unfrosted +unfrozen +unfunded +unglazed +ungloved +unglue +ungodly +ungraded +ungreased +unguarded +unguided +unhappily +unhappy +unharmed +unhealthy +unheard +unhearing +unheated +unhelpful +unhidden +unhinge +unhitched +unholy +unhook +unicorn +unicycle +unified +unifier +uniformed +uniformly +unify +unimpeded +uninjured +uninstall +uninsured +uninvited +union +uniquely +unisexual +unison +unissued +unit +universal +universe +unjustly +unkempt +unkind +unknotted +unknowing +unknown +unlaced +unlatch +unlawful +unleaded +unlearned +unleash +unless +unleveled +unlighted +unlikable +unlimited +unlined +unlinked +unlisted +unlit +unlivable +unloaded +unloader +unlocked +unlocking +unlovable +unloved +unlovely +unloving +unluckily +unlucky +unmade +unmanaged +unmanned +unmapped +unmarked +unmasked +unmasking +unmatched +unmindful +unmixable +unmixed +unmolded +unmoral +unmovable +unmoved +unmoving +unnamable +unnamed +unnatural +unneeded +unnerve +unnerving +unnoticed +unopened +unopposed +unpack +unpadded +unpaid +unpainted +unpaired +unpaved +unpeeled +unpicked +unpiloted +unpinned +unplanned +unplanted +unpleased +unpledged +unplowed +unplug +unpopular +unproven +unquote +unranked +unrated +unraveled +unreached +unread +unreal +unreeling +unrefined +unrelated +unrented +unrest +unretired +unrevised +unrigged +unripe +unrivaled +unroasted +unrobed +unroll +unruffled +unruly +unrushed +unsaddle +unsafe +unsaid +unsalted +unsaved +unsavory +unscathed +unscented +unscrew +unsealed +unseated +unsecured +unseeing +unseemly +unseen +unselect +unselfish +unsent +unsettled +unshackle +unshaken +unshaved +unshaven +unsheathe +unshipped +unsightly +unsigned +unskilled +unsliced +unsmooth +unsnap +unsocial +unsoiled +unsold +unsolved +unsorted +unspoiled +unspoken +unstable +unstaffed +unstamped +unsteady +unsterile +unstirred +unstitch +unstopped +unstuck +unstuffed +unstylish +unsubtle +unsubtly +unsuited +unsure +unsworn +untagged +untainted +untaken +untamed +untangled +untapped +untaxed +unthawed +unthread +untidy +untie +until +untimed +untimely +untitled +untoasted +untold +untouched +untracked +untrained +untreated +untried +untrimmed +untrue +untruth +unturned +untwist +untying +unusable +unused +unusual +unvalued +unvaried +unvarying +unveiled +unveiling +unvented +unviable +unvisited +unvocal +unwanted +unwarlike +unwary +unwashed +unwatched +unweave +unwed +unwelcome +unwell +unwieldy +unwilling +unwind +unwired +unwitting +unwomanly +unworldly +unworn +unworried +unworthy +unwound +unwoven +unwrapped +unwritten +unzip +upbeat +upchuck +upcoming +upcountry +update +upfront +upgrade +upheaval +upheld +uphill +uphold +uplifted +uplifting +upload +upon +upper +upright +uprising +upriver +uproar +uproot +upscale +upside +upstage +upstairs +upstart +upstate +upstream +upstroke +upswing +uptake +uptight +uptown +upturned +upward +upwind +uranium +urban +urchin +urethane +urgency +urgent +urging +urologist +urology +usable +usage +useable +used +uselessly +user +usher +usual +utensil +utility +utilize +utmost +utopia +utter +vacancy +vacant +vacate +vacation +vagabond +vagrancy +vagrantly +vaguely +vagueness +valiant +valid +valium +valley +valuables +value +vanilla +vanish +vanity +vanquish +vantage +vaporizer +variable +variably +varied +variety +various +varmint +varnish +varsity +varying +vascular +vaseline +vastly +vastness +veal +vegan +veggie +vehicular +velcro +velocity +velvet +vendetta +vending +vendor +veneering +vengeful +venomous +ventricle +venture +venue +venus +verbalize +verbally +verbose +verdict +verify +verse +version +versus +vertebrae +vertical +vertigo +very +vessel +vest +veteran +veto +vexingly +viability +viable +vibes +vice +vicinity +victory +video +viewable +viewer +viewing +viewless +viewpoint +vigorous +village +villain +vindicate +vineyard +vintage +violate +violation +violator +violet +violin +viper +viral +virtual +virtuous +virus +visa +viscosity +viscous +viselike +visible +visibly +vision +visiting +visitor +visor +vista +vitality +vitalize +vitally +vitamins +vivacious +vividly +vividness +vixen +vocalist +vocalize +vocally +vocation +voice +voicing +void +volatile +volley +voltage +volumes +voter +voting +voucher +vowed +vowel +voyage +wackiness +wad +wafer +waffle +waged +wager +wages +waggle +wagon +wake +waking +walk +walmart +walnut +walrus +waltz +wand +wannabe +wanted +wanting +wasabi +washable +washbasin +washboard +washbowl +washcloth +washday +washed +washer +washhouse +washing +washout +washroom +washstand +washtub +wasp +wasting +watch +water +waviness +waving +wavy +whacking +whacky +wham +wharf +wheat +whenever +whiff +whimsical +whinny +whiny +whisking +whoever +whole +whomever +whoopee +whooping +whoops +why +wick +widely +widen +widget +widow +width +wieldable +wielder +wife +wifi +wikipedia +wildcard +wildcat +wilder +wildfire +wildfowl +wildland +wildlife +wildly +wildness +willed +willfully +willing +willow +willpower +wilt +wimp +wince +wincing +wind +wing +winking +winner +winnings +winter +wipe +wired +wireless +wiring +wiry +wisdom +wise +wish +wisplike +wispy +wistful +wizard +wobble +wobbling +wobbly +wok +wolf +wolverine +womanhood +womankind +womanless +womanlike +womanly +womb +woof +wooing +wool +woozy +word +work +worried +worrier +worrisome +worry +worsening +worshiper +worst +wound +woven +wow +wrangle +wrath +wreath +wreckage +wrecker +wrecking +wrench +wriggle +wriggly +wrinkle +wrinkly +wrist +writing +written +wrongdoer +wronged +wrongful +wrongly +wrongness +wrought +xbox +xerox +yahoo +yam +yanking +yapping +yard +yarn +yeah +yearbook +yearling +yearly +yearning +yeast +yelling +yelp +yen +yesterday +yiddish +yield +yin +yippee +yo-yo +yodel +yoga +yogurt +yonder +yoyo +yummy +zap +zealous +zebra +zen +zeppelin +zero +zestfully +zesty +zigzagged +zipfile +zipping +zippy +zips +zit +zodiac +zombie +zone +zoning +zookeeper +zoologist +zoology +zoom diff --git a/public/words/eff-short.txt b/public/words/eff-short.txt new file mode 100644 index 0000000..9ac732f --- /dev/null +++ b/public/words/eff-short.txt @@ -0,0 +1,1296 @@ +aardvark +abandoned +abbreviate +abdomen +abhorrence +abiding +abnormal +abrasion +absorbing +abundant +abyss +academy +accountant +acetone +achiness +acid +acoustics +acquire +acrobat +actress +acuteness +aerosol +aesthetic +affidavit +afloat +afraid +aftershave +again +agency +aggressor +aghast +agitate +agnostic +agonizing +agreeing +aidless +aimlessly +ajar +alarmclock +albatross +alchemy +alfalfa +algae +aliens +alkaline +almanac +alongside +alphabet +already +also +altitude +aluminum +always +amazingly +ambulance +amendment +amiable +ammunition +amnesty +amoeba +amplifier +amuser +anagram +anchor +android +anesthesia +angelfish +animal +anklet +announcer +anonymous +answer +antelope +anxiety +anyplace +aorta +apartment +apnea +apostrophe +apple +apricot +aquamarine +arachnid +arbitrate +ardently +arena +argument +aristocrat +armchair +aromatic +arrowhead +arsonist +artichoke +asbestos +ascend +aseptic +ashamed +asinine +asleep +asocial +asparagus +astronaut +asymmetric +atlas +atmosphere +atom +atrocious +attic +atypical +auctioneer +auditorium +augmented +auspicious +automobile +auxiliary +avalanche +avenue +aviator +avocado +awareness +awhile +awkward +awning +awoke +axially +azalea +babbling +backpack +badass +bagpipe +bakery +balancing +bamboo +banana +barracuda +basket +bathrobe +bazooka +blade +blender +blimp +blouse +blurred +boatyard +bobcat +body +bogusness +bohemian +boiler +bonnet +boots +borough +bossiness +bottle +bouquet +boxlike +breath +briefcase +broom +brushes +bubblegum +buckle +buddhist +buffalo +bullfrog +bunny +busboy +buzzard +cabin +cactus +cadillac +cafeteria +cage +cahoots +cajoling +cakewalk +calculator +camera +canister +capsule +carrot +cashew +cathedral +caucasian +caviar +ceasefire +cedar +celery +cement +census +ceramics +cesspool +chalkboard +cheesecake +chimney +chlorine +chopsticks +chrome +chute +cilantro +cinnamon +circle +cityscape +civilian +clay +clergyman +clipboard +clock +clubhouse +coathanger +cobweb +coconut +codeword +coexistent +coffeecake +cognitive +cohabitate +collarbone +computer +confetti +copier +cornea +cosmetics +cotton +couch +coverless +coyote +coziness +crawfish +crewmember +crib +croissant +crumble +crystal +cubical +cucumber +cuddly +cufflink +cuisine +culprit +cup +curry +cushion +cuticle +cybernetic +cyclist +cylinder +cymbal +cynicism +cypress +cytoplasm +dachshund +daffodil +dagger +dairy +dalmatian +dandelion +dartboard +dastardly +datebook +daughter +dawn +daytime +dazzler +dealer +debris +decal +dedicate +deepness +defrost +degree +dehydrator +deliverer +democrat +dentist +deodorant +depot +deranged +desktop +detergent +device +dexterity +diamond +dibs +dictionary +diffuser +digit +dilated +dimple +dinnerware +dioxide +diploma +directory +dishcloth +ditto +dividers +dizziness +doctor +dodge +doll +dominoes +donut +doorstep +dorsal +double +downstairs +dozed +drainpipe +dresser +driftwood +droppings +drum +dryer +dubiously +duckling +duffel +dugout +dumpster +duplex +durable +dustpan +dutiful +duvet +dwarfism +dwelling +dwindling +dynamite +dyslexia +eagerness +earlobe +easel +eavesdrop +ebook +eccentric +echoless +eclipse +ecosystem +ecstasy +edged +editor +educator +eelworm +eerie +effects +eggnog +egomaniac +ejection +elastic +elbow +elderly +elephant +elfishly +eliminator +elk +elliptical +elongated +elsewhere +elusive +elves +emancipate +embroidery +emcee +emerald +emission +emoticon +emperor +emulate +enactment +enchilada +endorphin +energy +enforcer +engine +enhance +enigmatic +enjoyably +enlarged +enormous +enquirer +enrollment +ensemble +entryway +enunciate +envoy +enzyme +epidemic +equipment +erasable +ergonomic +erratic +eruption +escalator +eskimo +esophagus +espresso +essay +estrogen +etching +eternal +ethics +etiquette +eucalyptus +eulogy +euphemism +euthanize +evacuation +evergreen +evidence +evolution +exam +excerpt +exerciser +exfoliate +exhale +exist +exorcist +explode +exquisite +exterior +exuberant +fabric +factory +faded +failsafe +falcon +family +fanfare +fasten +faucet +favorite +feasibly +february +federal +feedback +feigned +feline +femur +fence +ferret +festival +fettuccine +feudalist +feverish +fiberglass +fictitious +fiddle +figurine +fillet +finalist +fiscally +fixture +flashlight +fleshiness +flight +florist +flypaper +foamless +focus +foggy +folksong +fondue +footpath +fossil +fountain +fox +fragment +freeway +fridge +frosting +fruit +fryingpan +gadget +gainfully +gallstone +gamekeeper +gangway +garlic +gaslight +gathering +gauntlet +gearbox +gecko +gem +generator +geographer +gerbil +gesture +getaway +geyser +ghoulishly +gibberish +giddiness +giftshop +gigabyte +gimmick +giraffe +giveaway +gizmo +glasses +gleeful +glisten +glove +glucose +glycerin +gnarly +gnomish +goatskin +goggles +goldfish +gong +gooey +gorgeous +gosling +gothic +gourmet +governor +grape +greyhound +grill +groundhog +grumbling +guacamole +guerrilla +guitar +gullible +gumdrop +gurgling +gusto +gutless +gymnast +gynecology +gyration +habitat +hacking +haggard +haiku +halogen +hamburger +handgun +happiness +hardhat +hastily +hatchling +haughty +hazelnut +headband +hedgehog +hefty +heinously +helmet +hemoglobin +henceforth +herbs +hesitation +hexagon +hubcap +huddling +huff +hugeness +hullabaloo +human +hunter +hurricane +hushing +hyacinth +hybrid +hydrant +hygienist +hypnotist +ibuprofen +icepack +icing +iconic +identical +idiocy +idly +igloo +ignition +iguana +illuminate +imaging +imbecile +imitator +immigrant +imprint +iodine +ionosphere +ipad +iphone +iridescent +irksome +iron +irrigation +island +isotope +issueless +italicize +itemizer +itinerary +itunes +ivory +jabbering +jackrabbit +jaguar +jailhouse +jalapeno +jamboree +janitor +jarring +jasmine +jaundice +jawbreaker +jaywalker +jazz +jealous +jeep +jelly +jeopardize +jersey +jetski +jezebel +jiffy +jigsaw +jingling +jobholder +jockstrap +jogging +john +joinable +jokingly +journal +jovial +joystick +jubilant +judiciary +juggle +juice +jujitsu +jukebox +jumpiness +junkyard +juror +justifying +juvenile +kabob +kamikaze +kangaroo +karate +kayak +keepsake +kennel +kerosene +ketchup +khaki +kickstand +kilogram +kimono +kingdom +kiosk +kissing +kite +kleenex +knapsack +kneecap +knickers +koala +krypton +laboratory +ladder +lakefront +lantern +laptop +laryngitis +lasagna +latch +laundry +lavender +laxative +lazybones +lecturer +leftover +leggings +leisure +lemon +length +leopard +leprechaun +lettuce +leukemia +levers +lewdness +liability +library +licorice +lifeboat +lightbulb +likewise +lilac +limousine +lint +lioness +lipstick +liquid +listless +litter +liverwurst +lizard +llama +luau +lubricant +lucidity +ludicrous +luggage +lukewarm +lullaby +lumberjack +lunchbox +luridness +luscious +luxurious +lyrics +macaroni +maestro +magazine +mahogany +maimed +majority +makeover +malformed +mammal +mango +mapmaker +marbles +massager +matchstick +maverick +maximum +mayonnaise +moaning +mobilize +moccasin +modify +moisture +molecule +momentum +monastery +moonshine +mortuary +mosquito +motorcycle +mousetrap +movie +mower +mozzarella +muckiness +mudflow +mugshot +mule +mummy +mundane +muppet +mural +mustard +mutation +myriad +myspace +myth +nail +namesake +nanosecond +napkin +narrator +nastiness +natives +nautically +navigate +nearest +nebula +nectar +nefarious +negotiator +neither +nemesis +neoliberal +nephew +nervously +nest +netting +neuron +nevermore +nextdoor +nicotine +niece +nimbleness +nintendo +nirvana +nuclear +nugget +nuisance +nullify +numbing +nuptials +nursery +nutcracker +nylon +oasis +oat +obediently +obituary +object +obliterate +obnoxious +observer +obtain +obvious +occupation +oceanic +octopus +ocular +office +oftentimes +oiliness +ointment +older +olympics +omissible +omnivorous +oncoming +onion +onlooker +onstage +onward +onyx +oomph +opaquely +opera +opium +opossum +opponent +optical +opulently +oscillator +osmosis +ostrich +otherwise +ought +outhouse +ovation +oven +owlish +oxford +oxidize +oxygen +oyster +ozone +pacemaker +padlock +pageant +pajamas +palm +pamphlet +pantyhose +paprika +parakeet +passport +patio +pauper +pavement +payphone +pebble +peculiarly +pedometer +pegboard +pelican +penguin +peony +pepperoni +peroxide +pesticide +petroleum +pewter +pharmacy +pheasant +phonebook +phrasing +physician +plank +pledge +plotted +plug +plywood +pneumonia +podiatrist +poetic +pogo +poison +poking +policeman +poncho +popcorn +porcupine +postcard +poultry +powerboat +prairie +pretzel +princess +propeller +prune +pry +pseudo +psychopath +publisher +pucker +pueblo +pulley +pumpkin +punchbowl +puppy +purse +pushup +putt +puzzle +pyramid +python +quarters +quesadilla +quilt +quote +racoon +radish +ragweed +railroad +rampantly +rancidity +rarity +raspberry +ravishing +rearrange +rebuilt +receipt +reentry +refinery +register +rehydrate +reimburse +rejoicing +rekindle +relic +remote +renovator +reopen +reporter +request +rerun +reservoir +retriever +reunion +revolver +rewrite +rhapsody +rhetoric +rhino +rhubarb +rhyme +ribbon +riches +ridden +rigidness +rimmed +riptide +riskily +ritzy +riverboat +roamer +robe +rocket +romancer +ropelike +rotisserie +roundtable +royal +rubber +rudderless +rugby +ruined +rulebook +rummage +running +rupture +rustproof +sabotage +sacrifice +saddlebag +saffron +sainthood +saltshaker +samurai +sandworm +sapphire +sardine +sassy +satchel +sauna +savage +saxophone +scarf +scenario +schoolbook +scientist +scooter +scrapbook +sculpture +scythe +secretary +sedative +segregator +seismology +selected +semicolon +senator +septum +sequence +serpent +sesame +settler +severely +shack +shelf +shirt +shovel +shrimp +shuttle +shyness +siamese +sibling +siesta +silicon +simmering +singles +sisterhood +sitcom +sixfold +sizable +skateboard +skeleton +skies +skulk +skylight +slapping +sled +slingshot +sloth +slumbering +smartphone +smelliness +smitten +smokestack +smudge +snapshot +sneezing +sniff +snowsuit +snugness +speakers +sphinx +spider +splashing +sponge +sprout +spur +spyglass +squirrel +statue +steamboat +stingray +stopwatch +strawberry +student +stylus +suave +subway +suction +suds +suffocate +sugar +suitcase +sulphur +superstore +surfer +sushi +swan +sweatshirt +swimwear +sword +sycamore +syllable +symphony +synagogue +syringes +systemize +tablespoon +taco +tadpole +taekwondo +tagalong +takeout +tallness +tamale +tanned +tapestry +tarantula +tastebud +tattoo +tavern +thaw +theater +thimble +thorn +throat +thumb +thwarting +tiara +tidbit +tiebreaker +tiger +timid +tinsel +tiptoeing +tirade +tissue +tractor +tree +tripod +trousers +trucks +tryout +tubeless +tuesday +tugboat +tulip +tumbleweed +tupperware +turtle +tusk +tutorial +tuxedo +tweezers +twins +tyrannical +ultrasound +umbrella +umpire +unarmored +unbuttoned +uncle +underwear +unevenness +unflavored +ungloved +unhinge +unicycle +unjustly +unknown +unlocking +unmarked +unnoticed +unopened +unpaved +unquenched +unroll +unscrewing +untied +unusual +unveiled +unwrinkled +unyielding +unzip +upbeat +upcountry +update +upfront +upgrade +upholstery +upkeep +upload +uppercut +upright +upstairs +uptown +upwind +uranium +urban +urchin +urethane +urgent +urologist +username +usher +utensil +utility +utmost +utopia +utterance +vacuum +vagrancy +valuables +vanquished +vaporizer +varied +vaseline +vegetable +vehicle +velcro +vendor +vertebrae +vestibule +veteran +vexingly +vicinity +videogame +viewfinder +vigilante +village +vinegar +violin +viperfish +virus +visor +vitamins +vivacious +vixen +vocalist +vogue +voicemail +volleyball +voucher +voyage +vulnerable +waffle +wagon +wakeup +walrus +wanderer +wasp +water +waving +wheat +whisper +wholesaler +wick +widow +wielder +wifeless +wikipedia +wildcat +windmill +wipeout +wired +wishbone +wizardry +wobbliness +wolverine +womb +woolworker +workbasket +wound +wrangle +wreckage +wristwatch +wrongdoing +xerox +xylophone +yacht +yahoo +yard +yearbook +yesterday +yiddish +yield +yo-yo +yodel +yogurt +yuppie +zealot +zebra +zeppelin +zestfully +zigzagged +zillion +zipping +zirconium +zodiac +zombie +zookeeper +zucchini diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..170f691 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Start the reze-test server. +# +# sh scripts/run.sh # listens on 0.0.0.0:3000 +# PORT=8080 sh scripts/run.sh # listens on 0.0.0.0:8080 +# +# POSIX sh only; no bashisms. Requires Node.js 22 or newer on PATH. +set -eu + +cd "$(dirname "$0")/.." + +PORT="${PORT:-3000}" +export PORT + +echo "reze-test: starting on port ${PORT}" >&2 +exec node server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..79ec58f --- /dev/null +++ b/server.js @@ -0,0 +1,289 @@ +import { createServer } from 'node:http'; +import { readFileSync, statSync, realpathSync } from 'node:fs'; +import { extname, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + estimateEntropy, + generateMemorizable, + generateRandom, +} from './src/generator.js'; +import { listMeta } from './src/words.js'; + +const PUBLIC_DIR = resolve(fileURLToPath(new URL('./public/', import.meta.url))); +const NOT_FOUND_PAGE = resolve(PUBLIC_DIR, '404.html'); +const MAX_BODY_BYTES = 64 * 1024; +const PORT = Number.parseInt(process.env.PORT || '3000', 10); +const HOST = '0.0.0.0'; + +const SECURITY_HEADERS = { + 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'no-referrer', + 'X-Frame-Options': 'DENY', + 'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'self'", +}; + +const CONTENT_TYPES = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.txt': 'text/plain; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.ico': 'image/x-icon', +}; + +function sendBuffer(req, res, status, body, headers) { + res.writeHead(status, { + ...SECURITY_HEADERS, + 'Content-Length': body.length, + ...headers, + }); + if (req.method === 'HEAD') { + res.end(); + return; + } + res.end(body); +} + +function sendJson(req, res, status, payload) { + const body = Buffer.from(JSON.stringify(payload)); + sendBuffer(req, res, status, body, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store', + }); +} + +function sendError(req, res, status, message) { + sendJson(req, res, status, { error: message }); +} + +function sendNotFoundPage(req, res) { + let body; + try { + body = readFileSync(NOT_FOUND_PAGE); + } catch { + body = Buffer.from('Not found

Not found

'); + } + sendBuffer(req, res, 404, body, { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store', + }); +} + +function contentTypeFor(filePath) { + return CONTENT_TYPES[extname(filePath).toLowerCase()] || 'application/octet-stream'; +} + +function resolveStaticPath(pathname) { + let decoded; + try { + decoded = decodeURIComponent(pathname); + } catch { + return null; + } + if (decoded.includes('\0')) return null; + + const relative = decoded === '/' ? 'index.html' : decoded.replace(/^\/+/, ''); + const target = resolve(PUBLIC_DIR, relative); + if (target !== PUBLIC_DIR && !target.startsWith(PUBLIC_DIR + sep)) { + return null; + } + return target; +} + +function serveStatic(req, res, pathname) { + const target = resolveStaticPath(pathname); + if (!target) { + sendNotFoundPage(req, res); + return; + } + + let stats; + let real; + try { + stats = statSync(target); + real = realpathSync(target); + } catch { + sendNotFoundPage(req, res); + return; + } + + if (!stats.isFile() || (real !== PUBLIC_DIR && !real.startsWith(PUBLIC_DIR + sep))) { + sendNotFoundPage(req, res); + return; + } + + let body; + try { + body = readFileSync(real); + } catch { + sendNotFoundPage(req, res); + return; + } + + sendBuffer(req, res, 200, body, { + 'Content-Type': contentTypeFor(real), + 'Cache-Control': 'no-cache', + }); +} + +function readJsonBody(req) { + return new Promise((resolvePromise, rejectPromise) => { + const chunks = []; + let size = 0; + let tooLarge = false; + + req.on('data', (chunk) => { + size += chunk.length; + if (size > MAX_BODY_BYTES) { + tooLarge = true; + chunks.length = 0; + if (size > MAX_BODY_BYTES * 8) { + req.destroy(); + } + return; + } + chunks.push(chunk); + }); + + req.on('error', (err) => { + rejectPromise(Object.assign(err, { statusCode: 400 })); + }); + + req.on('end', () => { + if (tooLarge) { + rejectPromise(Object.assign(new Error('request body too large (limit 64 KB)'), { statusCode: 413 })); + return; + } + const raw = Buffer.concat(chunks).toString('utf8'); + if (raw.trim() === '') { + resolvePromise({}); + return; + } + try { + const parsed = JSON.parse(raw); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + rejectPromise(Object.assign(new Error('request body must be a JSON object'), { statusCode: 400 })); + return; + } + resolvePromise(parsed); + } catch { + rejectPromise(Object.assign(new Error('malformed JSON body'), { statusCode: 400 })); + } + }); + }); +} + +function countOption(body) { + const raw = body.count === undefined || body.count === null ? 1 : Number(body.count); + if (!Number.isInteger(raw) || raw < 1 || raw > 10) { + throw Object.assign(new Error('count must be an integer between 1 and 10'), { statusCode: 400 }); + } + return raw; +} + +async function handleGenerate(req, res, body) { + const mode = body.mode === undefined || body.mode === null ? 'memorable' : String(body.mode); + if (mode !== 'memorable' && mode !== 'random') { + sendError(req, res, 400, `unknown mode "${mode}"`); + return; + } + + let count; + try { + count = countOption(body); + } catch (err) { + sendError(req, res, err.statusCode || 400, err.message); + return; + } + + let entropy; + try { + entropy = estimateEntropy(mode, body); + } catch (err) { + sendError(req, res, 400, err.message); + return; + } + + const passwords = []; + for (let i = 0; i < count; i += 1) { + let value; + try { + value = mode === 'memorable' ? generateMemorizable(body) : generateRandom(body); + } catch (err) { + sendError(req, res, 400, err.message); + return; + } + passwords.push({ value, mode, entropy }); + } + + sendJson(req, res, 200, { mode, count, passwords, entropy }); +} + +async function handleApi(req, res, pathname) { + if (pathname === '/api/meta') { + if (req.method !== 'GET' && req.method !== 'HEAD') { + sendError(req, res, 405, 'method not allowed'); + return; + } + sendJson(req, res, 200, { lists: listMeta() }); + return; + } + + if (pathname === '/api/generate') { + if (req.method !== 'POST') { + sendError(req, res, 405, 'method not allowed'); + return; + } + let body; + try { + body = await readJsonBody(req); + } catch (err) { + sendError(req, res, err.statusCode || 400, err.message); + return; + } + await handleGenerate(req, res, body); + return; + } + + sendError(req, res, 404, 'not found'); +} + +const server = createServer((req, res) => { + let pathname = '/'; + try { + pathname = new URL(req.url || '/', 'http://localhost').pathname; + } catch { + sendError(req, res, 400, 'bad request'); + return; + } + + if (pathname === '/healthz') { + if (req.method !== 'GET' && req.method !== 'HEAD') { + sendError(req, res, 405, 'method not allowed'); + return; + } + sendJson(req, res, 200, { status: 'ok' }); + return; + } + + if (pathname.startsWith('/api/')) { + handleApi(req, res, pathname).catch((err) => { + sendError(req, res, 500, err && err.message ? err.message : 'internal error'); + }); + return; + } + + if (req.method !== 'GET' && req.method !== 'HEAD') { + sendError(req, res, 405, 'method not allowed'); + return; + } + + serveStatic(req, res, pathname); +}); + +server.listen(PORT, HOST, () => { + console.log(`reze-test listening on http://${HOST}:${PORT}`); +}); diff --git a/src/generator.js b/src/generator.js new file mode 100644 index 0000000..a7b7b23 --- /dev/null +++ b/src/generator.js @@ -0,0 +1,249 @@ +import { randomInt } from 'node:crypto'; + +import { getWordList, listSize } from './words.js'; + +export const LOWER_CHARS = 'abcdefghijklmnopqrstuvwxyz'; +export const UPPER_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +export const DIGIT_CHARS = '0123456789'; +export const DEFAULT_SYMBOL_CHARS = '!@#$%^&*()-_=+[]{};:,.<>?'; +export const AMBIGUOUS_CHARS = '0O1lI|'; + +export const GROUP_SEPARATORS = ['-', '_', '.', ' ', '']; +export const DIGIT_SEPARATORS = ['', '-']; +export const CAPITALIZATIONS = ['none', 'title', 'upper']; + +const MEMORABLE_DEFAULTS = { + words: 4, + digits: 2, + digitSeparator: '', + separator: '-', + capitalization: 'none', + list: 'eff-large', +}; + +const RANDOM_DEFAULTS = { + length: 20, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: false, + customSymbols: '', + excludeAmbiguous: false, + requireEach: true, +}; + +function fail(message) { + throw new Error(message); +} + +function intOption(value, fallback, min, max, name) { + const n = value === undefined || value === null || value === '' ? fallback : Number(value); + if (!Number.isInteger(n)) { + fail(`${name} must be an integer`); + } + if (n < min || n > max) { + fail(`${name} must be between ${min} and ${max}`); + } + return n; +} + +function enumOption(value, fallback, allowed, name) { + const v = value === undefined || value === null ? fallback : String(value); + if (!allowed.includes(v)) { + fail(`${name} must be one of ${allowed.map((a) => JSON.stringify(a)).join(', ')}`); + } + return v; +} + +function boolOption(value, fallback) { + if (value === undefined || value === null || value === '') return fallback; + if (typeof value === 'boolean') return value; + if (value === 'true' || value === '1' || value === 1) return true; + if (value === 'false' || value === '0' || value === 0) return false; + return Boolean(value); +} + +function pick(items) { + return items[randomInt(items.length)]; +} + +function randomDigits(count) { + let out = ''; + for (let i = 0; i < count; i += 1) { + out += String(randomInt(10)); + } + return out; +} + +function applyCapitalization(word, capitalization) { + if (capitalization === 'upper') return word.toUpperCase(); + if (capitalization === 'title') { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + return word; +} + +function normalizeMemorizable(opts) { + const words = intOption(opts.words, MEMORABLE_DEFAULTS.words, 2, 10, 'words'); + const digits = intOption(opts.digits, MEMORABLE_DEFAULTS.digits, 0, 4, 'digits'); + const digitSeparator = enumOption( + opts.digitSeparator, + MEMORABLE_DEFAULTS.digitSeparator, + DIGIT_SEPARATORS, + 'digitSeparator', + ); + const separator = enumOption( + opts.separator, + MEMORABLE_DEFAULTS.separator, + GROUP_SEPARATORS, + 'separator', + ); + const capitalization = enumOption( + opts.capitalization, + MEMORABLE_DEFAULTS.capitalization, + CAPITALIZATIONS, + 'capitalization', + ); + const list = opts.list === undefined || opts.list === null ? MEMORABLE_DEFAULTS.list : String(opts.list); + getWordList(list); + + return { words, digits, digitSeparator, separator, capitalization, list }; +} + +function normalizeRandom(opts) { + const length = intOption(opts.length, RANDOM_DEFAULTS.length, 8, 64, 'length'); + const useLower = boolOption(opts.useLower, RANDOM_DEFAULTS.useLower); + const useUpper = boolOption(opts.useUpper, RANDOM_DEFAULTS.useUpper); + const useDigits = boolOption(opts.useDigits, RANDOM_DEFAULTS.useDigits); + const useSymbols = boolOption(opts.useSymbols, RANDOM_DEFAULTS.useSymbols); + const customSymbols = typeof opts.customSymbols === 'string' ? opts.customSymbols : ''; + const excludeAmbiguous = boolOption(opts.excludeAmbiguous, RANDOM_DEFAULTS.excludeAmbiguous); + const requireEach = boolOption(opts.requireEach, RANDOM_DEFAULTS.requireEach); + + return { + length, + useLower, + useUpper, + useDigits, + useSymbols, + customSymbols, + excludeAmbiguous, + requireEach, + }; +} + +function buildPools(options) { + const classes = []; + + if (options.useLower) classes.push({ name: 'lowercase', chars: LOWER_CHARS }); + if (options.useUpper) classes.push({ name: 'uppercase', chars: UPPER_CHARS }); + if (options.useDigits) classes.push({ name: 'digits', chars: DIGIT_CHARS }); + if (options.useSymbols) { + const symbols = options.customSymbols.length > 0 ? options.customSymbols : DEFAULT_SYMBOL_CHARS; + classes.push({ name: 'symbols', chars: symbols }); + } + + if (options.excludeAmbiguous) { + for (const cls of classes) { + cls.chars = Array.from(cls.chars) + .filter((ch) => !AMBIGUOUS_CHARS.includes(ch)) + .join(''); + } + } + + const seen = new Set(); + let union = ''; + for (const cls of classes) { + for (const ch of cls.chars) { + if (!seen.has(ch)) { + seen.add(ch); + union += ch; + } + } + } + + return { classes, union }; +} + +function shuffle(chars) { + for (let i = chars.length - 1; i > 0; i -= 1) { + const j = randomInt(i + 1); + const tmp = chars[i]; + chars[i] = chars[j]; + chars[j] = tmp; + } + return chars; +} + +export function generateMemorizable(opts = {}) { + const options = normalizeMemorizable(opts); + const words = getWordList(options.list); + + const groups = []; + for (let i = 0; i < options.words; i += 1) { + const digits = randomDigits(options.digits); + const word = applyCapitalization(pick(words), options.capitalization); + const glue = options.digits > 0 ? options.digitSeparator : ''; + groups.push(digits + glue + word); + } + + return groups.join(options.separator); +} + +export function generateRandom(opts = {}) { + const options = normalizeRandom(opts); + const { classes, union } = buildPools(options); + + if (union.length === 0) { + fail('no characters available: enable at least one character set'); + } + + const chars = []; + + if (options.requireEach) { + for (const cls of classes) { + if (chars.length >= options.length) break; + if (cls.chars.length === 0) continue; + chars.push(pick(cls.chars.split(''))); + } + } + + while (chars.length < options.length) { + chars.push(union[randomInt(union.length)]); + } + + return shuffle(chars).join(''); +} + +export function entropyForMemorizable(opts = {}) { + const options = normalizeMemorizable(opts); + const size = listSize(options.list); + const bits = options.words * Math.log2(size) + options.words * options.digits * Math.log2(10); + return round1(bits); +} + +export function entropyForRandom(opts = {}) { + const options = normalizeRandom(opts); + const { union } = buildPools(options); + if (union.length === 0) { + fail('no characters available: enable at least one character set'); + } + const bits = options.length * Math.log2(union.length); + return round1(bits); +} + +export function estimateEntropy(mode, opts = {}) { + if (mode === 'memorable') return entropyForMemorizable(opts); + if (mode === 'random') return entropyForRandom(opts); + return fail(`unknown mode "${mode}"`); +} + +export function generate(mode, opts = {}) { + if (mode === 'memorable') return generateMemorizable(opts); + if (mode === 'random') return generateRandom(opts); + return fail(`unknown mode "${mode}"`); +} + +function round1(n) { + return Math.round(n * 10) / 10; +} diff --git a/src/words.js b/src/words.js new file mode 100644 index 0000000..0395202 --- /dev/null +++ b/src/words.js @@ -0,0 +1,57 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const WORDS_DIR = fileURLToPath(new URL('../public/words/', import.meta.url)); + +const LIST_DEFS = [ + { id: 'eff-large', name: 'EFF Large Word List', file: 'eff-large.txt' }, + { id: 'eff-short', name: 'EFF Short Word List (2.0)', file: 'eff-short.txt' }, +]; + +const WORD_PATTERN = /^[a-z]+(?:-[a-z]+)*$/; + +const lists = new Map(); + +function loadList(def) { + const raw = readFileSync(WORDS_DIR + def.file, 'utf8'); + const words = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0); + + for (const word of words) { + if (!WORD_PATTERN.test(word)) { + throw new Error(`invalid word "${word}" in word list ${def.file}`); + } + } + if (words.length === 0) { + throw new Error(`word list ${def.id} is empty`); + } + + return { id: def.id, name: def.name, words }; +} + +for (const def of LIST_DEFS) { + lists.set(def.id, loadList(def)); +} + +export function listIds() { + return Array.from(lists.keys()); +} + +export function getWordList(id) { + const list = lists.get(id); + if (!list) { + throw new Error(`unknown word list "${id}"`); + } + return list.words; +} + +export function listSize(id) { + return getWordList(id).length; +} + +export function listMeta() { + return LIST_DEFS.map((def) => ({ + id: def.id, + name: def.name, + size: lists.get(def.id).words.length, + })); +} diff --git a/test/generator.test.js b/test/generator.test.js new file mode 100644 index 0000000..a0a202a --- /dev/null +++ b/test/generator.test.js @@ -0,0 +1,250 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + DEFAULT_SYMBOL_CHARS, + entropyForMemorizable, + entropyForRandom, + generateMemorizable, + generateRandom, +} from '../src/generator.js'; +import { getWordList, listIds, listSize } from '../src/words.js'; + +const LOWER = /[a-z]/; +const UPPER = /[A-Z]/; +const DIGIT = /[0-9]/; +const SYMBOL = /[^a-zA-Z0-9]/; + +test('word lists are loaded and expose the expected sizes', () => { + assert.deepEqual(listIds().sort(), ['eff-large', 'eff-short']); + assert.equal(listSize('eff-large'), 7776); + assert.equal(listSize('eff-short'), 1296); + assert.equal(getWordList('eff-large').length, 7776); + assert.throws(() => getWordList('nope'), /unknown word list/); +}); + +test('random mode respects the requested length and only uses enabled classes', () => { + const value = generateRandom({ + length: 24, + useLower: true, + useUpper: false, + useDigits: false, + useSymbols: false, + }); + + assert.equal(value.length, 24); + assert.match(value, /^[a-z]+$/); +}); + +test('random mode honours each enabled class', () => { + const value = generateRandom({ + length: 32, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: true, + }); + + assert.equal(value.length, 32); + assert.match(value, /[a-z]/); + assert.match(value, /[A-Z]/); + assert.match(value, /[0-9]/); + assert.match(value, SYMBOL); +}); + +test('random mode with requireEach contains at least one char from each enabled class', () => { + for (let attempt = 0; attempt < 25; attempt += 1) { + const value = generateRandom({ + length: 16, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: true, + requireEach: true, + }); + + assert.equal(value.length, 16); + assert.match(value, LOWER); + assert.match(value, UPPER); + assert.match(value, DIGIT); + assert.match(value, SYMBOL); + } +}); + +test('random mode uses the custom symbol set instead of the default one', () => { + const value = generateRandom({ + length: 20, + useLower: true, + useUpper: false, + useDigits: false, + useSymbols: true, + customSymbols: '@#', + requireEach: true, + }); + + assert.match(value, /^[a-z@#]+$/); + assert.match(value, /[@#]/); + assert.ok(!value.includes('!')); + assert.equal(DEFAULT_SYMBOL_CHARS.includes('!'), true); +}); + +test('random mode excludes ambiguous characters on request', () => { + const value = generateRandom({ + length: 60, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: true, + excludeAmbiguous: true, + }); + + assert.equal(value.length, 60); + for (const ch of ['0', 'O', '1', 'l', 'I', '|']) { + assert.ok(!value.includes(ch), `expected ${ch} to be excluded`); + } +}); + +test('random mode throws when every set is disabled', () => { + assert.throws( + () => generateRandom({ + length: 16, + useLower: false, + useUpper: false, + useDigits: false, + useSymbols: false, + }), + /no characters available/, + ); +}); + +test('memorizable output has the requested groups, digits and words', () => { + const words = getWordList('eff-large'); + const value = generateMemorizable({ + words: 3, + digits: 2, + digitSeparator: '', + separator: '_', + capitalization: 'none', + list: 'eff-large', + }); + + assert.match(value, /^[0-9a-zA-Z]*([\-_. ]?[0-9a-zA-Z]+)*$/); + + const groups = value.split('_'); + assert.equal(groups.length, 3); + + for (const group of groups) { + assert.match(group, /^\d{2}[a-z]+(-[a-z]+)*$/); + const word = group.slice(2); + assert.ok(words.includes(word), `expected "${word}" to come from the word list`); + } +}); + +test('memorizable output supports every group separator', () => { + for (const separator of ['_', '.', ' ']) { + const value = generateMemorizable({ + words: 2, + digits: 0, + separator, + capitalization: 'none', + list: 'eff-short', + }); + assert.equal(value.split(separator).length, 2); + } +}); + +test('memorizable output applies the digit separator', () => { + const value = generateMemorizable({ + words: 2, + digits: 3, + digitSeparator: '-', + separator: '_', + capitalization: 'none', + list: 'eff-short', + }); + + const groups = value.split('_'); + assert.equal(groups.length, 2); + for (const group of groups) { + assert.match(group, /^\d{3}-[a-z]+(-[a-z]+)*$/); + } +}); + +test('memorizable output applies capitalization', () => { + const none = generateMemorizable({ words: 2, digits: 0, capitalization: 'none', list: 'eff-short' }).split('-')[0]; + const title = generateMemorizable({ words: 2, digits: 0, capitalization: 'title', list: 'eff-short' }).split('-')[0]; + const upper = generateMemorizable({ words: 2, digits: 0, capitalization: 'upper', list: 'eff-short' }).split('-')[0]; + + assert.match(none, /^[a-z]+(-[a-z]+)*$/); + assert.match(title, /^[A-Z][a-z]*(-[a-z]+)*$/); + assert.match(upper, /^[A-Z]+(-[A-Z]+)*$/); +}); + +test('memorizable with digits=0 contains no digits', () => { + for (let attempt = 0; attempt < 10; attempt += 1) { + const value = generateMemorizable({ + words: 5, + digits: 0, + separator: '-', + capitalization: 'none', + list: 'eff-large', + }); + assert.ok(!/[0-9]/.test(value), 'expected no digits'); + assert.equal(value.split('-').length, 5); + } +}); + +test('memorizable rejects unknown word lists and out-of-range options', () => { + assert.throws( + () => generateMemorizable({ words: 2, digits: 2, list: 'nope' }), + /unknown word list/, + ); + assert.throws( + () => generateMemorizable({ words: 99, digits: 2 }), + /words must be between 2 and 10/, + ); + assert.throws( + () => generateMemorizable({ words: 2, digits: 9 }), + /digits must be between 0 and 4/, + ); +}); + +test('two consecutive calls differ', () => { + const a = generateRandom({ length: 24, useLower: true, useUpper: true, useDigits: true, useSymbols: true }); + const b = generateRandom({ length: 24, useLower: true, useUpper: true, useDigits: true, useSymbols: true }); + assert.notEqual(a, b); + + const m1 = generateMemorizable({ words: 4, digits: 2, separator: '-', capitalization: 'title' }); + const m2 = generateMemorizable({ words: 4, digits: 2, separator: '-', capitalization: 'title' }); + assert.notEqual(m1, m2); +}); + +test('entropy helpers return sane numbers', () => { + const short = entropyForRandom({ length: 8, useLower: true }); + const long = entropyForRandom({ length: 64, useLower: true }); + + assert.ok(short > 0, 'expected positive entropy'); + assert.ok(long > short, 'expected entropy to grow with length'); + assert.equal(short, Math.round(short * 10) / 10); + assert.equal(long, Math.round(long * 10) / 10); + + const symbols = entropyForRandom({ + length: 8, + useLower: true, + useUpper: true, + useDigits: true, + useSymbols: true, + }); + assert.ok(symbols > short); + + const memorable = entropyForMemorizable({ words: 4, digits: 2, list: 'eff-large' }); + const expected = 4 * Math.log2(7776) + 4 * 2 * Math.log2(10); + assert.equal(memorable, Math.round(expected * 10) / 10); + assert.ok(memorable > 0); + + const moreWords = entropyForMemorizable({ words: 5, digits: 2, list: 'eff-large' }); + assert.ok(moreWords > memorable); + + const shorter = entropyForMemorizable({ words: 4, digits: 0, list: 'eff-large' }); + assert.ok(shorter < memorable); +});