From 51043904b823779c835ad8191659531d8f4e768e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:19:48 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Enhance=20m?= =?UTF-8?q?aster=20password=20strength=20requirements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Increase UI minimum password length to 12 characters - Add complexity check for at least 4 unique characters - Implement defense-in-depth validation in crypto library (8 char min) - Centralize password limits in constants.js - Update Sentinel journal with tiered validation learnings Co-authored-by: apsolut <1828768+apsolut@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ components/EncryptionModal.jsx | 24 ++++++++++++++++++------ lib/constants.js | 2 ++ lib/crypto.js | 12 +++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index e5a3dcc..173d6d3 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -9,3 +9,8 @@ **Vulnerability:** Missing input length limits on snippets and projects, leading to potential client-side DoS or memory exhaustion. **Learning:** In a client-side application where all data is kept in memory (and localStorage), excessively large inputs can degrade performance or crash the browser. Input validation must happen at both the UI level and during data ingestion (imports). **Prevention:** Implement `maxLength` on all user-facing inputs and enforce the same limits in JSON parsing/import logic to ensure data stays within safe bounds. + +## 2025-05-16 - Tiered Password Validation for Defense in Depth +**Vulnerability:** Risk of users choosing weak master passwords, undermining the security of XSalsa20-Poly1305 encryption. +**Learning:** Security standards evolve faster than user habits. Enforcing a new, higher minimum length (12 chars) in the UI while maintaining a lower "legacy" minimum (8 chars) in the crypto library allows for improved security for new users without locking out existing ones. +**Prevention:** Always implement a tiered validation approach: strict in the UI for new data/actions, and defense-in-depth in the core logic that preserves backward compatibility for existing data. diff --git a/components/EncryptionModal.jsx b/components/EncryptionModal.jsx index b5a72c0..d38c2df 100644 --- a/components/EncryptionModal.jsx +++ b/components/EncryptionModal.jsx @@ -27,8 +27,14 @@ export function EncryptionSetupModal({ open, onClose, onSetup }) { const handleSetup = async () => { setError(''); - if (password.length < 8) { - setError('Password must be at least 8 characters'); + if (password.length < LIMITS.MASTER_PASSWORD_MIN) { + setError(`Password must be at least ${LIMITS.MASTER_PASSWORD_MIN} characters`); + return; + } + + const uniqueChars = new Set(password).size; + if (uniqueChars < 4) { + setError('Password must contain at least 4 unique characters'); return; } @@ -75,7 +81,7 @@ export function EncryptionSetupModal({ open, onClose, onSetup }) {
setPassword(e.target.value)} maxLength={LIMITS.MASTER_PASSWORD} @@ -355,8 +361,14 @@ export function ChangePasswordModal({ open, onClose, onChange }) { const handleChange = async () => { setError(''); - if (newPassword.length < 8) { - setError('New password must be at least 8 characters'); + if (newPassword.length < LIMITS.MASTER_PASSWORD_MIN) { + setError(`New password must be at least ${LIMITS.MASTER_PASSWORD_MIN} characters`); + return; + } + + const uniqueChars = new Set(newPassword).size; + if (uniqueChars < 4) { + setError('New password must contain at least 4 unique characters'); return; } @@ -419,7 +431,7 @@ export function ChangePasswordModal({ open, onClose, onChange }) { setNewPassword(e.target.value)} maxLength={LIMITS.MASTER_PASSWORD} diff --git a/lib/constants.js b/lib/constants.js index 1cd3a86..b4b3fba 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -10,6 +10,8 @@ export const LIMITS = { FIELD_LABEL: 50, FIELD_VALUE: 10000, MASTER_PASSWORD: 128, + MASTER_PASSWORD_MIN: 12, + MASTER_PASSWORD_LEGACY_MIN: 8, MAX_PROJECTS: 50, MAX_SNIPPETS_PER_PROJECT: 200, MAX_FIELDS_PER_SNIPPET: 20, diff --git a/lib/crypto.js b/lib/crypto.js index 2994db3..f3c0a77 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -1,6 +1,6 @@ import nacl from 'tweetnacl'; import naclUtil from 'tweetnacl-util'; -import { SECURITY } from './constants'; +import { SECURITY, LIMITS } from './constants'; const { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } = naclUtil; // Constants @@ -57,6 +57,11 @@ export function generateNonce() { * Returns base64 encoded: salt + nonce + ciphertext */ export async function encrypt(plaintext, password) { + // Defense-in-depth: enforce legacy minimum length + if (!password || password.length < LIMITS.MASTER_PASSWORD_LEGACY_MIN) { + throw new Error(`Password must be at least ${LIMITS.MASTER_PASSWORD_LEGACY_MIN} characters`); + } + const salt = generateSalt(); const key = await deriveKey(password, salt); const nonce = generateNonce(); @@ -114,6 +119,11 @@ export async function decrypt(encryptedData, password) { * This is stored to verify the password is correct without storing it */ export async function createPasswordHash(password) { + // Defense-in-depth: enforce legacy minimum length + if (!password || password.length < LIMITS.MASTER_PASSWORD_LEGACY_MIN) { + throw new Error(`Password must be at least ${LIMITS.MASTER_PASSWORD_LEGACY_MIN} characters`); + } + const salt = generateSalt(); const key = await deriveKey(password, salt);