471 lines
12 KiB
Markdown
471 lines
12 KiB
Markdown
# Security
|
|
|
|
## Secure Storage
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
yarn add react-native-keychain
|
|
```
|
|
|
|
### Secure Storage Implementation
|
|
|
|
```typescript
|
|
// src/utils/secureStorage.ts
|
|
import * as Keychain from 'react-native-keychain';
|
|
|
|
export class SecureStorage {
|
|
static async setItem(key: string, value: string): Promise<void> {
|
|
try {
|
|
await Keychain.setInternetCredentials(key, key, value);
|
|
} catch (error) {
|
|
console.error('Error storing secure item:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async getItem(key: string): Promise<string | null> {
|
|
try {
|
|
const credentials = await Keychain.getInternetCredentials(key);
|
|
return credentials ? credentials.password : null;
|
|
} catch (error) {
|
|
console.error('Error retrieving secure item:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async removeItem(key: string): Promise<void> {
|
|
try {
|
|
await Keychain.resetInternetCredentials(key);
|
|
} catch (error) {
|
|
console.error('Error removing secure item:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async clear(): Promise<void> {
|
|
try {
|
|
await Keychain.resetGenericPassword();
|
|
} catch (error) {
|
|
console.error('Error clearing secure storage:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Biometric authentication
|
|
static async setItemWithBiometrics(key: string, value: string): Promise<void> {
|
|
try {
|
|
await Keychain.setInternetCredentials(key, key, value, {
|
|
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
|
|
authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error storing item with biometrics:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async getItemWithBiometrics(key: string): Promise<string | null> {
|
|
try {
|
|
const credentials = await Keychain.getInternetCredentials(key, {
|
|
authenticationType: Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS,
|
|
showModal: true,
|
|
kLocalizedFallbackTitle: 'Please use your passcode',
|
|
});
|
|
return credentials ? credentials.password : null;
|
|
} catch (error) {
|
|
console.error('Error retrieving item with biometrics:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Data Encryption
|
|
|
|
### Encryption Utilities
|
|
|
|
```typescript
|
|
// src/utils/encryption.ts
|
|
import CryptoJS from 'crypto-js';
|
|
|
|
export class EncryptionService {
|
|
private static readonly SECRET_KEY = 'your-secret-key-here'; // Should be from secure config
|
|
|
|
static encrypt(text: string): string {
|
|
try {
|
|
return CryptoJS.AES.encrypt(text, this.SECRET_KEY).toString();
|
|
} catch (error) {
|
|
console.error('Encryption error:', error);
|
|
throw new Error('Failed to encrypt data');
|
|
}
|
|
}
|
|
|
|
static decrypt(encryptedText: string): string {
|
|
try {
|
|
const bytes = CryptoJS.AES.decrypt(encryptedText, this.SECRET_KEY);
|
|
return bytes.toString(CryptoJS.enc.Utf8);
|
|
} catch (error) {
|
|
console.error('Decryption error:', error);
|
|
throw new Error('Failed to decrypt data');
|
|
}
|
|
}
|
|
|
|
static hash(text: string): string {
|
|
return CryptoJS.SHA256(text).toString();
|
|
}
|
|
|
|
static generateSalt(): string {
|
|
return CryptoJS.lib.WordArray.random(128/8).toString();
|
|
}
|
|
|
|
static hashWithSalt(text: string, salt: string): string {
|
|
return CryptoJS.SHA256(text + salt).toString();
|
|
}
|
|
}
|
|
```
|
|
|
|
## API Security
|
|
|
|
### Request Interceptors
|
|
|
|
```typescript
|
|
// src/services/secureAPI.ts
|
|
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
|
import {SecureStorage} from '@utils/secureStorage';
|
|
import {EncryptionService} from '@utils/encryption';
|
|
|
|
const secureAPI = axios.create({
|
|
baseURL: 'https://api.saayam.com',
|
|
timeout: 10000,
|
|
});
|
|
|
|
// Request interceptor for authentication
|
|
secureAPI.interceptors.request.use(
|
|
async (config: AxiosRequestConfig) => {
|
|
// Add authentication token
|
|
const token = await SecureStorage.getItem('auth_token');
|
|
if (token) {
|
|
config.headers = {
|
|
...config.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
}
|
|
|
|
// Add request signature
|
|
const timestamp = Date.now().toString();
|
|
const signature = EncryptionService.hash(
|
|
`${config.method}${config.url}${timestamp}`
|
|
);
|
|
|
|
config.headers = {
|
|
...config.headers,
|
|
'X-Timestamp': timestamp,
|
|
'X-Signature': signature,
|
|
};
|
|
|
|
// Encrypt sensitive data
|
|
if (config.data && config.method === 'post') {
|
|
config.data = {
|
|
...config.data,
|
|
encrypted: true,
|
|
payload: EncryptionService.encrypt(JSON.stringify(config.data)),
|
|
};
|
|
}
|
|
|
|
return config;
|
|
},
|
|
(error) => {
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Response interceptor for decryption
|
|
secureAPI.interceptors.response.use(
|
|
(response: AxiosResponse) => {
|
|
// Decrypt response if needed
|
|
if (response.data?.encrypted) {
|
|
try {
|
|
response.data = JSON.parse(
|
|
EncryptionService.decrypt(response.data.payload)
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to decrypt response:', error);
|
|
}
|
|
}
|
|
return response;
|
|
},
|
|
async (error) => {
|
|
// Handle token refresh
|
|
if (error.response?.status === 401) {
|
|
try {
|
|
const refreshToken = await SecureStorage.getItem('refresh_token');
|
|
if (refreshToken) {
|
|
const response = await axios.post('/auth/refresh', {
|
|
refreshToken,
|
|
});
|
|
|
|
const newToken = response.data.token;
|
|
await SecureStorage.setItem('auth_token', newToken);
|
|
|
|
// Retry original request
|
|
error.config.headers.Authorization = `Bearer ${newToken}`;
|
|
return secureAPI.request(error.config);
|
|
}
|
|
} catch (refreshError) {
|
|
// Redirect to login
|
|
await SecureStorage.clear();
|
|
// Navigate to login screen
|
|
}
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
export default secureAPI;
|
|
```
|
|
|
|
## Input Validation
|
|
|
|
### Validation Schemas
|
|
|
|
```typescript
|
|
// src/utils/validation.ts
|
|
import * as yup from 'yup';
|
|
|
|
export const validationSchemas = {
|
|
email: yup
|
|
.string()
|
|
.email('Invalid email format')
|
|
.required('Email is required')
|
|
.matches(
|
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
'Invalid email format'
|
|
),
|
|
|
|
password: yup
|
|
.string()
|
|
.required('Password is required')
|
|
.min(8, 'Password must be at least 8 characters')
|
|
.matches(
|
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
|
|
'Password must contain uppercase, lowercase, number and special character'
|
|
),
|
|
|
|
phone: yup
|
|
.string()
|
|
.required('Phone number is required')
|
|
.matches(
|
|
/^\+?[1-9]\d{1,14}$/,
|
|
'Invalid phone number format'
|
|
),
|
|
|
|
amount: yup
|
|
.number()
|
|
.required('Amount is required')
|
|
.positive('Amount must be positive')
|
|
.max(1000000, 'Amount cannot exceed 1,000,000'),
|
|
};
|
|
|
|
export const sanitizeInput = (input: string): string => {
|
|
return input
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
.replace(/[<>]/g, '')
|
|
.trim();
|
|
};
|
|
|
|
export const validateAndSanitize = (
|
|
value: string,
|
|
schema: yup.StringSchema
|
|
): {isValid: boolean; sanitized: string; error?: string} => {
|
|
const sanitized = sanitizeInput(value);
|
|
|
|
try {
|
|
schema.validateSync(sanitized);
|
|
return {isValid: true, sanitized};
|
|
} catch (error) {
|
|
return {
|
|
isValid: false,
|
|
sanitized,
|
|
error: error instanceof Error ? error.message : 'Validation failed',
|
|
};
|
|
}
|
|
};
|
|
```
|
|
|
|
## Biometric Authentication
|
|
|
|
### Biometric Setup
|
|
|
|
```typescript
|
|
// src/utils/biometricAuth.ts
|
|
import TouchID from 'react-native-touch-id';
|
|
import {Alert, Platform} from 'react-native';
|
|
|
|
export class BiometricAuth {
|
|
static async isSupported(): Promise<boolean> {
|
|
try {
|
|
const biometryType = await TouchID.isSupported();
|
|
return !!biometryType;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static async getSupportedType(): Promise<string | null> {
|
|
try {
|
|
const biometryType = await TouchID.isSupported();
|
|
return biometryType;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async authenticate(reason: string = 'Authenticate to continue'): Promise<boolean> {
|
|
try {
|
|
const optionalConfigObject = {
|
|
title: 'Authentication Required',
|
|
subtitle: reason,
|
|
description: 'This app uses biometric authentication to protect your data',
|
|
fallbackLabel: 'Use Passcode',
|
|
cancelLabel: 'Cancel',
|
|
disableDeviceFallback: false,
|
|
showModal: true,
|
|
kLocalizedFallbackTitle: 'Use Passcode',
|
|
};
|
|
|
|
await TouchID.authenticate(reason, optionalConfigObject);
|
|
return true;
|
|
} catch (error: any) {
|
|
console.error('Biometric authentication failed:', error);
|
|
|
|
if (error.name === 'LAErrorUserFallback') {
|
|
// User chose to use passcode
|
|
return this.authenticateWithPasscode();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static async authenticateWithPasscode(): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
Alert.prompt(
|
|
'Enter Passcode',
|
|
'Please enter your device passcode',
|
|
[
|
|
{text: 'Cancel', onPress: () => resolve(false)},
|
|
{
|
|
text: 'OK',
|
|
onPress: (passcode) => {
|
|
// In a real app, you would validate the passcode
|
|
resolve(!!passcode);
|
|
},
|
|
},
|
|
],
|
|
'secure-text'
|
|
);
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Certificate Pinning
|
|
|
|
### SSL Pinning Implementation
|
|
|
|
```typescript
|
|
// src/utils/certificatePinning.ts
|
|
import {NetworkingModule} from 'react-native';
|
|
|
|
export class CertificatePinning {
|
|
private static readonly PINNED_CERTIFICATES = [
|
|
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Your server's certificate hash
|
|
];
|
|
|
|
static setupPinning() {
|
|
if (Platform.OS === 'ios') {
|
|
// iOS certificate pinning setup
|
|
NetworkingModule.addRequestInterceptor((request: any) => {
|
|
request.trusty = {
|
|
hosts: [
|
|
{
|
|
host: 'api.saayam.com',
|
|
certificates: this.PINNED_CERTIFICATES,
|
|
},
|
|
],
|
|
};
|
|
return request;
|
|
});
|
|
}
|
|
}
|
|
|
|
static validateCertificate(hostname: string, certificate: string): boolean {
|
|
return this.PINNED_CERTIFICATES.includes(certificate);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Security Headers
|
|
|
|
### Request Security Headers
|
|
|
|
```typescript
|
|
// src/utils/securityHeaders.ts
|
|
export const getSecurityHeaders = () => ({
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'X-Frame-Options': 'DENY',
|
|
'X-XSS-Protection': '1; mode=block',
|
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
|
'Content-Security-Policy': "default-src 'self'",
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
});
|
|
```
|
|
|
|
## Security Best Practices
|
|
|
|
### Security Checklist
|
|
|
|
```typescript
|
|
// src/utils/securityAudit.ts
|
|
export class SecurityAudit {
|
|
static performSecurityCheck(): {passed: boolean; issues: string[]} {
|
|
const issues: string[] = [];
|
|
|
|
// Check for debug mode in production
|
|
if (__DEV__ && process.env.NODE_ENV === 'production') {
|
|
issues.push('Debug mode is enabled in production');
|
|
}
|
|
|
|
// Check for console logs in production
|
|
if (process.env.NODE_ENV === 'production' && console.log.toString().includes('native code')) {
|
|
issues.push('Console logs are not disabled in production');
|
|
}
|
|
|
|
// Check for secure storage usage
|
|
if (!this.isUsingSecureStorage()) {
|
|
issues.push('Sensitive data is not stored securely');
|
|
}
|
|
|
|
// Check for HTTPS usage
|
|
if (!this.isUsingHTTPS()) {
|
|
issues.push('API calls are not using HTTPS');
|
|
}
|
|
|
|
return {
|
|
passed: issues.length === 0,
|
|
issues,
|
|
};
|
|
}
|
|
|
|
private static isUsingSecureStorage(): boolean {
|
|
// Check if secure storage is properly implemented
|
|
return true; // Implement actual check
|
|
}
|
|
|
|
private static isUsingHTTPS(): boolean {
|
|
// Check if all API endpoints use HTTPS
|
|
return true; // Implement actual check
|
|
}
|
|
}
|
|
``` |