mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-09 23:11:25 +00:00
Fixes reported from the live Vercel deployment:
- vercel.json: remove cleanUrls, which was breaking every internal .html
link (ERR_FAILED) since links already use explicit extensions
- Recovery export was silently broken after login or password rotation:
the crypto worker's session state only picked up the recovery capsule
event on register/recover, never on login/changePassword, even though
the decrypted payload always carries it. Surfaced recoveryCapsuleEvent
(and recoveryPublicKey for changePassword) through core's LoginResult /
ChangePasswordResult and wired them into the worker session; covered by
a new account.e2e.test.ts case
- New accounts never got a Nostr profile: publishProfileAndRelayLists is
now called with name: loginName after registration, so the npub's
profile defaults to the chosen login name
- Password manager never recognized the fields (shadow DOM + JS-driven
submission defeat the passive save-prompt heuristics, and the
generated-password screen never put the password in a real input at
all). Fixed with an explicit navigator.credentials.store() call after
every register/login/recover/changePassword; documented as
Chromium-only in docs.html
New: optional manual/custom password (packages/core/src/account/manualPassword.ts),
opt-in per spec §9.3's "future release may permit manual passwords behind
an advanced setting" allowance. Enforces a length floor, a common-password
blocklist, a login-name-substring check, a low-variety-pattern check, and
an entropy floor; the widget shows live non-blocking feedback as the user
types. The generated high-entropy passphrase remains the default,
recommended option.
Integration/interop support:
- window.nostr provider now carries a readonly _bitlogin marker
- window.bitlogin ({ version, isActiveSigner() }) is installed
unconditionally as soon as the script loads, independent of login
state or window.nostr's current occupant. Found and fixed a real bug
along the way: an unconditional assignment is required because browsers
auto-expose any element with a matching id/name as a same-named global,
so <bitlogin-auth id="bitlogin"> silently shadowed window.bitlogin
under a naive `if (!window.bitlogin)` guard. Renamed the demo's own
element id to avoid the same trap and documented the gotcha in docs.html
- docs.html: new section on supporting multiple signing methods
(extensions, NIP-46, BitLogin) alongside each other
- Added packages/demo/public/llms.txt for AI-assisted integration
Deployment: GitHub Pages requires a paid plan for private repos, so its
workflow and the .github directory are removed; README documents
Vercel-only deployment and the direct-from-CDN integration path.
Docs: added docs/future-recovery-options.md, laying out passkey-based
2FA/recovery options (pure 2FA, full independent recovery, and a
constrained middle ground) with pros/cons for future consideration —
not a development plan, nothing here is decided.
59/59 tests pass; core and widget typecheck clean; all fixes verified
live in Chromium via Playwright against an in-memory mock relay.
Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
/**
|
|
* Optional manual password support (§9.3). The alpha's default and
|
|
* recommendation is a generated credential (§9.2); a user may opt into
|
|
* typing their own, gated by a hard minimum-entropy estimate, a small
|
|
* known-weak-pattern check, and an explicit warning that offline guessing
|
|
* against a downloaded capsule cannot be prevented by any client-side check
|
|
* (§9.1). This is an alpha-grade heuristic, not a breach-database lookup —
|
|
* BitLogin is static and makes no third-party network calls, so it cannot
|
|
* check against HaveIBeenPwned-style corpora. A production release should
|
|
* use a proper strength estimator (e.g. zxcvbn) and/or a bundled breach list.
|
|
*/
|
|
|
|
export const MANUAL_PASSWORD_MIN_ENTROPY_BITS = 64;
|
|
export const MANUAL_PASSWORD_MIN_LENGTH = 12;
|
|
|
|
const COMMON_WEAK_PASSWORDS = new Set([
|
|
"password", "password1", "password123", "123456", "123456789", "12345678",
|
|
"qwerty", "qwerty123", "letmein", "welcome", "monkey", "dragon", "master",
|
|
"abc123", "iloveyou", "admin", "login", "starwars", "sunshine", "princess",
|
|
"football", "baseball", "trustno1", "000000", "111111", "123123", "1234567890",
|
|
"changeme", "passw0rd", "correcthorsebatterystaple"
|
|
]);
|
|
|
|
export interface ManualPasswordCheck {
|
|
ok: boolean;
|
|
entropyBits: number;
|
|
reason?: string;
|
|
}
|
|
|
|
function characterClassAlphabetSize(password: string): number {
|
|
let size = 0;
|
|
if (/[a-z]/u.test(password)) size += 26;
|
|
if (/[A-Z]/u.test(password)) size += 26;
|
|
if (/[0-9]/u.test(password)) size += 10;
|
|
if (/[^a-zA-Z0-9]/u.test(password)) size += 33; // common printable symbols
|
|
return size || 1;
|
|
}
|
|
|
|
/**
|
|
* A conservative, dependency-free entropy estimate: character-class alphabet
|
|
* size times length. This deliberately under-credits patterned passwords
|
|
* rather than over-trusts them, but it cannot detect every weak pattern —
|
|
* it is combined with the blocklist and structural checks below.
|
|
*/
|
|
export function estimatePasswordEntropyBits(password: string): number {
|
|
return password.length * Math.log2(characterClassAlphabetSize(password));
|
|
}
|
|
|
|
function hasLowVarietyPattern(password: string): boolean {
|
|
if (/^(.)\1*$/u.test(password)) return true; // all one character
|
|
if (/(.)\1{2,}/u.test(password)) return true; // 3+ repeated character run
|
|
const lower = password.toLowerCase();
|
|
const sequences = ["0123456789", "abcdefghijklmnopqrstuvwxyz"];
|
|
for (const seq of sequences) {
|
|
for (let i = 0; i + 4 <= seq.length; i++) {
|
|
const forward = seq.slice(i, i + 4);
|
|
const backward = [...forward].reverse().join("");
|
|
if (lower.includes(forward) || lower.includes(backward)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Validates a user-chosen password against the alpha's manual-password floor (§9.3). */
|
|
export function checkManualPassword(password: string, normalizedLoginName: string): ManualPasswordCheck {
|
|
const entropyBits = estimatePasswordEntropyBits(password);
|
|
|
|
if (password.length < MANUAL_PASSWORD_MIN_LENGTH) {
|
|
return { ok: false, entropyBits, reason: `Must be at least ${MANUAL_PASSWORD_MIN_LENGTH} characters.` };
|
|
}
|
|
if (COMMON_WEAK_PASSWORDS.has(password.toLowerCase())) {
|
|
return { ok: false, entropyBits, reason: "This is on a list of extremely common passwords." };
|
|
}
|
|
if (normalizedLoginName.length >= 3 && password.toLowerCase().includes(normalizedLoginName)) {
|
|
return { ok: false, entropyBits, reason: "Must not contain your login name." };
|
|
}
|
|
if (hasLowVarietyPattern(password)) {
|
|
return { ok: false, entropyBits, reason: "Too predictable (repeated characters or a simple sequence)." };
|
|
}
|
|
if (entropyBits < MANUAL_PASSWORD_MIN_ENTROPY_BITS) {
|
|
return {
|
|
ok: false,
|
|
entropyBits,
|
|
reason: `Estimated entropy (~${entropyBits.toFixed(0)} bits) is below the required ${MANUAL_PASSWORD_MIN_ENTROPY_BITS}. Use a longer password or mix character types.`
|
|
};
|
|
}
|
|
return { ok: true, entropyBits };
|
|
}
|