From acdf0bf0737b24e944897c6cd1cd9b9601982e8e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:16:08 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20plain=20text=20passwords=20in=20AsyncStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: TargetMisser <52361977+TargetMisser@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ src/screens/PasswordScreen.tsx | 30 +++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..40ac753 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,5 @@ + +## 2024-05-24 - Migrate plain text passwords to SecureStore +**Vulnerability:** Passwords in the password manager were stored in plain text in AsyncStorage. +**Learning:** AsyncStorage has a 2048-byte limit on Android, but plain text secrets are vulnerable. Hybrid storage allows managing lists in AsyncStorage with actual secrets stored individually in SecureStore. +**Prevention:** When storing sensitive data, use a hybrid approach to circumvent length limits while encrypting secrets. diff --git a/src/screens/PasswordScreen.tsx b/src/screens/PasswordScreen.tsx index c5338f1..0c6e22f 100644 --- a/src/screens/PasswordScreen.tsx +++ b/src/screens/PasswordScreen.tsx @@ -150,7 +150,28 @@ export default function PasswordScreen() { useEffect(() => { (async () => { const raw = await AsyncStorage.getItem(PASSWORDS_KEY); - if (raw) setEntries(JSON.parse(raw)); + if (raw) { + const loaded: PasswordEntry[] = JSON.parse(raw); + let needsMigration = false; + + const withSecrets = await Promise.all(loaded.map(async (entry) => { + if (entry.password !== '***') { + await SecureStore.setItemAsync(`aerostaff_pwd_${entry.id}`, entry.password); + needsMigration = true; + return entry; + } else { + const secret = await SecureStore.getItemAsync(`aerostaff_pwd_${entry.id}`); + return { ...entry, password: secret || '' }; + } + })); + + setEntries(withSecrets); + + if (needsMigration) { + const masked = withSecrets.map(e => ({ ...e, password: '***' })); + await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(masked)); + } + } const enabled = await AsyncStorage.getItem(PIN_ENABLED_KEY); const isEnabled = enabled === 'true'; setPinEnabled(isEnabled); @@ -160,7 +181,9 @@ export default function PasswordScreen() { const persist = useCallback(async (next: PasswordEntry[]) => { setEntries(next); - await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(next)); + await Promise.all(next.map(e => SecureStore.setItemAsync(`aerostaff_pwd_${e.id}`, e.password))); + const masked = next.map(e => ({ ...e, password: '***' })); + await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(masked)); }, []); // PIN toggle @@ -243,9 +266,10 @@ export default function PasswordScreen() { { text: 'Annulla', style: 'cancel' }, { text: 'Elimina', style: 'destructive', onPress: async () => { await persist(entries.filter(e => e.id !== id)); + await SecureStore.deleteItemAsync(`aerostaff_pwd_${id}`).catch(() => {}); }}, ]); - }, [entries, persist]); + }, [entries, persist, t]); // PIN overlays (setup and unlock) if (pinMode === 'unlock') {