Files
CBS-Mobile-App/src/services/storage/secureStorage.ts
T

169 lines
4.8 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Keychain from 'react-native-keychain';
import type { AuthSession } from '../../types/app';
const BIOMETRIC_KEY = 'bankcbs.biometricEnabled';
const SESSION_SERVICE = 'com.bankcbs.mobile.session';
/** The number and org last signed in with, so sign-in can prefill them. */
const PROFILE_SERVICE = 'com.bankcbs.mobile.lastProfile';
/**
* The MPIN, held only when the customer opts into biometric sign-in. The API
* has no refresh token — a session can only be opened by encrypting the MPIN —
* so biometric sign-in is impossible without keeping it. It lives behind the
* device's own biometric prompt and never in AsyncStorage.
*/
const BIOMETRIC_MPIN_SERVICE = 'com.bankcbs.mobile.biometricMpin';
export interface LastProfile {
orgId: string;
mobileNumber: string;
fullName?: string;
}
/**
* The request interceptor needs the token on every single call, and a Keystore
* read costs tens of milliseconds on Android. Held in memory alongside the
* Keychain copy, which stays the durable one across launches.
*/
let cachedSession: AuthSession | null = null;
let sessionCacheLoaded = false;
export const secureStorage = {
async saveSession(session: AuthSession) {
await Keychain.setGenericPassword('session', JSON.stringify(session), {
service: SESSION_SERVICE,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
});
cachedSession = session;
sessionCacheLoaded = true;
},
async getSession(): Promise<AuthSession | null> {
if (sessionCacheLoaded) {
return cachedSession;
}
const credentials = await Keychain.getGenericPassword({
service: SESSION_SERVICE,
});
if (!credentials) {
cachedSession = null;
sessionCacheLoaded = true;
return null;
}
try {
cachedSession = JSON.parse(credentials.password) as AuthSession;
sessionCacheLoaded = true;
return cachedSession;
} catch {
// A session written by an older build of the app. Treat it as absent
// rather than crashing the bootstrap.
await this.clearSession();
return null;
}
},
async getAccessToken() {
const session = await this.getSession();
return session?.sessionToken ?? null;
},
async clearSession() {
cachedSession = null;
sessionCacheLoaded = true;
await Keychain.resetGenericPassword({
service: SESSION_SERVICE,
});
},
async saveLastProfile(profile: LastProfile) {
await Keychain.setGenericPassword('lastProfile', JSON.stringify(profile), {
service: PROFILE_SERVICE,
accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
});
},
async getLastProfile(): Promise<LastProfile | null> {
const credentials = await Keychain.getGenericPassword({
service: PROFILE_SERVICE,
});
if (!credentials) {
return null;
}
try {
return JSON.parse(credentials.password) as LastProfile;
} catch {
return null;
}
},
async clearLastProfile() {
await Keychain.resetGenericPassword({ service: PROFILE_SERVICE });
},
/**
* Stored under the platform's biometric access control, so reading it back
* raises the fingerprint / Face ID prompt rather than the app faking one.
*/
async saveBiometricMpin(mpin: string) {
await Keychain.setGenericPassword('mpin', mpin, {
service: BIOMETRIC_MPIN_SERVICE,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
});
},
async getBiometricMpin(promptTitle: string): Promise<string | null> {
try {
const credentials = await Keychain.getGenericPassword({
service: BIOMETRIC_MPIN_SERVICE,
authenticationPrompt: { title: promptTitle },
});
return credentials ? credentials.password : null;
} catch {
// Cancelled prompt, or no enrolled biometric any more.
return null;
}
},
async hasBiometricMpin() {
try {
const services = await Keychain.getAllGenericPasswordServices();
return services.includes(BIOMETRIC_MPIN_SERVICE);
} catch {
return false;
}
},
async clearBiometricMpin() {
await Keychain.resetGenericPassword({ service: BIOMETRIC_MPIN_SERVICE });
},
async setBiometricEnabled(value: boolean) {
await AsyncStorage.setItem(BIOMETRIC_KEY, JSON.stringify(value));
if (!value) {
await this.clearBiometricMpin();
}
},
async getBiometricEnabled() {
const value = await AsyncStorage.getItem(BIOMETRIC_KEY);
return value ? JSON.parse(value) : false;
},
};