Agent skill
expo-encryption-patterns
Implement AES-256-CBC encryption with secure key storage for Expo/React Native apps using expo-secure-store and crypto-js. Use when encrypting sensitive user data (journal entries, health data, PII), storing encryption keys securely, implementing encrypt-then-MAC patterns, or working with expo-sqlite encrypted data. Covers key generation, encryption/decryption flows, and platform-agnostic crypto operations.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/expo-encryption-patterns
SKILL.md
Expo Encryption Patterns
Implement production-grade encryption for Expo/React Native apps with secure key management.
Core Pattern
1. Key Management
Store encryption keys ONLY in expo-secure-store (Keychain/Keystore). Never use AsyncStorage.
import * as SecureStore from 'expo-secure-store';
const ENCRYPTION_KEY_NAME = 'app_encryption_key';
async function getOrCreateKey(): Promise<string> {
let key = await SecureStore.getItemAsync(ENCRYPTION_KEY_NAME);
if (!key) {
key = await generateSecureKey();
await SecureStore.setItemAsync(ENCRYPTION_KEY_NAME, key);
}
return key;
}
2. Encrypt Content (AES-256-CBC + HMAC)
Format: {iv}:{ciphertext}:{mac}
import CryptoJS from 'crypto-js';
import * as Crypto from 'expo-crypto';
async function encryptContent(plaintext: string): Promise<string> {
const key = await getOrCreateKey();
// Generate unique IV
const ivBytes = await Crypto.getRandomBytesAsync(16);
const iv = Array.from(ivBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
const ivWordArray = CryptoJS.enc.Hex.parse(iv);
const keyWordArray = CryptoJS.enc.Hex.parse(key);
// Encrypt
const encrypted = CryptoJS.AES.encrypt(plaintext, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
// Generate MAC (encrypt-then-MAC)
const payload = `${iv}:${encrypted.toString()}`;
const macKey = CryptoJS.SHA256(keyWordArray);
const mac = CryptoJS.HmacSHA256(payload, macKey).toString(CryptoJS.enc.Hex);
return `${payload}:${mac}`;
}
3. Decrypt Content with MAC Verification
async function decryptContent(encrypted: string): Promise<string> {
const key = await getOrCreateKey();
const parts = encrypted.split(':');
if (parts.length !== 3) throw new Error('Invalid format');
const [iv, ciphertext, mac] = parts;
// Verify MAC (constant-time comparison)
const keyWordArray = CryptoJS.enc.Hex.parse(key);
const macKey = CryptoJS.SHA256(keyWordArray);
const payload = `${iv}:${ciphertext}`;
const expectedMac = CryptoJS.HmacSHA256(payload, macKey).toString(CryptoJS.enc.Hex);
if (!constantTimeEqual(expectedMac, mac)) {
throw new Error('Integrity check failed');
}
// Decrypt
const ivWordArray = CryptoJS.enc.Hex.parse(iv);
const decrypted = CryptoJS.AES.decrypt(ciphertext, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
function constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
Platform-Agnostic Crypto
Use different crypto sources for native vs web:
import { Platform } from 'react-native';
async function getRandomBytes(length: number): Promise<Uint8Array> {
if (Platform.OS === 'web') {
return crypto.getRandomValues(new Uint8Array(length));
}
const { getRandomBytesAsync } = await import('expo-crypto');
return getRandomBytesAsync(length);
}
Key Derivation (PBKDF2)
For user-password-based keys:
function deriveKey(password: string, salt: string): string {
return CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 100000,
}).toString();
}
Integration with SQLite
Encrypt before storing, decrypt after retrieval:
// Storing
const encrypted = await encryptContent(journalEntry);
await db.runAsync('INSERT INTO journal (encrypted_body) VALUES (?)', encrypted);
// Retrieving
const rows = await db.getAllAsync('SELECT encrypted_body FROM journal');
const entries = await Promise.all(
rows.map(async (row) => ({
...row,
content: await decryptContent(row.encrypted_body),
})),
);
Security Checklist
- Keys stored only in SecureStore
- Unique IV generated per encryption
- MAC verification before decryption
- Constant-time MAC comparison
- PBKDF2 for password-based keys (100k+ iterations)
- No sensitive data in logs
- Keys deleted on logout/account deletion
See Also
- references/web-crypto.md - Web-specific crypto patterns
- test-encryption skill - Testing encrypted flows
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?