Secure Storage
Stratégies de stockage sécurisé : expo-secure-store, AsyncStorage et chiffrement personnalisé.
Overview
YaniPay utilise différentes stratégies de stockage selon la sensibilité des données. Les données critiques (tokens, credentials) sont stockées de manière chiffrée tandis que les données de cache utilisent AsyncStorage.
| Données | Storage | Chiffrement |
|---|---|---|
| JWT Tokens | SecureStore | Hardware + Keychain |
| Biometric Credentials | SecureStore | Hardware + Keychain |
| User Preferences | AsyncStorage | Non |
| Query Cache | AsyncStorage | Non |
| PIN Code Hash | SecureStore | bcrypt + Hardware |
Expo Secure Store
Stockage chiffré utilisant le Keychain (iOS) ou EncryptedSharedPreferences (Android).
src/lib/secureStorage.tstypescript
import * as SecureStore from 'expo-secure-store';
// Storage keys
export const SECURE_STORAGE_KEYS = {
ACCESS_TOKEN: 'yanipay_access_token',
REFRESH_TOKEN: 'yanipay_refresh_token',
BIOMETRIC_CREDENTIALS: 'yanipay_biometric_credentials',
PIN_HASH: 'yanipay_pin_hash',
DEVICE_ID: 'yanipay_device_id',
ENCRYPTION_KEY: 'yanipay_encryption_key',
} as const;
// Keychain accessibility options
type KeychainAccessibility =
| typeof SecureStore.AFTER_FIRST_UNLOCK
| typeof SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY
| typeof SecureStore.ALWAYS
| typeof SecureStore.ALWAYS_THIS_DEVICE_ONLY
| typeof SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
| typeof SecureStore.WHEN_UNLOCKED
| typeof SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
// Storage options by security level
const STORAGE_OPTIONS = {
// Most secure - requires device passcode, device-only
CRITICAL: {
keychainAccessible: SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
},
// Secure - device-only, available after first unlock
HIGH: {
keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
},
// Standard - available after first unlock
STANDARD: {
keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
},
};
export const secureStorage = {
// Store with specified security level
async setItem(
key: string,
value: string,
securityLevel: keyof typeof STORAGE_OPTIONS = 'HIGH'
): Promise<void> {
await SecureStore.setItemAsync(key, value, STORAGE_OPTIONS[securityLevel]);
},
// Get item
async getItem(key: string): Promise<string | null> {
return await SecureStore.getItemAsync(key);
},
// Delete item
async deleteItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
// Store JSON object
async setJSON<T>(
key: string,
value: T,
securityLevel: keyof typeof STORAGE_OPTIONS = 'HIGH'
): Promise<void> {
const jsonString = JSON.stringify(value);
await this.setItem(key, jsonString, securityLevel);
},
// Get JSON object
async getJSON<T>(key: string): Promise<T | null> {
const jsonString = await this.getItem(key);
if (!jsonString) return null;
try {
return JSON.parse(jsonString) as T;
} catch {
return null;
}
},
// Clear all secure storage (logout)
async clearAll(): Promise<void> {
const keys = Object.values(SECURE_STORAGE_KEYS);
await Promise.all(keys.map((key) => this.deleteItem(key)));
},
// Check if item exists
async hasItem(key: string): Promise<boolean> {
const value = await this.getItem(key);
return value !== null;
},
};Async Storage
Stockage non-chiffré pour les données de cache et préférences non-sensibles.
src/lib/asyncStorage.tstypescript
import AsyncStorage from '@react-native-async-storage/async-storage';
// Storage keys for non-sensitive data
export const ASYNC_STORAGE_KEYS = {
QUERY_CACHE: 'YANIPAY_QUERY_CACHE',
USER_PREFERENCES: 'yanipay_user_preferences',
ONBOARDING_COMPLETE: 'yanipay_onboarding_complete',
LAST_SYNC_TIME: 'yanipay_last_sync',
THEME: 'yanipay_theme',
LANGUAGE: 'yanipay_language',
RECENT_SEARCHES: 'yanipay_recent_searches',
} as const;
export const asyncStorage = {
// Set item
async setItem(key: string, value: string): Promise<void> {
await AsyncStorage.setItem(key, value);
},
// Get item
async getItem(key: string): Promise<string | null> {
return await AsyncStorage.getItem(key);
},
// Remove item
async removeItem(key: string): Promise<void> {
await AsyncStorage.removeItem(key);
},
// Store JSON
async setJSON<T>(key: string, value: T): Promise<void> {
const jsonString = JSON.stringify(value);
await this.setItem(key, jsonString);
},
// Get JSON
async getJSON<T>(key: string): Promise<T | null> {
const jsonString = await this.getItem(key);
if (!jsonString) return null;
try {
return JSON.parse(jsonString) as T;
} catch {
return null;
}
},
// Multi-get
async multiGet(keys: string[]): Promise<Map<string, string | null>> {
const pairs = await AsyncStorage.multiGet(keys);
return new Map(pairs);
},
// Multi-set
async multiSet(keyValues: [string, string][]): Promise<void> {
await AsyncStorage.multiSet(keyValues);
},
// Get all keys
async getAllKeys(): Promise<string[]> {
return await AsyncStorage.getAllKeys();
},
// Clear all (use with caution)
async clearAll(): Promise<void> {
await AsyncStorage.clear();
},
// Clear only app-specific keys
async clearAppData(): Promise<void> {
const keys = Object.values(ASYNC_STORAGE_KEYS);
await AsyncStorage.multiRemove(keys);
},
};
// Usage with React Query persister
export const queryPersister = {
getItem: async (key: string) => {
const data = await asyncStorage.getItem(key);
return data ? JSON.parse(data) : null;
},
setItem: async (key: string, value: unknown) => {
await asyncStorage.setItem(key, JSON.stringify(value));
},
removeItem: async (key: string) => {
await asyncStorage.removeItem(key);
},
};Custom Encryption
Chiffrement additionnel pour les données sensibles nécessitant un contrôle fin.
src/lib/encryption.tstypescript
import * as Crypto from 'expo-crypto';
import { secureStorage, SECURE_STORAGE_KEYS } from './secureStorage';
// Generate or retrieve encryption key
async function getOrCreateEncryptionKey(): Promise<string> {
let key = await secureStorage.getItem(SECURE_STORAGE_KEYS.ENCRYPTION_KEY);
if (!key) {
// Generate new 256-bit key
const randomBytes = await Crypto.getRandomBytesAsync(32);
key = Array.from(new Uint8Array(randomBytes))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
await secureStorage.setItem(
SECURE_STORAGE_KEYS.ENCRYPTION_KEY,
key,
'CRITICAL'
);
}
return key;
}
// Simple XOR encryption (for demo - use proper encryption in production)
// In production, use react-native-aes-crypto or similar
function xorEncrypt(data: string, key: string): string {
let result = '';
for (let i = 0; i < data.length; i++) {
const charCode = data.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return Buffer.from(result, 'utf-8').toString('base64');
}
function xorDecrypt(encrypted: string, key: string): string {
const data = Buffer.from(encrypted, 'base64').toString('utf-8');
let result = '';
for (let i = 0; i < data.length; i++) {
const charCode = data.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return result;
}
export const encryption = {
// Encrypt data
async encrypt(data: string): Promise<string> {
const key = await getOrCreateEncryptionKey();
return xorEncrypt(data, key);
},
// Decrypt data
async decrypt(encrypted: string): Promise<string> {
const key = await getOrCreateEncryptionKey();
return xorDecrypt(encrypted, key);
},
// Hash with SHA-256
async hash(data: string): Promise<string> {
return await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
data
);
},
// Generate random ID
async generateId(length = 16): Promise<string> {
const bytes = await Crypto.getRandomBytesAsync(length);
return Array.from(new Uint8Array(bytes))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
},
};
// PIN code management with hashing
export const pinManager = {
// Hash and store PIN
async setPin(pin: string): Promise<void> {
// Add salt for security
const salt = await encryption.generateId(16);
const saltedPin = salt + pin;
const hash = await encryption.hash(saltedPin);
await secureStorage.setJSON(
SECURE_STORAGE_KEYS.PIN_HASH,
{ hash, salt },
'CRITICAL'
);
},
// Verify PIN
async verifyPin(pin: string): Promise<boolean> {
const stored = await secureStorage.getJSON<{ hash: string; salt: string }>(
SECURE_STORAGE_KEYS.PIN_HASH
);
if (!stored) return false;
const saltedPin = stored.salt + pin;
const hash = await encryption.hash(saltedPin);
return hash === stored.hash;
},
// Check if PIN is set
async hasPin(): Promise<boolean> {
return await secureStorage.hasItem(SECURE_STORAGE_KEYS.PIN_HASH);
},
// Remove PIN
async removePin(): Promise<void> {
await secureStorage.deleteItem(SECURE_STORAGE_KEYS.PIN_HASH);
},
};Storage Strategy
Stratégie unifiée de stockage avec sélection automatique.
src/lib/storageManager.tstypescript
import { secureStorage } from './secureStorage';
import { asyncStorage } from './asyncStorage';
import { encryption } from './encryption';
// Data sensitivity levels
export enum DataSensitivity {
CRITICAL = 'CRITICAL', // Tokens, credentials, financial data
HIGH = 'HIGH', // User PII, session data
MEDIUM = 'MEDIUM', // Preferences requiring encryption
LOW = 'LOW', // Cache, non-sensitive preferences
}
// Storage configuration per data type
const STORAGE_CONFIG: Record<string, {
sensitivity: DataSensitivity;
storage: 'secure' | 'async';
encrypt: boolean;
ttl?: number; // Time to live in seconds
}> = {
accessToken: { sensitivity: DataSensitivity.CRITICAL, storage: 'secure', encrypt: false },
refreshToken: { sensitivity: DataSensitivity.CRITICAL, storage: 'secure', encrypt: false },
biometricCredentials: { sensitivity: DataSensitivity.CRITICAL, storage: 'secure', encrypt: false },
pinHash: { sensitivity: DataSensitivity.CRITICAL, storage: 'secure', encrypt: false },
userProfile: { sensitivity: DataSensitivity.HIGH, storage: 'secure', encrypt: true },
transactionHistory: { sensitivity: DataSensitivity.MEDIUM, storage: 'async', encrypt: true },
preferences: { sensitivity: DataSensitivity.LOW, storage: 'async', encrypt: false },
queryCache: { sensitivity: DataSensitivity.LOW, storage: 'async', encrypt: false, ttl: 3600 },
};
export const storageManager = {
// Store data with automatic storage selection
async store<T>(
key: string,
data: T,
config?: Partial<typeof STORAGE_CONFIG[string]>
): Promise<void> {
const finalConfig = { ...STORAGE_CONFIG[key], ...config };
let value = JSON.stringify(data);
// Encrypt if needed
if (finalConfig?.encrypt) {
value = await encryption.encrypt(value);
}
// Add TTL wrapper if specified
if (finalConfig?.ttl) {
const wrapper = {
data: value,
expiresAt: Date.now() + finalConfig.ttl * 1000,
};
value = JSON.stringify(wrapper);
}
// Store in appropriate storage
if (finalConfig?.storage === 'secure') {
const securityLevel = finalConfig.sensitivity === DataSensitivity.CRITICAL
? 'CRITICAL'
: 'HIGH';
await secureStorage.setItem(key, value, securityLevel);
} else {
await asyncStorage.setItem(key, value);
}
},
// Retrieve data
async retrieve<T>(key: string): Promise<T | null> {
const config = STORAGE_CONFIG[key];
let value: string | null;
if (config?.storage === 'secure') {
value = await secureStorage.getItem(key);
} else {
value = await asyncStorage.getItem(key);
}
if (!value) return null;
// Check TTL if applicable
if (config?.ttl) {
try {
const wrapper = JSON.parse(value);
if (wrapper.expiresAt && Date.now() > wrapper.expiresAt) {
await this.remove(key);
return null;
}
value = wrapper.data;
} catch {
// Not a TTL wrapper, continue
}
}
// Decrypt if needed
if (config?.encrypt) {
try {
value = await encryption.decrypt(value);
} catch {
return null;
}
}
try {
return JSON.parse(value) as T;
} catch {
return null;
}
},
// Remove data
async remove(key: string): Promise<void> {
const config = STORAGE_CONFIG[key];
if (config?.storage === 'secure') {
await secureStorage.deleteItem(key);
} else {
await asyncStorage.removeItem(key);
}
},
// Clear all sensitive data (logout)
async clearSensitiveData(): Promise<void> {
const sensitiveKeys = Object.entries(STORAGE_CONFIG)
.filter(([, config]) =>
config.sensitivity === DataSensitivity.CRITICAL ||
config.sensitivity === DataSensitivity.HIGH
)
.map(([key]) => key);
await Promise.all(sensitiveKeys.map((key) => this.remove(key)));
},
};Best Practices
Bonnes pratiques de sécurité pour le stockage des données.
Rappels de sécurité
- • Ne jamais stocker de mots de passe en clair
- • Utiliser SecureStore pour tout ce qui est sensible
- • Implémenter un TTL pour les données de cache
- • Nettoyer les données sensibles au logout
Security Guidelinestypescript
// DO: Store tokens in SecureStore
await secureStorage.setItem('access_token', token, 'CRITICAL');
// DON'T: Store tokens in AsyncStorage
// await AsyncStorage.setItem('access_token', token); // UNSAFE!
// DO: Hash PINs before storing
const hash = await encryption.hash(salt + pin);
await secureStorage.setItem('pin_hash', hash);
// DON'T: Store PINs in plaintext
// await secureStorage.setItem('pin', '1234'); // UNSAFE!
// DO: Encrypt sensitive user data
const encrypted = await encryption.encrypt(JSON.stringify(userData));
await asyncStorage.setItem('user_data', encrypted);
// DON'T: Store PII unencrypted
// await asyncStorage.setItem('user_data', JSON.stringify(userData)); // UNSAFE!
// DO: Clear all sensitive data on logout
async function logout() {
await storageManager.clearSensitiveData();
await secureStorage.deleteItem('access_token');
await secureStorage.deleteItem('refresh_token');
}
// DO: Implement data expiration
const wrapper = {
data: sensitiveData,
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
};
// DO: Use device-only storage for critical data
await SecureStore.setItemAsync(key, value, {
keychainAccessible: SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
});