Architecting Secure Storage for Ionic Capacitor Applications
Implement a secure storage layer for Ionic Capacitor apps using Identity Vault to protect tokens with biometric encryption and a robust web-crypto fallback.
12 Aug 2025, 15:18 UTC

The Problem: Protecting Secrets in Hybrid Apps
Hybrid applications built with Ionic and Capacitor often need to store sensitive data, such as JWT authentication tokens or API keys. Standard localStorage or IndexedDB are unsuitable for this because they store data in plaintext, making them vulnerable to extraction if a device is lost, stolen, or compromised by other malicious apps.
The goal is to implement a storage layer that leverages hardware-backed encryption (iOS Keychain and Android Keystore) while maintaining a consistent API across native mobile platforms and Progressive Web Apps (PWAs).
Requirements
- Encryption: Data must be encrypted at rest using industry standards (e.g., AES-256 GCM).
- Hardware Integration: Use the device's secure enclave or keystore to manage encryption keys.
- Access Control: Support biometric authentication (FaceID, TouchID, Fingerprint) with a PIN/Password fallback.
- Cross-Platform Parity: Provide a graceful degradation path for browser-based environments where native plugins are unavailable.
- Observability: A mechanism to verify the vault's status (locked/unlocked) without exposing the secrets themselves.
Smallest Suitable Design
The most efficient architecture is a thin service wrapper around the @ionic-enterprise/identity-vault (or the community-supported @capacitor-community/secure-storage). This wrapper abstracts the plugin's complexity and provides a unified interface for the rest of the application.
Implementation Example (TypeScript)
import { Injectable } from '@angular/core';
import { IdentityVault, VaultType, VaultSecurity } from '@ionic-enterprise/identity-vault';
@Injectable({ providedIn: 'root' })
export class SecureStorageService {
private vault: IdentityVault | null = null;
private isNative = false;
constructor() {
// Check if running in a native Capacitor environment
this.isNative = !!(window as any).Capacitor;
if (this.isNative) {
this.vault = new IdentityVault({
key: 'app_secure_vault',
type: VaultType.Secure,
security: VaultSecurity.AfterLock,
backgroundBehavior: 'lock' // Automatically lock when app is backgrounded
});
}
}
async set(key: string, value: string): Promise {
if (this.isNative && this.vault) {
await this.vault.setValue(key, value);
} else {
await this.webFallbackSet(key, value);
}
}
async get(key: string): Promise {
if (this.isNative && this.vault) {
// This will trigger the native biometric/PIN prompt if the vault is locked
const value = await this.vault.getValue(key);
return value ?? null;
}
return await this.webFallbackGet(key);
}
async clear(): Promise {
if (this.isNative && this.vault) {
await this.vault.clear();
} else {
await this.webFallbackClear();
}
}
async getStatus() {
if (!this.isNative || !this.vault) return { native: false, locked: false };
return {
native: true,
locked: await this.vault.isLocked()
};
}
private async webFallbackSet(k: string, v: string) { /* Web Crypto + IndexedDB logic */ }
private async webFallbackGet(k: string) { return null; /* Web Crypto + IndexedDB logic */ }
private async webFallbackClear() { /* Clear IndexedDB */ }
}
Trust and Data Boundaries
The design establishes a strict boundary between the application logic and the sensitive data:
- Native Boundary: Encrypted blobs are stored within the app's sandbox. The actual decryption keys are managed by the OS (iOS Keychain/Android Keystore) and never enter the JavaScript heap in plaintext until the vault is explicitly unlocked.
- Web Boundary: In PWA mode, the boundary shifts to the Same-Origin Policy. Data is stored in IndexedDB, but should be encrypted via the Web Crypto API to prevent casual inspection via browser dev tools.
- Access Boundary: By setting
backgroundBehavior: 'lock', the data boundary is re-established every time the user leaves the app, requiring re-authentication to access the keys.
Operational Checks and Diagnostics
To ensure the storage layer is functioning correctly, implement the following checks during the app bootstrap phase:
- Plugin Availability: Check if
window.Capacitoris defined. If not, log a warning and switch to the web fallback. - Vault Health: Use the
getStatus()method to confirm the vault is initialized. - Biometric Readiness: On native devices, verify if biometrics are enabled in the OS settings. If disabled, ensure the flow redirects to the PIN/Password prompt.
Decision Logic for Initialization
if (Capacitor.isNativePlatform()) {
try {
await vault.initialize();
} catch (err) {
console.error('Secure Vault failed to init, falling back to software');
useSoftwareFallback = true;
}
} else {
useWebCrypto = true;
}
Failure Modes and Design Changes
Certain conditions require a change in the security posture or design:
- Lack of Secure Hardware: Some low-end Android devices lack a hardware-backed keystore. In these cases, the vault falls back to software encryption. If your threat model forbids software-only encryption, you must detect this state and block the app from storing high-value secrets.
- MDM Restrictions: Mobile Device Management (MDM) profiles may disable biometrics. The design must allow for a manual PIN entry system to avoid locking users out of the app.
- OS API Updates: Android's
BiometricPromptand iOS'sLocalAuthenticationevolve. When upgrading Capacitor versions, the lock/unlock flow must be re-verified on physical devices to ensure the native bridge is still compatible.
Practical Verification
Verification must be performed on physical hardware, as simulators often mock the keystore and bypass biometric prompts.
- Native Lock Test: Store a token, call the
lock()method, and attempt toget()the token. The app should trigger a biometric prompt. - Biometric Failure Test: Disable biometrics in the device settings. Attempt to retrieve the token; the app should automatically fall back to the device PIN or a custom password prompt.
- PWA Test: Run the app via
ionic serve. Verify that thegetStatus()method reportsnative: falseand that data is persisted in IndexedDB without crashing the app.
Limitations
- XSS Vulnerability: In the web fallback mode, if the application is vulnerable to Cross-Site Scripting (XSS), an attacker could potentially call the service methods to retrieve decrypted data.
- Rooted Devices: On rooted or jailbroken devices, the OS-level sandbox is compromised, which may allow sophisticated attackers to extract keys from the keystore.
- Version Alignment: Ensure the
@ionic-enterprise/identity-vaultversion matches your Capacitor and Ionic CLI versions to prevent native bridge mismatches.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.