From 387f40689e76fe9a12abe1494f58430421cbe154 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 26 Oct 2026 22:35:02 +0100 Subject: [PATCH] feat: an "Allow weak passwords" setting lets a new wallet have a short password (closes #674) Settings has a new "Allow weak passwords" checkbox, off in a new profile. While it is off, AddWallet refuses a password shorter than 12 characters, as before. While it is on, AddWallet accepts any password that is not empty. Key derivation is unchanged: one Argon2id derivation at libsodium's interactive setting. README's Key Decisions now states those parameters and why one light derivation is enough: the password's length is what protects the stored secret. The screens say "password", as README's wording rules require, so the setting is "Allow weak passwords". Model: opus-5-5 --- README.md | 29 +++++++--- docs/README.md | 2 + src/popup/index.html | 17 ++++++ src/popup/views/addWallet.js | 3 +- src/popup/views/settings.js | 6 ++ src/shared/persistedState.js | 6 ++ tests/allowWeakPasswords.test.js | 87 ++++++++++++++++++++++++++++ tests/persistedFieldContract.test.js | 11 +++- 8 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 tests/allowWeakPasswords.test.js diff --git a/README.md b/README.md index 4e627c2..2e64716 100644 --- a/README.md +++ b/README.md @@ -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 would hear of it. The hint line reserves its height, so switching tabs 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 to a screen whose button already starts near the bottom of the popup - **Transitions**: - - "Import" with a valid entry and a matching password of at least 12 - characters → creates the wallet, clears the navigation stack, and → - **Home**. When no address is active yet, as on the first wallet, the new - wallet's first address becomes the active one. The phrase and xprv modes - then scan for further used addresses and report the count as a flash - message. When the scan finds two or more used addresses, they replace the - wallet's list, which then holds the first address only if it was used. If - the first address was active and is not in the new list, the new list's - first address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is + - "Import" with a valid entry and a matching password that is long enough → + creates the wallet, clears the navigation stack, and → **Home**. When no + address is active yet, as on the first wallet, the new wallet's first + address becomes the active one. The phrase and xprv modes then scan for + further used addresses and report the count as a flash message. When the + scan finds two or more used addresses, they replace the wallet's list, + which then holds the first address only if it was used. If the first + address was active and is not in the new list, the new list's first + address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is broadcast. - "Import" with a missing, short or mismatched password → full-sentence 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 `[recovery phrase]` button on HD wallets only, and an `[x]` delete 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 `[x]` remove button, plus a "+ Add token" button - 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 256-bit encryption key. Argon2id is memory-hard, making GPU/ASIC brute 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 (`crypto_secretbox`), which provides authenticated encryption (the ciphertext cannot be tampered with without detection). diff --git a/docs/README.md b/docs/README.md index 560f3d7..8349567 100644 --- a/docs/README.md +++ b/docs/README.md @@ -425,6 +425,8 @@ see everything unfiltered. Click the gear icon on the home screen to access settings: - **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 token". - **Display**: Toggle whether tracked tokens with zero balance are shown, switch diff --git a/src/popup/index.html b/src/popup/index.html index bc758fb..81e3eb2 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -980,6 +980,23 @@ +
+

Passwords

+ +

+ Lets you choose a password shorter than 12 characters + when you add a wallet. +

+
+

Tracked Tokens

diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js index a339dae..b5629b5 100644 --- a/src/popup/views/addWallet.js +++ b/src/popup/views/addWallet.js @@ -132,7 +132,8 @@ function validatePassword() { showError("add-wallet-password-error", "Please choose a password."); return null; } - if (pw.length < 12) { + // "Allow weak passwords" in Settings lifts the 12-character minimum. + if (!state.allowWeakPasswords && pw.length < 12) { showError( "add-wallet-password-error", "Password must be at least 12 characters.", diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 1162a64..40da526 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -499,6 +499,12 @@ function init(_ctx) { 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-settings-add-token").addEventListener( diff --git a/src/shared/persistedState.js b/src/shared/persistedState.js index f6ced9f..6550e9d 100644 --- a/src/shared/persistedState.js +++ b/src/shared/persistedState.js @@ -56,6 +56,9 @@ const DEFAULT_STATE = { // else. useBlockscout: false, checkRecipientsOnEtherscan: false, + // "Allow weak passwords" in Settings. While it is off, AddWallet refuses a + // password shorter than 12 characters. + allowWeakPasswords: false, fraudContracts: [], tokenHolderCache: {}, theme: "system", @@ -418,6 +421,9 @@ function normalizePersisted(saved) { // service. out.useBlockscout = saved.useBlockscout === 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 // fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())` // and filterTransactions() maps the same list through normalizeAddress(), diff --git a/tests/allowWeakPasswords.test.js b/tests/allowWeakPasswords.test.js new file mode 100644 index 0000000..a298ab7 --- /dev/null +++ b/tests/allowWeakPasswords.test.js @@ -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"]); + }, + ); +}); diff --git a/tests/persistedFieldContract.test.js b/tests/persistedFieldContract.test.js index ec871df..d1e6c94 100644 --- a/tests/persistedFieldContract.test.js +++ b/tests/persistedFieldContract.test.js @@ -629,8 +629,15 @@ const CONTRACT = [ // the falsy answer is also the DEFAULT_STATE default. falsy: [0], })), - // Each lets a request reach another service, so anything but true is off. - ...["useBlockscout", "checkRecipientsOnEtherscan"].map((field) => ({ + // Anything but true is off for these three settings: + // - useBlockscout and checkRecipientsOnEtherscan let a request reach + // another service. + // - allowWeakPasswords lets AddWallet accept a short password. + ...[ + "useBlockscout", + "checkRecipientsOnEtherscan", + "allowWeakPasswords", + ].map((field) => ({ field, kind: KIND.SCALAR, hostile: [42, "notabool", { a: 1 }],