Three faults, all on the same screen, all only visible once passwords were actually turned off — which until now nobody had done on a live instance. wp-login.php runs wp_signon() on its way to drawing the form, even for a plain GET. Core tolerates that by discarding the result, but only when the error codes are exactly the pair its own password filters produce. Removing those filters is the whole point of turning passwords off, so what was left behind was the generic authentication_failed — printed, in red, above a form with no username and no password, on a site that has neither. Every visitor was told their sign-in had failed before they tried. The paste-a-signature path had no way to submit. It borrowed core's login button, and the stylesheet that retires the password form hid that button too. The one route for people whose keys live on another machine — sign in Electron Cash, paste the result — was therefore broken exactly on the sites most committed to not having passwords. It now carries its own plain submit button, so it also works on the screens where there is no core login form to borrow from, and with JavaScript off. And the username field outlived the password field, although authenticate() never reads it either. "Username or Email Address" sat at the top of a wallet login form: the one thing this fork exists to remove, in the first place anyone looks. Both go now, with their required attributes cleared, because a hidden required field does not fail quietly — it refuses the submit and points an error bubble at a control nobody can see.
232 lines
8.5 KiB
JavaScript
232 lines
8.5 KiB
JavaScript
// Wiring for the sign-in and registration screens.
|
|
//
|
|
// This file contains no cryptography — wallet.js does all of that. What it
|
|
// does is decide which of the three ways to sign is on offer and get out of
|
|
// the way of the other two:
|
|
//
|
|
// 1. A wallet the browser already has (Theseus exposes one). Best case: the
|
|
// key never comes near this page.
|
|
// 2. A recovery phrase typed here, used once, and wiped.
|
|
// 3. A signature produced somewhere else entirely and pasted in. This one
|
|
// works with JavaScript switched off, which is why the markup ships with
|
|
// the paste box present and the buttons hidden until this script decides
|
|
// they are usable.
|
|
//
|
|
// The progressive-enhancement direction matters: buttons start hidden and are
|
|
// revealed, rather than starting visible and being disabled. A script that
|
|
// fails to load leaves a page that still works.
|
|
|
|
(() => {
|
|
"use strict";
|
|
|
|
const CONFIG = window.SIRIUS_PRESS || {};
|
|
const strings = CONFIG.strings || {};
|
|
|
|
function setStatus(block, text, kind = "") {
|
|
const el = block.querySelector(".sirius-wallet__status");
|
|
if (!el) return;
|
|
el.textContent = text || "";
|
|
el.className = "sirius-wallet__status" + (kind ? " is-" + kind : "");
|
|
}
|
|
|
|
/** Submit the form the block sits in, once a signature is in place. */
|
|
function submitForm(block) {
|
|
const form = block.closest("form");
|
|
if (!form) return;
|
|
|
|
// WordPress marks its username and password inputs `required`, and the
|
|
// wallet path deliberately leaves both empty — the signature is the
|
|
// credential. Constraint validation would otherwise refuse the submit and
|
|
// point a "Please fill out this field" bubble at an input the visitor is
|
|
// not supposed to touch, with the signature already sitting in the form.
|
|
//
|
|
// Only reached from the wallet buttons, so password sign-in keeps its
|
|
// validation.
|
|
form.noValidate = true;
|
|
|
|
if (typeof form.requestSubmit === "function") {
|
|
form.requestSubmit();
|
|
} else {
|
|
form.submit();
|
|
}
|
|
}
|
|
|
|
function setSignature(block, signature, address) {
|
|
const field = block.querySelector(".sirius-wallet__signature");
|
|
if (field) field.value = signature;
|
|
// The address the signature claims. The server compares it against the
|
|
// one it recovers, which is the only way a signature over the wrong text
|
|
// becomes an error instead of a different identity.
|
|
const claim = block.querySelector(".sirius-wallet__address");
|
|
if (claim && address) claim.value = address;
|
|
}
|
|
|
|
/** On the paste path the visitor supplies the address themselves. */
|
|
function adoptTypedAddress(block) {
|
|
const typed = block.querySelector(".sirius-wallet__address-input");
|
|
const claim = block.querySelector(".sirius-wallet__address");
|
|
if (typed && claim && typed.value.trim() && !claim.value) {
|
|
claim.value = typed.value.trim();
|
|
}
|
|
}
|
|
|
|
async function signWithPhrase(block) {
|
|
const input = block.querySelector(".sirius-wallet__phrase-input");
|
|
const message = block.querySelector(".sirius-wallet__message").value;
|
|
const phrase = input ? input.value : "";
|
|
|
|
const check = window.SiriusWallet.validatePhrase(phrase);
|
|
if (!check.ok) {
|
|
setStatus(block, check.error, "error");
|
|
return;
|
|
}
|
|
|
|
setStatus(block, strings.signing || "Signing…");
|
|
let wallet;
|
|
try {
|
|
wallet = await window.SiriusWallet.fromPhrase(phrase, {
|
|
prefix: CONFIG.prefix,
|
|
path: CONFIG.path,
|
|
});
|
|
setSignature(block, await wallet.sign(message), wallet.address);
|
|
} catch (err) {
|
|
setStatus(block, err.message || String(err), "error");
|
|
return;
|
|
} finally {
|
|
// The phrase has done its one job. Clear it from the field and from the
|
|
// wallet object before anything else on the page runs.
|
|
if (input) input.value = "";
|
|
if (wallet && wallet.forget) wallet.forget();
|
|
}
|
|
submitForm(block);
|
|
}
|
|
|
|
async function signWithExternal(block) {
|
|
const external = window.SiriusWallet.external();
|
|
if (!external) {
|
|
setStatus(block, strings.noWallet || "No wallet found in this browser.", "error");
|
|
return;
|
|
}
|
|
const message = block.querySelector(".sirius-wallet__message").value;
|
|
setStatus(block, strings.signing || "Signing…");
|
|
try {
|
|
setSignature(block, await external.sign(message), await external.address());
|
|
} catch (err) {
|
|
// A user declining the wallet's approval dialog is a normal outcome, not
|
|
// an error worth shouting about.
|
|
const text = err && err.message ? err.message : String(err);
|
|
setStatus(block, /reject/i.test(text) ? "" : text, /reject/i.test(text) ? "" : "error");
|
|
return;
|
|
}
|
|
submitForm(block);
|
|
}
|
|
|
|
function enhance(block) {
|
|
// Whatever happens next, a hand-typed address must reach the server.
|
|
const form = block.closest("form");
|
|
if (form) form.addEventListener("submit", () => adoptTypedAddress(block));
|
|
|
|
const signButton = block.querySelector(".sirius-wallet__sign");
|
|
const externalButton = block.querySelector(".sirius-wallet__external");
|
|
const phraseArea = block.querySelector(".sirius-wallet__phrase");
|
|
|
|
if (!window.SiriusWallet || !window.crypto || !window.crypto.subtle) {
|
|
// No usable crypto — for instance an http:// origin, where WebCrypto is
|
|
// unavailable. Say so, and leave the paste path alone: it still works.
|
|
setStatus(
|
|
block,
|
|
"This browser cannot sign here (a secure https connection is required). Paste a signature from your own wallet instead.",
|
|
"error",
|
|
);
|
|
const manual = block.querySelector(".sirius-wallet__manual");
|
|
if (manual) manual.open = true;
|
|
return;
|
|
}
|
|
|
|
if (phraseArea) phraseArea.hidden = false;
|
|
if (signButton) {
|
|
signButton.hidden = false;
|
|
signButton.addEventListener("click", () => signWithPhrase(block));
|
|
}
|
|
if (externalButton && window.SiriusWallet.external()) {
|
|
externalButton.hidden = false;
|
|
externalButton.addEventListener("click", () => signWithExternal(block));
|
|
}
|
|
|
|
// Enter in the phrase box should sign, not submit an unsigned form.
|
|
const input = block.querySelector(".sirius-wallet__phrase-input");
|
|
if (input) {
|
|
input.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
event.preventDefault();
|
|
signWithPhrase(block);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** The "which address is this phrase?" helper on the recovery page. */
|
|
function enhanceDerive() {
|
|
const button = document.getElementById("sirius_derive_go");
|
|
const input = document.getElementById("sirius_derive_phrase");
|
|
const out = document.querySelector(".sirius-derive__out");
|
|
if (!button || !input || !out) return;
|
|
|
|
button.addEventListener("click", async () => {
|
|
const check = window.SiriusWallet.validatePhrase(input.value);
|
|
if (!check.ok) {
|
|
out.textContent = check.error;
|
|
out.className = "sirius-derive__out is-error";
|
|
return;
|
|
}
|
|
out.textContent = strings.signing || "Working…";
|
|
out.className = "sirius-derive__out";
|
|
try {
|
|
const wallet = await window.SiriusWallet.fromPhrase(input.value, {
|
|
prefix: CONFIG.prefix,
|
|
path: CONFIG.path,
|
|
});
|
|
out.textContent = wallet.address;
|
|
out.className = "sirius-derive__out is-address";
|
|
wallet.forget();
|
|
} catch (err) {
|
|
out.textContent = err.message || String(err);
|
|
out.className = "sirius-derive__out is-error";
|
|
} finally {
|
|
input.value = "";
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Let the form submit once core's credential fields have been retired.
|
|
*
|
|
* With passwords off they are hidden, unread and still marked `required`,
|
|
* and a hidden required field does not fail quietly: the browser refuses
|
|
* the submit and tries to focus a control that is not on screen. The wallet
|
|
* buttons work around this with `noValidate`, but the paste-a-signature
|
|
* path uses the form's own submit button and never reaches that code.
|
|
*/
|
|
function retireCredentialFields() {
|
|
if (CONFIG.passwords !== false) return;
|
|
["user_login", "user_pass"].forEach((id) => {
|
|
const field = document.getElementById(id);
|
|
if (!field) return;
|
|
field.required = false;
|
|
field.removeAttribute("required");
|
|
field.removeAttribute("autocomplete");
|
|
});
|
|
}
|
|
|
|
function start() {
|
|
retireCredentialFields();
|
|
document.querySelectorAll(".sirius-wallet").forEach(enhance);
|
|
enhanceDerive();
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", start);
|
|
} else {
|
|
start();
|
|
}
|
|
})();
|