11import AsyncStorage from '@react-native-async-storage/async-storage' ;
2+ import * as SecureStore from 'expo-secure-store' ;
23
34const TUNNEL_METADATA_KEY = 'speakmcp_tunnel_metadata_v1' ;
5+ const API_KEY_SECURE_KEY = 'speakmcp_api_key_secure' ;
46
57/**
68 * Tunnel metadata for connection persistence.
79 * Stores information needed to resume or reconnect to a tunnel.
10+ * API key is stored securely using expo-secure-store (iOS Keychain/Android Keystore).
811 */
912export interface TunnelMetadata {
1013 /** The base URL of the tunnel endpoint */
1114 baseUrl : string ;
12- /** API key for authentication */
15+ /** API key for authentication - stored in secure storage */
1316 apiKey : string ;
1417 /** Timestamp of last successful connection */
1518 lastConnectedAt : number ;
@@ -23,19 +26,27 @@ export interface TunnelMetadata {
2326
2427/**
2528 * Save tunnel metadata for later reconnection.
26- *
27- * TODO: Security enhancement - Consider storing the apiKey in secure storage
28- * (expo-secure-store for iOS Keychain/Android Keystore) instead of AsyncStorage
29- * for production deployments where the API key grants sensitive access.
29+ * API key is stored securely using expo-secure-store.
3030 */
3131export async function saveTunnelMetadata ( metadata : TunnelMetadata ) : Promise < void > {
3232 try {
33- await AsyncStorage . setItem ( TUNNEL_METADATA_KEY , JSON . stringify ( metadata ) ) ;
33+ // Store apiKey securely using expo-secure-store (iOS Keychain/Android Keystore)
34+ if ( metadata . apiKey ) {
35+ await SecureStore . setItemAsync ( API_KEY_SECURE_KEY , metadata . apiKey , {
36+ requireAuthentication : false ,
37+ } ) ;
38+ }
39+
40+ // Store non-sensitive metadata in AsyncStorage
41+ const { apiKey, ...safeMetadata } = metadata ;
42+ await AsyncStorage . setItem ( TUNNEL_METADATA_KEY , JSON . stringify ( safeMetadata ) ) ;
43+
3444 console . log ( '[TunnelPersistence] Saved tunnel metadata:' , {
3545 baseUrl : metadata . baseUrl ,
3646 hasApiKey : ! ! metadata . apiKey ,
3747 hasSessionId : ! ! metadata . sessionId ,
3848 hasResumeToken : ! ! metadata . resumeToken ,
49+ secureStorageUsed : true ,
3950 } ) ;
4051 } catch ( error ) {
4152 console . error ( '[TunnelPersistence] Failed to save tunnel metadata:' , error ) ;
@@ -45,27 +56,39 @@ export async function saveTunnelMetadata(metadata: TunnelMetadata): Promise<void
4556/**
4657 * Load previously saved tunnel metadata.
4758 * Returns null if no metadata exists or if it's invalid.
59+ * API key is retrieved from secure storage.
4860 */
4961export async function loadTunnelMetadata ( ) : Promise < TunnelMetadata | null > {
5062 try {
63+ // Load non-sensitive metadata from AsyncStorage
5164 const stored = await AsyncStorage . getItem ( TUNNEL_METADATA_KEY ) ;
5265 if ( ! stored ) {
5366 return null ;
5467 }
5568
5669 const parsed = JSON . parse ( stored ) ;
5770
58- // Validate required fields exist and have correct types
71+ // Validate required fields exist and have correct types (except apiKey)
5972 if (
6073 typeof parsed . baseUrl !== 'string' ||
61- typeof parsed . apiKey !== 'string' ||
6274 typeof parsed . lastConnectedAt !== 'number'
6375 ) {
6476 console . warn ( '[TunnelPersistence] Invalid stored metadata: missing required fields or incorrect types' ) ;
6577 return null ;
6678 }
6779
68- return parsed as TunnelMetadata ;
80+ // Retrieve apiKey from secure storage
81+ const apiKey = await SecureStore . getItemAsync ( API_KEY_SECURE_KEY ) ;
82+
83+ if ( ! apiKey ) {
84+ console . warn ( '[TunnelPersistence] API key not found in secure storage' ) ;
85+ return null ;
86+ }
87+
88+ return {
89+ ...parsed ,
90+ apiKey,
91+ } as TunnelMetadata ;
6992 } catch ( error ) {
7093 console . error ( '[TunnelPersistence] Failed to load tunnel metadata:' , error ) ;
7194 return null ;
@@ -96,11 +119,13 @@ export async function updateTunnelMetadata(
96119
97120/**
98121 * Clear tunnel metadata (for logout or reset scenarios).
122+ * Also clears the secure-stored API key.
99123 */
100124export async function clearTunnelMetadata ( ) : Promise < void > {
101125 try {
102126 await AsyncStorage . removeItem ( TUNNEL_METADATA_KEY ) ;
103- console . log ( '[TunnelPersistence] Cleared tunnel metadata' ) ;
127+ await SecureStore . deleteItemAsync ( API_KEY_SECURE_KEY ) ;
128+ console . log ( '[TunnelPersistence] Cleared tunnel metadata and secure API key' ) ;
104129 } catch ( error ) {
105130 console . error ( '[TunnelPersistence] Failed to clear tunnel metadata:' , error ) ;
106131 }
@@ -127,4 +152,3 @@ export async function isTunnelMetadataFresh(maxAgeMs: number = 24 * 60 * 60 * 10
127152 const age = Date . now ( ) - metadata . lastConnectedAt ;
128153 return age < maxAgeMs ;
129154}
130-
0 commit comments