diff --git a/src/app/auth/components/ChangePassword/ChangePassword.tsx b/src/app/auth/components/ChangePassword/ChangePassword.tsx index 1d93b44030..bd7afe2dbc 100644 --- a/src/app/auth/components/ChangePassword/ChangePassword.tsx +++ b/src/app/auth/components/ChangePassword/ChangePassword.tsx @@ -1,17 +1,17 @@ -import { Dispatch, SetStateAction, useState, RefObject, createRef, useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import authService from 'app/auth/services/auth.service'; -import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import testPasswordStrength from '@internxt/lib/dist/src/auth/testPasswordStrength'; -import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { Button, Input } from '@internxt/ui'; +import { CaretLeft, CheckCircle, FileArrowUp, Warning, WarningCircle } from '@phosphor-icons/react'; +import authService from 'app/auth/services/auth.service'; +import errorService from 'app/core/services/error.service'; +import localStorageService from 'app/core/services/local-storage.service'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import PasswordStrengthIndicator from 'app/shared/components/PasswordStrengthIndicator'; import { MAX_PASSWORD_LENGTH } from 'app/shared/components/ValidPassword'; -import { CaretLeft, FileArrowUp, Warning, WarningCircle, CheckCircle } from '@phosphor-icons/react'; import { validateMnemonic } from 'bip39'; -import errorService from 'app/core/services/error.service'; -import localStorageService from 'app/core/services/local-storage.service'; -import { BackupData } from 'app/utils/backupKeyUtils'; +import { Dispatch, RefObject, SetStateAction, createRef, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; + interface ChangePasswordProps { setHasBackupKey: Dispatch>; } @@ -68,6 +68,7 @@ export default function ChangePassword(props: Readonly): JS try { const backupData = JSON.parse(uploadedBackupKeyContent); + console.log({ backupData }); if (backupData.mnemonic && validateMnemonic(backupData.mnemonic)) { setBackupKeyContent(uploadedBackupKeyContent); return; @@ -124,18 +125,6 @@ export default function ChangePassword(props: Readonly): JS const token = window.location.pathname.split('/').pop(); const password = newPassword; - let mnemonic = backupKeyContent; - let backupData: BackupData | undefined; - - try { - backupData = JSON.parse(backupKeyContent) as BackupData; - if (backupData?.mnemonic) { - mnemonic = backupData.mnemonic; - } - } catch (err) { - // plain mnemonic - } - if (!token) { notificationsService.show({ text: translate('auth.recoverAccount.changePassword.tokenError'), @@ -146,11 +135,7 @@ export default function ChangePassword(props: Readonly): JS } try { - if (backupData && (backupData.privateKey || backupData.keys)) { - await authService.updateCredentialsWithToken(token, password, mnemonic, backupData); - } else { - await authService.updateCredentialsWithToken(token, password, mnemonic); - } + await authService.recoverAccountWithBackupKey(token, password, backupKeyContent); localStorageService.clear(); setIsEmailSent(true); diff --git a/src/app/auth/services/auth.service.ts b/src/app/auth/services/auth.service.ts index ada7307b47..daeed25295 100644 --- a/src/app/auth/services/auth.service.ts +++ b/src/app/auth/services/auth.service.ts @@ -40,13 +40,13 @@ import { productsThunks } from 'app/store/slices/products'; import { referralsThunks } from 'app/store/slices/referrals'; import { initializeUserThunk, userActions, userThunks } from 'app/store/slices/user'; import { workspaceThunks } from 'app/store/slices/workspaces/workspacesStore'; +import { BackupData, detectBackupKeyFormat, prepareOldBackupRecoverPayloadForBackend } from 'app/utils/backupKeyUtils'; import { generateMnemonic, validateMnemonic } from 'bip39'; import { SdkFactory } from '../../core/factory/sdk'; import envService from '../../core/services/env.service'; import errorService from '../../core/services/error.service'; import httpService from '../../core/services/http.service'; import vpnAuthService from './vpnAuth.service'; -import { BackupData } from 'app/utils/backupKeyUtils'; type ProfileInfo = { user: UserSettings; @@ -237,6 +237,52 @@ export const getPasswordDetails = async ( return { salt, hashedCurrentPassword, encryptedCurrentPassword }; }; +/** + * Recover account using backup key content + * This function detects the backup key format and uses appropriate recovery method + * + * @param token - The recovery token + * @param password - The new password + * @param backupKeyContent - The content of the backup key file + * @returns Promise + */ +export const recoverAccountWithBackupKey = async ( + token: string, + password: string, + backupKeyContent: string, +): Promise => { + try { + const { type, mnemonic, backupData } = detectBackupKeyFormat(backupKeyContent); + + if (type === 'old') { + return recoveryOldBackuptWithToken(token, password, mnemonic); + } else { + return updateCredentialsWithToken(token, password, mnemonic, backupData); + } + } catch (error) { + console.error('Error recovering account with backup key:', error); + throw error; + } +}; + +/** + * Recovery method for old backup keys format (mnemonic only) + */ +export const recoveryOldBackuptWithToken = async (token: string, password: string, mnemonic: string): Promise => { + try { + const authClient = SdkFactory.getNewApiInstance().createAuthClient(); + const recoverPayload = await prepareOldBackupRecoverPayloadForBackend({ mnemonic, password, token }); + + return authClient.legacyRecoverAccount(recoverPayload); + } catch (error) { + console.error('Error updating credentials with token:', error); + throw error; + } +}; + +/** + * Updates credentials with token (used for new backup format) + */ export const updateCredentialsWithToken = async ( token: string | undefined, newPassword: string, @@ -605,6 +651,7 @@ const authService = { requestUnblockAccount, unblockAccount, authenticateUser, + recoverAccountWithBackupKey, }; export default authService; diff --git a/src/app/utils/backupKeyUtils.test.ts b/src/app/utils/backupKeyUtils.test.ts index a998983776..29fa95ad18 100644 --- a/src/app/utils/backupKeyUtils.test.ts +++ b/src/app/utils/backupKeyUtils.test.ts @@ -1,9 +1,18 @@ -import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; -import { handleExportBackupKey, BackupData } from './backupKeyUtils'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import localStorageService from 'app/core/services/local-storage.service'; +import { getKeys } from 'app/crypto/services/keys.service'; +import { encryptText, encryptTextWithKey, passToHash } from 'app/crypto/services/utils'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import { validateMnemonic } from 'bip39'; import { saveAs } from 'file-saver'; -import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { encryptMessageWithPublicKey, hybridEncryptMessageWithPublicKey } from '../crypto/services/pgp.service'; +import { + BackupData, + detectBackupKeyFormat, + handleExportBackupKey, + prepareOldBackupRecoverPayloadForBackend, +} from './backupKeyUtils'; vi.mock('file-saver', () => ({ saveAs: vi.fn(), @@ -26,6 +35,25 @@ vi.mock('app/notifications/services/notifications.service', () => ({ }, })); +vi.mock('bip39', () => ({ + validateMnemonic: vi.fn(), +})); + +vi.mock('app/crypto/services/keys.service', () => ({ + getKeys: vi.fn(), +})); + +vi.mock('app/crypto/services/utils', () => ({ + encryptText: vi.fn(), + encryptTextWithKey: vi.fn(), + passToHash: vi.fn(), +})); + +vi.mock('../crypto/services/pgp.service', () => ({ + encryptMessageWithPublicKey: vi.fn(), + hybridEncryptMessageWithPublicKey: vi.fn(), +})); + describe('backupKeyUtils', () => { const mockTranslate = vi.fn((key) => `Translated: ${key}`); @@ -37,136 +65,312 @@ describe('backupKeyUtils', () => { vi.resetAllMocks(); }); - it('should export backup key successfully', () => { - const mockMnemonic = 'test mnemonic'; - const mockUser = { - privateKey: 'test-private-key', - keys: { - ecc: { - privateKey: 'test-ecc-private-key', - }, - kyber: { - privateKey: 'test-kyber-private-key', + describe('handleExportBackupKey', () => { + it('should export backup key successfully', () => { + const mockMnemonic = + 'whip pipe sphere rail witness sting hawk project east return unhappy focus shop dry midnight frog critic lion horror slide luxury consider vibrant timber'; + const mockUser = { + privateKey: 'test-private-key', + keys: { + ecc: { + privateKey: 'test-ecc-private-key', + }, + kyber: { + privateKey: 'test-kyber-private-key', + }, }, - }, - userId: 'test-user-id', - uuid: 'test-uuid', - email: 'test@example.com', - name: 'Test User', - lastname: 'User', - username: 'testuser', - bridgeUser: 'test-bridge-user', - bucket: 'test-bucket', - backupsBucket: null, - root_folder_id: 0, - rootFolderId: 'test-root-folder-id', - rootFolderUuid: 'test-root-folder-uuid', - sharedWorkspace: false, - credit: 0, - publicKey: 'test-public-key', - revocationKey: 'test-revocation-key', - appSumoDetails: null, - registerCompleted: false, - hasReferralsProgram: false, - createdAt: new Date(), - avatar: null, - emailVerified: false, - } as UserSettings; - - vi.mocked(localStorageService.get).mockReturnValue(mockMnemonic); - vi.mocked(localStorageService.getUser).mockReturnValue(mockUser); - - const expectedBackupData: BackupData = { - mnemonic: mockMnemonic, - privateKey: mockUser.privateKey, - keys: { - ecc: mockUser.keys?.ecc?.privateKey || '', - kyber: mockUser.keys?.kyber?.privateKey || '', - }, - }; - - handleExportBackupKey(mockTranslate); - - expect(localStorageService.get).toHaveBeenCalledWith('xMnemonic'); - expect(localStorageService.getUser).toHaveBeenCalled(); - - expect(saveAs).toHaveBeenCalledWith(expect.any(Blob), 'INTERNXT-BACKUP-KEY.txt'); - - const blobCall = vi.mocked(saveAs).mock.calls[0][0] as Blob; - expect(blobCall.type).toBe('text/plain'); - - expect(notificationsService.show).toHaveBeenCalledWith({ - text: mockTranslate('views.account.tabs.security.backupKey.success'), - type: ToastType.Success, + userId: 'test-user-id', + uuid: 'test-uuid', + email: 'test@example.com', + name: 'Test User', + lastname: 'User', + username: 'testuser', + bridgeUser: 'test-bridge-user', + bucket: 'test-bucket', + backupsBucket: null, + root_folder_id: 0, + rootFolderId: 'test-root-folder-id', + rootFolderUuid: 'test-root-folder-uuid', + sharedWorkspace: false, + credit: 0, + publicKey: 'test-public-key', + revocationKey: 'test-revocation-key', + appSumoDetails: null, + registerCompleted: false, + hasReferralsProgram: false, + createdAt: new Date(), + avatar: null, + emailVerified: false, + } as UserSettings; + + vi.mocked(localStorageService.get).mockReturnValue(mockMnemonic); + vi.mocked(localStorageService.getUser).mockReturnValue(mockUser); + + handleExportBackupKey(mockTranslate); + + expect(localStorageService.get).toHaveBeenCalledWith('xMnemonic'); + expect(localStorageService.getUser).toHaveBeenCalled(); + + expect(saveAs).toHaveBeenCalledWith(expect.any(Blob), 'INTERNXT-BACKUP-KEY.txt'); + + const blobCall = vi.mocked(saveAs).mock.calls[0][0] as Blob; + expect(blobCall.type).toBe('text/plain'); + + expect(notificationsService.show).toHaveBeenCalledWith({ + text: mockTranslate('views.account.tabs.security.backupKey.success'), + type: ToastType.Success, + }); + }); + + it('should handle missing mnemonic', () => { + vi.mocked(localStorageService.get).mockReturnValue(null); + vi.mocked(localStorageService.getUser).mockReturnValue({} as any); + + handleExportBackupKey(mockTranslate); + + expect(saveAs).not.toHaveBeenCalled(); + + expect(notificationsService.show).toHaveBeenCalledWith({ + text: mockTranslate('views.account.tabs.security.backupKey.error'), + type: ToastType.Error, + }); }); - }); - it('should handle missing mnemonic', () => { - vi.mocked(localStorageService.get).mockReturnValue(null); - vi.mocked(localStorageService.getUser).mockReturnValue({} as any); + it('should handle missing user', () => { + vi.mocked(localStorageService.get).mockReturnValue('test-mnemonic'); + vi.mocked(localStorageService.getUser).mockReturnValue(null); - handleExportBackupKey(mockTranslate); + handleExportBackupKey(mockTranslate); - expect(saveAs).not.toHaveBeenCalled(); + expect(saveAs).not.toHaveBeenCalled(); - expect(notificationsService.show).toHaveBeenCalledWith({ - text: mockTranslate('views.account.tabs.security.backupKey.error'), - type: ToastType.Error, + expect(notificationsService.show).toHaveBeenCalledWith({ + text: mockTranslate('views.account.tabs.security.backupKey.error'), + type: ToastType.Error, + }); + }); + + it('should handle missing key properties', () => { + const mockMnemonic = 'test mnemonic'; + const mockUser = { + privateKey: 'test-private-key', + userId: 'test-user-id', + uuid: 'test-uuid', + email: 'test@example.com', + name: 'Test User', + lastname: 'User', + username: 'testuser', + bridgeUser: 'test-bridge-user', + bucket: 'test-bucket', + backupsBucket: null, + root_folder_id: 0, + rootFolderId: 'test-root-folder-id', + rootFolderUuid: 'test-root-folder-uuid', + sharedWorkspace: false, + credit: 0, + publicKey: 'test-public-key', + revocationKey: 'test-revocation-key', + appSumoDetails: null, + registerCompleted: false, + hasReferralsProgram: false, + createdAt: new Date(), + avatar: null, + emailVerified: false, + } as UserSettings; + + vi.mocked(localStorageService.get).mockReturnValue(mockMnemonic); + vi.mocked(localStorageService.getUser).mockReturnValue(mockUser); + + handleExportBackupKey(mockTranslate); + + expect(saveAs).toHaveBeenCalled(); + + expect(notificationsService.show).toHaveBeenCalledWith({ + text: mockTranslate('views.account.tabs.security.backupKey.success'), + type: ToastType.Success, + }); }); }); - it('should handle missing user', () => { - vi.mocked(localStorageService.get).mockReturnValue('test-mnemonic'); - vi.mocked(localStorageService.getUser).mockReturnValue(null); + describe('detectBackupKeyFormat', () => { + it('should detect new backup key format with full data', () => { + const mockBackupData: BackupData = { + mnemonic: 'test mnemonic', + privateKey: 'test-private-key', + keys: { + ecc: 'test-ecc-key', + kyber: 'test-kyber-key', + }, + }; - handleExportBackupKey(mockTranslate); + const backupKeyContent = JSON.stringify(mockBackupData); - expect(saveAs).not.toHaveBeenCalled(); + const result = detectBackupKeyFormat(backupKeyContent); - expect(notificationsService.show).toHaveBeenCalledWith({ - text: mockTranslate('views.account.tabs.security.backupKey.error'), - type: ToastType.Error, + expect(result).toEqual({ + type: 'new', + mnemonic: mockBackupData.mnemonic, + backupData: mockBackupData, + }); + }); + + it('should detect old backup key format (mnemonic only)', () => { + const mockMnemonic = 'test mnemonic'; + + vi.mocked(validateMnemonic).mockReturnValueOnce(true); + + const result = detectBackupKeyFormat(mockMnemonic); + + expect(result).toEqual({ + type: 'old', + mnemonic: mockMnemonic, + }); + }); + + it('should throw error for invalid backup key format', () => { + const invalidContent = 'invalid content'; + + vi.mocked(validateMnemonic).mockReturnValueOnce(false); + + expect(() => detectBackupKeyFormat(invalidContent)).toThrow('Invalid backup key format'); + }); + + it('should handle partial JSON (with mnemonic but without other keys)', () => { + const partialData = JSON.stringify({ + mnemonic: 'test mnemonic', + privateKey: 'test-private-key', + // keys property is missing + }); + + vi.mocked(validateMnemonic).mockReturnValueOnce(false); + + expect(() => detectBackupKeyFormat(partialData)).toThrow('Invalid backup key format'); + }); + + it('should handle JSON with correct format but invalid data', () => { + const invalidData = JSON.stringify({ + mnemonic: 'test mnemonic', + privateKey: 'test-private-key', + keys: { + // ecc is missing + kyber: 'test-kyber-key', + }, + }); + + vi.mocked(validateMnemonic).mockReturnValueOnce(false); + + expect(() => detectBackupKeyFormat(invalidData)).toThrow('Invalid backup key format'); + }); + + it('should reject invalid mnemonic in old format', () => { + const invalidMnemonic = 'invalid mnemonic phrase'; + + vi.mocked(validateMnemonic).mockReturnValueOnce(false); + + expect(() => detectBackupKeyFormat(invalidMnemonic)).toThrow('Invalid backup key format'); }); }); - it('should handle missing key properties', () => { - const mockMnemonic = 'test mnemonic'; - const mockUser = { - privateKey: 'test-private-key', - userId: 'test-user-id', - uuid: 'test-uuid', - email: 'test@example.com', - name: 'Test User', - lastname: 'User', - username: 'testuser', - bridgeUser: 'test-bridge-user', - bucket: 'test-bucket', - backupsBucket: null, - root_folder_id: 0, - rootFolderId: 'test-root-folder-id', - rootFolderUuid: 'test-root-folder-uuid', - sharedWorkspace: false, - credit: 0, - publicKey: 'test-public-key', - revocationKey: 'test-revocation-key', - appSumoDetails: null, - registerCompleted: false, - hasReferralsProgram: false, - createdAt: new Date(), - avatar: null, - emailVerified: false, - } as UserSettings; - - vi.mocked(localStorageService.get).mockReturnValue(mockMnemonic); - vi.mocked(localStorageService.getUser).mockReturnValue(mockUser); - - handleExportBackupKey(mockTranslate); - - expect(saveAs).toHaveBeenCalled(); - - expect(notificationsService.show).toHaveBeenCalledWith({ - text: mockTranslate('views.account.tabs.security.backupKey.success'), - type: ToastType.Success, + describe('prepareOldBackupRecoverPayloadForBackend', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should prepare recovery payload correctly', async () => { + const mockMnemonic = + 'whip pipe sphere rail witness sting hawk project east return unhappy focus shop dry midnight frog critic lion horror slide luxury consider vibrant timber'; + const mockPassword = 'test-password'; + const mockToken = 'test-token'; + + const mockHashObj = { hash: 'test-hash', salt: 'test-salt' }; + vi.mocked(passToHash).mockReturnValue(mockHashObj); + vi.mocked(validateMnemonic).mockReturnValue(true); + + vi.mocked(encryptText).mockImplementation((text) => `encrypted-${text}`); + vi.mocked(encryptTextWithKey).mockImplementation((text) => `encrypted-with-key-${text}`); + + const mockGeneratedKeys = { + publicKey: 'test-public-key', + privateKeyEncrypted: 'test-private-key-encrypted', + revocationCertificate: 'test-revocation-cert', + ecc: { + publicKey: 'test-ecc-public-key', + privateKeyEncrypted: 'test-ecc-private-key-encrypted', + }, + kyber: { + publicKey: 'test-kyber-public-key', + privateKeyEncrypted: 'test-kyber-private-key-encrypted', + }, + }; + vi.mocked(getKeys).mockResolvedValue(mockGeneratedKeys); + + vi.mocked(encryptMessageWithPublicKey).mockResolvedValue('ecc-encrypted-mnemonic'); + vi.mocked(hybridEncryptMessageWithPublicKey).mockResolvedValue('hybrid-encrypted-mnemonic'); + + const result = await prepareOldBackupRecoverPayloadForBackend({ + mnemonic: mockMnemonic, + password: mockPassword, + token: mockToken, + }); + + expect(passToHash).toHaveBeenCalledWith({ password: mockPassword }); + expect(encryptText).toHaveBeenCalledWith(mockHashObj.hash); + expect(encryptText).toHaveBeenCalledWith(mockHashObj.salt); + expect(encryptTextWithKey).toHaveBeenCalledWith(mockMnemonic, mockPassword); + expect(getKeys).toHaveBeenCalledWith(mockPassword); + + expect(encryptMessageWithPublicKey).toHaveBeenCalledWith({ + message: mockMnemonic, + publicKeyInBase64: mockGeneratedKeys.publicKey, + }); + + expect(hybridEncryptMessageWithPublicKey).toHaveBeenCalledWith({ + message: mockMnemonic, + publicKeyInBase64: mockGeneratedKeys.publicKey, + publicKyberKeyBase64: mockGeneratedKeys.kyber.publicKey, + }); + + expect(result).toEqual({ + token: mockToken, + encryptedPassword: 'encrypted-test-hash', + encryptedSalt: 'encrypted-test-salt', + encryptedMnemonic: 'encrypted-with-key-' + mockMnemonic, + eccEncryptedMnemonic: 'ecc-encrypted-mnemonic', + kyberEncryptedMnemonic: 'hybrid-encrypted-mnemonic', + keys: { + ecc: { + public: mockGeneratedKeys.ecc.publicKey, + private: mockGeneratedKeys.ecc.privateKeyEncrypted, + revocationKey: mockGeneratedKeys.revocationCertificate, + }, + kyber: { + public: mockGeneratedKeys.kyber.publicKey, + private: mockGeneratedKeys.kyber.privateKeyEncrypted, + }, + }, + }); + }); + + it('should throw error if mnemonic is missing', async () => { + await expect( + prepareOldBackupRecoverPayloadForBackend({ mnemonic: 'fasd', password: 'password', token: 'token' }), + ).rejects.toThrow('Invalid mnemonic in backup key'); + }); + + it('should handle errors in payload preparation', async () => { + vi.mocked(passToHash).mockImplementation(() => { + throw new Error('Test error'); + }); + vi.mocked(validateMnemonic).mockReturnValueOnce(true); + + await expect( + prepareOldBackupRecoverPayloadForBackend({ + mnemonic: + 'whip pipe sphere rail witness sting hawk project east return unhappy focus shop dry midnight frog critic lion horror slide luxury consider vibrant timber', + password: 'password', + token: 'token', + }), + ).rejects.toThrow('Error preparing recovery payload'); }); }); }); diff --git a/src/app/utils/backupKeyUtils.ts b/src/app/utils/backupKeyUtils.ts index b6257d7d03..eb3c94d16c 100644 --- a/src/app/utils/backupKeyUtils.ts +++ b/src/app/utils/backupKeyUtils.ts @@ -1,12 +1,22 @@ -import { saveAs } from 'file-saver'; -import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import localStorageService from 'app/core/services/local-storage.service'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import { saveAs } from 'file-saver'; + +import { ChangePasswordWithLinkPayload } from '@internxt/sdk'; +import { getKeys } from 'app/crypto/services/keys.service'; +import { encryptText, encryptTextWithKey, passToHash } from 'app/crypto/services/utils'; +import { validateMnemonic } from 'bip39'; +import { encryptMessageWithPublicKey, hybridEncryptMessageWithPublicKey } from '../crypto/services/pgp.service'; /** - * Downloads the backup key of the user and shows a notification - * @param translate used for the notification message + * Interface representing the backup data structure + * @interface BackupData + * @property {string} mnemonic - The user's mnemonic phrase + * @property {string} privateKey - The user's private key + * @property {Object} keys - The user's encryption keys + * @property {string} keys.ecc - The user's ECC private key + * @property {string} keys.kyber - The user's Kyber private key */ - export interface BackupData { mnemonic: string; privateKey: string; @@ -15,6 +25,13 @@ export interface BackupData { kyber: string; }; } + +/** + * Downloads the backup key of the user and shows a notification + * @param {Function} translate - Translation function to localize notification messages + * @returns {void} + * @throws {Error} Implicitly throws if file saving fails + */ export function handleExportBackupKey(translate) { const mnemonic = localStorageService.get('xMnemonic'); const user = localStorageService.getUser(); @@ -43,3 +60,123 @@ export function handleExportBackupKey(translate) { }); } } + +/** + * Detects if a backup key file is in the old format (only mnemonic) or new format (has private keys) + * + * @param {string} backupKeyContent - The content of the backup key file to analyze + * @returns {Object} Format detection result + * @returns {('old'|'new')} return.type - The format type: 'old' for plain mnemonic, 'new' for JSON with keys + * @returns {string} return.mnemonic - The extracted mnemonic phrase + * @returns {BackupData} [return.backupData] - The full backup data (only for 'new' format) + * @throws {Error} If the backup key format is invalid or cannot be parsed + */ +export const detectBackupKeyFormat = ( + backupKeyContent: string, +): { type: 'old' | 'new'; mnemonic: string; backupData?: BackupData } => { + try { + const parsedData = JSON.parse(backupKeyContent); + if ( + parsedData && + parsedData.mnemonic && + parsedData.privateKey && + parsedData.keys && + parsedData.keys.ecc && + parsedData.keys.kyber + ) { + const backupData: BackupData = { + mnemonic: parsedData.mnemonic, + privateKey: parsedData.privateKey, + keys: { + ecc: parsedData.keys.ecc, + kyber: parsedData.keys.kyber, + }, + }; + return { + type: 'new', + mnemonic: parsedData.mnemonic, + backupData, + }; + } + } catch (err) { + // Not JSON, might be an old format (just plain mnemonic) + } + const trimmedContent = backupKeyContent.trim(); + if (validateMnemonic(trimmedContent)) { + return { + type: 'old', + mnemonic: trimmedContent, + }; + } + + throw new Error('Invalid backup key format'); +}; + +/** + * Prepares the payload in the format required by the backend for account recovery + * using the old backup format (mnemonic only) + * + * @param {Object} params - The parameters object + * @param {string} params.mnemonic - The mnemonic phrase from the backup key + * @param {string} params.password - The new password for the account + * @param {string} params.token - The recovery token provided by the system + * @returns {Promise} Promise with the recovery payload formatted for the backend + * @throws {Error} If the mnemonic is invalid or encryption fails + */ +export const prepareOldBackupRecoverPayloadForBackend = async ({ + mnemonic, + password, + token, +}: { + mnemonic: string; + password: string; + token: string; +}): Promise => { + if (!validateMnemonic(mnemonic)) { + throw new Error('Invalid mnemonic in backup key'); + } + + try { + const hashObj = passToHash({ password }); + const encryptedPassword = encryptText(hashObj.hash); + const encryptedSalt = encryptText(hashObj.salt); + const encryptedMnemonic = encryptTextWithKey(mnemonic, password); + + const generatedKeys = await getKeys(password); + const eccPublicKeyInBase64 = generatedKeys.publicKey; + const kyberPublicKeyInBase64 = generatedKeys.kyber.publicKey; + const eccEncryptedMnemonic = await encryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64: generatedKeys.publicKey, + }); + + const hybridEncryptedMnemonic = await hybridEncryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64: eccPublicKeyInBase64, + publicKyberKeyBase64: kyberPublicKeyInBase64 as string, + }); + + return { + token, + encryptedPassword: encryptedPassword, + encryptedSalt: encryptedSalt, + encryptedMnemonic: encryptedMnemonic, + eccEncryptedMnemonic: eccEncryptedMnemonic as string, + kyberEncryptedMnemonic: hybridEncryptedMnemonic as string, + keys: { + ecc: { + public: generatedKeys.ecc?.publicKey, + private: generatedKeys.ecc?.privateKeyEncrypted, + revocationKey: generatedKeys.revocationCertificate, + }, + kyber: { + public: generatedKeys.kyber.publicKey as string, + private: generatedKeys.kyber.privateKeyEncrypted as string, + }, + }, + }; + } catch (error) { + console.error('Error preparing recovery payload:', error); + throw new Error('Error preparing recovery payload'); + } +};