feat: an "Allow weak passwords" setting lets a new wallet have a short password (closes #674) #677

Merged
clawbot merged 1 commits from issue-674-allow-weak-passphrases into next 2026-10-26 22:35:03 +01:00
8 changed files with 149 additions and 12 deletions
+20 -9
View File
@@ -1787,18 +1787,20 @@ view would leave a wallet one click from deletion.
without it, the lost-password route on DeleteWallet is the first they without it, the lost-password route on DeleteWallet is the first they
would hear of it. The hint line reserves its height, so switching tabs would hear of it. The hint line reserves its height, so switching tabs
cannot move the password fields under the pointer. cannot move the password fields under the pointer.
- The password needs at least 12 characters. With "Allow weak passwords"
ticked in Settings, one character is enough.
- "Import" button, with the error line beside it so that it adds no height - "Import" button, with the error line beside it so that it adds no height
to a screen whose button already starts near the bottom of the popup to a screen whose button already starts near the bottom of the popup
- **Transitions**: - **Transitions**:
- "Import" with a valid entry and a matching password of at least 12 - "Import" with a valid entry and a matching password that is long enough →
characters → creates the wallet, clears the navigation stack, and → creates the wallet, clears the navigation stack, and → **Home**. When no
**Home**. When no address is active yet, as on the first wallet, the new address is active yet, as on the first wallet, the new wallet's first
wallet's first address becomes the active one. The phrase and xprv modes address becomes the active one. The phrase and xprv modes then scan for
then scan for further used addresses and report the count as a flash further used addresses and report the count as a flash message. When the
message. When the scan finds two or more used addresses, they replace the scan finds two or more used addresses, they replace the wallet's list,
wallet's list, which then holds the first address only if it was used. If which then holds the first address only if it was used. If the first
the first address was active and is not in the new list, the new list's address was active and is not in the new list, the new list's first
first address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is
broadcast. broadcast.
- "Import" with a missing, short or mismatched password → full-sentence - "Import" with a missing, short or mismatched password → full-sentence
error on the error line, no screen change error on the error line, no screen change
@@ -2235,6 +2237,10 @@ view would leave a wallet one click from deletion.
- Wallets: one row per wallet with its name (tap to rename inline), a - Wallets: one row per wallet with its name (tap to rename inline), a
`[recovery phrase]` button on HD wallets only, and an `[x]` delete button, `[recovery phrase]` button on HD wallets only, and an `[x]` delete button,
plus a "+ Add wallet" button plus a "+ Add wallet" button
- Passwords:
- "Allow weak passwords" checkbox, unticked until the user ticks it
- A sentence saying the setting lets the user choose a password shorter
than 12 characters on AddWallet
- Tracked Tokens: one row per token tracked on the active network with an - Tracked Tokens: one row per token tracked on the active network with an
`[x]` remove button, plus a "+ Add token" button `[x]` remove button, plus a "+ Add token" button
- Display: "Show tracked tokens with zero balance" checkbox, "UTC - Display: "Show tracked tokens with zero balance" checkbox, "UTC
@@ -3061,6 +3067,11 @@ logged.
- The user's password is run through Argon2id (`crypto_pwhash`) to derive a - The user's password is run through Argon2id (`crypto_pwhash`) to derive a
256-bit encryption key. Argon2id is memory-hard, making GPU/ASIC brute 256-bit encryption key. Argon2id is memory-hard, making GPU/ASIC brute
force attacks expensive. force attacks expensive.
- Each use of the password runs one Argon2id derivation at libsodium's
interactive setting: `OPSLIMIT_INTERACTIVE` and `MEMLIMIT_INTERACTIVE`, 2
passes over 64 MiB. Every signature asks for the password, so a light
setting keeps signing quick. The password's length, not the derivation's
cost, is what protects the stored secret.
- The derived key encrypts the secret material using XSalsa20-Poly1305 - The derived key encrypts the secret material using XSalsa20-Poly1305
(`crypto_secretbox`), which provides authenticated encryption (the (`crypto_secretbox`), which provides authenticated encryption (the
ciphertext cannot be tampered with without detection). ciphertext cannot be tampered with without detection).
+2
View File
@@ -425,6 +425,8 @@ see everything unfiltered.
Click the gear icon on the home screen to access settings: Click the gear icon on the home screen to access settings:
- **Wallets**: Your wallets, and "+ Add wallet". - **Wallets**: Your wallets, and "+ Add wallet".
- **Passwords**: Tick "Allow weak passwords" to choose a password shorter than
12 characters when you add a wallet.
- **Tracked Tokens**: The ERC-20 tokens tracked across all addresses, and "+ Add - **Tracked Tokens**: The ERC-20 tokens tracked across all addresses, and "+ Add
token". token".
- **Display**: Toggle whether tracked tokens with zero balance are shown, switch - **Display**: Toggle whether tracked tokens with zero balance are shown, switch
+17
View File
@@ -980,6 +980,23 @@
</button> </button>
</div> </div>
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Passwords</h3>
<label
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
>
<input
type="checkbox"
id="settings-allow-weak-passwords"
/>
Allow weak passwords
</label>
<p class="text-xs text-muted">
Lets you choose a password shorter than 12 characters
when you add a wallet.
</p>
</div>
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Tracked Tokens</h3> <h3 class="font-bold mb-1">Tracked Tokens</h3>
<p class="text-xs text-muted mb-2"> <p class="text-xs text-muted mb-2">
+2 -1
View File
@@ -132,7 +132,8 @@ function validatePassword() {
showError("add-wallet-password-error", "Please choose a password."); showError("add-wallet-password-error", "Please choose a password.");
return null; return null;
} }
if (pw.length < 12) { // "Allow weak passwords" in Settings lifts the 12-character minimum.
if (!state.allowWeakPasswords && pw.length < 12) {
showError( showError(
"add-wallet-password-error", "add-wallet-password-error",
"Password must be at least 12 characters.", "Password must be at least 12 characters.",
+6
View File
@@ -499,6 +499,12 @@ function init(_ctx) {
await saveState(); await saveState();
}); });
$("settings-allow-weak-passwords").checked = state.allowWeakPasswords;
$("settings-allow-weak-passwords").addEventListener("change", async () => {
state.allowWeakPasswords = $("settings-allow-weak-passwords").checked;
await saveState();
});
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView); $("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
$("btn-settings-add-token").addEventListener( $("btn-settings-add-token").addEventListener(
+6
View File
@@ -56,6 +56,9 @@ const DEFAULT_STATE = {
// else. // else.
useBlockscout: false, useBlockscout: false,
checkRecipientsOnEtherscan: false, checkRecipientsOnEtherscan: false,
// "Allow weak passwords" in Settings. While it is off, AddWallet refuses a
// password shorter than 12 characters.
allowWeakPasswords: false,
fraudContracts: [], fraudContracts: [],
tokenHolderCache: {}, tokenHolderCache: {},
theme: "system", theme: "system",
@@ -418,6 +421,9 @@ function normalizePersisted(saved) {
// service. // service.
out.useBlockscout = saved.useBlockscout === true; out.useBlockscout = saved.useBlockscout === true;
out.checkRecipientsOnEtherscan = saved.checkRecipientsOnEtherscan === true; out.checkRecipientsOnEtherscan = saved.checkRecipientsOnEtherscan === true;
// Anything but true is off: the setting lets AddWallet accept a short
// password.
out.allowWeakPasswords = saved.allowWeakPasswords === true;
// A list of contract addresses, floored the same way: send.js builds its // A list of contract addresses, floored the same way: send.js builds its
// fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())` // fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())`
// and filterTransactions() maps the same list through normalizeAddress(), // and filterTransactions() maps the same list through normalizeAddress(),
+87
View File
@@ -0,0 +1,87 @@
// "Allow weak passwords" in Settings is off until the user ticks it. While it
// is off, AddWallet refuses a password shorter than 12 characters. While it is
// on, AddWallet accepts any password that is not empty
// (https://git.eeqj.de/sneak/AutistMask/issues/674). These boot the real popup,
// set the checkbox in Settings as the user does, and then add a wallet.
jest.mock("../src/shared/vault", () => ({
decryptWithPassword: jest.fn(),
encryptWithPassword: jest.fn(async () => ({
salt: "salt",
nonce: "nonce",
ciphertext: "ciphertext",
})),
}));
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
} = require("./support/popupBoot");
const CHECKBOX = "settings-allow-weak-passwords";
const ERROR = "add-wallet-password-error";
afterEach(() => {
cleanupPopup();
});
// Boots a popup that holds one wallet and opens Settings, where the checkbox
// starts unticked. Ticks it when `allow` is true. Then opens AddWallet and
// presses the die for a recovery phrase.
async function openAddWallet(allow) {
const page = await bootPopup(unversionedValidProfile());
await page.click("btn-settings");
const checkbox = page.node(CHECKBOX);
expect(checkbox.checked).toBe(false);
if (allow) {
checkbox.checked = true;
for (const fn of checkbox.listeners.change) await fn();
expect(page.storage.read("autistmask").allowWeakPasswords).toBe(true);
}
await page.click("btn-main-add-wallet");
await page.click("btn-generate-phrase");
return page;
}
// Types `password` into both password fields and presses Import.
async function importWith(page, password) {
page.node("add-wallet-password").value = password;
page.node("add-wallet-password-confirm").value = password;
await page.click("btn-add-wallet-confirm");
}
function storedWalletCount(page) {
return page.storage.read("autistmask").wallets.length;
}
describe("Allow weak passwords", () => {
test("unticked: a password shorter than 12 characters is refused", async () => {
const page = await openAddWallet(false);
await importWith(page, "short");
expect(page.text(ERROR)).toBe(
"Password must be at least 12 characters.",
);
expect(storedWalletCount(page)).toBe(1);
expect(page.visibleViews()).toEqual(["add-wallet"]);
});
test("ticked: a password shorter than 12 characters is accepted", async () => {
const page = await openAddWallet(true);
await importWith(page, "short");
expect(page.text(ERROR)).toBe("");
expect(storedWalletCount(page)).toBe(2);
expect(page.visibleViews()).toEqual(["main"]);
});
test.each([false, true])(
"an empty password is refused (ticked: %s)",
async (allow) => {
const page = await openAddWallet(allow);
await importWith(page, "");
expect(page.text(ERROR)).toBe("Please choose a password.");
expect(storedWalletCount(page)).toBe(1);
expect(page.visibleViews()).toEqual(["add-wallet"]);
},
);
});
+9 -2
View File
@@ -629,8 +629,15 @@ const CONTRACT = [
// the falsy answer is also the DEFAULT_STATE default. // the falsy answer is also the DEFAULT_STATE default.
falsy: [0], falsy: [0],
})), })),
// Each lets a request reach another service, so anything but true is off. // Anything but true is off for these three settings:
...["useBlockscout", "checkRecipientsOnEtherscan"].map((field) => ({ // - useBlockscout and checkRecipientsOnEtherscan let a request reach
// another service.
// - allowWeakPasswords lets AddWallet accept a short password.
...[
"useBlockscout",
"checkRecipientsOnEtherscan",
"allowWeakPasswords",
].map((field) => ({
field, field,
kind: KIND.SCALAR, kind: KIND.SCALAR,
hostile: [42, "notabool", { a: 1 }], hostile: [42, "notabool", { a: 1 }],