Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/services/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
*/
import { aes } from '@internxt/lib';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import envService from 'services/env.service';
import localStorageService from 'services/local-storage.service';
import { SdkFactory } from 'app/core/factory/sdk';
import { LocalStorageItem } from 'app/core/types';
import * as keysService from 'app/crypto/services/keys.service';
import * as pgpService from 'app/crypto/services/pgp.service';
import { encryptText, encryptTextWithKey } from 'app/crypto/services/utils';
import { userActions } from 'app/store/slices/user';
import { BackupData } from 'utils/backupKeyUtils';
import { validateMnemonic } from 'bip39';
import { Buffer } from 'node:buffer';
import envService from 'services/env.service';
import localStorageService from 'services/local-storage.service';
import { BackupData } from 'utils/backupKeyUtils';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { SdkFactory } from 'app/core/factory/sdk';
import * as authService from './auth.service';
import { LocalStorageItem } from 'app/core/types';

const mockSecret = '123456789QWERTY';
const mockApi = 'https://mock';
Expand Down Expand Up @@ -691,7 +691,7 @@ describe('updateCredentialsWithToken', () => {
expect(keys).toBeUndefined();
});

it('should successfully update credentials with token and with backup data (ECC only)', async () => {
it('When backup data has no publicKeys (legacy backup), then it should send only privateKeys', async () => {
const mockToken = 'test-reset-token';
const mockNewPassword = 'newPassword123';
const mockMnemonic =
Expand Down Expand Up @@ -728,10 +728,11 @@ describe('updateCredentialsWithToken', () => {
expect(keys).toBeDefined();

expect(keys.private.ecc).toBe('mock-encrypted-data');
expect(keys.public).toBeUndefined();
expect(keys.private.kyber).toBeUndefined();
});

it('should successfully update credentials with token and with backup data (ECC and Kyber)', async () => {
it('should send both private and public keys when backup data has publicKeys', async () => {
const mockToken = 'test-reset-token';
const mockNewPassword = 'newPassword123';
const mockMnemonic =
Expand All @@ -743,6 +744,10 @@ describe('updateCredentialsWithToken', () => {
ecc: 'test-ecc-private-key',
kyber: 'test-kyber-private-key',
},
publicKeys: {
ecc: 'test-ecc-public-key',
kyber: 'test-kyber-public-key',
},
};

(validateMnemonic as any).mockReturnValue(true);
Expand All @@ -769,6 +774,10 @@ describe('updateCredentialsWithToken', () => {

expect(keys.private.ecc).toBe('mock-encrypted-data');
expect(keys.private.kyber).toBe('mock-encrypted-data');
expect(keys.public).toEqual({
ecc: 'test-ecc-public-key',
kyber: 'test-kyber-public-key',
});
});

it('should throw an error when mnemonic is invalid', async () => {
Expand Down
14 changes: 8 additions & 6 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SecurityDetails,
TwoFactorAuthQR,
} from '@internxt/sdk/dist/auth';
import { RecoveryKeys } from '@internxt/sdk/dist/auth/types';
import { StorageTypes } from '@internxt/sdk/dist/drive';
import { ChangePasswordPayloadNew } from '@internxt/sdk/dist/drive/users/types';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
Expand Down Expand Up @@ -340,12 +341,13 @@ export const updateCredentialsWithToken = async (

const authClient = SdkFactory.getNewApiInstance().createAuthClient();

const keys =
encryptedEccPrivateKey || encryptedKyberPrivateKey
? {
private: { ecc: encryptedEccPrivateKey, kyber: encryptedKyberPrivateKey },
}
: undefined;
const hasPrivateKeys = encryptedEccPrivateKey || encryptedKyberPrivateKey;
const keys: RecoveryKeys | undefined = hasPrivateKeys
? {
private: { ecc: encryptedEccPrivateKey, kyber: encryptedKyberPrivateKey },
public: backupData?.publicKeys,
}
: undefined;

return authClient.changePasswordWithLinkV2(
token,
Expand Down
1 change: 0 additions & 1 deletion src/services/local-storage.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,6 @@ describe('Testing the local storage service', () => {
});
});


describe('Clearing local storage', () => {
it('When clear storage is requested, then removes all keys', () => {
const expectedKeysToRemove = [
Expand Down
1 change: 0 additions & 1 deletion src/services/local-storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ function hasCompletedTutorial(id?: string): boolean {
return localStorage.getItem(STORAGE_KEYS.TUTORIAL_COMPLETED_ID) === id;
}


function clear(): void {
localStorage.setItem('theme', 'system');

Expand Down
206 changes: 201 additions & 5 deletions src/utils/backupKeyUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import localStorageService from 'services/local-storage.service';
import { LocalStorageItem } from 'app/core/types';
import { getKeys } from 'app/crypto/services/keys.service';
import { encryptMessageWithPublicKey, hybridEncryptMessageWithPublicKey } from 'app/crypto/services/pgp.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 localStorageService from 'services/local-storage.service';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { encryptMessageWithPublicKey, hybridEncryptMessageWithPublicKey } from 'app/crypto/services/pgp.service';
import {
BackupData,
detectBackupKeyFormat,
handleExportBackupKey,
prepareOldBackupRecoverPayloadForBackend,
} from './backupKeyUtils';
import { LocalStorageItem } from 'app/core/types';

vi.mock('file-saver', async () => {
const actual = await vi.importActual<typeof import('file-saver')>('file-saver');
Expand Down Expand Up @@ -66,7 +66,68 @@ describe('backupKeyUtils', () => {
});

describe('handleExportBackupKey', () => {
it('should export backup key successfully', () => {
it('When user has valid public keys, then backup should include publicKeys', 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 mockUser = {
privateKey: 'test-private-key',
keys: {
ecc: {
privateKey: 'test-ecc-private-key',
publicKey: 'test-ecc-public-key',
},
kyber: {
privateKey: 'test-kyber-private-key',
publicKey: 'test-kyber-public-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).toHaveBeenCalledWith(expect.any(Blob), 'INTERNXT-BACKUP-KEY.txt');

const blobCall = vi.mocked(saveAs).mock.calls[0][0] as Blob;
const blobContent = await blobCall.text();
const parsedBackup = JSON.parse(blobContent);

expect(parsedBackup.publicKeys).toEqual({
ecc: 'test-ecc-public-key',
kyber: 'test-kyber-public-key',
});

expect(notificationsService.show).toHaveBeenCalledWith({
text: mockTranslate('views.account.tabs.security.backupKey.success'),
type: ToastType.Success,
});
});

it('When user has no public keys, then backup should not include publicKeys', 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 mockUser = {
Expand Down Expand Up @@ -114,14 +175,67 @@ describe('backupKeyUtils', () => {
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');
const blobContent = await blobCall.text();
const parsedBackup = JSON.parse(blobContent);

expect(parsedBackup.publicKeys).toBeUndefined();

expect(notificationsService.show).toHaveBeenCalledWith({
text: mockTranslate('views.account.tabs.security.backupKey.success'),
type: ToastType.Success,
});
});

it('When user has only ecc public key, then backup should not include publicKeys', 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 mockUser = {
privateKey: 'test-private-key',
keys: {
ecc: {
privateKey: 'test-ecc-private-key',
publicKey: 'test-ecc-public-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);

handleExportBackupKey(mockTranslate);

const blobCall = vi.mocked(saveAs).mock.calls[0][0] as Blob;
const blobContent = await blobCall.text();
const parsedBackup = JSON.parse(blobContent);

expect(parsedBackup.publicKeys).toBeUndefined();
});

it('should handle missing mnemonic', () => {
vi.mocked(localStorageService.get).mockReturnValue(null);
vi.mocked(localStorageService.getUser).mockReturnValue({} as any);
Expand Down Expand Up @@ -193,6 +307,88 @@ describe('backupKeyUtils', () => {
});

describe('detectBackupKeyFormat', () => {
it('When backup has valid publicKeys, then result should include publicKeys', () => {
const mockBackupData = {
mnemonic: 'test mnemonic',
privateKey: 'test-private-key',
keys: {
ecc: 'test-ecc-key',
kyber: 'test-kyber-key',
},
publicKeys: {
ecc: 'test-ecc-public-key',
kyber: 'test-kyber-public-key',
},
};

const backupKeyContent = JSON.stringify(mockBackupData);

const result = detectBackupKeyFormat(backupKeyContent);

expect(result.backupData?.publicKeys).toEqual({
ecc: 'test-ecc-public-key',
kyber: 'test-kyber-public-key',
});
});

it('When backup has no publicKeys, then result should not include publicKeys', () => {
const mockBackupData: BackupData = {
mnemonic: 'test mnemonic',
privateKey: 'test-private-key',
keys: {
ecc: 'test-ecc-key',
kyber: 'test-kyber-key',
},
};

const backupKeyContent = JSON.stringify(mockBackupData);

const result = detectBackupKeyFormat(backupKeyContent);

expect(result.backupData?.publicKeys).toBeUndefined();
});

it('When backup has only ecc publicKey, then result should not include publicKeys', () => {
const mockBackupData = {
mnemonic: 'test mnemonic',
privateKey: 'test-private-key',
keys: {
ecc: 'test-ecc-key',
kyber: 'test-kyber-key',
},
publicKeys: {
ecc: 'test-ecc-public-key',
},
};

const backupKeyContent = JSON.stringify(mockBackupData);

const result = detectBackupKeyFormat(backupKeyContent);

expect(result.backupData?.publicKeys).toBeUndefined();
});

it('When backup has empty publicKeys, then result should not include publicKeys', () => {
const mockBackupData = {
mnemonic: 'test mnemonic',
privateKey: 'test-private-key',
keys: {
ecc: 'test-ecc-key',
kyber: 'test-kyber-key',
},
publicKeys: {
ecc: '',
kyber: '',
},
};

const backupKeyContent = JSON.stringify(mockBackupData);

const result = detectBackupKeyFormat(backupKeyContent);

expect(result.backupData?.publicKeys).toBeUndefined();
});

it('should detect new backup key format with full data', () => {
const mockBackupData: BackupData = {
mnemonic: 'test mnemonic',
Expand Down
Loading
Loading