@oxyhq/services 5.16.18 → 5.16.20
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/README.md +7 -8
- package/lib/commonjs/crypto/index.js +0 -7
- package/lib/commonjs/crypto/index.js.map +1 -1
- package/lib/commonjs/crypto/keyManager.js +2 -2
- package/lib/commonjs/crypto/polyfill.js +4 -4
- package/lib/commonjs/crypto/types.js +2 -0
- package/lib/commonjs/crypto/types.js.map +1 -0
- package/lib/commonjs/i18n/locales/en-US.json +1 -1
- package/lib/commonjs/index.js +0 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +54 -11
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +56 -15
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +52 -2
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/crypto/index.js +2 -4
- package/lib/module/crypto/index.js.map +1 -1
- package/lib/module/crypto/keyManager.js +2 -2
- package/lib/module/crypto/polyfill.js +4 -4
- package/lib/module/crypto/types.js +2 -0
- package/lib/module/crypto/types.js.map +1 -0
- package/lib/module/i18n/locales/en-US.json +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +49 -6
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +56 -16
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +52 -2
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/typescript/crypto/index.d.ts +2 -2
- package/lib/typescript/crypto/index.d.ts.map +1 -1
- package/lib/typescript/crypto/keyManager.d.ts +2 -2
- package/lib/typescript/crypto/polyfill.d.ts +2 -2
- package/lib/typescript/crypto/types.d.ts +18 -0
- package/lib/typescript/crypto/types.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +2 -2
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +3 -3
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts +7 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/crypto/index.ts +3 -6
- package/src/crypto/keyManager.ts +2 -2
- package/src/crypto/polyfill.ts +4 -4
- package/src/crypto/types.ts +19 -0
- package/src/i18n/locales/en-US.json +1 -1
- package/src/index.ts +2 -4
- package/src/ui/context/OxyContext.tsx +63 -7
- package/src/ui/context/hooks/useAuthOperations.ts +67 -17
- package/src/ui/hooks/useSessionSocket.ts +62 -2
- package/lib/commonjs/crypto/recoveryPhrase.js +0 -152
- package/lib/commonjs/crypto/recoveryPhrase.js.map +0 -1
- package/lib/commonjs/ui/hooks/useIdentityMutations.js +0 -111
- package/lib/commonjs/ui/hooks/useIdentityMutations.js.map +0 -1
- package/lib/module/crypto/recoveryPhrase.js +0 -147
- package/lib/module/crypto/recoveryPhrase.js.map +0 -1
- package/lib/module/ui/hooks/useIdentityMutations.js +0 -105
- package/lib/module/ui/hooks/useIdentityMutations.js.map +0 -1
- package/lib/typescript/crypto/recoveryPhrase.d.ts +0 -59
- package/lib/typescript/crypto/recoveryPhrase.d.ts.map +0 -1
- package/lib/typescript/types/bip39.d.ts +0 -32
- package/lib/typescript/ui/hooks/useIdentityMutations.d.ts +0 -29
- package/lib/typescript/ui/hooks/useIdentityMutations.d.ts.map +0 -1
- package/src/crypto/recoveryPhrase.ts +0 -166
- package/src/types/bip39.d.ts +0 -32
- package/src/ui/hooks/useIdentityMutations.ts +0 -115
package/src/crypto/polyfill.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* before any crypto operations are performed.
|
|
6
6
|
*
|
|
7
7
|
* Polyfills included:
|
|
8
|
-
* - Buffer: Required by
|
|
9
|
-
* - crypto.getRandomValues: Required
|
|
8
|
+
* - Buffer: Required by crypto libraries
|
|
9
|
+
* - crypto.getRandomValues: Required for secure random number generation
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
// Import Buffer polyfill for React Native compatibility
|
|
13
|
-
//
|
|
13
|
+
// Some crypto libraries depend on Buffer which isn't available in React Native
|
|
14
14
|
import { Buffer } from 'buffer';
|
|
15
15
|
|
|
16
16
|
// Get the global object in a cross-platform way
|
|
@@ -30,7 +30,7 @@ if (!globalObject.Buffer) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Polyfill crypto.getRandomValues for React Native
|
|
33
|
-
// This is required by
|
|
33
|
+
// This is required by crypto libraries for secure random number generation
|
|
34
34
|
type CryptoLike = {
|
|
35
35
|
getRandomValues: <T extends ArrayBufferView>(array: T) => T;
|
|
36
36
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic types for identity management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encrypted backup data structure
|
|
7
|
+
* Used for identity backup files and QR code transfers
|
|
8
|
+
*/
|
|
9
|
+
export interface BackupData {
|
|
10
|
+
/** Base64-encoded encrypted private key */
|
|
11
|
+
encrypted: string;
|
|
12
|
+
/** Hex-encoded salt used for key derivation */
|
|
13
|
+
salt: string;
|
|
14
|
+
/** Hex-encoded initialization vector */
|
|
15
|
+
iv: string;
|
|
16
|
+
/** Public key associated with the encrypted private key */
|
|
17
|
+
publicKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"enterCode": "Enter the 6‑digit code from your authenticator app.",
|
|
238
238
|
"newPassword": "Set New Password",
|
|
239
239
|
"resetSuccess": "Your password has been reset! You can now sign in.",
|
|
240
|
-
"noEmail": "We no longer send recovery emails. Please use your
|
|
240
|
+
"noEmail": "We no longer send recovery emails. Please use your backup file to restore your identity. Contact support if you need assistance.",
|
|
241
241
|
"password": {
|
|
242
242
|
"minLength": "Password must be at least 8 characters long",
|
|
243
243
|
"mismatch": "Passwords do not match",
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,7 @@ import './crypto/polyfill';
|
|
|
13
13
|
// Crypto/Identity exports (must be before core to ensure polyfills are available)
|
|
14
14
|
export {
|
|
15
15
|
KeyManager,
|
|
16
|
-
SignatureService
|
|
17
|
-
RecoveryPhraseService
|
|
16
|
+
SignatureService
|
|
18
17
|
} from './crypto';
|
|
19
18
|
|
|
20
19
|
// Core exports
|
|
@@ -23,8 +22,7 @@ export { OXY_CLOUD_URL, oxyClient } from './core';
|
|
|
23
22
|
export type {
|
|
24
23
|
KeyPair,
|
|
25
24
|
SignedMessage,
|
|
26
|
-
AuthChallenge
|
|
27
|
-
RecoveryPhraseResult
|
|
25
|
+
AuthChallenge
|
|
28
26
|
} from './crypto';
|
|
29
27
|
|
|
30
28
|
// React context
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
|
-
import { Platform } from 'react-native';
|
|
12
|
+
import { Platform, Alert } from 'react-native';
|
|
13
13
|
import { OxyServices } from '../../core';
|
|
14
14
|
import type { User, ApiError } from '../../models/interfaces';
|
|
15
15
|
import type { ClientSession } from '../../models/session';
|
|
@@ -29,7 +29,7 @@ import type { RouteName } from '../navigation/routes';
|
|
|
29
29
|
import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
|
|
30
30
|
import { useQueryClient } from '@tanstack/react-query';
|
|
31
31
|
import { clearQueryCache } from '../hooks/queryClient';
|
|
32
|
-
import { KeyManager } from '../../crypto
|
|
32
|
+
import { KeyManager, type BackupData } from '../../crypto';
|
|
33
33
|
import { translate } from '../../i18n';
|
|
34
34
|
import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
|
|
35
35
|
import { useAccountStore } from '../stores/accountStore';
|
|
@@ -50,8 +50,8 @@ export interface OxyContextState {
|
|
|
50
50
|
currentNativeLanguageName: string;
|
|
51
51
|
|
|
52
52
|
// Identity management (public key authentication - offline-first)
|
|
53
|
-
createIdentity: () => Promise<{
|
|
54
|
-
importIdentity: (
|
|
53
|
+
createIdentity: () => Promise<{ synced: boolean }>;
|
|
54
|
+
importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
|
|
55
55
|
signIn: (deviceName?: string) => Promise<User>;
|
|
56
56
|
hasIdentity: () => Promise<boolean>;
|
|
57
57
|
getPublicKey: () => Promise<string | null>;
|
|
@@ -216,7 +216,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
216
216
|
if (__DEV__) {
|
|
217
217
|
logger(restored
|
|
218
218
|
? 'Identity restored from backup successfully'
|
|
219
|
-
: 'Identity integrity check failed - user may need to restore from
|
|
219
|
+
: 'Identity integrity check failed - user may need to restore from backup file'
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
} else {
|
|
@@ -234,7 +234,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
234
234
|
if (__DEV__) {
|
|
235
235
|
logger('Error during identity integrity check', error);
|
|
236
236
|
}
|
|
237
|
-
// Don't block app startup - user can recover with
|
|
237
|
+
// Don't block app startup - user can recover with backup file
|
|
238
238
|
}
|
|
239
239
|
};
|
|
240
240
|
|
|
@@ -287,7 +287,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
287
287
|
|
|
288
288
|
const {
|
|
289
289
|
createIdentity,
|
|
290
|
-
importIdentity,
|
|
290
|
+
importIdentity: importIdentityBase,
|
|
291
291
|
signIn,
|
|
292
292
|
logout,
|
|
293
293
|
logoutAll,
|
|
@@ -320,6 +320,24 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
320
320
|
// syncIdentity - TanStack Query handles offline mutations automatically
|
|
321
321
|
const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
|
|
322
322
|
|
|
323
|
+
// Wrapper for importIdentity to handle legacy calls gracefully
|
|
324
|
+
const importIdentity = useCallback(
|
|
325
|
+
async (backupData: BackupData | string, password?: string): Promise<{ synced: boolean }> => {
|
|
326
|
+
// Handle legacy calls with single string argument (old recovery phrase signature)
|
|
327
|
+
if (typeof backupData === 'string') {
|
|
328
|
+
throw new Error('Recovery phrase import is no longer supported. Please use backup file import or QR code transfer instead.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Validate that password is provided
|
|
332
|
+
if (!password || typeof password !== 'string') {
|
|
333
|
+
throw new Error('Password is required for backup file import.');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return importIdentityBase(backupData, password);
|
|
337
|
+
},
|
|
338
|
+
[importIdentityBase]
|
|
339
|
+
);
|
|
340
|
+
|
|
323
341
|
// Clear all account data when identity is lost (for accounts app)
|
|
324
342
|
// In accounts app, identity = account, so losing identity means losing everything
|
|
325
343
|
const clearAllAccountData = useCallback(async (): Promise<void> => {
|
|
@@ -613,6 +631,43 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
613
631
|
logout().catch((remoteError) => logger('Failed to process remote sign out', remoteError));
|
|
614
632
|
}, [logger, logout]);
|
|
615
633
|
|
|
634
|
+
const handleIdentityTransferComplete = useCallback(
|
|
635
|
+
async (data: { transferId: string; sourceDeviceId: string; publicKey: string; completedAt: string }) => {
|
|
636
|
+
// Show confirmation dialog asking if user wants to delete identity from this device
|
|
637
|
+
return new Promise<void>((resolve) => {
|
|
638
|
+
Alert.alert(
|
|
639
|
+
'Identity Transfer Complete',
|
|
640
|
+
'Your identity has been successfully transferred to another device. Would you like to remove it from this device?',
|
|
641
|
+
[
|
|
642
|
+
{
|
|
643
|
+
text: 'Keep on This Device',
|
|
644
|
+
style: 'cancel',
|
|
645
|
+
onPress: () => resolve(),
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
text: 'Remove from This Device',
|
|
649
|
+
style: 'destructive',
|
|
650
|
+
onPress: async () => {
|
|
651
|
+
try {
|
|
652
|
+
// Delete identity with user confirmation
|
|
653
|
+
await deleteIdentityAndClearAccount(false, false, true);
|
|
654
|
+
toast.success('Identity removed from this device');
|
|
655
|
+
} catch (error: any) {
|
|
656
|
+
logger('Failed to delete identity after transfer', error);
|
|
657
|
+
toast.error(error?.message || 'Failed to remove identity from this device');
|
|
658
|
+
} finally {
|
|
659
|
+
resolve();
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
{ cancelable: true, onDismiss: () => resolve() }
|
|
665
|
+
);
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
[deleteIdentityAndClearAccount, logger]
|
|
669
|
+
);
|
|
670
|
+
|
|
616
671
|
useSessionSocket({
|
|
617
672
|
userId,
|
|
618
673
|
activeSessionId,
|
|
@@ -623,6 +678,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
623
678
|
baseURL: oxyServices.getBaseURL(),
|
|
624
679
|
onRemoteSignOut: handleRemoteSignOut,
|
|
625
680
|
onSessionRemoved: handleSessionRemoved,
|
|
681
|
+
onIdentityTransferComplete: handleIdentityTransferComplete,
|
|
626
682
|
});
|
|
627
683
|
|
|
628
684
|
const switchSessionForContext = useCallback(
|
|
@@ -7,7 +7,7 @@ import { fetchSessionsWithFallback, mapSessionsToClient } from '../../utils/sess
|
|
|
7
7
|
import { handleAuthError, isInvalidSessionError } from '../../utils/errorHandlers';
|
|
8
8
|
import type { StorageInterface } from '../../utils/storageHelpers';
|
|
9
9
|
import type { OxyServices } from '../../../core';
|
|
10
|
-
import { KeyManager, SignatureService,
|
|
10
|
+
import { KeyManager, SignatureService, type BackupData } from '../../../crypto';
|
|
11
11
|
|
|
12
12
|
export interface UseAuthOperationsOptions {
|
|
13
13
|
oxyServices: OxyServices;
|
|
@@ -34,9 +34,9 @@ export interface UseAuthOperationsOptions {
|
|
|
34
34
|
|
|
35
35
|
export interface UseAuthOperationsResult {
|
|
36
36
|
/** Create a new identity locally (offline-first) and optionally sync with server */
|
|
37
|
-
createIdentity: () => Promise<{
|
|
38
|
-
/** Import an existing identity from
|
|
39
|
-
importIdentity: (
|
|
37
|
+
createIdentity: () => Promise<{ synced: boolean }>;
|
|
38
|
+
/** Import an existing identity from backup file data */
|
|
39
|
+
importIdentity: (backupData: BackupData, password: string) => Promise<{ synced: boolean }>;
|
|
40
40
|
/** Sign in with existing identity on device */
|
|
41
41
|
signIn: (deviceName?: string) => Promise<User>;
|
|
42
42
|
/** Logout from current session */
|
|
@@ -274,18 +274,19 @@ export const useAuthOperations = ({
|
|
|
274
274
|
);
|
|
275
275
|
|
|
276
276
|
/**
|
|
277
|
-
* Create a new identity
|
|
277
|
+
* Create a new identity (offline-first)
|
|
278
278
|
* Identity is purely cryptographic - no username or email required
|
|
279
279
|
*/
|
|
280
280
|
const createIdentity = useCallback(
|
|
281
|
-
async (): Promise<{
|
|
281
|
+
async (): Promise<{ synced: boolean }> => {
|
|
282
282
|
if (!storage) throw new Error('Storage not initialized');
|
|
283
283
|
|
|
284
284
|
setAuthState({ isLoading: true, error: null });
|
|
285
285
|
|
|
286
286
|
try {
|
|
287
|
-
// Generate new
|
|
288
|
-
const {
|
|
287
|
+
// Generate new key pair directly (works offline)
|
|
288
|
+
const { publicKey, privateKey } = await KeyManager.generateKeyPair();
|
|
289
|
+
await KeyManager.importKeyPair(privateKey);
|
|
289
290
|
|
|
290
291
|
// Mark as not synced
|
|
291
292
|
await storage.setItem('oxy_identity_synced', 'false');
|
|
@@ -301,7 +302,6 @@ export const useAuthOperations = ({
|
|
|
301
302
|
setIdentitySynced(true);
|
|
302
303
|
|
|
303
304
|
return {
|
|
304
|
-
recoveryPhrase: words,
|
|
305
305
|
synced: true,
|
|
306
306
|
};
|
|
307
307
|
} catch (syncError) {
|
|
@@ -311,13 +311,12 @@ export const useAuthOperations = ({
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
return {
|
|
314
|
-
recoveryPhrase: words,
|
|
315
314
|
synced: false,
|
|
316
315
|
};
|
|
317
316
|
}
|
|
318
317
|
} catch (error) {
|
|
319
318
|
// CRITICAL: Never delete identity on error - it may have been successfully created
|
|
320
|
-
// Only log the error and let the user recover using their
|
|
319
|
+
// Only log the error and let the user recover using their backup file
|
|
321
320
|
// Identity deletion should ONLY happen when explicitly requested by the user
|
|
322
321
|
if (__DEV__ && logger) {
|
|
323
322
|
logger('Error during identity creation (identity may still exist):', error);
|
|
@@ -330,7 +329,7 @@ export const useAuthOperations = ({
|
|
|
330
329
|
await storage.setItem('oxy_identity_synced', 'false').catch(() => {});
|
|
331
330
|
setIdentitySynced(false);
|
|
332
331
|
if (__DEV__ && logger) {
|
|
333
|
-
logger('Identity was created but sync failed - user can sync later using
|
|
332
|
+
logger('Identity was created but sync failed - user can sync later using backup file');
|
|
334
333
|
}
|
|
335
334
|
} else {
|
|
336
335
|
// No identity exists - this was a generation failure, safe to clean up sync flag
|
|
@@ -434,17 +433,68 @@ export const useAuthOperations = ({
|
|
|
434
433
|
);
|
|
435
434
|
|
|
436
435
|
/**
|
|
437
|
-
* Import identity from
|
|
436
|
+
* Import identity from backup file data (offline-first)
|
|
438
437
|
*/
|
|
439
438
|
const importIdentity = useCallback(
|
|
440
|
-
async (
|
|
439
|
+
async (backupData: BackupData, password: string): Promise<{ synced: boolean }> => {
|
|
441
440
|
if (!storage) throw new Error('Storage not initialized');
|
|
442
441
|
|
|
442
|
+
// Validate arguments - ensure backupData is an object, not a string (old signature)
|
|
443
|
+
if (!backupData || typeof backupData !== 'object' || Array.isArray(backupData)) {
|
|
444
|
+
throw new Error('Invalid backup data. Please use the backup file import feature.');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!backupData.encrypted || !backupData.salt || !backupData.iv || !backupData.publicKey) {
|
|
448
|
+
throw new Error('Invalid backup data structure. Missing required fields.');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!password || typeof password !== 'string') {
|
|
452
|
+
throw new Error('Password is required for backup file import.');
|
|
453
|
+
}
|
|
454
|
+
|
|
443
455
|
setAuthState({ isLoading: true, error: null });
|
|
444
456
|
|
|
445
457
|
try {
|
|
446
|
-
//
|
|
447
|
-
const
|
|
458
|
+
// Decrypt private key from backup data
|
|
459
|
+
const Crypto = await import('expo-crypto');
|
|
460
|
+
|
|
461
|
+
// Convert hex strings to Uint8Array
|
|
462
|
+
const saltBytes = new Uint8Array(
|
|
463
|
+
backupData.salt.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
|
464
|
+
);
|
|
465
|
+
const ivBytes = new Uint8Array(
|
|
466
|
+
backupData.iv.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Derive key from password (same algorithm as EncryptedBackupGenerator)
|
|
470
|
+
const saltHex = Array.from(saltBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
471
|
+
let key = password + saltHex;
|
|
472
|
+
for (let i = 0; i < 10000; i++) {
|
|
473
|
+
key = await Crypto.digestStringAsync(
|
|
474
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
475
|
+
key
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const keyBytes = new Uint8Array(32);
|
|
479
|
+
for (let i = 0; i < 64 && i < key.length; i += 2) {
|
|
480
|
+
keyBytes[i / 2] = parseInt(key.substring(i, i + 2), 16);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Decrypt private key (XOR decryption - same as encryption)
|
|
484
|
+
const encryptedBytes = Buffer.from(backupData.encrypted, 'base64');
|
|
485
|
+
const decryptedBytes = new Uint8Array(encryptedBytes.length);
|
|
486
|
+
for (let i = 0; i < encryptedBytes.length; i++) {
|
|
487
|
+
decryptedBytes[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length] ^ ivBytes[i % ivBytes.length];
|
|
488
|
+
}
|
|
489
|
+
const privateKey = new TextDecoder().decode(decryptedBytes);
|
|
490
|
+
|
|
491
|
+
// Import the key pair
|
|
492
|
+
const publicKey = await KeyManager.importKeyPair(privateKey);
|
|
493
|
+
|
|
494
|
+
// Verify public key matches
|
|
495
|
+
if (publicKey !== backupData.publicKey) {
|
|
496
|
+
throw new Error('Backup file is corrupted or password is incorrect');
|
|
497
|
+
}
|
|
448
498
|
|
|
449
499
|
// Mark as not synced
|
|
450
500
|
await storage.setItem('oxy_identity_synced', 'false');
|
|
@@ -478,7 +528,7 @@ export const useAuthOperations = ({
|
|
|
478
528
|
}
|
|
479
529
|
} catch (error) {
|
|
480
530
|
const message = handleAuthError(error, {
|
|
481
|
-
defaultMessage: 'Failed to import identity',
|
|
531
|
+
defaultMessage: 'Failed to import identity. Please check your password and backup file.',
|
|
482
532
|
code: REGISTER_ERROR_CODE,
|
|
483
533
|
onError,
|
|
484
534
|
setAuthError: (msg: string) => setAuthState({ error: msg }),
|
|
@@ -13,9 +13,10 @@ interface UseSessionSocketProps {
|
|
|
13
13
|
baseURL: string;
|
|
14
14
|
onRemoteSignOut?: () => void;
|
|
15
15
|
onSessionRemoved?: (sessionId: string) => void;
|
|
16
|
+
onIdentityTransferComplete?: (data: { transferId: string; sourceDeviceId: string; publicKey: string; completedAt: string }) => void;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
|
|
19
|
+
export function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete }: UseSessionSocketProps) {
|
|
19
20
|
const socketRef = useRef<any>(null);
|
|
20
21
|
const joinedRoomRef = useRef<string | null>(null);
|
|
21
22
|
|
|
@@ -25,6 +26,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
25
26
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
26
27
|
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
27
28
|
const onSessionRemovedRef = useRef(onSessionRemoved);
|
|
29
|
+
const onIdentityTransferCompleteRef = useRef(onIdentityTransferComplete);
|
|
28
30
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
29
31
|
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
30
32
|
|
|
@@ -35,9 +37,10 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
35
37
|
clearSessionStateRef.current = clearSessionState;
|
|
36
38
|
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
37
39
|
onSessionRemovedRef.current = onSessionRemoved;
|
|
40
|
+
onIdentityTransferCompleteRef.current = onIdentityTransferComplete;
|
|
38
41
|
activeSessionIdRef.current = activeSessionId;
|
|
39
42
|
currentDeviceIdRef.current = currentDeviceId;
|
|
40
|
-
}, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
|
|
43
|
+
}, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, onIdentityTransferComplete, activeSessionId, currentDeviceId]);
|
|
41
44
|
|
|
42
45
|
useEffect(() => {
|
|
43
46
|
if (!userId || !baseURL) {
|
|
@@ -192,6 +195,63 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
192
195
|
}
|
|
193
196
|
});
|
|
194
197
|
}
|
|
198
|
+
} else if (data.type === 'identity_transfer_complete') {
|
|
199
|
+
// Handle identity transfer completion notification
|
|
200
|
+
const transferData = data as {
|
|
201
|
+
type: 'identity_transfer_complete';
|
|
202
|
+
transferId: string;
|
|
203
|
+
sourceDeviceId: string;
|
|
204
|
+
publicKey: string;
|
|
205
|
+
completedAt: string;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (__DEV__) {
|
|
209
|
+
logger.debug('Received identity_transfer_complete event', {
|
|
210
|
+
component: 'useSessionSocket',
|
|
211
|
+
transferId: transferData.transferId,
|
|
212
|
+
sourceDeviceId: transferData.sourceDeviceId,
|
|
213
|
+
currentDeviceId,
|
|
214
|
+
activeSessionId: activeSessionIdRef.current,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if this device is the source device
|
|
219
|
+
// Match by deviceId if available, otherwise show prompt if we have an active session
|
|
220
|
+
// (user can decide if they want to delete)
|
|
221
|
+
const isSourceDevice = transferData.sourceDeviceId && (
|
|
222
|
+
transferData.sourceDeviceId === currentDeviceId ||
|
|
223
|
+
// Fallback: if we don't have currentDeviceId but have an active session,
|
|
224
|
+
// show the prompt anyway (user can decide)
|
|
225
|
+
(currentDeviceId === null && activeSessionIdRef.current !== null)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (isSourceDevice) {
|
|
229
|
+
if (__DEV__) {
|
|
230
|
+
logger.debug('Matched source device, showing deletion prompt', {
|
|
231
|
+
component: 'useSessionSocket',
|
|
232
|
+
transferId: transferData.transferId,
|
|
233
|
+
sourceDeviceId: transferData.sourceDeviceId,
|
|
234
|
+
currentDeviceId,
|
|
235
|
+
matchedBy: transferData.sourceDeviceId === currentDeviceId ? 'deviceId' : 'session',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// This is the source device - notify callback to show deletion prompt
|
|
239
|
+
if (onIdentityTransferCompleteRef.current) {
|
|
240
|
+
onIdentityTransferCompleteRef.current({
|
|
241
|
+
transferId: transferData.transferId,
|
|
242
|
+
sourceDeviceId: transferData.sourceDeviceId,
|
|
243
|
+
publicKey: transferData.publicKey,
|
|
244
|
+
completedAt: transferData.completedAt,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
} else if (__DEV__) {
|
|
248
|
+
logger.debug('Not the source device, ignoring transfer completion', {
|
|
249
|
+
component: 'useSessionSocket',
|
|
250
|
+
sourceDeviceId: transferData.sourceDeviceId,
|
|
251
|
+
currentDeviceId,
|
|
252
|
+
hasActiveSession: activeSessionIdRef.current !== null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
195
255
|
} else {
|
|
196
256
|
// For other event types (e.g., session_created), refresh sessions (with error handling)
|
|
197
257
|
refreshSessionsRef.current().catch((error) => {
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = exports.RecoveryPhraseService = void 0;
|
|
7
|
-
var bip39 = _interopRequireWildcard(require("bip39"));
|
|
8
|
-
var _keyManager = require("./keyManager");
|
|
9
|
-
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
10
|
-
/**
|
|
11
|
-
* Recovery Phrase Service - BIP39 Mnemonic Generation
|
|
12
|
-
*
|
|
13
|
-
* Handles generation and restoration of recovery phrases (mnemonic seeds)
|
|
14
|
-
* for backing up and restoring user identities.
|
|
15
|
-
*
|
|
16
|
-
* Note: This module requires the polyfill to be loaded first (done via crypto/index.ts)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Convert Uint8Array or array-like to hexadecimal string
|
|
21
|
-
* Works in both Node.js and React Native without depending on Buffer
|
|
22
|
-
*/
|
|
23
|
-
function toHex(data) {
|
|
24
|
-
// Convert to array of numbers if needed
|
|
25
|
-
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
26
|
-
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
27
|
-
}
|
|
28
|
-
class RecoveryPhraseService {
|
|
29
|
-
/**
|
|
30
|
-
* Generate a new identity with a recovery phrase
|
|
31
|
-
* Returns the mnemonic phrase (should only be shown once to the user)
|
|
32
|
-
*/
|
|
33
|
-
static async generateIdentityWithRecovery() {
|
|
34
|
-
// Generate 128-bit entropy for 12-word mnemonic
|
|
35
|
-
const mnemonic = bip39.generateMnemonic(128);
|
|
36
|
-
|
|
37
|
-
// Derive private key from mnemonic
|
|
38
|
-
// Using the seed directly as the private key (simplified approach)
|
|
39
|
-
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
40
|
-
|
|
41
|
-
// Use first 32 bytes of seed as private key
|
|
42
|
-
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
43
|
-
const privateKeyHex = toHex(seedSlice);
|
|
44
|
-
|
|
45
|
-
// Import the derived key pair
|
|
46
|
-
const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
|
|
47
|
-
return {
|
|
48
|
-
phrase: mnemonic,
|
|
49
|
-
words: mnemonic.split(' '),
|
|
50
|
-
publicKey
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Generate a 24-word recovery phrase for higher security
|
|
56
|
-
*/
|
|
57
|
-
static async generateIdentityWithRecovery24() {
|
|
58
|
-
// Generate 256-bit entropy for 24-word mnemonic
|
|
59
|
-
const mnemonic = bip39.generateMnemonic(256);
|
|
60
|
-
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
61
|
-
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
62
|
-
const privateKeyHex = toHex(seedSlice);
|
|
63
|
-
const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
|
|
64
|
-
return {
|
|
65
|
-
phrase: mnemonic,
|
|
66
|
-
words: mnemonic.split(' '),
|
|
67
|
-
publicKey
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Restore an identity from a recovery phrase
|
|
73
|
-
*/
|
|
74
|
-
static async restoreFromPhrase(phrase) {
|
|
75
|
-
// Normalize and validate the phrase
|
|
76
|
-
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
77
|
-
if (!bip39.validateMnemonic(normalizedPhrase)) {
|
|
78
|
-
throw new Error('Invalid recovery phrase. Please check the words and try again.');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Derive the same private key from the mnemonic
|
|
82
|
-
const seed = await bip39.mnemonicToSeed(normalizedPhrase);
|
|
83
|
-
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
84
|
-
const privateKeyHex = toHex(seedSlice);
|
|
85
|
-
|
|
86
|
-
// Import and store the key pair
|
|
87
|
-
const publicKey = await _keyManager.KeyManager.importKeyPair(privateKeyHex);
|
|
88
|
-
return publicKey;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Validate a recovery phrase without importing it
|
|
93
|
-
*/
|
|
94
|
-
static validatePhrase(phrase) {
|
|
95
|
-
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
96
|
-
return bip39.validateMnemonic(normalizedPhrase);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get the word list for autocomplete/validation
|
|
101
|
-
*/
|
|
102
|
-
static getWordList() {
|
|
103
|
-
return bip39.wordlists.english;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Check if a word is valid in the BIP39 word list
|
|
108
|
-
*/
|
|
109
|
-
static isValidWord(word) {
|
|
110
|
-
return bip39.wordlists.english.includes(word.toLowerCase());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get suggestions for a partial word
|
|
115
|
-
*/
|
|
116
|
-
static getSuggestions(partial, limit = 5) {
|
|
117
|
-
const lowerPartial = partial.toLowerCase();
|
|
118
|
-
return bip39.wordlists.english.filter(word => word.startsWith(lowerPartial)).slice(0, limit);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Derive the public key from a phrase without storing
|
|
123
|
-
* Useful for verification before importing
|
|
124
|
-
*/
|
|
125
|
-
static async derivePublicKeyFromPhrase(phrase) {
|
|
126
|
-
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
127
|
-
if (!bip39.validateMnemonic(normalizedPhrase)) {
|
|
128
|
-
throw new Error('Invalid recovery phrase');
|
|
129
|
-
}
|
|
130
|
-
const seed = await bip39.mnemonicToSeed(normalizedPhrase);
|
|
131
|
-
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
132
|
-
const privateKeyHex = toHex(seedSlice);
|
|
133
|
-
return _keyManager.KeyManager.derivePublicKey(privateKeyHex);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Convert a phrase to its word array
|
|
138
|
-
*/
|
|
139
|
-
static phraseToWords(phrase) {
|
|
140
|
-
return phrase.trim().toLowerCase().split(/\s+/);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Convert a word array to a phrase string
|
|
145
|
-
*/
|
|
146
|
-
static wordsToPhrase(words) {
|
|
147
|
-
return words.map(w => w.toLowerCase().trim()).join(' ');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
exports.RecoveryPhraseService = RecoveryPhraseService;
|
|
151
|
-
var _default = exports.default = RecoveryPhraseService;
|
|
152
|
-
//# sourceMappingURL=recoveryPhrase.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["bip39","_interopRequireWildcard","require","_keyManager","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","toHex","data","bytes","Uint8Array","Array","from","map","b","toString","padStart","join","RecoveryPhraseService","generateIdentityWithRecovery","mnemonic","generateMnemonic","seed","mnemonicToSeed","seedSlice","subarray","slice","privateKeyHex","publicKey","KeyManager","importKeyPair","phrase","words","split","generateIdentityWithRecovery24","restoreFromPhrase","normalizedPhrase","trim","toLowerCase","validateMnemonic","Error","validatePhrase","getWordList","wordlists","english","isValidWord","word","includes","getSuggestions","partial","limit","lowerPartial","filter","startsWith","derivePublicKeyFromPhrase","derivePublicKey","phraseToWords","wordsToPhrase","w","exports","_default"],"sourceRoot":"../../../src","sources":["crypto/recoveryPhrase.ts"],"mappings":"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","ignoreList":[]}
|