Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2026-04-20 - Insecure Password Storage Migration
**Vulnerability:** User passwords were saved in plain text in `AsyncStorage`, an unencrypted storage mechanism.
**Learning:** The entire list of passwords was originally stored in `AsyncStorage` because `SecureStore` has a 2048-byte limit on Android, which makes storing large JSON arrays of passwords fail. This led to storing secrets in plain text as a naive workaround.
**Prevention:** Use a hybrid storage pattern: lists of metadata are stored in `AsyncStorage` with secrets masked (e.g., `password: '***'`), and the actual secrets are stored individually in `SecureStore` using row-specific dynamic keys (e.g., `aerostaff_pwd_${id}`). When migrating, always overwrite the legacy unencrypted data with masked values immediately to prevent plain text persistence.
27 changes: 24 additions & 3 deletions src/screens/PasswordScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,25 @@ export default function PasswordScreen() {
useEffect(() => {
(async () => {
const raw = await AsyncStorage.getItem(PASSWORDS_KEY);
if (raw) setEntries(JSON.parse(raw));
if (raw) {
const parsed = JSON.parse(raw);
let migrated = false;
const loaded = await Promise.all(parsed.map(async (e: PasswordEntry) => {
if (e.password !== '***') {
await SecureStore.setItemAsync(`aerostaff_pwd_${e.id}`, e.password);
migrated = true;
return e;
} else {
const pwd = await SecureStore.getItemAsync(`aerostaff_pwd_${e.id}`);
return { ...e, password: pwd || '' };
}
}));
setEntries(loaded);
if (migrated) {
const masked = loaded.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);
Expand All @@ -160,7 +178,9 @@ export default function PasswordScreen() {

const persist = useCallback(async (next: PasswordEntry[]) => {
setEntries(next);
await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(next));
const masked = next.map(e => ({ ...e, password: '***' }));
await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(masked));
await Promise.all(next.map(e => SecureStore.setItemAsync(`aerostaff_pwd_${e.id}`, e.password)));
}, []);

// PIN toggle
Expand Down Expand Up @@ -243,9 +263,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') {
Expand Down
Loading