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
This commit was merged in pull request #677.
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -980,6 +980,23 @@
|
||||
</button>
|
||||
</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">
|
||||
<h3 class="font-bold mb-1">Tracked Tokens</h3>
|
||||
<p class="text-xs text-muted mb-2">
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user