-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPasswordGenerator.html
More file actions
202 lines (179 loc) · 10.2 KB
/
PasswordGenerator.html
File metadata and controls
202 lines (179 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mouse‑Entropy Password Generator</title>
<style>
:root { --bg:#0b0f14; --fg:#e6eef8; --muted:#91a4b7; --card:#131a22; --accent:#4dd0e1; --ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; }
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial}
.wrap{max-width:900px;margin:40px auto;padding:24px}
.title{font-weight:700;font-size:22px;letter-spacing:.2px;margin:0 0 6px}
.sub{color:var(--muted);margin:0 0 18px}
.grid{display:grid;grid-template-columns:1fr;gap:16px}
@media (min-width:860px){ .grid{grid-template-columns:1.2fr .8fr} }
.card{background:var(--card);border:1px solid #1e2937;border-radius:14px;padding:16px}
.pad{padding:12px 14px;border-radius:12px;background:#0f1620;border:1px dashed #2a3a4d;min-height:260px;cursor:crosshair;position:relative;overflow:hidden}
.hint{position:absolute;inset:0;display:grid;place-items:center;color:var(--muted);pointer-events:none;text-align:center}
.stats{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
.stat{background:#0f1620;border:1px solid #233244;border-radius:10px;padding:8px 10px}
.bar{height:10px;background:#0f1620;border:1px solid #233244;border-radius:999px;overflow:hidden}
.bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--bad),var(--warn),var(--ok));transition:width .25s ease}
label{display:block;margin:10px 0 6px}
input[type="number"],input[type="text"],textarea{width:100%;padding:10px;border-radius:10px;border:1px solid #2a3a4d;background:#0f1620;color:var(--fg)}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:1px solid #2a3a4d;background:#111a25;color:var(--fg);padding:10px 14px;border-radius:12px;cursor:pointer;font-weight:600}
.btn.primary{border-color:#2dd4bf;background:linear-gradient(180deg,#17333a,#0f2024)}
.mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}
.out{font-size:18px;letter-spacing:.4px}
.pill{display:inline-block;border:1px solid #2a3a4d;background:#0f1620;color:var(--muted);padding:2px 8px;border-radius:999px;margin-left:8px}
.tiny{font-size:12px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<h1 class="title">Mouse‑Entropy Password Generator</h1>
<p class="sub">More mouse movement → more entropy. Outputs meet the classic Microsoft/Windows complexity rule (3 of 4: upper, lower, digit, symbol), and default to length 16.</p>
<div class="grid">
<!-- Entropy Surface -->
<div class="card">
<div id="surface" class="pad" aria-label="Move your mouse here to add entropy" role="application">
<canvas id="dots" width="800" height="400" style="position:absolute;inset:0;width:100%;height:100%"></canvas>
<div class="hint" id="hint">Move your mouse around this box, swirl, zig‑zag, pause, speed up, slow down…</div>
</div>
<div class="stats">
<div class="stat">Samples: <span id="samples">0</span></div>
<div class="stat">Estimated entropy: <span id="entropy">0.0</span> bits <span class="pill tiny">rough estimate</span></div>
</div>
<div class="bar" style="margin-top:8px"><span id="entropyBar"></span></div>
<p class="tiny" style="margin-top:8px">We always use the system CSPRNG (<code class="mono">crypto.getRandomValues</code>). Your mouse input is mixed in as an extra source of unpredictability.</p>
</div>
<!-- Controls -->
<div class="card">
<label for="length">Password length</label>
<input id="length" type="number" min="8" max="128" step="1" value="16" />
<div class="row" style="margin:8px 0 4px">
<label class="row" style="gap:6px"><input id="msComplex" type="checkbox" checked /> Enforce Microsoft complexity (3 of 4)</label>
</div>
<div class="row" style="margin-top:12px">
<button id="gen" class="btn primary">Generate</button>
<button id="copy" class="btn">Copy</button>
<span class="tiny" id="status"></span>
</div>
<label style="margin-top:14px">Password</label>
<div class="pad" style="min-height:auto">
<div id="output" class="mono out" style="word-break:break-all"></div>
</div>
<div id="why" class="tiny" style="margin-top:8px"></div>
</div>
</div>
</div>
<script>
(() => {
const CHARSETS = {
lower: "abcdefghijklmnopqrstuvwxyz",
upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digit: "0123456789",
symbol: "!@#$%^&*()-_=+[]{};:,.<>/?"
};
const ALL = Object.values(CHARSETS).join("");
const $ = (id) => document.getElementById(id);
const els = {
surface: $("surface"), dots: $("dots"), hint: $("hint"),
samples: $("samples"), entropy: $("entropy"), entropyBar: $("entropyBar"),
gen: $("gen"), copy: $("copy"), length: $("length"), ms: $("msComplex"),
out: $("output"), why: $("why"), status: $("status")
};
const ctx = els.dots.getContext("2d");
const enc = new TextEncoder();
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const bitbar = (bits, maxBits=128) => `${clamp((bits/maxBits)*100,0,100).toFixed(1)}%`;
const fisherYates = (arr, sample) => { for (let i=arr.length-1;i>0;i--){ const j = sample(i+1); [arr[i],arr[j]]=[arr[j],arr[i]]; } return arr; };
const pool = [];
let samples = 0; let last = { t: 0, x: 0, y: 0 };
const add = (v) => { pool.push(v>>>0); };
const drawDot = (x,y) => { const r = 1 + Math.random()*2; ctx.fillStyle='rgba(77,208,225,0.6)'; ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill(); };
const resizeCanvas = () => { const r = els.surface.getBoundingClientRect(); els.dots.width=Math.max(400, r.width|0); els.dots.height=Math.max(220, r.height|0); };
window.addEventListener('resize', resizeCanvas, {passive:true}); resizeCanvas();
function handleMouseMove(e){
const r = els.surface.getBoundingClientRect();
const x = e.clientX - r.left, y = e.clientY - r.top, t = performance.now();
const dx = x - last.x, dy = y - last.y, dt = t - last.t; last = {x,y,t};
add((x*73856093) ^ (y*19349663) ^ ((t*1000)|0));
add(((dx*83492791)|0) ^ ((dy*1234567)|0) ^ ((dt*1000)|0));
const sys = new Uint32Array(2); crypto.getRandomValues(sys); add(sys[0]); add(sys[1]);
drawDot(x,y);
samples++; els.samples.textContent = String(samples); els.hint.style.display='none';
const est = Math.min(256, Math.log2(samples+1)*6);
els.entropy.textContent = est.toFixed(1); els.entropyBar.style.width = bitbar(est);
}
els.surface.addEventListener('mousemove', handleMouseMove, {passive:true});
async function deriveSeed(){
const rand = new Uint8Array(64); crypto.getRandomValues(rand);
const poolBytes = new Uint8Array(new Uint32Array(pool).buffer);
const concat = new Uint8Array(rand.length + poolBytes.length);
concat.set(rand,0); concat.set(poolBytes, rand.length);
const hash = await crypto.subtle.digest('SHA-256', concat);
return new Uint8Array(hash);
}
async function createPrng(seed){
const key = await crypto.subtle.importKey('raw', seed, {name:'HMAC', hash:'SHA-256'}, false, ['sign']);
let ctr = 0, buf = new Uint8Array(0), idx = 0;
async function refill(min=32){
while (buf.length - idx < min){
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc.encode('ctr:'+ (ctr++))));
if(idx === 0){
const next = new Uint8Array(buf.length + mac.length); next.set(buf,0); next.set(mac,buf.length); buf = next;
} else {
buf = buf.slice(idx); idx = 0; const next = new Uint8Array(buf.length + mac.length); next.set(buf,0); next.set(mac,buf.length); buf = next;
}
}
}
return {
async next(n){ await refill(n); const out = buf.slice(idx, idx+n); idx += n; return out; },
async u32(){ const b = await this.next(4); return (b[0] | (b[1]<<8) | (b[2]<<16) | (b[3]<<24)) >>> 0; },
async sample(max){
const UINT32_MAX = 0xFFFFFFFF; const limit = Math.floor(UINT32_MAX / max) * max;
while(true){ const v = await this.u32(); if(v < limit) return v % max; }
}
};
}
const categoriesCount = (pw) => [/[a-z]/, /[A-Z]/, /\d/, /[^\da-zA-Z]/].reduce((n,re)=> n + (re.test(pw)?1:0), 0);
const meetsMs = (pw) => categoriesCount(pw) >= 3;
async function buildPassword(len, enforce){
len = clamp(len|0, 8, 128);
const seed = await deriveSeed();
const prng = await createPrng(seed);
const sets = [CHARSETS.lower, CHARSETS.upper, CHARSETS.digit, CHARSETS.symbol];
for(let attempt=0; attempt<100; attempt++){
const chars = [];
if(enforce){
const picks = new Set();
while(picks.size < 3){ picks.add(await prng.sample(4)); }
for(const i of picks){ chars.push(sets[i][await prng.sample(sets[i].length)]); }
}
while(chars.length < len){ chars.push(ALL[await prng.sample(ALL.length)]); }
fisherYates(chars, (n)=>Math.floor(Math.random()*n));
const pw = chars.join("");
if(!enforce || meetsMs(pw)) return { pw, why: enforce ? 'Meets 3-of-4 complexity' : 'No additional constraints' };
}
throw new Error('Could not construct a compliant password');
}
els.gen.addEventListener('click', async () => {
els.status.textContent = '';
try {
const len = parseInt(els.length.value || '16', 10);
const enforce = els.ms.checked;
const { pw, why } = await buildPassword(len, enforce);
els.out.textContent = pw; els.why.textContent = why;
if(samples < 20){ els.status.textContent = 'Tip: add more mouse movement for extra entropy (CSPRNG is always used).'; }
} catch (e){ els.status.textContent = 'Error: ' + e.message; }
});
els.copy.addEventListener('click', async () => {
const pw = els.out.textContent.trim(); if(!pw){ els.status.textContent = 'Nothing to copy yet.'; return; }
try{ await navigator.clipboard.writeText(pw); els.status.textContent = 'Copied to clipboard.'; } catch(e){ els.status.textContent = 'Copy failed: ' + e.message; }
});
})();
</script>
</body>
</html>