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-25 - Cleartext Password Storage in AsyncStorage
**Vulnerability:** Passwords in the PasswordScreen were stored in plain text in `AsyncStorage`, which is unencrypted and accessible via backups or physical device access.
**Learning:** To handle the 2048-byte limit of `SecureStore` on Android while securing sensitive data, a hybrid storage pattern is required.
**Prevention:** Migrate the legacy unencrypted data to the secure store (`SecureStore`) using row-specific dynamic keys (e.g., `aerostaff_pwd_${id}`), and immediately overwrite the `AsyncStorage` values with masked strings (e.g., `***`) to prevent sensitive data from lingering in plain text.
38 changes: 35 additions & 3 deletions src/screens/PasswordScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,31 @@ export default function PasswordScreen() {
// Load on mount
useEffect(() => {
(async () => {
const raw = await AsyncStorage.getItem(PASSWORDS_KEY);
if (raw) setEntries(JSON.parse(raw));
try {
const raw = await AsyncStorage.getItem(PASSWORDS_KEY);
if (raw) {
const parsed = JSON.parse(raw);
let needsMigration = false;
const hydrated = await Promise.all(parsed.map(async (e: PasswordEntry) => {
if (e.password === '***') {
const sec = await SecureStore.getItemAsync(`aerostaff_pwd_${e.id}`);
return { ...e, password: sec || '' };
} else {
needsMigration = true;
if (e.password) {
await SecureStore.setItemAsync(`aerostaff_pwd_${e.id}`, e.password);
}
return e;
}
}));
setEntries(hydrated);
if (needsMigration) {
const masked = hydrated.map(e => ({ ...e, password: '***' }));
await AsyncStorage.setItem(PASSWORDS_KEY, JSON.stringify(masked));
}
}
} catch(e) { console.error('Error loading passwords', e); }

const enabled = await AsyncStorage.getItem(PIN_ENABLED_KEY);
const isEnabled = enabled === 'true';
setPinEnabled(isEnabled);
Expand All @@ -160,7 +183,15 @@ export default function PasswordScreen() {

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

// PIN toggle
Expand Down Expand Up @@ -243,6 +274,7 @@ 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]);
Expand Down
Loading