KHÔNG BAO GIỜ lưu tokens, passwords, API keys vào AsyncStorage.
AsyncStorage lưu data dưới dạng plaintext trong SQLite database (Android) hoặc plist file (iOS). Bất kỳ ai có physical access hoặc root access đều đọc được. Trên Android, file nằm ở /data/data/com.yourapp/databases/RKStorage — dùng adb shell là đọc thẳng.
Keychain (iOS) / Keystore (Android) là hardware-backed secure storage do OS cung cấp:
Cài đặt và sử dụng react-native-keychain:
import * as Keychain from 'react-native-keychain';
const saveToken = async (token: string): Promise<boolean> => {
const result = await Keychain.setGenericPassword('auth_token', token, {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE,
});
return result !== false;
};
const getToken = async (): Promise<string | null> => {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
return credentials.password;
}
return null;
};
const removeToken = async (): Promise<void> => {
await Keychain.resetGenericPassword();
};
Option WHEN_UNLOCKED_THIS_DEVICE_ONLY đảm bảo token chỉ accessible khi device unlocked và không được sync qua iCloud backup. SECURE_HARDWARE yêu cầu hardware-backed encryption trên Android.
| Tiêu chí | AsyncStorage | MMKV |
|---|---|---|
| Performance | Chậm (async, serialization overhead) | Nhanh ~30x (memory-mapped, synchronous) |
| API | Async only (await) | Sync + Async |
| Data types | String only (phải JSON.stringify) | String, number, boolean, Buffer |
| Encryption | Không | Có (AES-CFB) |
| Size limit | 6MB default (Android) | Không giới hạn |
| Multi-process | Không | Có |
| Storage engine | SQLite (Android) / file (iOS) | Memory-mapped file (mmap) |
| Use case | Legacy apps, simple needs | Production apps, settings, cache |
Lưu ý: MMKV nhanh nhưng vẫn KHÔNG phải nơi lưu tokens/secrets. MMKV encrypted mode dùng app-level key — không phải hardware-backed như Keychain/Keystore. Dùng MMKV cho user preferences, cache, feature flags. Dùng Keychain cho tokens, passwords, biometric data.
Setup MMKV:
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV({
id: 'app-storage',
encryptionKey: 'your-encryption-key',
});
storage.set('user.theme', 'dark');
storage.set('user.onboarded', true);
storage.set('cache.lastFetch', Date.now());
const theme = storage.getString('user.theme');
const onboarded = storage.getBoolean('user.onboarded');
storage.delete('cache.lastFetch');
const allKeys = storage.getAllKeys();
Tích hợp MMKV với Zustand persist:
import { create } from 'zustand';
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV({ id: 'zustand-storage' });
const zustandStorage: StateStorage = {
setItem: (name: string, value: string) => {
mmkv.set(name, value);
},
getItem: (name: string) => {
return mmkv.getString(name) ?? null;
},
removeItem: (name: string) => {
mmkv.delete(name);
},
};
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (lang: string) => void;
}
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'vi',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings',
storage: createJSONStorage(() => zustandStorage),
}
)
);
Offline-first là kiến trúc ưu tiên dữ liệu local trước, sync với server khi có network. User experience không bị gián đoạn dù mất mạng.
3 cấp độ offline support:
import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { MMKV } from 'react-native-mmkv';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24,
staleTime: 1000 * 60 * 5,
},
},
});
const mmkv = new MMKV({ id: 'query-cache' });
const persister = createSyncStoragePersister({
storage: {
getItem: (key: string) => mmkv.getString(key) ?? null,
setItem: (key: string, value: string) => mmkv.set(key, value),
removeItem: (key: string) => mmkv.delete(key),
},
});
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister, maxAge: 1000 * 60 * 60 * 24 }}
>
<MainApp />
</PersistQueryClientProvider>
);
}
import { useMutation, useQueryClient, onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newPost: CreatePostInput) => api.createPost(newPost),
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
queryClient.setQueryData<Post[]>(['posts'], (old) => [
...(old ?? []),
{ ...newPost, id: `temp-${Date.now()}`, pending: true },
]);
return { previousPosts };
},
onError: (_err, _newPost, context) => {
queryClient.setQueryData(['posts'], context?.previousPosts);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
Pattern trên sử dụng optimistic update: UI cập nhật ngay lập tức, nếu mutation fail thì rollback. Kết hợp với onlineManager, mutations sẽ được queue khi offline và tự retry khi online.
| Library | Type | Sync | Performance | Use case |
|---|---|---|---|---|
| SQLite (expo-sqlite) | Relational | Manual | Tốt | Complex queries, existing SQL skills |
| WatermelonDB | Relational (lazy) | Built-in sync | Rất tốt (lazy loading) | Large datasets, offline-first apps |
| Realm | Object DB | Atlas Sync | Rất tốt | MongoDB ecosystem |
| MMKV | Key-Value | Không | Cực nhanh | Settings, simple cache |
Certificate Pinning đảm bảo app chỉ trust certificate cụ thể của server, không phải bất kỳ CA nào trong trust store. Chống man-in-the-middle (MITM) attacks ngay cả khi attacker install malicious CA certificate trên device.
Tại sao cần:
Implement certificate pinning với react-native-ssl-pinning:
import { fetch as sslFetch } from 'react-native-ssl-pinning';
const fetchWithPinning = async (url: string) => {
const response = await sslFetch(url, {
method: 'GET',
timeoutInterval: 10000,
sslPinning: {
certs: ['server_cert'],
},
headers: {
'Content-Type': 'application/json',
},
});
return response.json();
};
Cách khác: dùng TrustKit (iOS) hoặc cấu hình network_security_config.xml (Android):
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.yourapp.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
Rủi ro của Certificate Pinning: Nếu certificate rotate mà app chưa update pin → app sẽ không connect được. Luôn pin backup certificate và có mechanism để update pins remotely (hoặc dùng public key pinning thay vì certificate pinning).
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';
const biometrics = new ReactNativeBiometrics();
const checkBiometricAvailability = async () => {
const { available, biometryType } = await biometrics.isSensorAvailable();
if (!available) {
return { supported: false, type: null };
}
return {
supported: true,
type: biometryType,
isFaceID: biometryType === BiometryTypes.FaceID,
isFingerprint: biometryType === BiometryTypes.Biometrics,
};
};
const authenticateWithBiometrics = async (): Promise<boolean> => {
const { success } = await biometrics.simplePrompt({
promptMessage: 'Xác nhận danh tính',
cancelButtonText: 'Hủy',
});
return success;
};
const createBiometricKey = async () => {
const { publicKey } = await biometrics.createKeys();
await sendPublicKeyToServer(publicKey);
};
const signWithBiometrics = async (payload: string) => {
const { signature } = await biometrics.createSignature({
promptMessage: 'Xác nhận giao dịch',
payload,
});
return signature;
};
createKeys() tạo RSA key pair trong Secure Enclave (iOS) / Keystore (Android). Private key không bao giờ rời khỏi device. createSignature() ký data bằng private key sau khi user xác thực biometrics — server verify bằng public key.
Rooted (Android) / Jailbroken (iOS) devices có nguy cơ bảo mật cao hơn: app sandbox bị phá, attacker có thể đọc Keychain, hook vào app process (Frida, Xposed).
Phương pháp detect:
import JailMonkey from 'jail-monkey';
const performSecurityChecks = () => {
const isRooted = JailMonkey.isJailBroken();
const canMockLocation = JailMonkey.canMockLocation();
const isDebugMode = JailMonkey.isDebuggedMode();
const isOnExternalStorage = JailMonkey.isOnExternalStorage();
const threats: string[] = [];
if (isRooted) threats.push('ROOTED_DEVICE');
if (canMockLocation) threats.push('MOCK_LOCATION');
if (isDebugMode) threats.push('DEBUG_MODE');
if (isOnExternalStorage) threats.push('EXTERNAL_STORAGE');
return {
isSecure: threats.length === 0,
threats,
};
};
const enforceSecurityPolicy = (threats: string[]) => {
if (threats.includes('ROOTED_DEVICE')) {
return 'block';
}
if (threats.includes('DEBUG_MODE')) {
return 'warn';
}
return 'allow';
};
Cân nhắc: Root detection không phải giải pháp tuyệt đối — Magisk Hide, Frida scripts có thể bypass. Nên kết hợp nhiều checks (defense in depth) và không chỉ dựa vào client-side validation. Banking apps thường block rooted devices; e-commerce apps có thể chỉ warn.
Hardcode API keys, credentials trong source code. Dùng react-native-config cho environment variables, lưu secrets ở server-side.
Third-party libraries có thể chứa malicious code. Luôn audit dependencies (npm audit), pin versions, dùng lockfile.
Weak password policies, thiếu MFA, không validate JWT expiry ở client. Implement token refresh flow đúng cách, check token expiry trước mỗi request.
SQL injection, XSS qua WebView. Validate mọi user input ở cả client và server. Dùng parameterized queries cho SQLite.
HTTP thay vì HTTPS, thiếu certificate pinning. Enforce TLS 1.2+, implement certificate pinning cho critical APIs.
Collect data không cần thiết, không anonymize PII. Chỉ collect data cần thiết, encrypt PII, comply với GDPR/CCPA.
Reverse engineering, code tampering. Dùng ProGuard/R8 obfuscation, Hermes bytecode (RN), integrity checks.
Debug mode trong production, backup enabled, cleartext traffic allowed. Disable debug flags, configure android:allowBackup="false".
Sensitive data trong AsyncStorage, logs, screenshots. Dùng Keychain/Keystore, disable screenshots cho sensitive screens.
Weak algorithms (MD5, SHA1 cho passwords), hardcoded encryption keys. Dùng AES-256, bcrypt/scrypt cho passwords, key management qua Keystore.
Data lưu trên device phải được encrypt:
react-native-fs + encryption libraryimport { MMKV } from 'react-native-mmkv';
const encryptedStorage = new MMKV({
id: 'encrypted-store',
encryptionKey: 'your-256-bit-key-here',
});
encryptedStorage.set('sensitiveData', JSON.stringify({
cardLastFour: '4242',
expiryMonth: 12,
}));
Mọi network communication phải qua HTTPS (TLS 1.2+):
network_security_config.xml để block cleartextBlock cleartext traffic trên Android:
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
import Config from 'react-native-config';
const API_BASE_URL = Config.API_BASE_URL;
const ANALYTICS_KEY = Config.ANALYTICS_KEY;
const ENVIRONMENT = Config.ENVIRONMENT;
File .env:
API_BASE_URL=https://api.yourapp.com
ANALYTICS_KEY=UA-XXXXXXXXX
ENVIRONMENT=production
QUAN TRỌNG: react-native-config inject biến vào app bundle — chúng KHÔNG an toàn, có thể decompile ra. KHÔNG lưu secrets (API secret keys, database passwords) trong .env. Chỉ dùng cho public configuration (API URLs, feature flags, analytics IDs). Secrets phải lưu ở server và truy cập qua authenticated API.
Ngăn screenshot/screen recording cho sensitive screens (banking, passwords):
import { Platform } from 'react-native';
import { useEffect } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import ScreenCapture from 'react-native-screen-capture';
const usePreventScreenCapture = () => {
useFocusEffect(
useCallback(() => {
ScreenCapture.enableSecureView();
return () => {
ScreenCapture.disableSecureView();
};
}, [])
);
};
Trên Android, cơ chế này set FLAG_SECURE trên Window. Trên iOS, overlay hidden text field để trigger DRM protection. Expo users có thể dùng expo-screen-capture.
Obfuscation làm cho code khó đọc hơn sau khi decompile. R8 (replacement cho ProGuard) là default trong Android Gradle Plugin:
Enable trong android/app/build.gradle:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
ProGuard rules cho React Native (proguard-rules.pro):
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }
-keep class com.facebook.jni.** { *; }
-dontwarn com.facebook.react.**
-keepclassmembers class * {
@com.facebook.react.uimanager.annotations.ReactProp *;
}
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
}
Hermes bytecode: React Native với Hermes compiler đã convert JS thành bytecode — khó đọc hơn plain JS nhưng không phải true obfuscation. Kết hợp với R8 cho native code và Hermes cho JS code để có protection tốt nhất.
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| User mới thấy data user cũ | Query cache, persisted store, secure storage, navigation reset | Logout clear token/cache/store/offline queue và reset nav |
| Token vẫn tồn tại sau logout | Keychain/SecureStore remove có await chưa, error bị nuốt không | Await clear, handle failure, verify storage empty |
| Secret bị lộ trong APK/IPA | .env, BuildConfig, bundle strings | Không nhúng secret client-side; chuyển secret về server |
| Pinning làm app mất kết nối | Cert rotate, pin backup, domain config | Có kế hoạch rotate và backup pin |
Khung trả lời phỏng vấn: Mobile security không có một lớp bảo vệ tuyệt đối. Trả lời theo data sensitivity, storage choice, transport security, logout cleanup, device compromise và operational risk.
AsyncStorage lưu data dưới dạng plaintext trong SQLite (Android) hoặc plist file (iOS). Bất kỳ ai có physical access, root access, hoặc adb debug access đều đọc được. Trên Android, file nằm ở /data/data/<package>/databases/RKStorage.
Giải pháp: Dùng react-native-keychain để lưu vào iOS Keychain / Android Keystore. Đây là hardware-backed storage, encrypted bởi OS, sandboxed per-app. Có thể thêm biometric gating (yêu cầu FaceID/fingerprint trước khi trả token).
Phân loại storage:
Certificate Pinning là kỹ thuật đảm bảo app chỉ chấp nhận certificate cụ thể khi kết nối HTTPS, thay vì trust bất kỳ CA nào trong system trust store.
Tại sao cần: Ngăn MITM attacks — attacker install rogue CA certificate trên device (qua corporate proxy, malware) có thể intercept HTTPS traffic. Với pinning, app sẽ reject connections dù certificate valid theo CA nhưng không match pin.
Rủi ro:
Performance: MMKV nhanh hơn ~30x nhờ memory-mapped file (mmap) thay vì SQLite queries. MMKV hỗ trợ synchronous API — không cần await, không block JS thread vì read từ memory.
AsyncStorage dùng khi: legacy project không thể migrate, hoặc cần cross-platform web compatibility (AsyncStorage có web adapter). Hầu hết mọi trường hợp khác nên dùng MMKV.
MMKV dùng cho: user settings, theme, language, onboarding state, cache, feature flags. Tích hợp tốt với Zustand persist, Redux persist.
Không dùng cả hai cho: tokens, passwords, secrets → dùng Keychain/Keystore.
Offline-first ưu tiên data local, sync với server khi có mạng. User không thấy loading states khi mở app (data từ cache).
React Query implement:
Cho complex apps cần full offline (e.g., field service app), dùng WatermelonDB hoặc PowerSync thay vì chỉ React Query persist.
M9 — Insecure Data Storage: Lưu tokens/passwords vào AsyncStorage, logs chứa sensitive data, clipboard exposure. Giải pháp: Keychain/Keystore cho secrets, MMKV encrypted cho preferences, không log sensitive data.
M5 — Insecure Communication: HTTP requests, thiếu certificate pinning, trust bất kỳ CA. Giải pháp: enforce HTTPS only, certificate pinning cho critical APIs, disable cleartext traffic.
M3 — Insecure Authentication/Authorization: Token không expire, thiếu refresh flow, không validate server-side. Giải pháp: short-lived access tokens + refresh tokens, validate JWT expiry client-side, implement proper token refresh interceptor.
Phương pháp: Dùng jail-monkey hoặc react-native-device-info để check:
/system, /private)Độ tin cậy: Thấp. Magisk Hide, Frida scripts có thể bypass hầu hết client-side checks. Root detection chỉ là 1 layer trong defense-in-depth strategy. Không nên dùng root detection như primary security measure — phải combine với server-side validation, certificate pinning, code obfuscation.
Policy: Banking apps block rooted devices. E-commerce apps warn. Internal tools có thể ignore.
react-native-config đọc file .env và inject vào app bundle tại build time. Trên Android, values được inject vào BuildConfig và res/values. Trên iOS, inject vào Info.plist.
KHÔNG AN TOÀN cho secrets. Bất kỳ giá trị nào trong .env đều có thể extract từ app bundle sau khi decompile. APK/IPA chỉ là ZIP file — ai cũng có thể giải nén và tìm strings.
Chỉ dùng cho: API base URLs, analytics IDs, feature flags, environment identifiers (staging/production).
Secrets phải: Lưu ở server, truy cập qua authenticated API. Hoặc dùng build-time CI/CD secrets injection + native code embedding (vẫn có thể extract nhưng khó hơn).
At rest (dữ liệu lưu trữ):
react-native-aes-cryptoIn transit (dữ liệu truyền tải):
network_security_config.xml block cleartextNguyên tắc: Sensitive data phải encrypted cả khi lưu trữ và khi truyền tải. Dùng strong algorithms (AES-256, RSA-2048+). Không implement crypto algorithms tự — dùng OS/library cung cấp.
Xây dựng complete authentication flow với security best practices:
Yêu cầu:
react-native-keychainAcceptance Criteria:
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import * as Keychain from 'react-native-keychain';
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: Error) => void;
}> = [];
const processQueue = (error: Error | null, token: string | null) => {
failedQueue.forEach((promise) => {
if (error) {
promise.reject(error);
} else {
promise.resolve(token!);
}
});
failedQueue = [];
};
const apiClient = axios.create({
baseURL: 'https://api.yourapp.com',
});
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
const credentials = await Keychain.getGenericPassword({ service: 'accessToken' });
if (credentials) {
config.headers.Authorization = `Bearer ${credentials.password}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshCreds = await Keychain.getGenericPassword({ service: 'refreshToken' });
if (!refreshCreds) {
processQueue(new Error('No refresh token'), null);
isRefreshing = false;
navigateToLogin();
return Promise.reject(error);
}
const response = await axios.post('https://api.yourapp.com/auth/refresh', {
refreshToken: refreshCreds.password,
});
const { accessToken, refreshToken } = response.data;
await Keychain.setGenericPassword('token', accessToken, { service: 'accessToken' });
await Keychain.setGenericPassword('token', refreshToken, { service: 'refreshToken' });
processQueue(null, accessToken);
isRefreshing = false;
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
}
);
Pattern trên sử dụng queue để handle concurrent 401 responses. Khi request đầu tiên nhận 401, nó trigger refresh. Các requests tiếp theo thấy isRefreshing = true sẽ được đẩy vào queue, đợi refresh hoàn thành rồi retry với token mới.
.env là secret, trong khi app bundle có thể bị decompile.Case: User logout khỏi app, nhưng React Query cache vẫn còn profile và order cũ. Người dùng khác đăng nhập trên cùng máy thấy dữ liệu cũ trong vài giây.
Cách xử lý: Khi logout, clear secure token, reset auth store, clear query cache/persisted cache, reset navigation và hủy offline mutations chứa dữ liệu user cũ.
Bảo mật mobile là giảm thiểu rủi ro ở nhiều lớp: lưu trữ đúng, truyền tải đúng, logout sạch, không nhúng secret, và có kế hoạch khi device/app bị compromise.