From 54ff210d6540982069b91e7e0e651dfd997e1bd9 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 20 Aug 2026 11:05:32 +0200 Subject: [PATCH 1/3] add hybrid encryption for bucket key --- src/app/share/services/share.crypto.test.ts | 108 ++++++++++++++++++ src/app/share/services/share.crypto.ts | 102 +++++++++++++++++ src/app/share/services/share.service.test.ts | 2 +- src/app/share/services/share.service.ts | 29 +---- src/app/store/slices/sharedLinks/index.ts | 12 +- .../slices/workspaces/workspacesStore.test.ts | 4 +- .../slices/workspaces/workspacesStore.ts | 2 +- src/utils/backupKeyUtils.ts | 11 +- src/views/Shared/SharedView.tsx | 3 +- .../components/SharedItemListContainer.tsx | 3 +- .../Signup/ShareGuestSignUpView.test.tsx | 3 + 11 files changed, 230 insertions(+), 49 deletions(-) create mode 100644 src/app/share/services/share.crypto.test.ts create mode 100644 src/app/share/services/share.crypto.ts diff --git a/src/app/share/services/share.crypto.test.ts b/src/app/share/services/share.crypto.test.ts new file mode 100644 index 0000000000..433109e8dc --- /dev/null +++ b/src/app/share/services/share.crypto.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, vi, Mock, beforeEach, test } from 'vitest'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; + +import { generateNewKeys, encryptMessageWithPublicKey } from '../../crypto/services/pgp.service'; +import encryptedStorageService from 'services/encrypted-storage.service'; +import { decryptMnemonic, encryptMnemonic } from './share.crypto'; + +vi.mock('services/error.service', () => ({ + default: { + castError: vi.fn().mockImplementation((e) => ({ + message: typeof e === 'string' ? e : e.message || 'Default error message', + requestId: 'test-request-id', + })), + reportError: vi.fn(), + }, +})); + +vi.mock('services/encrypted-storage.service', () => ({ + default: { + getUser: vi.fn(), + }, +})); + +async function getMockUser( + keys: { + privateKeyArmored: string; + publicKeyArmored: string; + publicKyberKeyBase64: string; + privateKyberKeyBase64: string; + }, + encryptedMnemonicInBase64: string, +): Promise { + const mockUser: UserSettings = { + uuid: 'mock-uuid', + email: 'mock@test.com', + mnemonic: encryptedMnemonicInBase64, + userId: 'mock-user-id', + name: 'mock-name', + lastname: 'mock-lastname', + username: 'mock-username', + bridgeUser: 'mock-bridgeUser', + bucket: 'mock-bucket', + backupsBucket: null, + root_folder_id: 0, + rootFolderId: 'mock-rootFolderId', + rootFolderUuid: undefined, + sharedWorkspace: false, + credit: 0, + keys: { + ecc: { + publicKey: keys.publicKeyArmored, + privateKey: Buffer.from(keys.privateKeyArmored).toString('base64'), + }, + kyber: { + publicKey: keys.publicKyberKeyBase64, + privateKey: keys.privateKyberKeyBase64, + }, + }, + appSumoDetails: null, + registerCompleted: false, + hasReferralsProgram: false, + createdAt: new Date(), + avatar: null, + emailVerified: false, + }; + return mockUser; +} + +describe('should decrypt mnemonics', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + }); + + test('should decrypt mnemonic encrypted without kyber', async () => { + const mnemonic = + 'truck arch rather sell tilt return warm nurse rack vacuum rubber tribe unfold scissors copper sock panel ozone harsh ahead danger soda legal state'; + const keys = await generateNewKeys(); + const encriptedMnemonic = await encryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64: keys.publicKeyArmored, + }); + const encryptedMnemonicInBase64 = btoa(encriptedMnemonic as string); + + const mockUser = await getMockUser(keys, encryptedMnemonicInBase64); + + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const ownerMnemonic = await decryptMnemonic(mockUser.mnemonic); + expect(encryptedStorageService.getUser).toHaveBeenCalled(); + expect(ownerMnemonic).toEqual(mnemonic); + }); + + test('should decrypt mnemonic encrypted with kyber', async () => { + const mnemonic = + 'until bonus summer risk chunk oyster census ability frown win pull steel measure employ rigid improve riot remind system earn inch broken chalk clip'; + const keys = await generateNewKeys(); + const encriptedMnemonic = await encryptMnemonic(mnemonic, keys.publicKeyArmored, keys.publicKyberKeyBase64); + + const mockUser = await getMockUser(keys, encriptedMnemonic); + + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const ownerMnemonic = await decryptMnemonic(mockUser.mnemonic); + expect(encryptedStorageService.getUser).toHaveBeenCalled(); + expect(ownerMnemonic).toEqual(mnemonic); + }); +}); diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts new file mode 100644 index 0000000000..08cb23c7b1 --- /dev/null +++ b/src/app/share/services/share.crypto.ts @@ -0,0 +1,102 @@ +import { + hybridDecryptMessageWithPrivateKey, + hybridEncryptMessageWithPublicKey, + encryptBucketKeyHybrid, + decryptBucketKeyHybrid, + isBucketKeyCiphertext, +} from '../../crypto/services/pgp.service'; +import encryptedStorageService from 'services/encrypted-storage.service'; +import notificationsService, { ToastType } from '../../notifications/services/notifications.service'; +import { t } from 'i18next'; +import errorService from 'services/error.service'; +import { generateFileBucketKey } from 'app/network/crypto'; +import { FileKey } from 'app/network/types/helper-types'; + +export const decryptMnemonic = async (encryptionKey: string): Promise => { + const user = await encryptedStorageService.getUser(); + if (user) { + let decryptedKey; + try { + const privateKeyInBase64 = user.keys.ecc.privateKey; + const privateKyberKeyInBase64 = user.keys.kyber.privateKey; + decryptedKey = await hybridDecryptMessageWithPrivateKey({ + encryptedMessageInBase64: encryptionKey, + privateKeyInBase64, + privateKyberKeyInBase64, + }); + } catch (err) { + decryptedKey = user.mnemonic; + } + return decryptedKey; + } else { + handleError('User Not Found', 'error.decryptMnemonic'); + } +}; + +export const encryptMnemonic = async ( + mnemonic: string, + publicKeyInBase64: string, + publicKyberKeyBase64?: string, +): Promise => { + return hybridEncryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64, + publicKyberKeyBase64, + }); +}; + +export const encryptBucketKey = async ( + mnemonic: string, + bucketId: string, + publicKeyInBase64: string, + publicKyberKeyBase64: string, +): Promise => { + const bucketKey = await generateFileBucketKey(mnemonic, bucketId); + return encryptBucketKeyHybrid({ + bucketKey, + publicKeyInBase64, + publicKyberKeyBase64, + }); +}; + +const handleError = (err: unknown, keyLabel: string) => { + const error = errorService.castError(err); + errorService.reportError(error); + + notificationsService.show({ + text: t(keyLabel, { message: error.message }), + type: ToastType.Error, + }); +}; + +export const decryptBucketKey = async (encryptionKey: string): Promise => { + const user = await encryptedStorageService.getUser(); + if (user) { + let decryptedKey; + try { + const privateKeyInBase64 = user.keys.ecc.privateKey; + const privateKyberKeyInBase64 = user.keys.kyber.privateKey; + decryptedKey = await decryptBucketKeyHybrid({ + encryptedMessageInBase64: encryptionKey, + privateKeyInBase64, + privateKyberKeyInBase64, + }); + } catch (err) { + handleError(err, 'error.decryptBucketKey'); + decryptedKey = undefined; + } + return decryptedKey; + } else { + handleError('User Not Found', 'error.decryptBucketKey'); + } +}; + +export const decryptSharingKey = async (encryptionKey: string): Promise => { + if (isBucketKeyCiphertext(encryptionKey)) { + const bucketKey = await decryptBucketKey(encryptionKey); + return bucketKey ? { bucketKey: Buffer.from(bucketKey) } : undefined; + } + + const mnemonic = await decryptMnemonic(encryptionKey); + return mnemonic ? { mnemonic } : undefined; +}; diff --git a/src/app/share/services/share.service.test.ts b/src/app/share/services/share.service.test.ts index 24007f4048..6f11663194 100644 --- a/src/app/share/services/share.service.test.ts +++ b/src/app/share/services/share.service.test.ts @@ -16,12 +16,12 @@ import { downloadFile } from 'app/network/download'; import { FlatFolderZip } from 'services/zip.service'; import { AdvancedSharedItem } from '../types'; import shareService, { - decryptMnemonic, derivePublicSharingKey, downloadPublicSharedFolder, downloadPublicSharedItems, getPublicShareLink, } from './share.service'; +import { decryptMnemonic } from './share.crypto'; import { stringUtils, aes } from '@internxt/lib'; import notificationsService from 'app/notifications/services/notifications.service'; import { SharedFiles, SharingMeta } from '@internxt/sdk/dist/drive/share/types'; diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index e75e8774ce..d47562aeec 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -26,7 +26,6 @@ import { Iterator } from '../../core/collections'; import { SdkFactory } from '../../core/factory/sdk'; import errorService from 'services/error.service'; import workspacesService from 'services/workspace.service'; -import { hybridDecryptMessageWithPrivateKey } from '../../crypto/services/pgp.service'; import { downloadFolderAsZip } from 'app/drive/services/folder.service'; import { DownloadManager } from '../../network/DownloadManager'; import { downloadFile } from 'app/network/download'; @@ -42,6 +41,7 @@ import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; import referralService from 'services/referral.service'; import { generateFileBucketKey } from 'app/network/crypto'; import encryptedStorageService from 'services/encrypted-storage.service'; +import { decryptMnemonic } from './share.crypto'; interface CreateShareResponse { created: boolean; @@ -593,33 +593,6 @@ class DirectoryPublicSharedFilesIterator implements Iterator { } } -export const decryptMnemonic = async (encryptionKey: string): Promise => { - const user = await encryptedStorageService.getUser(); - if (user) { - let decryptedKey; - try { - const privateKeyInBase64 = user.keys.ecc.privateKey; - const privateKyberKeyInBase64 = user.keys.kyber.privateKey; - decryptedKey = await hybridDecryptMessageWithPrivateKey({ - encryptedMessageInBase64: encryptionKey, - privateKeyInBase64, - privateKyberKeyInBase64, - }); - } catch (err) { - decryptedKey = user.mnemonic; - } - return decryptedKey; - } else { - const error = errorService.castError('User Not Found'); - errorService.reportError(error); - - notificationsService.show({ - text: t('error.decryptMnemonic', { message: error.message }), - type: ToastType.Error, - }); - } -}; - export async function downloadSharedFiles({ creds, decryptedEncryptionKey, diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index 2b596f4df5..500145465d 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -11,7 +11,7 @@ import { UserRoles } from 'app/share/types'; import referralService from 'services/referral.service'; import { t } from 'i18next'; import userService from 'services/user.service'; -import { hybridEncryptMessageWithPublicKey } from '../../../crypto/services/pgp.service'; +import { encryptMnemonic } from 'app/share/services/share.crypto'; export const HYBRID_ALGORITHM = 'hybrid'; export const STANDARD_ALGORITHM = 'ed25519'; @@ -58,15 +58,11 @@ const shareItemWithUser = createAsyncThunk ({ @@ -23,7 +23,7 @@ vi.mock('../../../core/types', () => ({ AppView: vi.fn(), LocalStorageItem: vi.fn(), })); -vi.mock('../../../share/services/share.service', () => ({ +vi.mock('../../../share/services/share.crypto', () => ({ decryptMnemonic: vi.fn(), })); vi.mock('../plan', () => ({ diff --git a/src/app/store/slices/workspaces/workspacesStore.ts b/src/app/store/slices/workspaces/workspacesStore.ts index 0439de099b..e63ace6cc2 100644 --- a/src/app/store/slices/workspaces/workspacesStore.ts +++ b/src/app/store/slices/workspaces/workspacesStore.ts @@ -12,7 +12,7 @@ import { saveWorkspaceAvatarToDatabase, } from '../../../../views/NewSettings/components/Sections/Workspace/Overview/components/WorkspaceAvatarWrapper'; import notificationsService, { ToastType } from '../../../notifications/services/notifications.service'; -import { decryptMnemonic } from '../../../share/services/share.service'; +import { decryptMnemonic } from '../../../share/services/share.crypto'; import { planThunks } from '../plan'; import sessionThunks from '../session/session.thunks'; import workspacesSelectors from './workspaces.selectors'; diff --git a/src/utils/backupKeyUtils.ts b/src/utils/backupKeyUtils.ts index 3c7dc52af0..e6e07db776 100644 --- a/src/utils/backupKeyUtils.ts +++ b/src/utils/backupKeyUtils.ts @@ -5,8 +5,9 @@ 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 'app/crypto/services/pgp.service'; +import { encryptMessageWithPublicKey } from 'app/crypto/services/pgp.service'; import encryptedStorageService from 'services/encrypted-storage.service'; +import { encryptMnemonic } from 'app/share/services/share.crypto'; /** * Interface representing the backup data structure @@ -161,7 +162,7 @@ export const prepareOldBackupRecoverPayloadForBackend = async ({ const generatedKeys = await getKeys(password); const eccPublicKeyInBase64 = generatedKeys.ecc.publicKey; - const kyberPublicKeyInBase64 = generatedKeys.kyber.publicKey; + const kyberPublicKeyInBase64 = generatedKeys.kyber.publicKey ?? undefined; const eccEncryptedMnemonic = await encryptMessageWithPublicKey({ message: mnemonic, publicKeyInBase64: generatedKeys.ecc.publicKey, @@ -169,11 +170,7 @@ export const prepareOldBackupRecoverPayloadForBackend = async ({ const base64EccEncryptedMnemonic = btoa(eccEncryptedMnemonic as string); - const hybridEncryptedMnemonic = await hybridEncryptMessageWithPublicKey({ - message: mnemonic, - publicKeyInBase64: eccPublicKeyInBase64, - publicKyberKeyBase64: kyberPublicKeyInBase64 as string, - }); + const hybridEncryptedMnemonic = await encryptMnemonic(mnemonic, eccPublicKeyInBase64, kyberPublicKeyInBase64); return { token, diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index 1a2c6f02a5..813f0d491e 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -6,7 +6,8 @@ import { ChangeEvent, useEffect, useLayoutEffect, useRef, useState } from 'react import { DriveItemData } from '../../app/drive/types'; import { useTranslationContext } from '../../app/i18n/provider/TranslationProvider'; import notificationsService, { ToastType } from '../../app/notifications/services/notifications.service'; -import shareService, { decryptMnemonic } from '../../app/share/services/share.service'; +import shareService from '../../app/share/services/share.service'; +import { decryptMnemonic } from '../../app/share/services/share.crypto'; import { useAppDispatch, useAppSelector } from '../../app/store/hooks'; import { storageActions } from '../../app/store/slices/storage'; import { uiActions } from '../../app/store/slices/ui'; diff --git a/src/views/Shared/components/SharedItemListContainer.tsx b/src/views/Shared/components/SharedItemListContainer.tsx index b80b6fa063..7d5d25fdb3 100644 --- a/src/views/Shared/components/SharedItemListContainer.tsx +++ b/src/views/Shared/components/SharedItemListContainer.tsx @@ -12,7 +12,8 @@ import workspacesService from 'services/workspace.service'; import { OrderDirection } from '../../../app/core/types'; import { sharedThunks } from '../../../app/store/slices/sharedLinks'; import workspacesSelectors from '../../../app/store/slices/workspaces/workspaces.selectors'; -import shareService, { decryptMnemonic } from '../../../app/share/services/share.service'; +import shareService from '../../../app/share/services/share.service'; +import { decryptMnemonic } from '../../../app/share/services/share.crypto'; import { setOrderBy, setPage, setSelectedItems } from '../context/SharedViewContext.actions'; import { useShareViewContext } from '../context/SharedViewContextProvider'; import useSharedContextMenu from '../hooks/useSharedContextMenu'; diff --git a/src/views/Signup/ShareGuestSignUpView.test.tsx b/src/views/Signup/ShareGuestSignUpView.test.tsx index 6bd6a11d5e..936a67715b 100644 --- a/src/views/Signup/ShareGuestSignUpView.test.tsx +++ b/src/views/Signup/ShareGuestSignUpView.test.tsx @@ -120,6 +120,9 @@ vi.mock('app/share/services/share.service', () => ({ validateSharingInvitation: vi.fn(), }, validateSharingInvitation: vi.fn(), +})); + +vi.mock('app/share/services/share.crypto', () => ({ decryptMnemonic: vi.fn(), })); From 30b7f42efd584de3fe201442f6dfa0fbb3903c46 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 20 Aug 2026 12:55:52 +0200 Subject: [PATCH 2/3] add more tests --- src/app/share/services/share.crypto.test.ts | 137 +++++++++++++++++++- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/src/app/share/services/share.crypto.test.ts b/src/app/share/services/share.crypto.test.ts index 433109e8dc..e1e770e249 100644 --- a/src/app/share/services/share.crypto.test.ts +++ b/src/app/share/services/share.crypto.test.ts @@ -3,7 +3,16 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { generateNewKeys, encryptMessageWithPublicKey } from '../../crypto/services/pgp.service'; import encryptedStorageService from 'services/encrypted-storage.service'; -import { decryptMnemonic, encryptMnemonic } from './share.crypto'; +import notificationsService from '../../notifications/services/notifications.service'; +import { + decryptMnemonic, + encryptMnemonic, + encryptBucketKey, + decryptBucketKey, + decryptSharingKey, +} from './share.crypto'; +import { isBucketKeyCiphertext } from '../../crypto/services/pgp.service'; +import { generateFileBucketKey } from 'app/network/crypto'; vi.mock('services/error.service', () => ({ default: { @@ -72,9 +81,11 @@ describe('should decrypt mnemonics', () => { vi.resetModules(); }); + const mnemonic = + 'truck arch rather sell tilt return warm nurse rack vacuum rubber tribe unfold scissors copper sock panel ozone harsh ahead danger soda legal state'; + const bucketId = '0123456789abcdef0123456789abcdef01234567'; + test('should decrypt mnemonic encrypted without kyber', async () => { - const mnemonic = - 'truck arch rather sell tilt return warm nurse rack vacuum rubber tribe unfold scissors copper sock panel ozone harsh ahead danger soda legal state'; const keys = await generateNewKeys(); const encriptedMnemonic = await encryptMessageWithPublicKey({ message: mnemonic, @@ -92,8 +103,6 @@ describe('should decrypt mnemonics', () => { }); test('should decrypt mnemonic encrypted with kyber', async () => { - const mnemonic = - 'until bonus summer risk chunk oyster census ability frown win pull steel measure employ rigid improve riot remind system earn inch broken chalk clip'; const keys = await generateNewKeys(); const encriptedMnemonic = await encryptMnemonic(mnemonic, keys.publicKeyArmored, keys.publicKyberKeyBase64); @@ -105,4 +114,122 @@ describe('should decrypt mnemonics', () => { expect(encryptedStorageService.getUser).toHaveBeenCalled(); expect(ownerMnemonic).toEqual(mnemonic); }); + + describe('bucket key encryption/decryption', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should encrypt and decrypt a bucket key roundtrip without kyber', async () => { + const keys = await generateNewKeys(); + + const encrypted = await encryptBucketKey(mnemonic, bucketId, keys.publicKeyArmored, ''); + + const mockUser = await getMockUser(keys, 'unused'); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const decrypted = await decryptBucketKey(encrypted); + const expectedBucketKey = await generateFileBucketKey(mnemonic, bucketId); + + expect(decrypted).toBeDefined(); + expect(Buffer.from(decrypted as Uint8Array)).toEqual(expectedBucketKey.subarray(0, 32)); + }); + + test('should encrypt and decrypt a bucket key roundtrip with kyber', async () => { + const keys = await generateNewKeys(); + const encrypted = await encryptBucketKey(mnemonic, bucketId, keys.publicKeyArmored, keys.publicKyberKeyBase64); + + const mockUser = await getMockUser(keys, 'unused'); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const decrypted = await decryptBucketKey(encrypted); + const expectedBucketKey = await generateFileBucketKey(mnemonic, bucketId); + + expect(decrypted).toBeDefined(); + expect(Buffer.from(decrypted as Uint8Array)).toEqual(expectedBucketKey.subarray(0, 32)); + }); + + test('isBucketKeyCiphertext returns true for a hybrid bucket key ciphertext', async () => { + const keys = await generateNewKeys(); + const encrypted = await encryptBucketKey(mnemonic, bucketId, keys.publicKeyArmored, keys.publicKyberKeyBase64); + + expect(isBucketKeyCiphertext(encrypted)).toBe(true); + }); + + test('isBucketKeyCiphertext returns false for a plain mnemonic ciphertext', async () => { + const keys = await generateNewKeys(); + const encrypted = await encryptMnemonic('some differnt mnemonic', keys.publicKeyArmored); + + expect(isBucketKeyCiphertext(btoa(encrypted as unknown as string))).toBe(false); + }); + + test('decryptBucketKey returns undefined and notifies when user is not found', async () => { + (encryptedStorageService.getUser as Mock).mockResolvedValue(undefined); + const showSpy = vi.spyOn(notificationsService, 'show'); + + const result = await decryptBucketKey('anything'); + + expect(result).toBeUndefined(); + expect(showSpy).toHaveBeenCalled(); + }); + + test('decryptBucketKey returns undefined and notifies on malformed ciphertext', async () => { + const keys = await generateNewKeys(); + const mockUser = await getMockUser(keys, 'unused'); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + const showSpy = vi.spyOn(notificationsService, 'show'); + + const result = await decryptBucketKey('not-a-valid-ciphertext'); + + expect(result).toBeUndefined(); + expect(showSpy).toHaveBeenCalled(); + }); + }); + + describe('decryptSharingKey', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('routes to bucket key decryption when ciphertext is a bucket key', async () => { + const keys = await generateNewKeys(); + const encrypted = await encryptBucketKey(mnemonic, bucketId, keys.publicKeyArmored, keys.publicKyberKeyBase64); + + const mockUser = await getMockUser(keys, 'unused'); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const result = await decryptSharingKey(encrypted); + + expect(result).toBeDefined(); + expect(result?.bucketKey).toBeDefined(); + expect(result?.mnemonic).toBeUndefined(); + }); + + test('routes to mnemonic decryption when ciphertext is not a bucket key', async () => { + const keys = await generateNewKeys(); + const encrypted = await encryptMnemonic(mnemonic, keys.publicKeyArmored); + + const mockUser = await getMockUser(keys, encrypted); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + + const result = await decryptSharingKey(encrypted); + + expect(result).toBeDefined(); + expect(result?.mnemonic).toEqual(mnemonic); + expect(result?.bucketKey).toBeUndefined(); + }); + + test('returns undefined when bucket key decryption fails', async () => { + const keys = await generateNewKeys(); + const mockUser = await getMockUser(keys, 'unused'); + (encryptedStorageService.getUser as Mock).mockResolvedValue(mockUser); + vi.spyOn(notificationsService, 'show'); + + const malformed = 'SHlicmlkQnVja2V0S2V5$onlyonepart'; + + const result = await decryptSharingKey(malformed); + + expect(result).toBeUndefined(); + }); + }); }); From caf501c92d080c664c9ce0989793fb84d87558b6 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 20 Aug 2026 13:33:50 +0200 Subject: [PATCH 3/3] add comment --- src/app/share/services/share.crypto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts index 08cb23c7b1..9936fc1c3e 100644 --- a/src/app/share/services/share.crypto.ts +++ b/src/app/share/services/share.crypto.ts @@ -25,6 +25,7 @@ export const decryptMnemonic = async (encryptionKey: string): Promise