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 @@
## 2024-05-24 - Fix Insecure Password Storage
**Vulnerability:** User passwords were saved to `AsyncStorage` in plain text.
**Learning:** `AsyncStorage` is unencrypted and exposes sensitive data on rooted/jailbroken devices or via app backup extraction. However, `SecureStore` on Android has a 2048-byte limit, which is too small to store an entire list of passwords in one go.
**Prevention:** Always use `SecureStore` (or equivalent encrypted storage) for sensitive secrets. When dealing with lists of sensitive objects that may exceed the 2048-byte limit, implement a hybrid storage pattern: store the metadata list in `AsyncStorage` with secrets masked (e.g., `password: '***'`), and store each actual secret individually in `SecureStore` using row-specific dynamic keys (e.g., `aerostaff_pwd_${id}`).
71 changes: 61 additions & 10 deletions src/screens/PasswordScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,51 @@ export default function PasswordScreen() {
// Load on mount
useEffect(() => {
(async () => {
const raw = await AsyncStorage.getItem(PASSWORDS_KEY);
if (raw) setEntries(JSON.parse(raw));
const enabled = await AsyncStorage.getItem(PIN_ENABLED_KEY);
const isEnabled = enabled === 'true';
setPinEnabled(isEnabled);
if (isEnabled) setPinMode('unlock');
try {
const raw = await AsyncStorage.getItem(PASSWORDS_KEY);
if (raw) {
const loaded = JSON.parse(raw) as PasswordEntry[];
let needsMigration = false;
const withRealPasswords = await Promise.all(loaded.map(async (e) => {
if (e.password !== '***') {
needsMigration = true;
try {
await SecureStore.setItemAsync(`aerostaff_pwd_${e.id}`, e.password);
} catch (err) {
if (__DEV__) console.error('[password] migration error', err);
}
return e; // e already has the real password
} else {
let realPw = '***';
try {
realPw = await SecureStore.getItemAsync(`aerostaff_pwd_${e.id}`) || '***';
} catch (err) {
if (__DEV__) console.error('[password] load error', err);
}
return { ...e, password: realPw };
}
}));
setEntries(withRealPasswords);

if (needsMigration) {
const masked = withRealPasswords.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);
if (isEnabled) setPinMode('unlock');
} catch (err) {
if (__DEV__) console.error('[password] load error', err);
}
})();
}, []);

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));
}, []);

// PIN toggle
Expand Down Expand Up @@ -218,17 +251,30 @@ export default function PasswordScreen() {
const saveModal = useCallback(async () => {
if (!modal.name.trim()) { Alert.alert('Errore', t('passwordErrName')); return; }
if (!modal.password.trim()) { Alert.alert('Errore', t('passwordErrPw')); return; }

const entryId = modal.editingId || Date.now().toString();
const cleanPw = modal.password.trim();

// Save to SecureStore
try {
await SecureStore.setItemAsync(`aerostaff_pwd_${entryId}`, cleanPw);
} catch (e) {
if (__DEV__) console.error('[password] save error', e);
Alert.alert('Errore', 'Impossibile salvare la password in modo sicuro.');
return;
}

let next: PasswordEntry[];
if (modal.editingId) {
next = entries.map(e => e.id === modal.editingId
? { ...e, name: modal.name.trim(), username: modal.username.trim(), password: modal.password.trim(), notes: modal.notes.trim() }
? { ...e, name: modal.name.trim(), username: modal.username.trim(), password: cleanPw, notes: modal.notes.trim() }
: e);
} else {
const entry: PasswordEntry = {
id: Date.now().toString(),
id: entryId,
name: modal.name.trim(),
username: modal.username.trim(),
password: modal.password.trim(),
password: cleanPw,
notes: modal.notes.trim(),
};
next = [...entries, entry];
Expand All @@ -242,6 +288,11 @@ export default function PasswordScreen() {
Alert.alert(t('passwordDeleteTitle'), t('passwordDeleteMsg'), [
{ text: 'Annulla', style: 'cancel' },
{ text: 'Elimina', style: 'destructive', onPress: async () => {
try {
await SecureStore.deleteItemAsync(`aerostaff_pwd_${id}`);
} catch (e) {
if (__DEV__) console.error('[password] delete error', e);
}
await persist(entries.filter(e => e.id !== id));
}},
]);
Expand Down
Loading