@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.
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +99 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/keychainHelpers.d.ts +15 -0
- package/dist/keychainHelpers.d.ts.map +1 -0
- package/dist/keychainHelpers.js +56 -0
- package/dist/logger.d.ts +75 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +90 -0
- package/dist/secureStorage.d.ts +104 -0
- package/dist/secureStorage.d.ts.map +1 -0
- package/dist/secureStorage.js +558 -0
- package/dist/utils.d.ts +59 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +170 -0
- package/dist/validation.d.ts +48 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +130 -0
- package/package.json +8 -3
- package/src/__tests__/__mocks__/expo-local-authentication.ts +1 -1
- package/src/__tests__/__mocks__/react-native-keychain.ts +1 -1
|
@@ -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
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -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"}
|