Skip to content
Open
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
20 changes: 14 additions & 6 deletions src/services/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,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 @@ -726,11 +726,11 @@ describe('updateCredentialsWithToken', () => {
expect(encryptedMnemonic).toBeDefined();
expect(keys).toBeDefined();

expect(keys.ecc).toBe('mock-encrypted-data');
expect(keys.kyber).toBeUndefined();
expect(keys.private.ecc).toBe('mock-encrypted-data');
expect(keys.public).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 @@ -742,6 +742,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 @@ -766,8 +770,12 @@ describe('updateCredentialsWithToken', () => {
expect(encryptedMnemonic).toBeDefined();
expect(keys).toBeDefined();

expect(keys.ecc).toBe('mock-encrypted-data');
expect(keys.kyber).toBe('mock-encrypted-data');
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
9 changes: 8 additions & 1 deletion src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,21 @@ export const updateCredentialsWithToken = async (

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

const keys =
const privateKeys =
encryptedEccPrivateKey || encryptedKyberPrivateKey
? {
ecc: encryptedEccPrivateKey,
kyber: encryptedKyberPrivateKey,
}
: undefined;

const keys = privateKeys
? {
private: privateKeys,
...(backupData?.publicKeys && { public: backupData.publicKeys }),
}
: undefined;

return authClient.changePasswordWithLinkV2(
token,
encryptedHashedNewPassword,
Expand Down
201 changes: 197 additions & 4 deletions src/utils/backupKeyUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,19 @@ 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',
Expand Down Expand Up @@ -107,20 +109,129 @@ describe('backupKeyUtils', () => {

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;
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 = {
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);

handleExportBackupKey(mockTranslate);

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 @@ -192,6 +303,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
23 changes: 23 additions & 0 deletions src/utils/backupKeyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { encryptMessageWithPublicKey, hybridEncryptMessageWithPublicKey } from '
* @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
* @property {Object} [publicKeys] - The user's public keys (for backup validation)
* @property {string} [publicKeys.ecc] - The user's ECC public key
* @property {string} [publicKeys.kyber] - The user's Kyber public key
*/
export interface BackupData {
mnemonic: string;
Expand All @@ -24,6 +27,10 @@ export interface BackupData {
ecc: string;
kyber: string;
};
publicKeys?: {
ecc?: string;
kyber?: string;
};
}

/**
Expand All @@ -42,13 +49,21 @@ export function handleExportBackupKey(translate) {
type: ToastType.Error,
});
} else {
const hasPublicKeys = user.keys?.ecc?.publicKey && user.keys?.kyber?.publicKey;

const backupData: BackupData = {
mnemonic,
privateKey: user.privateKey,
keys: {
ecc: user.keys?.ecc?.privateKey || user.privateKey,
kyber: user.keys?.kyber?.privateKey || '',
},
...(hasPublicKeys && {
publicKeys: {
ecc: user.keys.ecc.publicKey,
kyber: user.keys.kyber.publicKey,
},
}),
};

const backupContent = JSON.stringify(backupData, null, 2);
Expand Down Expand Up @@ -77,13 +92,21 @@ export const detectBackupKeyFormat = (
try {
const parsedData = JSON.parse(backupKeyContent);
if (parsedData?.mnemonic && parsedData.privateKey && parsedData?.keys?.ecc && parsedData?.keys?.kyber) {
const hasPublicKeys = parsedData.publicKeys?.ecc && parsedData.publicKeys?.kyber;

const backupData: BackupData = {
mnemonic: parsedData.mnemonic,
privateKey: parsedData.privateKey,
keys: {
ecc: parsedData.keys.ecc,
kyber: parsedData.keys.kyber,
},
...(hasPublicKeys && {
publicKeys: {
ecc: parsedData.publicKeys.ecc,
kyber: parsedData.publicKeys.kyber,
},
}),
};
return {
type: 'new',
Expand Down
Loading