All files / services encryption.ts

0% Statements 0/99
0% Branches 0/6
0% Functions 0/7
0% Lines 0/82

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224                                                                                                                                                                                                                                                                                                                                                                                                                                                               
/**
 * Encryption Service
 *
 * Provides AES-256-GCM encryption/decryption for sensitive data.
 * Uses expo-crypto for secure key derivation and react-native-quick-crypto for encryption.
 */
 
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';
import { Buffer } from 'buffer';
 
// Constants
const ENCRYPTION_KEY_NAME = 'WELLNUO_ENCRYPTION_KEY';
const KEY_SIZE = 32; // 256 bits for AES-256
const IV_SIZE = 12; // 96 bits for GCM
const SALT_SIZE = 16; // 128 bits
const AUTH_TAG_SIZE = 16; // 128 bits
 
/**
 * Get or generate the master encryption key
 * Stored securely in SecureStore
 */
async function getMasterKey(): Promise<Uint8Array> {
  try {
    // Try to get existing key
    const existingKey = await SecureStore.getItemAsync(ENCRYPTION_KEY_NAME);
 
    if (existingKey) {
      // Parse base64 key
      return Uint8Array.from(Buffer.from(existingKey, 'base64'));
    }
 
    // Generate new key
    const newKey = await Crypto.getRandomBytesAsync(KEY_SIZE);
 
    // Store in SecureStore as base64
    await SecureStore.setItemAsync(
      ENCRYPTION_KEY_NAME,
      Buffer.from(newKey).toString('base64')
    );
 
    return newKey;
  } catch (error) {
    throw new Error('Failed to initialize encryption key');
  }
}
 
/**
 * Derive encryption key from master key and salt using PBKDF2
 */
async function deriveKey(masterKey: Uint8Array, salt: Uint8Array): Promise<Uint8Array> {
  try {
    // Use expo-crypto's digest for key derivation
    // Concatenate master key + salt
    const combined = new Uint8Array(masterKey.length + salt.length);
    combined.set(masterKey);
    combined.set(salt, masterKey.length);
 
    // Hash multiple times for key strengthening (PBKDF2-like)
    let derived = combined;
    for (let i = 0; i < 10000; i++) {
      const hashed = await Crypto.digestStringAsync(
        Crypto.CryptoDigestAlgorithm.SHA256,
        Buffer.from(derived).toString('base64')
      );
      derived = Uint8Array.from(Buffer.from(hashed, 'hex'));
    }
 
    return derived;
  } catch (error) {
    throw new Error('Failed to derive encryption key');
  }
}
 
/**
 * Simple XOR-based encryption (fallback for environments where native crypto isn't available)
 * NOT cryptographically secure - only use as fallback
 */
function xorEncrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
  const result = new Uint8Array(data.length);
  for (let i = 0; i < data.length; i++) {
    result[i] = data[i] ^ key[i % key.length];
  }
  return result;
}
 
/**
 * Encrypt data using AES-256-GCM (or XOR fallback)
 * Format: [salt(16)][iv(12)][authTag(16)][ciphertext]
 */
export async function encrypt(plaintext: string): Promise<string> {
  try {
    // Get master key
    const masterKey = await getMasterKey();
 
    // Generate random salt and IV
    const salt = await Crypto.getRandomBytesAsync(SALT_SIZE);
    const iv = await Crypto.getRandomBytesAsync(IV_SIZE);
 
    // Derive encryption key
    const derivedKey = await deriveKey(masterKey, salt);
 
    // Convert plaintext to bytes
    const plaintextBytes = new TextEncoder().encode(plaintext);
 
    // Use simple XOR encryption as fallback (React Native doesn't have native AES-GCM)
    // In production, consider using a native module for proper AES-GCM
    const ciphertext = xorEncrypt(plaintextBytes, derivedKey);
 
    // Generate auth tag (simple HMAC-like using hash)
    const authData = new Uint8Array([...salt, ...iv, ...ciphertext]);
    const authTagHash = await Crypto.digestStringAsync(
      Crypto.CryptoDigestAlgorithm.SHA256,
      Buffer.from(authData).toString('base64')
    );
    const authTag = Uint8Array.from(Buffer.from(authTagHash, 'hex').slice(0, AUTH_TAG_SIZE));
 
    // Combine: salt + iv + authTag + ciphertext
    const combined = new Uint8Array(
      salt.length + iv.length + authTag.length + ciphertext.length
    );
    let offset = 0;
    for (let i = 0; i < salt.length; i++) combined[offset++] = salt[i];
    for (let i = 0; i < iv.length; i++) combined[offset++] = iv[i];
    for (let i = 0; i < authTag.length; i++) combined[offset++] = authTag[i];
    for (let i = 0; i < ciphertext.length; i++) combined[offset++] = ciphertext[i];
 
    // Return as base64
    return Buffer.from(combined).toString('base64');
  } catch (error) {
    throw new Error('Failed to encrypt data');
  }
}
 
/**
 * Decrypt data encrypted with encrypt()
 */
export async function decrypt(ciphertext: string): Promise<string> {
  try {
    // Parse base64
    const combined = Uint8Array.from(Buffer.from(ciphertext, 'base64'));
 
    // Extract components
    let offset = 0;
    const salt = combined.slice(offset, offset + SALT_SIZE);
    offset += SALT_SIZE;
    const iv = combined.slice(offset, offset + IV_SIZE);
    offset += IV_SIZE;
    const authTag = combined.slice(offset, offset + AUTH_TAG_SIZE);
    offset += AUTH_TAG_SIZE;
    const encrypted = combined.slice(offset);
 
    // Get master key
    const masterKey = await getMasterKey();
 
    // Derive decryption key
    const derivedKey = await deriveKey(masterKey, salt);
 
    // Verify auth tag
    const authData = new Uint8Array(salt.length + iv.length + encrypted.length);
    let authOffset = 0;
    for (let i = 0; i < salt.length; i++) authData[authOffset++] = salt[i];
    for (let i = 0; i < iv.length; i++) authData[authOffset++] = iv[i];
    for (let i = 0; i < encrypted.length; i++) authData[authOffset++] = encrypted[i];
 
    const expectedAuthHash = await Crypto.digestStringAsync(
      Crypto.CryptoDigestAlgorithm.SHA256,
      Buffer.from(authData).toString('base64')
    );
    const expectedAuthTag = Uint8Array.from(
      Buffer.from(expectedAuthHash, 'hex').slice(0, AUTH_TAG_SIZE)
    );
 
    // Compare auth tags
    let authValid = true;
    for (let i = 0; i < AUTH_TAG_SIZE; i++) {
      if (authTag[i] !== expectedAuthTag[i]) {
        authValid = false;
        break;
      }
    }
 
    if (!authValid) {
      throw new Error('Authentication tag verification failed - data may be corrupted or tampered');
    }
 
    // Decrypt using XOR
    const decrypted = xorEncrypt(encrypted, derivedKey);
 
    // Convert to string
    return new TextDecoder().decode(decrypted);
  } catch (error) {
    throw new Error('Failed to decrypt data');
  }
}
 
/**
 * Check if a string is encrypted (base64 format with correct length)
 */
export function isEncrypted(data: string): boolean {
  try {
    // Must be base64
    const decoded = Buffer.from(data, 'base64');
 
    // Must have minimum length (salt + iv + authTag + at least 1 byte)
    const minLength = SALT_SIZE + IV_SIZE + AUTH_TAG_SIZE + 1;
 
    return decoded.length >= minLength;
  } catch {
    return false;
  }
}
 
/**
 * Clear the master encryption key (use on logout)
 */
export async function clearEncryptionKey(): Promise<void> {
  try {
    await SecureStore.deleteItemAsync(ENCRYPTION_KEY_NAME);
  } catch (error) {
    throw error;
  }
}