@tetherto/wdk-react-native-secure-storage 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,558 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createSecureStorage = createSecureStorage;
37
+ // External packages
38
+ const Keychain = __importStar(require("react-native-keychain"));
39
+ const LocalAuthentication = __importStar(require("expo-local-authentication"));
40
+ // Internal modules
41
+ const constants_1 = require("./constants");
42
+ const errors_1 = require("./errors");
43
+ const keychainHelpers_1 = require("./keychainHelpers");
44
+ const logger_1 = require("./logger");
45
+ const utils_1 = require("./utils");
46
+ const validation_1 = require("./validation");
47
+ // Secure storage keys (base keys without identifier)
48
+ const ENCRYPTION_KEY = (0, utils_1.createStorageKey)('wallet_encryption_key');
49
+ const ENCRYPTED_SEED = (0, utils_1.createStorageKey)('wallet_encrypted_seed');
50
+ const ENCRYPTED_ENTROPY = (0, utils_1.createStorageKey)('wallet_encrypted_entropy');
51
+ /**
52
+ * Secure storage wrapper factory for wallet credentials
53
+ *
54
+ * Uses react-native-keychain which provides encrypted storage with selective cloud sync.
55
+ * Creates a new instance each time it's called, allowing different configurations
56
+ * for different use cases. For most apps, you should create one instance and reuse it.
57
+ *
58
+ * SECURITY:
59
+ * - Storage is app-scoped by the OS (isolated by bundle ID/package name)
60
+ * - iOS: Uses Keychain Services with iCloud Keychain sync (when user signed into iCloud)
61
+ * - Android: Uses KeyStore with Google Cloud backup (when device backup enabled)
62
+ * - Data is ALWAYS encrypted at rest by Keychain (iOS) / KeyStore (Android)
63
+ * - Cloud sync behavior:
64
+ * - Encryption key: ACCESSIBLE.WHEN_UNLOCKED enables iCloud Keychain sync (iOS) and Google Cloud backup (Android)
65
+ * - Encrypted seed and entropy: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY prevents cloud sync (device-only storage)
66
+ * - Data is encrypted by Apple/Google's E2EE infrastructure
67
+ * - Encryption key requires device unlock + biometric/PIN authentication to access (when available)
68
+ * - Encrypted seed and entropy do not require authentication but are still encrypted at rest
69
+ * - On devices without authentication, data is still encrypted at rest but accessible when device is unlocked
70
+ * - Device-level keychain/keystore provides rate limiting and lockout mechanisms
71
+ * - Input validation prevents injection attacks
72
+ *
73
+ * Two different apps will NOT share data because storage is isolated by bundle ID/package name.
74
+ *
75
+ * @param options - Optional configuration for logger, authentication messages, and timeouts
76
+ * @returns SecureStorage instance
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const storage = createSecureStorage({
81
+ * logger: customLogger,
82
+ * authentication: {
83
+ * promptMessage: 'Authenticate to access wallet',
84
+ * },
85
+ * timeoutMs: 30000,
86
+ * })
87
+ * ```
88
+ */
89
+ function createSecureStorage(options) {
90
+ const logger = options?.logger || logger_1.defaultLogger;
91
+ const authOptions = options?.authentication || {};
92
+ // Validate timeout value if provided
93
+ const requestedTimeout = (0, validation_1.validateTimeout)(options?.timeoutMs);
94
+ // Validate authentication options if provided
95
+ (0, validation_1.validateAuthenticationOptions)(authOptions);
96
+ const timeoutMs = requestedTimeout || constants_1.DEFAULT_TIMEOUT_MS;
97
+ /**
98
+ * Error log message generators for consistent error handling
99
+ */
100
+ const getErrorLogMessage = (error, operation) => {
101
+ if (error instanceof errors_1.AuthenticationError) {
102
+ return `Authentication failed for ${operation}`;
103
+ }
104
+ if (error instanceof errors_1.TimeoutError) {
105
+ return `Timeout during ${operation}`;
106
+ }
107
+ if (error instanceof errors_1.ValidationError) {
108
+ return `Validation error during ${operation}`;
109
+ }
110
+ return `Failed to ${operation}`;
111
+ };
112
+ /**
113
+ * Standardized error handling helper
114
+ *
115
+ * @param error - The error to handle
116
+ * @param operation - Name of the operation for logging
117
+ * @param errorType - The error type to wrap unexpected errors in
118
+ * @param context - Additional context for logging
119
+ * @throws The error (either rethrown or wrapped)
120
+ * @internal
121
+ */
122
+ function handleSecureStorageError(error, operation, errorType, context) {
123
+ if (error instanceof errors_1.SecureStorageError) {
124
+ const errorContext = error instanceof errors_1.TimeoutError ? { ...context, timeoutMs } : context;
125
+ logger.error(getErrorLogMessage(error, operation), error, errorContext);
126
+ throw error;
127
+ }
128
+ const err = error instanceof Error ? error : new Error(String(error));
129
+ const wrappedError = new errorType(`Unexpected error during ${operation}`, err);
130
+ logger.error(`Unexpected error during ${operation}`, wrappedError, context);
131
+ throw wrappedError;
132
+ }
133
+ /**
134
+ * Get the device security level
135
+ *
136
+ * @returns The security level: NONE, SECRET (PIN/pattern), or BIOMETRIC
137
+ * @internal
138
+ */
139
+ async function getDeviceSecurityLevel() {
140
+ try {
141
+ const securityLevel = await LocalAuthentication.getEnrolledLevelAsync();
142
+ logger.debug('Device security level', { securityLevel });
143
+ return securityLevel;
144
+ }
145
+ catch (error) {
146
+ const err = error instanceof Error ? error : new Error(String(error));
147
+ logger.warn('Failed to check device security level, assuming NONE', { error: err.message });
148
+ return LocalAuthentication.SecurityLevel.NONE;
149
+ }
150
+ }
151
+ /**
152
+ * Check if biometric authentication is available
153
+ */
154
+ async function checkBiometricAvailable() {
155
+ try {
156
+ const compatible = await LocalAuthentication.hasHardwareAsync();
157
+ const enrolled = await LocalAuthentication.isEnrolledAsync();
158
+ return compatible && enrolled;
159
+ }
160
+ catch (error) {
161
+ const err = error instanceof Error ? error : new Error(String(error));
162
+ logger.error('Failed to check biometric availability', err, {});
163
+ return false;
164
+ }
165
+ }
166
+ /**
167
+ * Authenticate with biometrics
168
+ *
169
+ * @returns true if authentication succeeded, false otherwise
170
+ */
171
+ async function performAuthentication() {
172
+ try {
173
+ const options = {
174
+ promptMessage: authOptions.promptMessage || 'Authenticate to access your wallet',
175
+ cancelLabel: authOptions.cancelLabel || 'Cancel',
176
+ disableDeviceFallback: authOptions.disableDeviceFallback ?? false,
177
+ };
178
+ logger.debug('Starting biometric authentication');
179
+ const result = await LocalAuthentication.authenticateAsync(options);
180
+ if (result.success) {
181
+ logger.info('Biometric authentication successful');
182
+ return true;
183
+ }
184
+ else {
185
+ logger.warn('Biometric authentication failed or cancelled');
186
+ return false;
187
+ }
188
+ }
189
+ catch (error) {
190
+ const err = error instanceof Error ? error : new Error(String(error));
191
+ const authError = new errors_1.AuthenticationError('Biometric authentication failed', err);
192
+ logger.error('Biometric authentication error', authError, {});
193
+ throw authError;
194
+ }
195
+ }
196
+ /**
197
+ * Generic setter for secure values
198
+ *
199
+ * @param baseKey - The base storage key (e.g., ENCRYPTION_KEY)
200
+ * @param value - The value to store (must be non-empty string, max 10KB)
201
+ * @param identifier - Optional identifier for multiple wallets
202
+ * @param requireAuth - Whether authentication should be required (default: true)
203
+ * @param syncable - Whether the value should sync across devices (default: true)
204
+ * @throws {ValidationError} If input validation fails
205
+ * @throws {KeychainWriteError} If keychain operation fails
206
+ * @throws {TimeoutError} If operation times out
207
+ * @internal
208
+ */
209
+ async function setSecureValue(baseKey, value, identifier, requireAuth = true, syncable = true) {
210
+ (0, validation_1.validateValue)(value, 'value');
211
+ (0, validation_1.validateIdentifier)(identifier);
212
+ try {
213
+ // Use getEnrolledLevelAsync for more accurate device security detection
214
+ // This handles the Android case where device might not have any security configured
215
+ const [securityLevel, storageKey] = await Promise.all([
216
+ getDeviceSecurityLevel(),
217
+ (0, utils_1.getStorageKey)(baseKey, identifier),
218
+ ]);
219
+ // Device has authentication if security level is not NONE
220
+ const deviceAuthAvailable = securityLevel !== LocalAuthentication.SecurityLevel.NONE;
221
+ // If auth was requested but device has no security, log a warning but proceed without auth
222
+ // Data will still be encrypted at rest by the OS, just not protected by user authentication
223
+ if (requireAuth && !deviceAuthAvailable) {
224
+ logger.warn('Device has no security configured. Storing data without authentication protection.', {
225
+ baseKey,
226
+ identifier,
227
+ securityLevel
228
+ });
229
+ }
230
+ logger.debug('Storing secure value', { baseKey, identifier, requireAuth, syncable });
231
+ const result = await (0, utils_1.withTimeout)(Keychain.setGenericPassword(baseKey, value, {
232
+ service: storageKey,
233
+ ...(0, keychainHelpers_1.createKeychainOptions)(deviceAuthAvailable, requireAuth, syncable),
234
+ }), timeoutMs, `setSecureValue(${baseKey})`);
235
+ if (result === false) {
236
+ throw new errors_1.KeychainWriteError(`Failed to store ${baseKey}`);
237
+ }
238
+ logger.info('Secure value stored successfully', { baseKey, identifier });
239
+ }
240
+ catch (error) {
241
+ handleSecureStorageError(error, `store ${baseKey}`, errors_1.KeychainWriteError, { identifier, baseKey, requireAuth, syncable });
242
+ }
243
+ }
244
+ /**
245
+ * Check if a key exists in keychain without reading its value
246
+ * Used by hasWallet to check existence without authentication
247
+ *
248
+ * @param storageKey - The storage key to check
249
+ * @returns true if key exists with valid password, false if not found
250
+ * @throws {KeychainReadError} If keychain operation fails (not just "key not found")
251
+ * @throws {TimeoutError} If operation times out
252
+ * @internal
253
+ */
254
+ async function checkKeyExists(storageKey) {
255
+ try {
256
+ const credentials = await (0, utils_1.withTimeout)(Keychain.getGenericPassword({
257
+ service: storageKey,
258
+ // NO authenticationPrompt - we're just checking existence
259
+ }), timeoutMs, `checkKeyExists(${storageKey})`);
260
+ // Keychain.getGenericPassword returns:
261
+ // - false when key doesn't exist (not an error, just missing)
262
+ // - {username, password} object when key exists
263
+ // - null in some edge cases (treat as not found)
264
+ return (0, utils_1.isKeychainCredentials)(credentials);
265
+ }
266
+ catch (error) {
267
+ // Any exception here indicates a real keychain failure (not just "key not found")
268
+ // These should be propagated as errors, not treated as "key doesn't exist"
269
+ handleSecureStorageError(error, `check key existence (${storageKey})`, errors_1.KeychainReadError, { storageKey });
270
+ }
271
+ }
272
+ /**
273
+ * Generic getter for secure values
274
+ *
275
+ * @param baseKey - The base storage key (e.g., ENCRYPTION_KEY)
276
+ * @param identifier - Optional identifier for multiple wallets
277
+ * @param requireAuth - Whether authentication is required (default: true)
278
+ * @returns The stored value, or null if not found
279
+ * @throws {ValidationError} If identifier validation fails
280
+ * @throws {AuthenticationError} If authentication fails (when required and device has security)
281
+ * @throws {KeychainReadError} If keychain operation fails
282
+ * @throws {TimeoutError} If operation times out (only for non-auth operations)
283
+ * @internal
284
+ */
285
+ async function getSecureValue(baseKey, identifier, requireAuth = true) {
286
+ (0, validation_1.validateIdentifier)(identifier);
287
+ try {
288
+ // Check device security level to determine if auth is actually possible
289
+ const [securityLevel, storageKey] = await Promise.all([
290
+ getDeviceSecurityLevel(),
291
+ (0, utils_1.getStorageKey)(baseKey, identifier),
292
+ ]);
293
+ // Device has authentication if security level is not NONE
294
+ const deviceAuthAvailable = securityLevel !== LocalAuthentication.SecurityLevel.NONE;
295
+ // If auth was requested but device has no security, read without auth
296
+ // Data was stored without auth protection on this device, so we can read it without auth
297
+ const actuallyRequireAuth = requireAuth && deviceAuthAvailable;
298
+ if (requireAuth && !deviceAuthAvailable) {
299
+ logger.warn('Device has no security configured. Reading data without authentication.', {
300
+ baseKey,
301
+ identifier,
302
+ securityLevel
303
+ });
304
+ }
305
+ logger.debug('Retrieving secure value', { baseKey, identifier, requireAuth, actuallyRequireAuth });
306
+ const keychainOptions = actuallyRequireAuth
307
+ ? {
308
+ service: storageKey,
309
+ authenticationPrompt: {
310
+ title: authOptions.promptMessage || 'Authenticate to access your wallet',
311
+ cancel: authOptions.cancelLabel || 'Cancel',
312
+ },
313
+ }
314
+ : { service: storageKey };
315
+ // For auth-required operations (biometrics), don't use timeout.
316
+ // Biometric authentication should wait indefinitely for user interaction.
317
+ // Only apply timeout for non-auth operations which should complete quickly.
318
+ const keychainPromise = Keychain.getGenericPassword(keychainOptions);
319
+ const credentials = actuallyRequireAuth
320
+ ? await keychainPromise // No timeout for biometrics - wait for user
321
+ : await (0, utils_1.withTimeout)(keychainPromise, timeoutMs, `getSecureValue(${baseKey})`);
322
+ if (!(0, utils_1.isKeychainCredentials)(credentials)) {
323
+ logger.debug('Secure value not found', { baseKey, identifier });
324
+ return null;
325
+ }
326
+ logger.info('Secure value retrieved successfully', { baseKey, identifier });
327
+ return credentials.password;
328
+ }
329
+ catch (error) {
330
+ handleSecureStorageError(error, `get ${baseKey}`, errors_1.KeychainReadError, { identifier, baseKey, requireAuth });
331
+ }
332
+ }
333
+ // Create and return the instance
334
+ return {
335
+ /**
336
+ * Check if device security (PIN/pattern/password/biometrics) is enabled
337
+ *
338
+ * Apps can use this to check device security status and decide whether to
339
+ * require users to enable a PIN/pattern/password before storing sensitive data.
340
+ * The library will function without device security (data is still encrypted at rest).
341
+ *
342
+ * @returns Promise resolving to true if device security is enabled, false otherwise
343
+ */
344
+ async isDeviceSecurityEnabled() {
345
+ try {
346
+ const securityLevel = await LocalAuthentication.getEnrolledLevelAsync();
347
+ const isEnabled = securityLevel !== LocalAuthentication.SecurityLevel.NONE;
348
+ logger.debug('Device security check', { securityLevel, isEnabled });
349
+ return isEnabled;
350
+ }
351
+ catch (error) {
352
+ const err = error instanceof Error ? error : new Error(String(error));
353
+ logger.warn('Failed to check device security level', { error: err.message });
354
+ // If we can't check, assume it's not enabled to be safe
355
+ return false;
356
+ }
357
+ },
358
+ /**
359
+ * Check if biometric authentication is available
360
+ */
361
+ async isBiometricAvailable() {
362
+ return checkBiometricAvailable();
363
+ },
364
+ /**
365
+ * Authenticate with biometrics
366
+ *
367
+ * @returns true if authentication succeeded, false otherwise
368
+ */
369
+ async authenticate() {
370
+ return performAuthentication();
371
+ },
372
+ /**
373
+ * Store encryption key securely
374
+ *
375
+ * @param key - The encryption key to store (must be non-empty string, max 10KB)
376
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
377
+ * @throws {ValidationError} If key or identifier is invalid
378
+ * @throws {KeychainWriteError} If keychain operation fails
379
+ * @throws {TimeoutError} If operation times out
380
+ */
381
+ async setEncryptionKey(key, identifier, options) {
382
+ const requireAuth = options?.requireBiometrics ?? true;
383
+ return setSecureValue(ENCRYPTION_KEY, key, identifier, requireAuth, true);
384
+ },
385
+ /**
386
+ * Get encryption key from secure storage
387
+ *
388
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
389
+ * @returns The encryption key, or null if not found
390
+ *
391
+ * @throws {ValidationError} If identifier is invalid format
392
+ * @throws {AuthenticationError} If authentication fails
393
+ * @throws {KeychainReadError} If keychain operation fails
394
+ * @throws {TimeoutError} If operation times out
395
+ */
396
+ async getEncryptionKey(identifier, options) {
397
+ const requireAuth = options?.requireBiometrics ?? true;
398
+ return getSecureValue(ENCRYPTION_KEY, identifier, requireAuth);
399
+ },
400
+ /**
401
+ * Store encrypted seed securely
402
+ *
403
+ * @param encryptedSeed - The encrypted seed to store (must be non-empty string, max 10KB)
404
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
405
+ *
406
+ * @throws {ValidationError} If encryptedSeed is invalid
407
+ * @throws {ValidationError} If identifier is invalid format
408
+ * @throws {KeychainWriteError} If keychain operation fails
409
+ * @throws {TimeoutError} If operation times out
410
+ *
411
+ * Note: Encrypted seed does not require authentication for access and is stored device-only (not synced across devices)
412
+ */
413
+ async setEncryptedSeed(encryptedSeed, identifier) {
414
+ return setSecureValue(ENCRYPTED_SEED, encryptedSeed, identifier, false, false);
415
+ },
416
+ /**
417
+ * Get encrypted seed from secure storage
418
+ *
419
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
420
+ * @returns The encrypted seed, or null if not found
421
+ *
422
+ * @throws {ValidationError} If identifier is invalid format
423
+ * @throws {KeychainReadError} If keychain operation fails
424
+ * @throws {TimeoutError} If operation times out
425
+ *
426
+ * Note: Encrypted seed does not require authentication for access and is stored device-only (not synced across devices)
427
+ */
428
+ async getEncryptedSeed(identifier) {
429
+ return getSecureValue(ENCRYPTED_SEED, identifier, false);
430
+ },
431
+ /**
432
+ * Store encrypted entropy securely
433
+ *
434
+ * @param encryptedEntropy - The encrypted entropy to store (must be non-empty string, max 10KB)
435
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
436
+ *
437
+ * @throws {ValidationError} If encryptedEntropy is invalid
438
+ * @throws {ValidationError} If identifier is invalid format
439
+ * @throws {KeychainWriteError} If keychain operation fails
440
+ * @throws {TimeoutError} If operation times out
441
+ *
442
+ * Note: Encrypted entropy does not require authentication for access and is stored device-only (not synced across devices)
443
+ */
444
+ async setEncryptedEntropy(encryptedEntropy, identifier) {
445
+ return setSecureValue(ENCRYPTED_ENTROPY, encryptedEntropy, identifier, false, false);
446
+ },
447
+ /**
448
+ * Get encrypted entropy from secure storage
449
+ *
450
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
451
+ * @returns The encrypted entropy, or null if not found
452
+ *
453
+ * @throws {ValidationError} If identifier is invalid format
454
+ * @throws {KeychainReadError} If keychain operation fails
455
+ * @throws {TimeoutError} If operation times out
456
+ *
457
+ * Note: Encrypted entropy does not require authentication for access and is stored device-only (not synced across devices)
458
+ */
459
+ async getEncryptedEntropy(identifier) {
460
+ return getSecureValue(ENCRYPTED_ENTROPY, identifier, false);
461
+ },
462
+ /**
463
+ * Get all encrypted wallet data at once (seed, entropy, and encryption key)
464
+ * Uses Promise.all, so fails fast if any operation fails.
465
+ *
466
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
467
+ * @returns Object containing seed, entropy, and encryptionKey (may be null if not found)
468
+ * @throws {ValidationError} If identifier is invalid format
469
+ * @throws {AuthenticationError} If authentication fails
470
+ * @throws {KeychainReadError} If keychain operation fails
471
+ * @throws {TimeoutError} If operation times out
472
+ */
473
+ async getAllEncrypted(identifier) {
474
+ const [encryptedSeed, encryptedEntropy, encryptionKey] = await Promise.all([
475
+ this.getEncryptedSeed(identifier),
476
+ this.getEncryptedEntropy(identifier),
477
+ this.getEncryptionKey(identifier),
478
+ ]);
479
+ return {
480
+ encryptedSeed,
481
+ encryptedEntropy,
482
+ encryptionKey,
483
+ };
484
+ },
485
+ /**
486
+ * Check if wallet credentials exist (without requiring authentication)
487
+ * Returns false only when wallet is definitively not found. Errors are thrown.
488
+ *
489
+ * IMPORTANT: Only checks encrypted seed, NOT encryption key.
490
+ * Encryption key is protected with biometrics, so checking it would trigger
491
+ * an authentication prompt. Encrypted seed is stored without auth requirement,
492
+ * so checking it won't trigger biometrics.
493
+ *
494
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
495
+ * @returns true if wallet exists, false if definitively not found
496
+ * @throws {ValidationError} If identifier is invalid format
497
+ * @throws {TimeoutError} If operation times out
498
+ * @throws {KeychainReadError} If keychain operation fails
499
+ */
500
+ async hasWallet(identifier) {
501
+ // ONLY check encrypted seed - it does NOT require biometrics
502
+ // Encryption key is protected with ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
503
+ // so checking it would trigger a biometric prompt with the default
504
+ // "Authenticate to retrieve secret" message from react-native-keychain.
505
+ // By only checking the seed (which is stored without auth requirement),
506
+ // we can determine wallet existence without triggering biometrics.
507
+ const seedStorageKey = await (0, utils_1.getStorageKey)(ENCRYPTED_SEED, identifier);
508
+ return await checkKeyExists(seedStorageKey);
509
+ },
510
+ /**
511
+ * Delete all wallet credentials
512
+ *
513
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
514
+ *
515
+ * @throws {ValidationError} If identifier is invalid format
516
+ * @throws {SecureStorageError} If deletion fails (with details of which items failed)
517
+ * @throws {TimeoutError} If operation times out
518
+ */
519
+ async deleteWallet(identifier) {
520
+ // Batch storage key generation
521
+ const [encryptionKey, encryptedSeed, encryptedEntropy] = await Promise.all([
522
+ (0, utils_1.getStorageKey)(ENCRYPTION_KEY, identifier),
523
+ (0, utils_1.getStorageKey)(ENCRYPTED_SEED, identifier),
524
+ (0, utils_1.getStorageKey)(ENCRYPTED_ENTROPY, identifier),
525
+ ]);
526
+ const serviceKeys = [encryptionKey, encryptedSeed, encryptedEntropy];
527
+ const serviceNames = ['encryptionKey', 'encryptedSeed', 'encryptedEntropy'];
528
+ logger.debug('Deleting wallet', { identifier, services: serviceNames });
529
+ const results = await Promise.allSettled(serviceKeys.map((key) => (0, utils_1.withTimeout)(Keychain.resetGenericPassword({ service: key }), timeoutMs, `deleteWallet(${key})`)));
530
+ const failedServices = serviceNames.filter((_, index) => {
531
+ const result = results[index];
532
+ if (!result)
533
+ return true;
534
+ return result.status === 'rejected' || (result.status === 'fulfilled' && result.value === false);
535
+ });
536
+ if (failedServices.length > 0) {
537
+ const error = new errors_1.SecureStorageError(`Failed to delete wallet: ${failedServices.join(', ')}`, 'WALLET_DELETE_ERROR');
538
+ logger.error('Wallet deletion failed', error, { identifier, failedServices });
539
+ throw error;
540
+ }
541
+ logger.info('Wallet deleted successfully', { identifier });
542
+ },
543
+ /**
544
+ * Cleanup method to release resources associated with this storage instance
545
+ *
546
+ * Currently, this is a no-op as the module uses shared resources. This method is provided
547
+ * for future extensibility and to maintain a consistent API.
548
+ *
549
+ * Note: This does NOT delete stored wallet data - use deleteWallet() for that purpose.
550
+ */
551
+ cleanup() {
552
+ // Currently a no-op, but provided for future extensibility
553
+ // If instance-specific resources are added in the future, they should be
554
+ // cleaned up here
555
+ logger.debug('Storage instance cleanup called (no-op)', {});
556
+ },
557
+ };
558
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Type for keychain credentials returned by react-native-keychain
3
+ */
4
+ export type KeychainCredentials = {
5
+ username: string;
6
+ password: string;
7
+ service: string;
8
+ storage?: string;
9
+ };
10
+ /**
11
+ * Type guard to check if a value is valid keychain credentials
12
+ *
13
+ * @param value - The value to check
14
+ * @returns true if value is valid keychain credentials with non-empty password
15
+ */
16
+ export declare function isKeychainCredentials(value: unknown): value is KeychainCredentials;
17
+ /**
18
+ * Valid storage key names
19
+ * These are the only allowed base keys for secure storage
20
+ */
21
+ export declare const VALID_STORAGE_KEYS: readonly ["wallet_encryption_key", "wallet_encrypted_seed", "wallet_encrypted_entropy"];
22
+ /**
23
+ * Type-safe storage key names
24
+ * Union type of valid storage keys
25
+ */
26
+ export type StorageKey = (typeof VALID_STORAGE_KEYS)[number];
27
+ /**
28
+ * Create a StorageKey from a string with runtime validation
29
+ *
30
+ * @param key - The key string to convert to StorageKey
31
+ * @returns The validated StorageKey
32
+ * @throws {ValidationError} If the key is not a valid storage key
33
+ */
34
+ export declare function createStorageKey(key: string): StorageKey;
35
+ /**
36
+ * Generate a secure storage key from base key and optional identifier
37
+ *
38
+ * @param baseKey - The base storage key (must be a valid StorageKey)
39
+ * @param identifier - Optional identifier (e.g., email) to support multiple wallets
40
+ * @returns Promise that resolves to the storage key
41
+ * @throws {ValidationError} If baseKey is not a valid storage key
42
+ */
43
+ export declare function getStorageKey(baseKey: StorageKey, identifier?: string): Promise<string>;
44
+ /**
45
+ * Create a timeout wrapper for promises
46
+ *
47
+ * **IMPORTANT:** Uses Promise.race() which does NOT cancel the underlying promise.
48
+ * The original promise continues executing after timeout (result is ignored).
49
+ * This is acceptable for keychain operations as they're fast and OS-bounded.
50
+ *
51
+ * @param promise - The promise to wrap
52
+ * @param timeoutMs - Timeout in milliseconds (should be validated via validateTimeout before calling)
53
+ * @param operation - Name of the operation for error messages
54
+ * @returns Promise that rejects on timeout
55
+ * @throws {ValidationError} If timeoutMs is invalid
56
+ * @throws {TimeoutError} If operation times out
57
+ */
58
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T>;
59
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAkBlF;AAYD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,yFAIrB,CAAA;AAEV;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA;AAmB5D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB7F;AAGD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,CAyBZ"}