From be3e82ded22623fa39f08d58086fd8e83e7b9808 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 26 Oct 2026 20:49:27 +0000 Subject: [PATCH] feat: an "Allow weak passwords" setting lets a new wallet have a short password (closes #674) Settings gains a Passwords section with an "Allow weak passwords" checkbox. It is off in a new profile, and only a stored true turns it on. 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. The README describes the setting under Settings and AddWallet. Its Key Decisions section now states the Argon2id parameters and why they are light. The user guide lists the setting under Settings. 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 }], -- 2.54.0