react-native-sop-docs/08-security/secure-storage.md

12 KiB

Security

Secure Storage

Installation

yarn add react-native-keychain

Secure Storage Implementation

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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
  }
}