From 405c0999f3755ab82988f081903a44d580715825 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 14 Aug 2026 15:29:40 +0200 Subject: [PATCH 01/22] move decrypt mnemonic to a new file --- src/app/share/services/share.service.test.ts | 93 +------------------ src/app/share/services/share.service.ts | 29 +----- .../slices/workspaces/workspacesStore.test.ts | 4 +- .../slices/workspaces/workspacesStore.ts | 2 +- src/views/Shared/SharedView.tsx | 3 +- .../components/SharedItemListContainer.tsx | 3 +- .../Signup/ShareGuestSignUpView.test.tsx | 1 - 7 files changed, 10 insertions(+), 125 deletions(-) diff --git a/src/app/share/services/share.service.test.ts b/src/app/share/services/share.service.test.ts index e9037ef2f..2fe2a5867 100644 --- a/src/app/share/services/share.service.test.ts +++ b/src/app/share/services/share.service.test.ts @@ -1,13 +1,9 @@ /** * @jest-environment jsdom */ -import { describe, expect, it, vi, Mock, beforeEach, beforeAll, test } from 'vitest'; +import { describe, expect, it, vi, beforeEach, beforeAll, test } from 'vitest'; import { Buffer } from 'buffer'; -import { - generateNewKeys, - encryptMessageWithPublicKey, - hybridEncryptMessageWithPublicKey, -} from '../../crypto/services/pgp.service'; +import { generateNewKeys, hybridEncryptMessageWithPublicKey } from '../../crypto/services/pgp.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { downloadFolderAsZip } from 'app/drive/services/folder.service'; @@ -16,7 +12,6 @@ import { downloadFile } from 'app/network/download'; import { FlatFolderZip } from 'services/zip.service'; import { AdvancedSharedItem } from '../types'; import shareService, { - decryptMnemonic, derivePublicSharingKey, downloadPublicSharedFolder, downloadPublicSharedItems, @@ -115,90 +110,6 @@ describe('Encryption and Decryption', () => { vi.resetModules(); }); - 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; - } - it('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).mockReturnValue(mockUser); - expect(encryptedStorageService.getUser() as UserSettings).toEqual(mockUser); - - const ownerMnemonic = await decryptMnemonic(mockUser.mnemonic); - expect(encryptedStorageService.getUser).toHaveBeenCalled(); - expect(ownerMnemonic).toEqual(mnemonic); - }); - - it('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 hybridEncryptMessageWithPublicKey({ - message: mnemonic, - publicKeyInBase64: keys.publicKeyArmored, - publicKyberKeyBase64: keys.publicKyberKeyBase64, - }); - - const mockUser = await getMockUser(keys, encriptedMnemonic); - - (encryptedStorageService.getUser as Mock).mockReturnValue(mockUser); - expect(encryptedStorageService.getUser() as UserSettings).toEqual(mockUser); - - const ownerMnemonic = await decryptMnemonic(mockUser.mnemonic); - expect(encryptedStorageService.getUser).toHaveBeenCalled(); - expect(ownerMnemonic).toEqual(mnemonic); - }); - it('should return the same UUID if the input is a valid UUIDv4', () => { const validUuid = 'f32a91da-c799-4e13-aa17-8c4d9e0323c9'; const result = stringUtils.decodeV4Uuid(validUuid); diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index ea814b033..6d1a24d8e 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 = 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/workspaces/workspacesStore.test.ts b/src/app/store/slices/workspaces/workspacesStore.test.ts index 68d26b8c7..4041aa9a7 100644 --- a/src/app/store/slices/workspaces/workspacesStore.test.ts +++ b/src/app/store/slices/workspaces/workspacesStore.test.ts @@ -12,7 +12,7 @@ import notificationsService from 'app/notifications/services/notifications.servi const { setupWorkspace, setSelectedWorkspace } = workspaceThunks; import { workspacesActions } from './workspacesStore'; import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; -import { decryptMnemonic } from '../../../share/services/share.service'; +import { decryptMnemonic } from '../../../share/services/share.crypto'; import encryptedStorageService from 'services/encrypted-storage.service'; vi.mock('i18next', () => ({ @@ -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 0439de099..e63ace6cc 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/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index ba697a942..d84a5cd37 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -6,7 +6,8 @@ import { ChangeEvent, useEffect, useLayoutEffect, useRef } 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 dbffa4c84..6311d4d77 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 0e66bf5d0..f00301263 100644 --- a/src/views/Signup/ShareGuestSignUpView.test.tsx +++ b/src/views/Signup/ShareGuestSignUpView.test.tsx @@ -123,7 +123,6 @@ describe('onSubmit', () => { validateSharingInvitation: vi.fn(), }, validateSharingInvitation: vi.fn(), - decryptMnemonic: vi.fn(), })); vi.mock('services/navigation.service', () => ({ From 917f3a2034354fbc6a5ec9648faead7f1c9d3550 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 14 Aug 2026 15:37:05 +0200 Subject: [PATCH 02/22] add new files --- src/app/share/services/share.crypto.test.ts | 118 ++++++++++++++++++++ src/app/share/services/share.crypto.ts | 32 ++++++ 2 files changed, 150 insertions(+) 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 000000000..301c8ecfc --- /dev/null +++ b/src/app/share/services/share.crypto.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, vi, Mock, beforeEach, test } from 'vitest'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; + +import { + generateNewKeys, + encryptMessageWithPublicKey, + hybridEncryptMessageWithPublicKey, +} from '../../crypto/services/pgp.service'; +import encryptedStorageService from 'services/encrypted-storage.service'; +import { decryptMnemonic } 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).mockReturnValue(mockUser); + expect(encryptedStorageService.getUser() as UserSettings).toEqual(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 hybridEncryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64: keys.publicKeyArmored, + publicKyberKeyBase64: keys.publicKyberKeyBase64, + }); + + const mockUser = await getMockUser(keys, encriptedMnemonic); + + (encryptedStorageService.getUser as Mock).mockReturnValue(mockUser); + expect(encryptedStorageService.getUser() as UserSettings).toEqual(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 000000000..599753b7c --- /dev/null +++ b/src/app/share/services/share.crypto.ts @@ -0,0 +1,32 @@ +import { hybridDecryptMessageWithPrivateKey } 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'; + +export const decryptMnemonic = async (encryptionKey: string): Promise => { + const user = 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, + }); + } +}; From 5a920bca53f4f01b91f973c146020903c2c7c640 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 14 Aug 2026 16:22:39 +0200 Subject: [PATCH 03/22] use encryptMnemonic --- src/app/share/services/share.crypto.test.ts | 14 +++----------- src/app/share/services/share.crypto.ts | 17 ++++++++++++++++- src/app/share/services/share.service.test.ts | 15 ++++----------- src/app/store/slices/sharedLinks/index.ts | 10 +++------- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/app/share/services/share.crypto.test.ts b/src/app/share/services/share.crypto.test.ts index 301c8ecfc..e8b3774c5 100644 --- a/src/app/share/services/share.crypto.test.ts +++ b/src/app/share/services/share.crypto.test.ts @@ -1,13 +1,9 @@ import { describe, expect, vi, Mock, beforeEach, test } from 'vitest'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; -import { - generateNewKeys, - encryptMessageWithPublicKey, - hybridEncryptMessageWithPublicKey, -} from '../../crypto/services/pgp.service'; +import { generateNewKeys, encryptMessageWithPublicKey } from '../../crypto/services/pgp.service'; import encryptedStorageService from 'services/encrypted-storage.service'; -import { decryptMnemonic } from './share.crypto'; +import { decryptMnemonic, encryptMnemonic } from './share.crypto'; vi.mock('services/error.service', () => ({ default: { @@ -100,11 +96,7 @@ describe('should decrypt mnemonics', () => { 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 hybridEncryptMessageWithPublicKey({ - message: mnemonic, - publicKeyInBase64: keys.publicKeyArmored, - publicKyberKeyBase64: keys.publicKyberKeyBase64, - }); + const encriptedMnemonic = await encryptMnemonic(mnemonic, keys.publicKeyArmored, keys.publicKyberKeyBase64); const mockUser = await getMockUser(keys, encriptedMnemonic); diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts index 599753b7c..6805f81c9 100644 --- a/src/app/share/services/share.crypto.ts +++ b/src/app/share/services/share.crypto.ts @@ -1,4 +1,7 @@ -import { hybridDecryptMessageWithPrivateKey } from '../../crypto/services/pgp.service'; +import { + hybridDecryptMessageWithPrivateKey, + hybridEncryptMessageWithPublicKey, +} from '../../crypto/services/pgp.service'; import encryptedStorageService from 'services/encrypted-storage.service'; import notificationsService, { ToastType } from '../../notifications/services/notifications.service'; import { t } from 'i18next'; @@ -30,3 +33,15 @@ export const decryptMnemonic = async (encryptionKey: string): Promise => { + return hybridEncryptMessageWithPublicKey({ + message: mnemonic, + publicKeyInBase64, + publicKyberKeyBase64, + }); +}; diff --git a/src/app/share/services/share.service.test.ts b/src/app/share/services/share.service.test.ts index 2fe2a5867..7e63e8808 100644 --- a/src/app/share/services/share.service.test.ts +++ b/src/app/share/services/share.service.test.ts @@ -3,7 +3,7 @@ */ import { describe, expect, it, vi, beforeEach, beforeAll, test } from 'vitest'; import { Buffer } from 'buffer'; -import { generateNewKeys, hybridEncryptMessageWithPublicKey } from '../../crypto/services/pgp.service'; +import { generateNewKeys } from '../../crypto/services/pgp.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { downloadFolderAsZip } from 'app/drive/services/folder.service'; @@ -26,6 +26,7 @@ import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; import referralService from 'services/referral.service'; import { ToastType } from 'app/notifications/services/notifications.service'; import encryptedStorageService from 'services/encrypted-storage.service'; +import { encryptMnemonic } from './share.crypto'; vi.mock('utils/copyToClipboard.utils', () => ({ copyTextToClipboard: vi.fn(), @@ -307,11 +308,7 @@ describe('Get public shared link', async () => { } as UserSettings); const spyDecrypt = vi.spyOn(aes, 'decrypt'); const mockDifferentMnemonic = 'mock mnemonic'; - const encryptedMnemonic = await hybridEncryptMessageWithPublicKey({ - message: mockDifferentMnemonic, - publicKeyInBase64, - publicKyberKeyBase64, - }); + const encryptedMnemonic = await encryptMnemonic(mockDifferentMnemonic, publicKeyInBase64, publicKyberKeyBase64); const { SdkFactory } = await import('../../core/factory/sdk'); const mockSharingMetaWithEncryptedMnemonic = { ...mockSharingMeta, @@ -360,11 +357,7 @@ describe('Get public shared link', async () => { } as UserSettings); const spyDecrypt = vi.spyOn(aes, 'decrypt'); const mockDifferentMnemonic = 'mock mnemonic'; - const encryptedMnemonic = await hybridEncryptMessageWithPublicKey({ - message: mockDifferentMnemonic, - publicKeyInBase64, - publicKyberKeyBase64, - }); + const encryptedMnemonic = await encryptMnemonic(mockDifferentMnemonic, publicKeyInBase64, publicKyberKeyBase64); const newBucketKey = await generateFileBucketKey(mockDifferentMnemonic, bucket); const newBucketKeyHex = Buffer.from(newBucketKey.subarray(0, 32)).toString('hex'); const { SdkFactory } = await import('../../core/factory/sdk'); diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index 2b596f4df..31c683eae 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'; @@ -60,13 +60,9 @@ const shareItemWithUser = createAsyncThunk Date: Mon, 17 Aug 2026 13:49:04 +0200 Subject: [PATCH 04/22] use v2 for creating private sharing --- src/app/crypto/services/pgp.service.ts | 114 ++++++++++++++++++ src/app/share/services/share.crypto.ts | 57 +++++++-- .../store/slices/sharedLinks/index.test.ts | 90 +++++--------- src/app/store/slices/sharedLinks/index.ts | 11 +- test/unit/services/pgp.service.test.ts | 41 +++++-- 5 files changed, 234 insertions(+), 79 deletions(-) diff --git a/src/app/crypto/services/pgp.service.ts b/src/app/crypto/services/pgp.service.ts index 27b90d12d..c21b7614a 100644 --- a/src/app/crypto/services/pgp.service.ts +++ b/src/app/crypto/services/pgp.service.ts @@ -5,6 +5,7 @@ import kemBuilder from '@dashlane/pqc-kem-kyber512-browser'; import { extendSecret } from './utils'; const WORDS_HYBRID_MODE_IN_BASE64 = 'SHlicmlkTW9kZQ=='; // 'HybridMode' in BASE64 format +const WORDS_HYBRID_BUCKET_KEY_IN_BASE64 = 'SHlicmlkQnVja2V0S2V5'; // 'HybridBucketKey' in BASE64 format type Data = Uint8Array | string; export async function getOpenpgp(): Promise { @@ -202,3 +203,116 @@ export const decryptMessageWithPrivateKey = async ({ return decryptedMessage; }; + +function xorUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { + const result = new Uint8Array(a.length); + for (let i = 0; i < a.length; i++) { + result[i] = a[i] ^ b[i]; + } + return result; +} + +/** + * Encrypts bucket key using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only + * @param {Uint8Array} bucketKey - The bucket key to encrypt + * @param {string} publicKeyInBase64 - The ecc public key in Base64 + * @param {string}[publicKyberKeyBase64] - The kyber public key in Base64 + * @returns {Promise} The encrypted message. + */ +export const encryptBucketKeyHybrid = async ({ + bucketKey, + publicKeyInBase64, + publicKyberKeyBase64, +}: { + bucketKey: Uint8Array; + publicKeyInBase64: string; + publicKyberKeyBase64?: string; +}): Promise => { + let result = ''; + let plaintext: Uint8Array = bucketKey.subarray(0, 32); + if (publicKyberKeyBase64) { + const kem = await kemBuilder(); + + const publicKyberKey = Buffer.from(publicKyberKeyBase64, 'base64'); + const { ciphertext, sharedSecret: secret } = await kem.encapsulate(new Uint8Array(publicKyberKey)); + const kyberCiphertextStr = Buffer.from(ciphertext).toString('base64'); + + plaintext = xorUint8Arrays(plaintext, secret); + result = WORDS_HYBRID_BUCKET_KEY_IN_BASE64.concat('$', kyberCiphertextStr, '$'); + } + + const openpgp = await getOpenpgp(); + + const publicKeyArmored = Buffer.from(publicKeyInBase64, 'base64').toString(); + const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + + const encryptedMessage = await openpgp.encrypt({ + message: await openpgp.createMessage({ binary: plaintext }), + encryptionKeys: publicKey, + }); + const eccCiphertextStr = btoa(encryptedMessage as string); + + result = result.concat(eccCiphertextStr); + + return result; +}; + +/** + * Decrypts ciphertext using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only + * @param {string} encryptedMessageInBase64 - The encrypted message + * @param {string} privateKeyInBase64 - The ecc private key in Base64 + * @param {string}[privateKyberKeyInBase64] - The kyber private key in Base64 + * @returns {Promise} The decrypted bucket key. + */ +export const decryptBucketKeyHybrid = async ({ + encryptedMessageInBase64, + privateKeyInBase64, + privateKyberKeyInBase64, +}: { + encryptedMessageInBase64: string; + privateKeyInBase64: string; + privateKyberKeyInBase64?: string; +}): Promise => { + let eccCiphertextStr = encryptedMessageInBase64; + let kyberSecret; + const ciphertexts = encryptedMessageInBase64.split('$'); + const prefix = ciphertexts[0]; + const isHybridMode = prefix === WORDS_HYBRID_BUCKET_KEY_IN_BASE64; + + if (isHybridMode) { + if (!privateKyberKeyInBase64) { + return Promise.reject(new Error('Attempted to decrypt hybrid ciphertex without Kyber key')); + } + const kem = await kemBuilder(); + + const kyberCiphertextBase64 = ciphertexts[1]; + eccCiphertextStr = ciphertexts[2]; + + const privateKyberKey = Buffer.from(privateKyberKeyInBase64, 'base64'); + const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); + const decapsulate = await kem.decapsulate(new Uint8Array(kyberCiphertext), new Uint8Array(privateKyberKey)); + kyberSecret = decapsulate.sharedSecret; + } + const openpgp = await getOpenpgp(); + + const privateKeyArmored = Buffer.from(privateKeyInBase64, 'base64').toString(); + const privateKey = await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }); + + const message = await openpgp.readMessage({ + armoredMessage: atob(eccCiphertextStr), + }); + + const { data: decryptedMessage } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey, + format: 'binary', + }); + + let result = decryptedMessage; + if (isHybridMode) { + const xored = xorUint8Arrays(result, kyberSecret); + result = xored; + } + + return result; +}; diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts index 6805f81c9..503bf8115 100644 --- a/src/app/share/services/share.crypto.ts +++ b/src/app/share/services/share.crypto.ts @@ -1,11 +1,14 @@ import { hybridDecryptMessageWithPrivateKey, hybridEncryptMessageWithPublicKey, + encryptBucketKeyHybrid, + decryptBucketKeyHybrid, } 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'; export const decryptMnemonic = async (encryptionKey: string): Promise => { const user = encryptedStorageService.getUser(); @@ -24,13 +27,7 @@ export const decryptMnemonic = async (encryptionKey: 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 = 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'); + } +}; diff --git a/src/app/store/slices/sharedLinks/index.test.ts b/src/app/store/slices/sharedLinks/index.test.ts index bfc201d88..f5458f50e 100644 --- a/src/app/store/slices/sharedLinks/index.test.ts +++ b/src/app/store/slices/sharedLinks/index.test.ts @@ -8,13 +8,9 @@ import { Buffer } from 'buffer'; import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { RootState } from '../..'; import userService from 'services/user.service'; +import { decryptBucketKeyHybrid, generateNewKeys } from '../../../crypto/services/pgp.service'; import { - decryptMessageWithPrivateKey, - generateNewKeys, - hybridDecryptMessageWithPrivateKey, -} from '../../../crypto/services/pgp.service'; -import { - HYBRID_ALGORITHM, + HYBRID_ALGORITHM_WITH_BUCKET_KEY, removeUserFromSharedFolder, sharedThunks, ShareFileWithUserPayload, @@ -22,9 +18,10 @@ import { stopSharingItem, } from './index'; import notificationsService from 'app/notifications/services/notifications.service'; +import { generateFileBucketKey } from 'app/network/crypto'; const { shareItemWithUser } = sharedThunks; -describe('Encryption and Decryption', () => { +describe('Encryption and Decryption', async () => { beforeAll(() => { vi.mock('services/navigation.service', () => ({ default: { push: vi.fn() }, @@ -67,6 +64,17 @@ describe('Encryption and Decryption', () => { vi.resetModules(); }); + const bucket = 'mock-bucket'; + 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 mockUser: Partial = { + mnemonic, + bucket, + }; + + const mockKey = await generateFileBucketKey(mnemonic, bucket); + const mockBucketKey = new Uint8Array(mockKey).slice(0, 32); + it('shareItemWithUser encrypts with kyber for an existing user', async () => { const keys = await generateNewKeys(); const mockPayload: ShareFileWithUserPayload = { @@ -79,11 +87,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -116,13 +119,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await hybridDecryptMessageWithPrivateKey({ + const decryptedMessage = await decryptBucketKeyHybrid({ encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), privateKyberKeyInBase64: keys.privateKyberKeyBase64, }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -131,7 +134,7 @@ describe('Encryption and Decryption', () => { notifyUser: mockPayload.notifyUser, notificationMessage: mockPayload.notificationMessage, encryptionKey: encryptionKey, - encryptionAlgorithm: HYBRID_ALGORITHM, + encryptionAlgorithm: HYBRID_ALGORITHM_WITH_BUCKET_KEY, roleId: mockPayload.roleId, persistPreviousSharing: true, }), @@ -150,11 +153,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -188,12 +186,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await decryptMessageWithPrivateKey({ - encryptedMessage: atob(encryptionKey), + const decryptedMessage = await decryptBucketKeyHybrid({ + encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), + privateKyberKeyInBase64: '', }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -221,11 +220,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -261,13 +255,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await hybridDecryptMessageWithPrivateKey({ + const decryptedMessage = await decryptBucketKeyHybrid({ encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), privateKyberKeyInBase64: keys.privateKyberKeyBase64, }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -276,7 +270,7 @@ describe('Encryption and Decryption', () => { notifyUser: mockPayload.notifyUser, notificationMessage: mockPayload.notificationMessage, encryptionKey: encryptionKey, - encryptionAlgorithm: HYBRID_ALGORITHM, + encryptionAlgorithm: HYBRID_ALGORITHM_WITH_BUCKET_KEY, roleId: mockPayload.roleId, persistPreviousSharing: true, }), @@ -295,11 +289,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -333,13 +322,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await hybridDecryptMessageWithPrivateKey({ + const decryptedMessage = await decryptBucketKeyHybrid({ encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), privateKyberKeyInBase64: '', }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -367,11 +356,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -405,13 +389,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await hybridDecryptMessageWithPrivateKey({ + const decryptedMessage = await decryptBucketKeyHybrid({ encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), privateKyberKeyInBase64: keys.privateKyberKeyBase64, }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -420,7 +404,7 @@ describe('Encryption and Decryption', () => { notifyUser: mockPayload.notifyUser, notificationMessage: mockPayload.notificationMessage, encryptionKey: encryptionKey, - encryptionAlgorithm: HYBRID_ALGORITHM, + encryptionAlgorithm: HYBRID_ALGORITHM_WITH_BUCKET_KEY, roleId: mockPayload.roleId, persistPreviousSharing: true, }), @@ -439,11 +423,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; @@ -476,13 +455,13 @@ describe('Encryption and Decryption', () => { expect(inviteUserToSharedFolderInput.encryptionKey).toBeDefined(); const { encryptionKey = '' } = inviteUserToSharedFolderInput; - const decryptedMessage = await hybridDecryptMessageWithPrivateKey({ + const decryptedMessage = await decryptBucketKeyHybrid({ encryptedMessageInBase64: encryptionKey, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), privateKyberKeyInBase64: '', }); - expect(decryptedMessage).toEqual(mockUser.mnemonic); + expect(decryptedMessage).toEqual(mockBucketKey); expect(mockShareService.inviteUserToSharedFolder).toHaveBeenCalledWith( expect.objectContaining({ itemId: mockPayload.itemId, @@ -509,11 +488,6 @@ describe('Encryption and Decryption', () => { roleId: 'mock-roleId', }; - const mockUser: Partial = { - 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 mockRootState: Partial = { user: { user: mockUser as UserSettings, isInitializing: false, isAuthenticated: false, isInitialized: false }, }; diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index 31c683eae..5fb0cc80e 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -11,9 +11,10 @@ import { UserRoles } from 'app/share/types'; import referralService from 'services/referral.service'; import { t } from 'i18next'; import userService from 'services/user.service'; -import { encryptMnemonic } from 'app/share/services/share.crypto'; +import { encryptBucketKey } from 'app/share/services/share.crypto'; export const HYBRID_ALGORITHM = 'hybrid'; +export const HYBRID_ALGORITHM_WITH_BUCKET_KEY = 'hybrid-v2'; export const STANDARD_ALGORITHM = 'ed25519'; export interface ShareLinksState { @@ -53,16 +54,16 @@ const shareItemWithUser = createAsyncThunk { @@ -44,41 +46,41 @@ describe('Encryption and Decryption', () => { expect(encryptedMessage).toBeDefined(); }); - it('XOR should throw an error when strings are of different length', async () => { + it('XOR should throw an error when strings are of different length', () => { const messageHex = '74686973206973207468652074657374206d657373616765'; const secretHex = '74686973206973207468652074657374206d65737361676574686973206973207468652074657374206d657373616765'; expect(() => { XORhex(messageHex, secretHex); - }).toThrowError('Can XOR only strings with identical length'); + }).toThrow('Can XOR only strings with identical length'); }); - it('XOR should work for the given fixed example', async () => { + it('XOR should work for the given fixed example', () => { const firstHex = '74686973206973207468652074657374206d657373616765'; const secondHex = '7468697320697320746865207365636f6e64206d65737361'; const resultHex = '0000000000000000000000000700101b4e09451e16121404'; - const xoredMessage = await XORhex(firstHex, secondHex); + const xoredMessage = XORhex(firstHex, secondHex); expect(xoredMessage).toEqual(resultHex); }); - it('XOR of two identical strings should result in zero string', async () => { + it('XOR of two identical strings should result in zero string', () => { const strHex = '74686973206973207468652074657374206d657373616765'; const resultHex = '000000000000000000000000000000000000000000000000'; - const xoredMessage = await XORhex(strHex, strHex); + const xoredMessage = XORhex(strHex, strHex); expect(xoredMessage).toEqual(resultHex); }); - it('XOR of str1, str2 and str1 should result in str2', async () => { + it('XOR of str1, str2 and str1 should result in str2', () => { const str1 = '74686973206973207468652074657374206d657373616765'; const str2 = '7468697320697320746865207365636f6e64206d65737361'; - const str3 = await XORhex(str1, str2); - const should_be_str2 = await XORhex(str3, str1); + const str3 = XORhex(str1, str2); + const should_be_str2 = XORhex(str3, str1); expect(should_be_str2).toEqual(str2); }); @@ -308,3 +310,24 @@ describe('Encryption and Decryption', () => { expect(decryptedMnemonic).toEqual(testMnemonic); }); }); + +describe('Hybrid encryption and decryption of a bucket key', () => { + it('should encrypt and decrypt a bucket key successfully', async () => { + const bucketKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit key + const keys = await generateNewKeys(); + + const encryptedMessage = await encryptBucketKeyHybrid({ + bucketKey, + publicKeyInBase64: keys.publicKeyArmored, + publicKyberKeyBase64: keys.publicKyberKeyBase64, + }); + + const decryptedMessage = await decryptBucketKeyHybrid({ + encryptedMessageInBase64: encryptedMessage, + privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), + privateKyberKeyInBase64: keys.privateKyberKeyBase64, + }); + + expect(decryptedMessage).toEqual(bucketKey); + }); +}); From 439c97bb665ac03a433637b3de69d50b0ab4dcb8 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 09:52:17 +0200 Subject: [PATCH 05/22] switch download to file key --- .../FileViewer/FileViewerWrapper.tsx | 2 +- .../FileVideoViewer/FileVideoViewer.tsx | 2 +- .../download.service/fetchFileBlob.ts | 10 +- .../VideoStreamingSession.test.ts | 2 +- .../VideoStreamingSession.ts | 4 +- .../services/video-streaming.service/index.ts | 4 +- .../video-streaming.service.test.ts | 4 +- src/app/network/NetworkFacade.test.ts | 16 +- src/app/network/NetworkFacade.ts | 114 +++++++++----- src/app/network/download.test.ts | 4 +- src/app/network/download.ts | 5 +- .../download/MultipartDownload.test.ts | 2 +- src/app/network/download/MultipartDownload.ts | 23 +-- src/app/network/download/v2.test.ts | 10 +- src/app/network/download/v2.ts | 148 +++++++----------- src/app/network/types/index.ts | 3 +- src/app/share/types/index.ts | 4 +- src/app/store/slices/sharedLinks/index.ts | 2 +- src/views/PublicShared/ShareFileView.tsx | 23 ++- src/views/Shared/SharedView.tsx | 2 +- .../components/SharedItemListContainer.tsx | 5 +- src/views/Shared/hooks/useFetchSharedData.tsx | 1 + 22 files changed, 208 insertions(+), 182 deletions(-) diff --git a/src/app/drive/components/FileViewer/FileViewerWrapper.tsx b/src/app/drive/components/FileViewer/FileViewerWrapper.tsx index 0e5c98f48..0284589da 100644 --- a/src/app/drive/components/FileViewer/FileViewerWrapper.tsx +++ b/src/app/drive/components/FileViewer/FileViewerWrapper.tsx @@ -214,7 +214,7 @@ const FileViewerWrapper = ({ abortController, }, currentFile.credentials, - currentFile.mnemonic, + currentFile.key, ); } diff --git a/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx b/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx index a4a41f3cf..a090d5d59 100644 --- a/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx +++ b/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx @@ -88,7 +88,7 @@ const FileVideoViewer = ({ bucketId: file.bucket, fileSize: file.size, fileType: file.type, - mnemonic: file.mnemonic ?? mnemonic, + key: { mnemonic: file.key?.mnemonic ?? mnemonic, bucketKey: file.key?.bucketKey }, credentials: file.credentials ? { user: file.credentials?.user, pass: file.credentials?.pass } : { user: bridgeUser, pass: userId }, diff --git a/src/app/drive/services/download.service/fetchFileBlob.ts b/src/app/drive/services/download.service/fetchFileBlob.ts index b5859a0d0..6df365657 100644 --- a/src/app/drive/services/download.service/fetchFileBlob.ts +++ b/src/app/drive/services/download.service/fetchFileBlob.ts @@ -1,7 +1,7 @@ import { binaryStreamToBlob } from 'services/stream.service'; import { Downloadable, downloadFile } from 'app/network/download'; import { getEnvironmentConfig } from '../network.service'; -import { NetworkCredentials } from 'app/network/types/helper-types'; +import { FileKey, NetworkCredentials } from 'app/network/types/helper-types'; type FetchFileBlobOptions = { updateProgressCallback: (progress: number) => void; @@ -14,9 +14,9 @@ export default async function fetchFileBlob( item: Downloadable, options: FetchFileBlobOptions, credentials?: NetworkCredentials, - mnemonic?: string, + key?: FileKey, ): Promise { - const { bridgeUser, bridgePass, encryptionKey } = await getEnvironmentConfig(!!options.isWorkspace); + const { bridgeUser, bridgePass } = await getEnvironmentConfig(!!options.isWorkspace); const creds = credentials ? credentials : { pass: bridgePass, user: bridgeUser }; @@ -24,9 +24,7 @@ export default async function fetchFileBlob( bucketId: item.bucketId, fileId: item.fileId, creds, - key: { - mnemonic: mnemonic ? mnemonic : encryptionKey, - }, + key, options: { notifyProgress: (totalBytes, downloadedBytes) => { options.updateProgressCallback(downloadedBytes / totalBytes); diff --git a/src/app/drive/services/video-streaming.service/VideoStreamingSession.test.ts b/src/app/drive/services/video-streaming.service/VideoStreamingSession.test.ts index 11b8dfbc8..fbd437ad7 100644 --- a/src/app/drive/services/video-streaming.service/VideoStreamingSession.test.ts +++ b/src/app/drive/services/video-streaming.service/VideoStreamingSession.test.ts @@ -16,7 +16,7 @@ const createConfig = (): VideoStreamingSessionConfig => ({ bucketId: 'bucket-456', fileSize: 1000000, fileType: 'video/mp4', - mnemonic: 'test mnemonic', + key: { mnemonic: 'test mnemonic' }, credentials: { user: 'user', pass: 'pass' }, }); diff --git a/src/app/drive/services/video-streaming.service/VideoStreamingSession.ts b/src/app/drive/services/video-streaming.service/VideoStreamingSession.ts index 8def1f5d1..e30a9f1c9 100644 --- a/src/app/drive/services/video-streaming.service/VideoStreamingSession.ts +++ b/src/app/drive/services/video-streaming.service/VideoStreamingSession.ts @@ -1,4 +1,4 @@ -import { NetworkCredentials } from 'app/network/types/helper-types'; +import { FileKey, NetworkCredentials } from 'app/network/types/helper-types'; import { VideoSessionDestroyedError } from '../errors/video-streaming.errors'; import { VideoStreamingService } from './index'; @@ -7,7 +7,7 @@ export interface VideoStreamingSessionConfig { bucketId: string; fileSize: number; fileType: string; - mnemonic: string; + key: FileKey; credentials: NetworkCredentials; } diff --git a/src/app/drive/services/video-streaming.service/index.ts b/src/app/drive/services/video-streaming.service/index.ts index 33e963b9e..1eee1c3ae 100644 --- a/src/app/drive/services/video-streaming.service/index.ts +++ b/src/app/drive/services/video-streaming.service/index.ts @@ -2,6 +2,7 @@ import { downloadChunkFile } from 'app/network/download/v2'; import { binaryStreamToUint8Array } from 'services'; import { VideoSessionDestroyedError } from '../errors/video-streaming.errors'; import { VideoStreamingSessionConfig } from './VideoStreamingSession'; +import { FileKey } from 'app/network/types/helper-types'; export interface VideoStreamSession { fileSize: number; @@ -49,10 +50,11 @@ export class VideoStreamingService { private async downloadChunk(start: number, end: number, cacheKey: string): Promise { try { + console.log('CHECK: Downloading chunk with mnemonic'); const stream = await downloadChunkFile({ bucketId: this.config.bucketId, fileId: this.config.fileId, - key: { mnemonic: this.config.mnemonic }, + key: this.config.key, creds: this.config.credentials, chunkStart: start, chunkEnd: end, diff --git a/src/app/drive/services/video-streaming.service/video-streaming.service.test.ts b/src/app/drive/services/video-streaming.service/video-streaming.service.test.ts index d327a6f2c..078447704 100644 --- a/src/app/drive/services/video-streaming.service/video-streaming.service.test.ts +++ b/src/app/drive/services/video-streaming.service/video-streaming.service.test.ts @@ -19,7 +19,7 @@ const createConfig = (): VideoStreamingSessionConfig => ({ bucketId: 'bucket-456', fileSize: 1024000, fileType: 'video/mp4', - mnemonic: 'test mnemonic', + key: { mnemonic: 'test mnemonic' }, credentials: { user: 'user', pass: 'pass' }, }); @@ -45,7 +45,7 @@ describe('Video Streaming Service', () => { expect.objectContaining({ bucketId: config.bucketId, fileId: config.fileId, - key: { mnemonic: config.mnemonic }, + key: config.key, chunkStart: 0, chunkEnd: 1024, }), diff --git a/src/app/network/NetworkFacade.test.ts b/src/app/network/NetworkFacade.test.ts index b2fc115e7..3a2015f96 100644 --- a/src/app/network/NetworkFacade.test.ts +++ b/src/app/network/NetworkFacade.test.ts @@ -186,7 +186,7 @@ describe('NetworkFacade', () => { ); vi.mocked(decryptStream).mockReturnValue(mockDecryptedStream); - const result = await networkFacade.downloadChunk({ bucketId, fileId, mnemonic, chunkStart, chunkEnd }); + const result = await networkFacade.downloadChunk({ bucketId, fileId, key: { mnemonic }, chunkStart, chunkEnd }); expect(result).toStrictEqual(mockDecryptedStream); @@ -215,9 +215,9 @@ describe('NetworkFacade', () => { }, ); - await expect(networkFacade.downloadChunk({ bucketId, fileId, mnemonic, chunkStart, chunkEnd })).rejects.toThrow( - new DownloadFailedWithUnknownError(mockResponse.status), - ); + await expect( + networkFacade.downloadChunk({ bucketId, fileId, key: { mnemonic }, chunkStart, chunkEnd }), + ).rejects.toThrow(new DownloadFailedWithUnknownError(mockResponse.status)); }); test('When there is no body in the response, then an error indicating so is thrown', async () => { @@ -233,9 +233,9 @@ describe('NetworkFacade', () => { }, ); - await expect(networkFacade.downloadChunk({ bucketId, fileId, mnemonic, chunkStart, chunkEnd })).rejects.toThrow( - NoContentReceivedError, - ); + await expect( + networkFacade.downloadChunk({ bucketId, fileId, key: { mnemonic }, chunkStart, chunkEnd }), + ).rejects.toThrow(NoContentReceivedError); }); it('When the download is aborted, then an DownloadAbortedByUserError error is thrown', async () => { @@ -251,7 +251,7 @@ describe('NetworkFacade', () => { networkFacade.downloadChunk({ bucketId, fileId, - mnemonic, + key: { mnemonic }, chunkStart, chunkEnd, options: { diff --git a/src/app/network/NetworkFacade.ts b/src/app/network/NetworkFacade.ts index 00bc91f63..9bab43316 100644 --- a/src/app/network/NetworkFacade.ts +++ b/src/app/network/NetworkFacade.ts @@ -371,7 +371,7 @@ export class NetworkFacade { async downloadChunk({ bucketId, fileId, - mnemonic, + key: { mnemonic, bucketKey }, chunkStart, chunkEnd, options, @@ -379,46 +379,90 @@ export class NetworkFacade { const encryptedContentStreams: ReadableStream[] = []; let fileStream: ReadableStream; - await downloadFile( - fileId, - bucketId, - mnemonic, - this.network, - this.cryptoLib, - Buffer.from, - async (downloadables) => { - for (const downloadable of downloadables) { - if (options?.abortController?.signal.aborted) { - throw new DownloadAbortedByUserError(); - } + if (mnemonic) { + await downloadFile( + fileId, + bucketId, + mnemonic, + this.network, + this.cryptoLib, + Buffer.from, + async (downloadables) => { + for (const downloadable of downloadables) { + if (options?.abortController?.signal.aborted) { + throw new DownloadAbortedByUserError(); + } - const response = await fetch(downloadable.url, { - signal: options?.abortController?.signal, - headers: { - Range: `bytes=${chunkStart}-${chunkEnd}`, - Connection: 'keep-alive', - }, - keepalive: true, - }); + const response = await fetch(downloadable.url, { + signal: options?.abortController?.signal, + headers: { + Range: `bytes=${chunkStart}-${chunkEnd}`, + Connection: 'keep-alive', + }, + keepalive: true, + }); - const statusCode = response.status; + const statusCode = response.status; - if (statusCode !== 206 && statusCode !== 200) { - throw new DownloadFailedWithUnknownError(statusCode); - } + if (statusCode !== 206 && statusCode !== 200) { + throw new DownloadFailedWithUnknownError(statusCode); + } + + if (!response.body) { + throw new NoContentReceivedError(); + } - if (!response.body) { - throw new NoContentReceivedError(); + encryptedContentStreams.push(response.body); } + }, + async (algorithm, key, iv, fileSize) => { + fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, chunkStart); + }, + (options?.token && { token: options.token }) || undefined, + ); + } + if (bucketKey) { + await downloadFileWithBucketKey( + fileId, + bucketId, + bucketKey, + this.network, + this.cryptoLibBucketKey, + Buffer.from, + async (downloadables) => { + for (const downloadable of downloadables) { + if (options?.abortController?.signal.aborted) { + throw new DownloadAbortedByUserError(); + } - encryptedContentStreams.push(response.body); - } - }, - async (algorithm, key, iv, fileSize) => { - fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, chunkStart); - }, - (options?.token && { token: options.token }) || undefined, - ); + const response = await fetch(downloadable.url, { + signal: options?.abortController?.signal, + headers: { + Range: `bytes=${chunkStart}-${chunkEnd}`, + Connection: 'keep-alive', + }, + keepalive: true, + }); + + const statusCode = response.status; + + if (statusCode !== 206 && statusCode !== 200) { + throw new DownloadFailedWithUnknownError(statusCode); + } + + if (!response.body) { + throw new NoContentReceivedError(); + } + + encryptedContentStreams.push(response.body); + } + }, + async (algorithm, key, iv, fileSize) => { + fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, chunkStart); + }, + (options?.token && { token: options.token }) || undefined, + ); + } return fileStream!; } diff --git a/src/app/network/download.test.ts b/src/app/network/download.test.ts index beb0789f7..47f818bd3 100644 --- a/src/app/network/download.test.ts +++ b/src/app/network/download.test.ts @@ -20,7 +20,7 @@ describe('Multipart Download File', () => { bucketId: 'test-bucket', fileId: 'test-file', token: 'test-token', - encryptionKey: Buffer.from('0'.repeat(64), 'hex'), + key: { mnemonic: '0'.repeat(64) }, fileSize: 1024, options: { notifyProgress: vi.fn(), @@ -43,7 +43,7 @@ describe('Multipart Download File', () => { bucketId: 'test-bucket', fileId: 'test-file', token: 'test-token', - encryptionKey: Buffer.from('0'.repeat(64), 'hex'), + key: { mnemonic: '0'.repeat(64) }, fileSize: 1024, options: { notifyProgress: vi.fn(), diff --git a/src/app/network/download.ts b/src/app/network/download.ts index 8c57cdd49..dca7c961b 100644 --- a/src/app/network/download.ts +++ b/src/app/network/download.ts @@ -93,7 +93,6 @@ export interface IDownloadParams { creds?: NetworkCredentials; key?: FileKey; token?: string; - encryptionKey?: Buffer; options?: { notifyProgress: DownloadProgressCallback; abortController?: AbortController; @@ -175,9 +174,7 @@ export async function _downloadFile(params: IDownloadParams): Promise { - const { bucketId, fileId, mnemonic, fileSize, options } = params; + const { bucketId, fileId, key, fileSize, options } = params; const tasks = this.createDownloadTasks(fileSize); @@ -44,7 +45,7 @@ export class MultipartDownload { tasks, bucketId, fileId, - mnemonic, + key, fileSize, controller, options, @@ -89,12 +90,12 @@ export class MultipartDownload { tasks: DownloadChunkTask[]; bucketId: string; fileId: string; - mnemonic: string; + key: FileKey; fileSize: number; controller: ReadableStreamDefaultController; options?: DownloadOptions; }): Promise { - const { tasks, bucketId, fileId, mnemonic, fileSize, controller, options } = params; + const { tasks, bucketId, fileId, key, fileSize, controller, options } = params; this.downloadQueue = queue( async (task: DownloadChunkTask) => @@ -102,7 +103,7 @@ export class MultipartDownload { task, bucketId, fileId, - mnemonic, + key, fileSize, controller, options, @@ -182,7 +183,7 @@ export class MultipartDownload { private async downloadChunk( bucketId: string, fileId: string, - mnemonic: string, + key: FileKey, chunkStart: number, chunkEnd: number, options?: DownloadOptions, @@ -190,7 +191,7 @@ export class MultipartDownload { const chunkStream = await this.network.downloadChunk({ bucketId, fileId, - mnemonic, + key, chunkStart, chunkEnd, options, @@ -209,14 +210,14 @@ export class MultipartDownload { task: DownloadChunkTask; bucketId: string; fileId: string; - mnemonic: string; + key: FileKey; fileSize: number; controller: ReadableStreamDefaultController; options?: DownloadOptions; }): Promise { - const { task, bucketId, fileId, mnemonic, fileSize, controller, options } = params; + const { task, bucketId, fileId, key, fileSize, controller, options } = params; - const chunkData = await this.downloadChunk(bucketId, fileId, mnemonic, task.chunkStart, task.chunkEnd, options); + const chunkData = await this.downloadChunk(bucketId, fileId, key, task.chunkStart, task.chunkEnd, options); this.registerCompletedChunk(task.index, chunkData); diff --git a/src/app/network/download/v2.test.ts b/src/app/network/download/v2.test.ts index 238b5c170..7b08d3832 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -5,6 +5,7 @@ import envService from 'services/env.service'; import { Network } from '@internxt/sdk/dist/network'; import { NetworkFacade } from '../NetworkFacade'; import downloadFile from './v2'; +import { FileKey } from '../types/helper-types'; vi.mock('../../crypto/services/utils'); @@ -65,7 +66,7 @@ describe('Download V2', () => { expect(downloadFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - mnemonic: params.key.mnemonic, + key: { mnemonic: params.key.mnemonic }, fileSize: params.fileSize, options: { downloadingCallback: progressCallback, @@ -116,7 +117,9 @@ describe('Download V2', () => { expect(downloadSingleFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - mnemonic: params.key.mnemonic, + key: { + mnemonic: params.key.mnemonic, + }, chunkStart: params.chunkStart, chunkEnd: params.chunkEnd, options: { @@ -141,9 +144,8 @@ describe('Download V2', () => { const result = await downloadFile({ bucketId: 'test-bucket', fileId: 'test-file', - key: { fileEncryptionKey: 'unused' } as any, + key: { mnemonic: encryptionKey } as FileKey, token, - encryptionKey, options: { notifyProgress: progressCallback, abortController }, } as any); diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index d9b5a2bae..d8582a63e 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -16,36 +16,26 @@ interface DownloadFileParams { options?: DownloadFileOptions; } -interface DownloadOwnFileWithMnemonicParams extends DownloadFileParams { +interface DownloadOwnFile extends DownloadFileParams { creds: NetworkCredentials; - key: { mnemonic: string; bucketKey?: never }; - token?: never; - encryptionKey?: never; -} - -interface DownloadOwnFileWithBucketKeyParams extends DownloadFileParams { - creds: NetworkCredentials; - key: { bucketKey: Buffer; mnemonic?: never }; + key: FileKey; token?: never; - encryptionKey?: never; } interface DownloadSharedFileParams extends DownloadFileParams { creds?: never; key: FileKey; token: string; - encryptionKey: string; } type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => DownloadFileResponse; -type DownloadFileFunction = ( - params: DownloadSharedFileParams | DownloadOwnFileWithMnemonicParams | DownloadOwnFileWithBucketKeyParams, -) => DownloadFileResponse; +type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFile) => DownloadFileResponse; const downloadSharedFile: DownloadSharedFileFunction = (params) => { - const { bucketId, fileId, encryptionKey, token, options } = params; + console.log('CHECK: downloadSharedFile is called'); + const { bucketId, fileId, key, token, options } = params; - return new NetworkFacade( + const networkFacade = new NetworkFacade( Network.client( envService.getVariable('storjBridge'), { @@ -57,12 +47,25 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { userId: '', }, ), - ).download(bucketId, fileId, '', { - key: Buffer.from(encryptionKey, 'hex'), - token, - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); + ); + if (key.mnemonic) { + return networkFacade.download(bucketId, fileId, '', { + key: Buffer.from(key.mnemonic, 'hex'), + token, + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); + } + if (key.bucketKey) { + return networkFacade.download(bucketId, fileId, '', { + key: key.bucketKey, + token, + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); + } else { + throw new Error('DOWNLOAD ERRNO. 1'); + } }; async function getAuthFromCredentials(creds: NetworkCredentials): Promise<{ username: string; password: string }> { @@ -72,16 +75,11 @@ async function getAuthFromCredentials(creds: NetworkCredentials): Promise<{ user }; } -const downloadOwnFile = async (params: DownloadOwnFileWithMnemonicParams) => { - const { - bucketId, - fileId, - key: { mnemonic }, - options, - } = params; +const downloadOwnFile = async (params: DownloadOwnFile) => { + const { bucketId, fileId, key, options } = params; const auth = await getAuthFromCredentials(params.creds); - return new NetworkFacade( + const networkFacade = new NetworkFacade( Network.client( envService.getVariable('storjBridge'), { @@ -93,49 +91,26 @@ const downloadOwnFile = async (params: DownloadOwnFileWithMnemonicParams) => { userId: auth.password, }, ), - ).download(bucketId, fileId, mnemonic, { - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); -}; - -const downloadOwnFileWithBucketKey = async (params: DownloadOwnFileWithBucketKeyParams) => { - const { - bucketId, - fileId, - key: { bucketKey }, - options, - } = params; - const auth = await getAuthFromCredentials(params.creds); + ); - return new NetworkFacade( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ).downloadWithBucketKey(bucketId, fileId, bucketKey, { - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); + if (key.mnemonic) { + return networkFacade.download(bucketId, fileId, key.mnemonic, { + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); + } + if (key.bucketKey) { + return networkFacade.downloadWithBucketKey(bucketId, fileId, key.bucketKey, { + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); + } else { + throw new Error('DOWNLOAD ERRNO. 1'); + } }; -export async function multipartDownload( - params: DownloadOwnFileWithMnemonicParams & { fileSize: number }, -): Promise { - const { - bucketId, - fileId, - key: { mnemonic }, - fileSize, - options, - } = params; +export async function multipartDownload(params: DownloadOwnFile & { fileSize: number }): Promise { + const { bucketId, fileId, key, fileSize, options } = params; const auth = await getAuthFromCredentials(params.creds); const networkFacade = new NetworkFacade( @@ -157,7 +132,7 @@ export async function multipartDownload( return multipartDownload.downloadFile({ bucketId, fileId, - mnemonic, + key, fileSize, options: { downloadingCallback: options?.notifyProgress, @@ -167,19 +142,12 @@ export async function multipartDownload( } export async function downloadChunkFile( - params: DownloadOwnFileWithMnemonicParams & { chunkStart: number; chunkEnd: number }, + params: DownloadOwnFile & { chunkStart: number; chunkEnd: number }, ): Promise { - const { - bucketId, - fileId, - key: { mnemonic }, - chunkStart, - chunkEnd, - options, - } = params; + const { bucketId, fileId, key, chunkStart, chunkEnd, options } = params; const auth = await getAuthFromCredentials(params.creds); - return new NetworkFacade( + const networkFacade = new NetworkFacade( Network.client( envService.getVariable('storjBridge'), { @@ -191,10 +159,12 @@ export async function downloadChunkFile( userId: auth.password, }, ), - ).downloadChunk({ + ); + + return networkFacade.downloadChunk({ bucketId, fileId, - mnemonic, + key, chunkStart, chunkEnd, options: { @@ -205,12 +175,12 @@ export async function downloadChunkFile( } const downloadFile: DownloadFileFunction = (params) => { - if (params.token && params.encryptionKey) { - return downloadSharedFile(params); - } else if (params.creds && params.key.mnemonic) { - return downloadOwnFile(params as DownloadOwnFileWithMnemonicParams); - } else if (params.creds && params.key.bucketKey) { - return downloadOwnFileWithBucketKey(params as DownloadOwnFileWithBucketKeyParams); + if (params.token) { + console.log('CHECK: downloadSharedFile called with params:', params); + return downloadSharedFile(params as DownloadSharedFileParams); + } else if (params.creds) { + console.log('CHECK: downloadOwnFile called with params:', params); + return downloadOwnFile(params as DownloadOwnFile); } else { throw new Error('DOWNLOAD ERRNO. 0'); } diff --git a/src/app/network/types/index.ts b/src/app/network/types/index.ts index 6ee586af9..95ec5a6e5 100644 --- a/src/app/network/types/index.ts +++ b/src/app/network/types/index.ts @@ -1,4 +1,5 @@ import { DownloadProgressCallback } from '../download'; +import { FileKey } from './helper-types'; export interface LegacyShardMeta { hash: string; @@ -28,7 +29,7 @@ export interface DownloadChunkTask { export interface DownloadChunkPayload { bucketId: string; fileId: string; - mnemonic: string; + key: FileKey; chunkStart: number; chunkEnd: number; options?: DownloadOptions; diff --git a/src/app/share/types/index.ts b/src/app/share/types/index.ts index fffb628c7..d6b48f876 100644 --- a/src/app/share/types/index.ts +++ b/src/app/share/types/index.ts @@ -1,5 +1,5 @@ import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; -import { NetworkCredentials } from 'app/network/types/helper-types'; +import { FileKey, NetworkCredentials } from 'app/network/types/helper-types'; import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; export type AdvancedSharedItem = SharedFolders & @@ -21,7 +21,7 @@ export type SharedNetworkCredentials = { export type PreviewFileItem = DriveFileData & { credentials?: NetworkCredentials; - mnemonic?: string; + key?: FileKey; folderUuid: string; }; diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index 5fb0cc80e..b23cc734f 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -14,7 +14,7 @@ import userService from 'services/user.service'; import { encryptBucketKey } from 'app/share/services/share.crypto'; export const HYBRID_ALGORITHM = 'hybrid'; -export const HYBRID_ALGORITHM_WITH_BUCKET_KEY = 'hybrid-v2'; +export const HYBRID_ALGORITHM_WITH_BUCKET_KEY = 'hybrid'; export const STANDARD_ALGORITHM = 'ed25519'; export interface ShareLinksState { diff --git a/src/views/PublicShared/ShareFileView.tsx b/src/views/PublicShared/ShareFileView.tsx index 547672949..0e9f6ecb3 100644 --- a/src/views/PublicShared/ShareFileView.tsx +++ b/src/views/PublicShared/ShareFileView.tsx @@ -18,7 +18,6 @@ import { CheckIcon, DownloadSimpleIcon, EyeIcon } from '@phosphor-icons/react'; import downloadService from 'app/drive/services/download.service'; import './components/ShareView.scss'; -import { ShareTypes } from '@internxt/sdk/dist/drive'; import { PublicSharedItemInfo, SharingMeta } from '@internxt/sdk/dist/drive/share/types'; import errorService from 'services/error.service'; import { binaryStreamToBlob } from 'services/stream.service'; @@ -30,6 +29,8 @@ import { stringUtils } from '@internxt/lib'; import { SendBanner, ShareItemPwdView } from './components'; import useBeforeUnload from 'hooks/useBeforeUnload'; import { isFileSizePreviewable } from 'services'; +import { HYBRID_ALGORITHM_WITH_BUCKET_KEY } from 'app/store/slices/sharedLinks'; +import { IDownloadParams } from 'app/network/download'; export interface ShareViewProps extends ShareViewState { match: match<{ @@ -162,16 +163,24 @@ export default function ShareFileView(props: Readonly): JSX.Elem } }; - function getBlob(abortController: AbortController): Promise { - const fileInfo = info as unknown as ShareTypes.ShareLink; + const getSharedFileKeyParams = (fileInfo: SharingMeta): Pick => { + if (fileInfo.encryptionAlgorithm === HYBRID_ALGORITHM_WITH_BUCKET_KEY) { + console.log('CHECK: Using bucket key for download'); + return { key: { bucketKey: Buffer.from(fileInfo.encryptionKey, 'hex') } }; + } + + console.log('CHECK: Using encryption key for download'); + return { key: { mnemonic: fileInfo.encryptionKey } }; + }; - const encryptionKey = fileInfo.encryptionKey; + function getBlob(abortController: AbortController): Promise { + const fileInfo = info; const readable = network.downloadFile({ bucketId: fileInfo.item.bucket, fileId: fileInfo.item?.fileId, - encryptionKey: Buffer.from(encryptionKey, 'hex'), token: fileInfo.itemToken, + ...getSharedFileKeyParams(fileInfo as SharingMeta), options: { abortController, notifyProgress: (totalBytes, downloadedBytes) => { @@ -195,14 +204,12 @@ export default function ShareFileView(props: Readonly): JSX.Elem const MIN_PROGRESS = 0; if (fileInfo) { - const encryptionKey = fileInfo.encryptionKey; - setProgress(MIN_PROGRESS); setIsDownloading(true); const readable = await network.downloadFile({ bucketId: fileInfo.item.bucket, fileId: fileInfo.item.fileId, - encryptionKey: Buffer.from(encryptionKey, 'hex'), + ...getSharedFileKeyParams(fileInfo as SharingMeta), token: fileInfo.itemToken, options: { notifyProgress: (totalProgress, downloadedBytes) => { diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index d84a5cd37..d90f73118 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -357,7 +357,7 @@ function SharedView({ const mnemonic = selectedWorkspace?.workspaceUser.key ?? (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : clickedShareItemEncryptionKey)); - handleOpenItemPreview(true, { ...previewItem, mnemonic }); + handleOpenItemPreview(true, { ...previewItem, key: { mnemonic } }); } catch (err) { const error = errorService.castError(err); errorService.reportError(error); diff --git a/src/views/Shared/components/SharedItemListContainer.tsx b/src/views/Shared/components/SharedItemListContainer.tsx index 6311d4d77..db6582dcf 100644 --- a/src/views/Shared/components/SharedItemListContainer.tsx +++ b/src/views/Shared/components/SharedItemListContainer.tsx @@ -98,7 +98,9 @@ const SharedItemListContainer = ({ const downloadItem = async (shareItem: AdvancedSharedItem): Promise => { try { + console.log('CHECK: downloadItem is called'); if (shareItem.isRootLink) { + console.log('CHECK: downloadItem with if'); const encryptionKey = selectedWorkspace?.workspaceUser?.key ?? (await decryptMnemonic(shareItem.encryptionKey)); await shareService.downloadSharedFiles({ creds: { @@ -113,6 +115,7 @@ const SharedItemListContainer = ({ workspaceCredentials, }); } else { + console.log('CHECK: downloadItem with else'); const pageItemsNumber = 5; let sharedToken; if (workspaceCredentials && workspaceId) { @@ -179,7 +182,7 @@ const SharedItemListContainer = ({ const mnemonic = selectedWorkspace?.workspaceUser.key ?? (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : sharedItemEncryptionKey)); - onOpenItemPreview({ ...previewItem, mnemonic }); + onOpenItemPreview({ ...previewItem, key: { mnemonic } }); } catch (err) { const error = errorService.castError(err); errorService.reportError(error); diff --git a/src/views/Shared/hooks/useFetchSharedData.tsx b/src/views/Shared/hooks/useFetchSharedData.tsx index cdc54318c..8c22e8836 100644 --- a/src/views/Shared/hooks/useFetchSharedData.tsx +++ b/src/views/Shared/hooks/useFetchSharedData.tsx @@ -41,6 +41,7 @@ const parseSharedFolderResponseItems = ({ isFromRootFolder: boolean; }) => { return sharedFolderResponseItems.map((file) => { + console.log('CHECK: enc version', file.encryptVersion); const shareItem = file as AdvancedSharedItem; shareItem.isFolder = isFolder; shareItem.isRootLink = isFromRootFolder; From 010fd9564fc7e4cd233b0987cb6c6528bff57556 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 10:45:00 +0200 Subject: [PATCH 06/22] pass key in download shared files --- src/app/crypto/services/pgp.service.ts | 4 ++++ src/app/drive/services/downloadManager.service.ts | 14 ++++++++------ src/app/network/download/v2.test.ts | 3 +-- src/app/network/download/v2.ts | 6 ++---- src/app/share/services/share.crypto.ts | 12 ++++++++++++ src/app/share/services/share.service.ts | 8 +++----- src/app/share/types/index.ts | 1 + .../Shared/components/SharedItemListContainer.tsx | 14 ++++++++++---- 8 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/app/crypto/services/pgp.service.ts b/src/app/crypto/services/pgp.service.ts index c21b7614a..ebf7597ad 100644 --- a/src/app/crypto/services/pgp.service.ts +++ b/src/app/crypto/services/pgp.service.ts @@ -316,3 +316,7 @@ export const decryptBucketKeyHybrid = async ({ return result; }; + +export function isBucketKeyCiphertext(encryptedMessageInBase64: string): boolean { + return encryptedMessageInBase64.split('$')[0] === WORDS_HYBRID_BUCKET_KEY_IN_BASE64; +} diff --git a/src/app/drive/services/downloadManager.service.ts b/src/app/drive/services/downloadManager.service.ts index 2fdf68afb..c25318c15 100644 --- a/src/app/drive/services/downloadManager.service.ts +++ b/src/app/drive/services/downloadManager.service.ts @@ -337,6 +337,12 @@ export class DownloadManagerService { const failedItems: DownloadItemType[] = []; let downloadedProgress = 0; + const resolveItemKey = (driveItem: DownloadItemType): FileKey => { + const shareCredentials = (driveItem as AdvancedSharedItem).credentials; + if (shareCredentials?.bucketKey) return { bucketKey: shareCredentials.bucketKey }; + return { mnemonic: shareCredentials?.mnemonic ?? credentials.key.mnemonic }; + }; + items.forEach((_, index) => { downloadProgress[index] = 0; lastReportedBytes[index] = 0; @@ -391,9 +397,7 @@ export class DownloadManagerService { user: (driveItem as AdvancedSharedItem).credentials?.networkUser ?? credentials.credentials.user, pass: (driveItem as AdvancedSharedItem).credentials?.networkPass ?? credentials.credentials.pass, }, - key: { - mnemonic: (driveItem as AdvancedSharedItem).credentials?.mnemonic ?? credentials.key.mnemonic, - }, + key: resolveItemKey(driveItem), options: { abortController, notifyProgress: notifyProgressCallback, @@ -431,9 +435,7 @@ export class DownloadManagerService { user: (driveItem as AdvancedSharedItem).credentials?.networkUser ?? credentials.credentials.user, pass: (driveItem as AdvancedSharedItem).credentials?.networkPass ?? credentials.credentials.pass, }, - key: { - mnemonic: (driveItem as AdvancedSharedItem).credentials?.mnemonic ?? credentials.key.mnemonic, - }, + key: resolveItemKey(driveItem), workspaceId: credentials.workspaceId, }, abortController, diff --git a/src/app/network/download/v2.test.ts b/src/app/network/download/v2.test.ts index 7b08d3832..ed11c5d57 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -154,8 +154,7 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: '', userId: '' }, ); - expect(downloadSpy).toHaveBeenCalledWith('test-bucket', 'test-file', '', { - key: Buffer.from(encryptionKey, 'hex'), + expect(downloadSpy).toHaveBeenCalledWith('test-bucket', 'test-file', encryptionKey, { token, downloadingCallback: progressCallback, abortController, diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index d8582a63e..2aa164c39 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -49,16 +49,14 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { ), ); if (key.mnemonic) { - return networkFacade.download(bucketId, fileId, '', { - key: Buffer.from(key.mnemonic, 'hex'), + return networkFacade.download(bucketId, fileId, key.mnemonic, { token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, }); } if (key.bucketKey) { - return networkFacade.download(bucketId, fileId, '', { - key: key.bucketKey, + return networkFacade.downloadWithBucketKey(bucketId, fileId, key.bucketKey, { token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts index 503bf8115..6c1d5a990 100644 --- a/src/app/share/services/share.crypto.ts +++ b/src/app/share/services/share.crypto.ts @@ -3,12 +3,14 @@ import { 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 = encryptedStorageService.getUser(); @@ -88,3 +90,13 @@ export const decryptBucketKey = 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.ts b/src/app/share/services/share.service.ts index 6d1a24d8e..21949b5d5 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -595,7 +595,7 @@ class DirectoryPublicSharedFilesIterator implements Iterator { export async function downloadSharedFiles({ creds, - decryptedEncryptionKey, + key, selectedItems, token, teamId, @@ -603,7 +603,7 @@ export async function downloadSharedFiles({ workspaceCredentials, }: { creds: { user: string; pass: string }; - decryptedEncryptionKey: string; + key: FileKey; selectedItems: AdvancedSharedItem[]; token?: string; teamId?: string; @@ -612,9 +612,7 @@ export async function downloadSharedFiles({ }): Promise { const sharingCredentials = { credentials: { ...creds }, - key: { - mnemonic: decryptedEncryptionKey, - }, + key, }; if (selectedItems.length === 1 && !selectedItems[0].isFolder) { diff --git a/src/app/share/types/index.ts b/src/app/share/types/index.ts index d6b48f876..ed4fd711b 100644 --- a/src/app/share/types/index.ts +++ b/src/app/share/types/index.ts @@ -17,6 +17,7 @@ export type SharedNetworkCredentials = { networkUser: string; networkPass: string; mnemonic?: string; + bucketKey?: Buffer; }; export type PreviewFileItem = DriveFileData & { diff --git a/src/views/Shared/components/SharedItemListContainer.tsx b/src/views/Shared/components/SharedItemListContainer.tsx index db6582dcf..d5ad4181e 100644 --- a/src/views/Shared/components/SharedItemListContainer.tsx +++ b/src/views/Shared/components/SharedItemListContainer.tsx @@ -19,6 +19,8 @@ import { useShareViewContext } from '../context/SharedViewContextProvider'; import useSharedContextMenu from '../hooks/useSharedContextMenu'; import { isItemsOwnedByCurrentUser, sortSharedItems } from '../utils/sharedViewUtils'; import encryptedStorageService from 'services/encrypted-storage.service'; +import { FileKey } from 'app/network/types/helper-types'; +import { decryptSharingKey } from 'app/share/services/share.crypto'; type ShareItemListContainerProps = { disableKeyboardShortcuts: boolean; @@ -101,13 +103,15 @@ const SharedItemListContainer = ({ console.log('CHECK: downloadItem is called'); if (shareItem.isRootLink) { console.log('CHECK: downloadItem with if'); - const encryptionKey = selectedWorkspace?.workspaceUser?.key ?? (await decryptMnemonic(shareItem.encryptionKey)); + const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key + ? { mnemonic: selectedWorkspace.workspaceUser.key } + : await decryptSharingKey(shareItem.encryptionKey); await shareService.downloadSharedFiles({ creds: { user: shareItem.credentials.networkUser, pass: shareItem.credentials.networkPass, }, - decryptedEncryptionKey: encryptionKey as string, + key: key as FileKey, selectedItems, token: undefined, teamId: defaultTeamId, @@ -139,13 +143,15 @@ const SharedItemListContainer = ({ ); sharedToken = token; } - const encryptionKey = selectedWorkspace?.workspaceUser?.key ?? (await decryptMnemonic(sharedItemEncryptionKey)); + const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key + ? { mnemonic: selectedWorkspace.workspaceUser.key } + : await decryptSharingKey(sharedItemEncryptionKey); await shareService.downloadSharedFiles({ creds: { user: shareItem.credentials.networkUser, pass: shareItem.credentials.networkPass, }, - decryptedEncryptionKey: encryptionKey as string, + key: key as FileKey, selectedItems, token: sharedToken, teamId: defaultTeamId, From c5b038ee77e65c7a8b13399fab88d941b6bada1e Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 11:17:20 +0200 Subject: [PATCH 07/22] remove logs, fix getSharedFileKeyParams --- .../drive/services/video-streaming.service/index.ts | 1 - src/app/network/download/v2.ts | 3 --- src/app/share/services/share.service.ts | 6 ++++-- src/views/PublicShared/ShareFileView.tsx | 6 ++---- src/views/Shared/SharedView.tsx | 2 +- .../Shared/components/SharedItemListContainer.tsx | 13 +++++-------- src/views/Shared/hooks/useFetchSharedData.tsx | 1 - 7 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/app/drive/services/video-streaming.service/index.ts b/src/app/drive/services/video-streaming.service/index.ts index 1eee1c3ae..31041c93e 100644 --- a/src/app/drive/services/video-streaming.service/index.ts +++ b/src/app/drive/services/video-streaming.service/index.ts @@ -50,7 +50,6 @@ export class VideoStreamingService { private async downloadChunk(start: number, end: number, cacheKey: string): Promise { try { - console.log('CHECK: Downloading chunk with mnemonic'); const stream = await downloadChunkFile({ bucketId: this.config.bucketId, fileId: this.config.fileId, diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index 2aa164c39..2b26609be 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -32,7 +32,6 @@ type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => Download type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFile) => DownloadFileResponse; const downloadSharedFile: DownloadSharedFileFunction = (params) => { - console.log('CHECK: downloadSharedFile is called'); const { bucketId, fileId, key, token, options } = params; const networkFacade = new NetworkFacade( @@ -174,10 +173,8 @@ export async function downloadChunkFile( const downloadFile: DownloadFileFunction = (params) => { if (params.token) { - console.log('CHECK: downloadSharedFile called with params:', params); return downloadSharedFile(params as DownloadSharedFileParams); } else if (params.creds) { - console.log('CHECK: downloadOwnFile called with params:', params); return downloadOwnFile(params as DownloadOwnFile); } else { throw new Error('DOWNLOAD ERRNO. 0'); diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index 21949b5d5..f901698b2 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -41,7 +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'; +import { decryptMnemonic, decryptSharingKey } from './share.crypto'; interface CreateShareResponse { created: boolean; @@ -648,11 +648,13 @@ export async function downloadSharedFiles({ for (const selectedItem of selectedItems) { const item = selectedItem; + const itemKey = await decryptSharingKey(item.encryptionKey); payload.push({ ...item, credentials: { ...item.credentials, - mnemonic: await decryptMnemonic(item.encryptionKey), + mnemonic: itemKey?.mnemonic, + bucketKey: itemKey?.bucketKey, }, }); } diff --git a/src/views/PublicShared/ShareFileView.tsx b/src/views/PublicShared/ShareFileView.tsx index 0e9f6ecb3..83cc12dea 100644 --- a/src/views/PublicShared/ShareFileView.tsx +++ b/src/views/PublicShared/ShareFileView.tsx @@ -29,8 +29,8 @@ import { stringUtils } from '@internxt/lib'; import { SendBanner, ShareItemPwdView } from './components'; import useBeforeUnload from 'hooks/useBeforeUnload'; import { isFileSizePreviewable } from 'services'; -import { HYBRID_ALGORITHM_WITH_BUCKET_KEY } from 'app/store/slices/sharedLinks'; import { IDownloadParams } from 'app/network/download'; +import { isBucketKeyCiphertext } from 'app/crypto/services/pgp.service'; export interface ShareViewProps extends ShareViewState { match: match<{ @@ -164,12 +164,10 @@ export default function ShareFileView(props: Readonly): JSX.Elem }; const getSharedFileKeyParams = (fileInfo: SharingMeta): Pick => { - if (fileInfo.encryptionAlgorithm === HYBRID_ALGORITHM_WITH_BUCKET_KEY) { - console.log('CHECK: Using bucket key for download'); + if (isBucketKeyCiphertext(fileInfo.encryptionKey)) { return { key: { bucketKey: Buffer.from(fileInfo.encryptionKey, 'hex') } }; } - console.log('CHECK: Using encryption key for download'); return { key: { mnemonic: fileInfo.encryptionKey } }; }; diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index d90f73118..f23e707df 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -7,7 +7,7 @@ import { DriveItemData } from '../../app/drive/types'; import { useTranslationContext } from '../../app/i18n/provider/TranslationProvider'; import notificationsService, { ToastType } from '../../app/notifications/services/notifications.service'; import shareService from '../../app/share/services/share.service'; -import { decryptMnemonic } from '../../app/share/services/share.crypto'; +import { decryptMnemonic, decryptSharingKey } 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 d5ad4181e..c4b8b3d16 100644 --- a/src/views/Shared/components/SharedItemListContainer.tsx +++ b/src/views/Shared/components/SharedItemListContainer.tsx @@ -13,7 +13,6 @@ import { OrderDirection } from '../../../app/core/types'; import { sharedThunks } from '../../../app/store/slices/sharedLinks'; import workspacesSelectors from '../../../app/store/slices/workspaces/workspaces.selectors'; 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'; @@ -100,9 +99,7 @@ const SharedItemListContainer = ({ const downloadItem = async (shareItem: AdvancedSharedItem): Promise => { try { - console.log('CHECK: downloadItem is called'); if (shareItem.isRootLink) { - console.log('CHECK: downloadItem with if'); const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key ? { mnemonic: selectedWorkspace.workspaceUser.key } : await decryptSharingKey(shareItem.encryptionKey); @@ -119,7 +116,6 @@ const SharedItemListContainer = ({ workspaceCredentials, }); } else { - console.log('CHECK: downloadItem with else'); const pageItemsNumber = 5; let sharedToken; if (workspaceCredentials && workspaceId) { @@ -185,10 +181,11 @@ const SharedItemListContainer = ({ }; try { - const mnemonic = - selectedWorkspace?.workspaceUser.key ?? - (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : sharedItemEncryptionKey)); - onOpenItemPreview({ ...previewItem, key: { mnemonic } }); + const encryptionKey = shareItem.encryptionKey ? shareItem.encryptionKey : sharedItemEncryptionKey; + const key: FileKey | undefined = selectedWorkspace?.workspaceUser.key + ? { mnemonic: selectedWorkspace.workspaceUser.key } + : await decryptSharingKey(encryptionKey); + onOpenItemPreview({ ...previewItem, key }); } catch (err) { const error = errorService.castError(err); errorService.reportError(error); diff --git a/src/views/Shared/hooks/useFetchSharedData.tsx b/src/views/Shared/hooks/useFetchSharedData.tsx index 8c22e8836..cdc54318c 100644 --- a/src/views/Shared/hooks/useFetchSharedData.tsx +++ b/src/views/Shared/hooks/useFetchSharedData.tsx @@ -41,7 +41,6 @@ const parseSharedFolderResponseItems = ({ isFromRootFolder: boolean; }) => { return sharedFolderResponseItems.map((file) => { - console.log('CHECK: enc version', file.encryptVersion); const shareItem = file as AdvancedSharedItem; shareItem.isFolder = isFolder; shareItem.isRootLink = isFromRootFolder; From e3f719ed5fb0846276e0982ab5430336c38bc22a Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 11:47:29 +0200 Subject: [PATCH 08/22] fix file preview --- src/app/drive/services/download.service/fetchFileBlob.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/drive/services/download.service/fetchFileBlob.ts b/src/app/drive/services/download.service/fetchFileBlob.ts index 6df365657..ff0984bde 100644 --- a/src/app/drive/services/download.service/fetchFileBlob.ts +++ b/src/app/drive/services/download.service/fetchFileBlob.ts @@ -16,7 +16,7 @@ export default async function fetchFileBlob( credentials?: NetworkCredentials, key?: FileKey, ): Promise { - const { bridgeUser, bridgePass } = await getEnvironmentConfig(!!options.isWorkspace); + const { bridgeUser, bridgePass, encryptionKey } = await getEnvironmentConfig(!!options.isWorkspace); const creds = credentials ? credentials : { pass: bridgePass, user: bridgeUser }; @@ -24,7 +24,7 @@ export default async function fetchFileBlob( bucketId: item.bucketId, fileId: item.fileId, creds, - key, + key: key ?? { mnemonic: encryptionKey }, options: { notifyProgress: (totalBytes, downloadedBytes) => { options.updateProgressCallback(downloadedBytes / totalBytes); From 8dc8500dbd9dd6bc366ce4ac9f4928e7635aa1a4 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 12:02:32 +0200 Subject: [PATCH 09/22] fix sonar complains --- src/app/crypto/services/pgp.service.ts | 2 +- .../services/video-streaming.service/index.ts | 1 - src/app/share/services/share.service.test.ts | 71 ++-- .../store/slices/sharedLinks/index.test.ts | 73 ++-- src/views/Shared/SharedView.tsx | 2 +- .../Signup/ShareGuestSignUpView.test.tsx | 389 +++++++++--------- 6 files changed, 268 insertions(+), 270 deletions(-) diff --git a/src/app/crypto/services/pgp.service.ts b/src/app/crypto/services/pgp.service.ts index ebf7597ad..d539ac8d0 100644 --- a/src/app/crypto/services/pgp.service.ts +++ b/src/app/crypto/services/pgp.service.ts @@ -281,7 +281,7 @@ export const decryptBucketKeyHybrid = async ({ if (isHybridMode) { if (!privateKyberKeyInBase64) { - return Promise.reject(new Error('Attempted to decrypt hybrid ciphertex without Kyber key')); + throw new Error('Attempted to decrypt hybrid ciphertex without Kyber key'); } const kem = await kemBuilder(); diff --git a/src/app/drive/services/video-streaming.service/index.ts b/src/app/drive/services/video-streaming.service/index.ts index 31041c93e..7235b924c 100644 --- a/src/app/drive/services/video-streaming.service/index.ts +++ b/src/app/drive/services/video-streaming.service/index.ts @@ -2,7 +2,6 @@ import { downloadChunkFile } from 'app/network/download/v2'; import { binaryStreamToUint8Array } from 'services'; import { VideoSessionDestroyedError } from '../errors/video-streaming.errors'; import { VideoStreamingSessionConfig } from './VideoStreamingSession'; -import { FileKey } from 'app/network/types/helper-types'; export interface VideoStreamSession { fileSize: number; diff --git a/src/app/share/services/share.service.test.ts b/src/app/share/services/share.service.test.ts index 7e63e8808..7591fafe1 100644 --- a/src/app/share/services/share.service.test.ts +++ b/src/app/share/services/share.service.test.ts @@ -66,44 +66,45 @@ vi.mock('services/zip.service', () => ({ createFolderWithFilesWritable: vi.fn(), })); +vi.mock('app/drive/services/folder.service', () => ({ + default: {}, + downloadFolderAsZip: vi.fn(), + createFilesIterator: vi.fn(), + createFoldersIterator: vi.fn(), + checkIfCachedSourceIsOlder: vi.fn(), +})); +vi.mock('../../core/factory/sdk', () => ({ + SdkFactory: { + getNewApiInstance: vi.fn(() => ({ + createShareClient: vi.fn(), + })), + }, +})); +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(), + }, +})); +vi.mock('services/workspace.service', () => ({ + default: { + getAllWorkspaceTeamSharedFolderFolders: vi.fn(), + getAllWorkspaceTeamSharedFolderFiles: vi.fn(), + }, +})); +vi.mock('./DomainManager', () => ({ domainManager: { getDomainsList: vi.fn() } })); + describe('Encryption and Decryption', () => { beforeAll(() => { globalThis.Buffer = Buffer; - vi.mock('app/drive/services/folder.service', () => ({ - default: {}, - downloadFolderAsZip: vi.fn(), - createFilesIterator: vi.fn(), - createFoldersIterator: vi.fn(), - checkIfCachedSourceIsOlder: vi.fn(), - })); - vi.mock('../../core/factory/sdk', () => ({ - SdkFactory: { - getNewApiInstance: vi.fn(() => ({ - createShareClient: vi.fn(), - })), - }, - })); - 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(), - }, - })); - vi.mock('services/workspace.service', () => ({ - default: { - getAllWorkspaceTeamSharedFolderFolders: vi.fn(), - getAllWorkspaceTeamSharedFolderFiles: vi.fn(), - }, - })); - vi.mock('./DomainManager', () => ({ domainManager: { getDomainsList: vi.fn() } })); }); beforeEach(() => { diff --git a/src/app/store/slices/sharedLinks/index.test.ts b/src/app/store/slices/sharedLinks/index.test.ts index f5458f50e..165ac4be5 100644 --- a/src/app/store/slices/sharedLinks/index.test.ts +++ b/src/app/store/slices/sharedLinks/index.test.ts @@ -5,7 +5,7 @@ import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import navigationService from 'services/navigation.service'; import shareService from 'app/share/services/share.service'; import { Buffer } from 'buffer'; -import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import { RootState } from '../..'; import userService from 'services/user.service'; import { decryptBucketKeyHybrid, generateNewKeys } from '../../../crypto/services/pgp.service'; @@ -21,44 +21,41 @@ import notificationsService from 'app/notifications/services/notifications.servi import { generateFileBucketKey } from 'app/network/crypto'; const { shareItemWithUser } = sharedThunks; +vi.mock('services/navigation.service', () => ({ + default: { push: vi.fn() }, +})); +vi.mock('app/share/services/share.service', () => ({ + default: { + inviteUserToSharedFolder: vi.fn(), + getSharedFolderInvitationsAsInvitedUser: vi.fn(), + getSharingRoles: vi.fn(), + stopSharingItem: vi.fn(), + removeUserRole: vi.fn(), + }, +})); +vi.mock('services/user.service', () => ({ + default: { + getPublicKeyWithPrecreation: vi.fn(), + }, +})); +vi.mock('utils', () => ({ + generateCaptchaToken: vi.fn().mockResolvedValue('mock-captcha-token'), +})); + +vi.mock('services/referral.service', () => ({ + default: { + trackShareCreated: vi.fn(), + }, +})); +vi.mock('services/error.service', () => ({ + default: { + castError: vi + .fn() + .mockImplementation((e) => ({ message: e.message || 'Default error message', requestId: 'test-request-id' })), + reportError: vi.fn(), + }, +})); describe('Encryption and Decryption', async () => { - beforeAll(() => { - vi.mock('services/navigation.service', () => ({ - default: { push: vi.fn() }, - })); - vi.mock('app/share/services/share.service', () => ({ - default: { - inviteUserToSharedFolder: vi.fn(), - getSharedFolderInvitationsAsInvitedUser: vi.fn(), - getSharingRoles: vi.fn(), - stopSharingItem: vi.fn(), - removeUserRole: vi.fn(), - }, - })); - vi.mock('services/user.service', () => ({ - default: { - getPublicKeyWithPrecreation: vi.fn(), - }, - })); - vi.mock('utils', () => ({ - generateCaptchaToken: vi.fn().mockResolvedValue('mock-captcha-token'), - })); - - vi.mock('services/referral.service', () => ({ - default: { - trackShareCreated: vi.fn(), - }, - })); - vi.mock('services/error.service', () => ({ - default: { - castError: vi - .fn() - .mockImplementation((e) => ({ message: e.message || 'Default error message', requestId: 'test-request-id' })), - reportError: vi.fn(), - }, - })); - }); - beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index f23e707df..d90f73118 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -7,7 +7,7 @@ import { DriveItemData } from '../../app/drive/types'; import { useTranslationContext } from '../../app/i18n/provider/TranslationProvider'; import notificationsService, { ToastType } from '../../app/notifications/services/notifications.service'; import shareService from '../../app/share/services/share.service'; -import { decryptMnemonic, decryptSharingKey } from '../../app/share/services/share.crypto'; +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/Signup/ShareGuestSignUpView.test.tsx b/src/views/Signup/ShareGuestSignUpView.test.tsx index f00301263..d09b2b48f 100644 --- a/src/views/Signup/ShareGuestSignUpView.test.tsx +++ b/src/views/Signup/ShareGuestSignUpView.test.tsx @@ -16,209 +16,210 @@ const mockHostname = 'hostname'; const mockPassword = 'mock-password'; const mockEmal = 'mock@email.com'; const mockToken = 'mock-token'; -describe('onSubmit', () => { - beforeAll(() => { - globalThis.Buffer = Buffer; - - vi.spyOn(globalThis, 'decodeURIComponent').mockImplementation((value) => { - return value; - }); - - vi.mock('services/local-storage.service', () => ({ - default: { - get: vi.fn(), - clear: vi.fn(), - set: vi.fn(), - }, - })); - vi.mock('services/encrypted-storage.service', () => ({ - default: { - getToken: vi.fn(), - setToken: vi.fn(), - getUser: vi.fn(), - }, - })); - - vi.mock('react-helmet-async', () => ({ - Helmet: vi.fn(), - })); - - vi.mock('@phosphor-icons/react', () => ({ - Info: () =>
Mocked Info Icon
, - WarningCircle: () =>
Mocked Warning Circle Icon
, - CheckCircle: () =>
Mocked Check Circle Icon
, - Eye: () =>
Mocked Eye Icon
, - EyeSlash: () =>
Mocked Eye Slash Icon
, - MagnifyingGlass: () =>
Mocked Magnifyin Glass Icon
, - Warning: () =>
Mocked Warning Icon
, - WarningOctagon: () =>
Mocked Warning Octagon Icon
, - X: () =>
Mocked X Icon
, - })); - - vi.mock('components/PasswordInput', () => { - return { - __esModule: true, - default: vi.fn(({ register, ...props }) => ( - - )), - }; - }); - vi.mock('./components/SignupForm', () => ({ - Views: vi.fn(), - })); - - vi.mock('./hooks/useSignup', () => ({ - useSignUp: vi.fn().mockReturnValue({ doRegisterPreCreatedUser: vi.fn() }), - })); - - vi.mock('./hooks/useGuestSignupState', () => ({ - useGuestSignupState: vi.fn(() => ({ - isValidPassword: true, - setIsValidPassword: vi.fn(), - signupError: undefined, - setSignupError: vi.fn(), - showError: false, - setShowError: vi.fn(), - isLoading: false, - setIsLoading: vi.fn(), - passwordState: { tag: 'success', label: 'Password is strong' } as const, - setPasswordState: vi.fn(), - invitationId: 'test-invitation', - setInvitationId: vi.fn(), - showPasswordIndicator: true, - setShowPasswordIndicator: vi.fn(), - user: null, - mnemonic: null, - })), - })); - - vi.mock('./hooks/useInvitationValidation', () => ({ - useInvitationValidation: vi.fn().mockReturnValue({ - invitationValidation: { isLoading: false, isValid: true }, - }), - })); - - vi.mock('components/PasswordStrengthIndicator', () => ({ - default: () =>
Mocked Password Strength Indicator
, - })); +vi.spyOn(globalThis, 'decodeURIComponent').mockImplementation((value) => { + return value; +}); - vi.mock('services/error.service', () => ({ - default: { - castError: vi.fn().mockImplementation((e) => ({ message: e.message || 'Default error message' })), - reportError: vi.fn(), - }, - })); +vi.mock('services/local-storage.service', () => ({ + default: { + get: vi.fn(), + clear: vi.fn(), + set: vi.fn(), + }, +})); +vi.mock('services/encrypted-storage.service', () => ({ + default: { + getToken: vi.fn(), + setToken: vi.fn(), + getUser: vi.fn(), + }, +})); + +vi.mock('react-helmet-async', () => ({ + Helmet: vi.fn(), +})); + +vi.mock('@phosphor-icons/react', () => ({ + Info: () =>
Mocked Info Icon
, + WarningCircle: () =>
Mocked Warning Circle Icon
, + CheckCircle: () =>
Mocked Check Circle Icon
, + Eye: () =>
Mocked Eye Icon
, + EyeSlash: () =>
Mocked Eye Slash Icon
, + MagnifyingGlass: () =>
Mocked Magnifyin Glass Icon
, + Warning: () =>
Mocked Warning Icon
, + WarningOctagon: () =>
Mocked Warning Octagon Icon
, + X: () =>
Mocked X Icon
, +})); + +vi.mock('components/PasswordInput', () => { + return { + __esModule: true, + default: vi.fn(({ register, ...props }) => ( + + )), + }; +}); - vi.mock('app/share/services/share.service', () => ({ - default: { - validateSharingInvitation: vi.fn(), +vi.mock('./components/SignupForm', () => ({ + Views: vi.fn(), +})); + +vi.mock('./hooks/useSignup', () => ({ + useSignUp: vi.fn().mockReturnValue({ doRegisterPreCreatedUser: vi.fn() }), +})); + +vi.mock('./hooks/useGuestSignupState', () => ({ + useGuestSignupState: vi.fn(() => ({ + isValidPassword: true, + setIsValidPassword: vi.fn(), + signupError: undefined, + setSignupError: vi.fn(), + showError: false, + setShowError: vi.fn(), + isLoading: false, + setIsLoading: vi.fn(), + passwordState: { tag: 'success', label: 'Password is strong' } as const, + setPasswordState: vi.fn(), + invitationId: 'test-invitation', + setInvitationId: vi.fn(), + showPasswordIndicator: true, + setShowPasswordIndicator: vi.fn(), + user: null, + mnemonic: null, + })), +})); + +vi.mock('./hooks/useInvitationValidation', () => ({ + useInvitationValidation: vi.fn().mockReturnValue({ + invitationValidation: { isLoading: false, isValid: true }, + }), +})); + +vi.mock('components/PasswordStrengthIndicator', () => ({ + default: () =>
Mocked Password Strength Indicator
, +})); + +vi.mock('services/error.service', () => ({ + default: { + castError: vi.fn().mockImplementation((e) => ({ message: e.message || 'Default error message' })), + reportError: vi.fn(), + }, +})); + +vi.mock('app/share/services/share.service', () => ({ + default: { + validateSharingInvitation: vi.fn(), + }, + validateSharingInvitation: vi.fn(), +})); + +vi.mock('services/navigation.service', () => ({ + default: { + push: vi.fn(), + history: { + location: { + search: '?email=mock@email.com&invitation=test-invitation', }, - validateSharingInvitation: vi.fn(), - })); - - vi.mock('services/navigation.service', () => ({ - default: { - push: vi.fn(), - history: { - location: { - search: '?email=mock@email.com&invitation=test-invitation', - }, - }, - }, - })); - - vi.mock('app/i18n/provider/TranslationProvider', () => ({ - useTranslationContext: vi.fn().mockReturnValue({ - translate: vi.fn().mockImplementation((value: string) => { - return value; - }), - }), - })); - - vi.mock('components', () => ({ - ExpiredLinkView: vi.fn(() =>
Mocked Expired Link View
), - MAX_PASSWORD_LENGTH: 256, - })); - - vi.mock('query-string', () => ({ - parse: vi.fn().mockImplementation((input: string) => input), - })); - - vi.mock('react-hook-form', () => { - const mockEmail = 'mock@email.com'; - const mockToken = 'mock-token'; - const mockPassword = 'mock-password'; - const mockValues = { email: mockEmail, token: mockToken, password: mockPassword }; + }, + }, +})); +vi.mock('app/i18n/provider/TranslationProvider', () => ({ + useTranslationContext: vi.fn().mockReturnValue({ + translate: vi.fn().mockImplementation((value: string) => { + return value; + }), + }), +})); + +vi.mock('components', () => ({ + ExpiredLinkView: vi.fn(() =>
Mocked Expired Link View
), + MAX_PASSWORD_LENGTH: 256, +})); + +vi.mock('query-string', () => ({ + parse: vi.fn().mockImplementation((input: string) => input), +})); + +vi.mock('react-hook-form', () => { + const mockEmail = 'mock@email.com'; + const mockToken = 'mock-token'; + const mockPassword = 'mock-password'; + const mockValues = { email: mockEmail, token: mockToken, password: mockPassword }; + + return { + SubmitHandler: vi.fn(), + useForm() { return { - SubmitHandler: vi.fn(), - useForm() { - return { - register: vi.fn((name: string) => ({ onChange: vi.fn(), onBlur: vi.fn(), ref: vi.fn(), name })), - handleSubmit: vi.fn((fn) => (event: Event) => { - event?.preventDefault(); - return fn(mockValues, event); - }), - formState: { errors: {}, isValid: true }, - control: {}, - watch: vi.fn((name: string) => mockValues[name]), - }; - }, - useWatch: vi.fn(() => mockPassword), - }; - }); - - vi.mock('react-redux', () => ({ - useSelector: vi.fn(), - useDispatch: vi.fn(() => vi.fn()), - })); - - vi.mock('../../utils', () => ({ - onChangePasswordHandler: vi.fn(), - })); - - vi.mock('services/workspace.service', () => ({ - default: { - validateWorkspaceInvitation: vi.fn().mockImplementation(() => { - return true; + register: vi.fn((name: string) => ({ onChange: vi.fn(), onBlur: vi.fn(), ref: vi.fn(), name })), + handleSubmit: vi.fn((fn) => (event: Event) => { + event?.preventDefault(); + return fn(mockValues, event); }), - }, - })); - - vi.mock('app/store/hooks', () => ({ - useAppDispatch: vi.fn().mockReturnValue(vi.fn()), - })); - - vi.mock('app/store/slices/plan', () => ({ - planThunks: { - initializeThunk: vi.fn(), - }, - })); + formState: { errors: {}, isValid: true }, + control: {}, + watch: vi.fn((name: string) => mockValues[name]), + }; + }, + useWatch: vi.fn(() => mockPassword), + }; +}); - vi.mock('app/store/slices/referrals', () => ({ - referralsThunks: { - initializeThunk: vi.fn(), - }, - })); +vi.mock('react-redux', () => ({ + useSelector: vi.fn(), + useDispatch: vi.fn(() => vi.fn()), +})); + +vi.mock('../../utils', () => ({ + onChangePasswordHandler: vi.fn(), +})); + +vi.mock('services/workspace.service', () => ({ + default: { + validateWorkspaceInvitation: vi.fn().mockImplementation(() => { + return true; + }), + }, +})); + +vi.mock('app/store/hooks', () => ({ + useAppDispatch: vi.fn().mockReturnValue(vi.fn()), +})); + +vi.mock('app/store/slices/plan', () => ({ + planThunks: { + initializeThunk: vi.fn(), + }, +})); + +vi.mock('app/store/slices/referrals', () => ({ + referralsThunks: { + initializeThunk: vi.fn(), + }, +})); + +vi.mock('app/store/slices/user', () => ({ + initializeUserThunk: vi.fn(), + userActions: { + setUser: vi.fn(), + }, + userThunks: { + initializeUserThunk: vi.fn(), + }, +})); - vi.mock('app/store/slices/user', () => ({ - initializeUserThunk: vi.fn(), - userActions: { - setUser: vi.fn(), - }, - userThunks: { - initializeUserThunk: vi.fn(), - }, - })); +describe('onSubmit', () => { + beforeAll(() => { + globalThis.Buffer = Buffer; }); beforeEach(() => { From 9680050b76535ed96d19efedb54b1d2535666360 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 19 Aug 2026 17:27:34 +0200 Subject: [PATCH 10/22] use master hybrid encryption --- src/app/crypto/services/pgp.service.ts | 231 +++++++++++++------------ test/unit/services/pgp.service.test.ts | 18 +- 2 files changed, 132 insertions(+), 117 deletions(-) diff --git a/src/app/crypto/services/pgp.service.ts b/src/app/crypto/services/pgp.service.ts index d539ac8d0..311a729aa 100644 --- a/src/app/crypto/services/pgp.service.ts +++ b/src/app/crypto/services/pgp.service.ts @@ -8,28 +8,78 @@ const WORDS_HYBRID_MODE_IN_BASE64 = 'SHlicmlkTW9kZQ=='; // 'HybridMode' in BASE6 const WORDS_HYBRID_BUCKET_KEY_IN_BASE64 = 'SHlicmlkQnVja2V0S2V5'; // 'HybridBucketKey' in BASE64 format type Data = Uint8Array | string; -export async function getOpenpgp(): Promise { +export const getOpenpgp = async (): Promise => { return import('openpgp'); -} +}; -export function comparePrivateKeyCiphertextIDs(privateKey: PrivateKey, encryptedMessage: Message): boolean { - const messageKeyID = encryptedMessage.getEncryptionKeyIDs()[0].toHex(); - const privateKeyID = privateKey.getSubkeys()[0].getKeyID().toHex(); - return messageKeyID === privateKeyID; -} +export const comparePrivateKeyCiphertextIDs = (privateKey: PrivateKey, encryptedMessage: Message): boolean => { + const [messageKeyId] = encryptedMessage.getEncryptionKeyIDs(); + const [privateSubkey] = privateKey.getSubkeys(); + + if (!messageKeyId || !privateSubkey) { + throw new Error('Cannot compare key IDs: message or private key has no key IDs'); + } -export function compareKeyPairIDs(privateKey: PrivateKey, publicKey: PublicKey): boolean { - const publiKeyID = publicKey.getSubkeys()[0].getKeyID().toHex(); - const privateKeyID = privateKey.getSubkeys()[0].getKeyID().toHex(); - return publiKeyID === privateKeyID; + return messageKeyId.toHex() === privateSubkey.getKeyID().toHex(); +}; + +export const compareKeyPairIDs = (privateKey: PrivateKey, publicKey: PublicKey): boolean => { + const [publicSubkey] = publicKey.getSubkeys(); + const [privateSubkey] = privateKey.getSubkeys(); + + if (!publicSubkey || !privateSubkey) { + throw new Error('Cannot compare key IDs: public or private key has no subkeys'); + } + + return publicSubkey.getKeyID().toHex() === privateSubkey.getKeyID().toHex(); +}; + +export const kyberEncapsulate = async ( + publicKyberKeyBase64: string, +): Promise<{ ciphertextBase64: string; secret: Uint8Array }> => { + const kem = await kemBuilder(); + const publicKyberKey = Buffer.from(publicKyberKeyBase64, 'base64'); + const { ciphertext, sharedSecret } = await kem.encapsulate(new Uint8Array(publicKyberKey)); + return { ciphertextBase64: Buffer.from(ciphertext).toString('base64'), secret: sharedSecret }; +}; + +export const kyberDecapsulate = async ( + kyberCiphertextBase64: string, + privateKyberKeyBase64: string | undefined, +): Promise => { + if (!privateKyberKeyBase64) throw new Error('Attempted to decrypt hybrid ciphertex without Kyber key'); + + const kem = await kemBuilder(); + const privateKyberKey = Buffer.from(privateKyberKeyBase64, 'base64'); + const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); + const { sharedSecret } = await kem.decapsulate(new Uint8Array(kyberCiphertext), new Uint8Array(privateKyberKey)); + return sharedSecret; +}; + +interface HybridSplitResult { + kyberCiphertextBase64?: string; + eccCiphertextStr: string; } -export async function generateNewKeys(): Promise<{ +const splitHybridCiphertext = (input: string, hybridPrefix: string): HybridSplitResult => { + const parts = input.split('$'); + const isHybridMode = parts[0] === hybridPrefix; + + if (!isHybridMode) { + return { eccCiphertextStr: input }; + } + if (parts.length !== 3) { + throw new Error('Malformed hybrid ciphertext'); + } + return { kyberCiphertextBase64: parts[1], eccCiphertextStr: parts[2] }; +}; + +export const generateNewKeys = async (): Promise<{ privateKeyArmored: string; publicKeyArmored: string; publicKyberKeyBase64: string; privateKyberKeyBase64: string; -}> { +}> => { const openpgp = await getOpenpgp(); const { privateKey, publicKey } = await openpgp.generateKey({ @@ -46,24 +96,19 @@ export async function generateNewKeys(): Promise<{ publicKyberKeyBase64: Buffer.from(publicKyberKey).toString('base64'), privateKyberKeyBase64: Buffer.from(privateKyberKey).toString('base64'), }; -} +}; /** * XORs two strings of the identical length * @param {string} a - The first string * @param {string} b - The second string - * @returns {string} The result of XOR of strings a and b. + * @returns {Uint8Array} The result of XOR of strings a and b. */ -export function XORhex(a: string, b: string): string { - let res = '', - i = a.length, - j = b.length; - if (i != j) { - throw new Error('Can XOR only strings with identical length'); - } - while (i-- > 0 && j-- > 0) res = (parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(j), 16)).toString(16) + res; - return res; -} +export const XORhex = (a: string, b: string): Uint8Array => { + const aBytes = Buffer.from(a, 'hex'); + const bBytes = Buffer.from(b, 'hex'); + return xorUint8Arrays(new Uint8Array(aBytes), new Uint8Array(bBytes)); +}; /** * Encrypts message using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only @@ -84,18 +129,15 @@ export const hybridEncryptMessageWithPublicKey = async ({ let result = ''; let plaintext = message; if (publicKyberKeyBase64) { - const kem = await kemBuilder(); - - const publicKyberKey = Buffer.from(publicKyberKeyBase64, 'base64'); - const { ciphertext, sharedSecret: secret } = await kem.encapsulate(new Uint8Array(publicKyberKey)); - const kyberCiphertextStr = Buffer.from(ciphertext).toString('base64'); + const { ciphertextBase64, secret } = await kyberEncapsulate(publicKyberKeyBase64); const bits = message.length * 8; const secretHex = await extendSecret(secret, bits); const messageHex = Buffer.from(message).toString('hex'); - plaintext = XORhex(messageHex, secretHex); - result = WORDS_HYBRID_MODE_IN_BASE64.concat('$', kyberCiphertextStr, '$'); + const xored = XORhex(messageHex, secretHex); + plaintext = Buffer.from(xored).toString('hex'); + result = WORDS_HYBRID_MODE_IN_BASE64.concat('$', ciphertextBase64, '$'); } const encryptedMessage = await encryptMessageWithPublicKey({ message: plaintext, publicKeyInBase64 }); @@ -122,36 +164,23 @@ export const hybridDecryptMessageWithPrivateKey = async ({ privateKeyInBase64: string; privateKyberKeyInBase64?: string; }): Promise => { - let eccCiphertextStr = encryptedMessageInBase64; - let kyberSecret; - const ciphertexts = encryptedMessageInBase64.split('$'); - const prefix = ciphertexts[0]; - const isHybridMode = prefix === WORDS_HYBRID_MODE_IN_BASE64; - - if (isHybridMode) { - if (!privateKyberKeyInBase64) { - return Promise.reject(new Error('Attempted to decrypt hybrid ciphertex without Kyber key')); - } - const kem = await kemBuilder(); - - const kyberCiphertextBase64 = ciphertexts[1]; - eccCiphertextStr = ciphertexts[2]; - - const privateKyberKey = Buffer.from(privateKyberKeyInBase64, 'base64'); - const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); - const decapsulate = await kem.decapsulate(new Uint8Array(kyberCiphertext), new Uint8Array(privateKyberKey)); - kyberSecret = decapsulate.sharedSecret; - } + const { kyberCiphertextBase64, eccCiphertextStr } = splitHybridCiphertext( + encryptedMessageInBase64, + WORDS_HYBRID_MODE_IN_BASE64, + ); + const decryptedMessage = await decryptMessageWithPrivateKey({ encryptedMessage: atob(eccCiphertextStr), privateKeyInBase64, }); let result = decryptedMessage as string; - if (isHybridMode) { + + if (kyberCiphertextBase64) { + const sharedSecret = await kyberDecapsulate(kyberCiphertextBase64, privateKyberKeyInBase64); const bits = result.length * 4; - const secretHex = await extendSecret(kyberSecret, bits); + const secretHex = await extendSecret(sharedSecret, bits); const xored = XORhex(result, secretHex); - result = Buffer.from(xored, 'hex').toString('utf8'); + result = Buffer.from(xored).toString('utf8'); } return result; @@ -161,7 +190,7 @@ export const encryptMessageWithPublicKey = async ({ message, publicKeyInBase64, }: { - message: string; + message: Data; publicKeyInBase64: string; }): Promise> => { const openpgp = await getOpenpgp(); @@ -169,8 +198,13 @@ export const encryptMessageWithPublicKey = async ({ const publicKeyArmored = Buffer.from(publicKeyInBase64, 'base64').toString(); const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); + const messageToEncrypt = + typeof message === 'string' + ? await openpgp.createMessage({ text: message }) + : await openpgp.createMessage({ binary: message }); + const encryptedMessage = await openpgp.encrypt({ - message: await openpgp.createMessage({ text: message }), + message: messageToEncrypt, encryptionKeys: publicKey, }); @@ -180,9 +214,11 @@ export const encryptMessageWithPublicKey = async ({ export const decryptMessageWithPrivateKey = async ({ encryptedMessage, privateKeyInBase64, + format = 'utf8', }: { encryptedMessage: WebStream; privateKeyInBase64: string; + format?: 'utf8' | 'binary'; }): Promise & WebStream> => { const openpgp = await getOpenpgp(); @@ -194,23 +230,27 @@ export const decryptMessageWithPrivateKey = async ({ }); if (!comparePrivateKeyCiphertextIDs(privateKey, message)) { - return Promise.reject(new Error('The key does not correspond to the ciphertext')); + throw new Error('The key does not correspond to the ciphertext'); } const { data: decryptedMessage } = await openpgp.decrypt({ message, decryptionKeys: privateKey, + format, }); return decryptedMessage; }; -function xorUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { +const xorUint8Arrays = (a: Uint8Array, b: Uint8Array): Uint8Array => { + if (a.length !== b.length) { + throw new Error('Can XOR only identical lengths'); + } const result = new Uint8Array(a.length); for (let i = 0; i < a.length; i++) { result[i] = a[i] ^ b[i]; } return result; -} +}; /** * Encrypts bucket key using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only @@ -229,27 +269,18 @@ export const encryptBucketKeyHybrid = async ({ publicKyberKeyBase64?: string; }): Promise => { let result = ''; + if (bucketKey.length < 32) { + throw new Error('bucketKey must be at least 32 bytes'); + } let plaintext: Uint8Array = bucketKey.subarray(0, 32); if (publicKyberKeyBase64) { - const kem = await kemBuilder(); - - const publicKyberKey = Buffer.from(publicKyberKeyBase64, 'base64'); - const { ciphertext, sharedSecret: secret } = await kem.encapsulate(new Uint8Array(publicKyberKey)); - const kyberCiphertextStr = Buffer.from(ciphertext).toString('base64'); + const { ciphertextBase64, secret } = await kyberEncapsulate(publicKyberKeyBase64); plaintext = xorUint8Arrays(plaintext, secret); - result = WORDS_HYBRID_BUCKET_KEY_IN_BASE64.concat('$', kyberCiphertextStr, '$'); + result = WORDS_HYBRID_BUCKET_KEY_IN_BASE64.concat('$', ciphertextBase64, '$'); } - const openpgp = await getOpenpgp(); - - const publicKeyArmored = Buffer.from(publicKeyInBase64, 'base64').toString(); - const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }); - - const encryptedMessage = await openpgp.encrypt({ - message: await openpgp.createMessage({ binary: plaintext }), - encryptionKeys: publicKey, - }); + const encryptedMessage = await encryptMessageWithPublicKey({ message: plaintext, publicKeyInBase64 }); const eccCiphertextStr = btoa(encryptedMessage as string); result = result.concat(eccCiphertextStr); @@ -273,50 +304,28 @@ export const decryptBucketKeyHybrid = async ({ privateKeyInBase64: string; privateKyberKeyInBase64?: string; }): Promise => { - let eccCiphertextStr = encryptedMessageInBase64; - let kyberSecret; - const ciphertexts = encryptedMessageInBase64.split('$'); - const prefix = ciphertexts[0]; - const isHybridMode = prefix === WORDS_HYBRID_BUCKET_KEY_IN_BASE64; - - if (isHybridMode) { - if (!privateKyberKeyInBase64) { - throw new Error('Attempted to decrypt hybrid ciphertex without Kyber key'); - } - const kem = await kemBuilder(); - - const kyberCiphertextBase64 = ciphertexts[1]; - eccCiphertextStr = ciphertexts[2]; - - const privateKyberKey = Buffer.from(privateKyberKeyInBase64, 'base64'); - const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); - const decapsulate = await kem.decapsulate(new Uint8Array(kyberCiphertext), new Uint8Array(privateKyberKey)); - kyberSecret = decapsulate.sharedSecret; - } - const openpgp = await getOpenpgp(); - - const privateKeyArmored = Buffer.from(privateKeyInBase64, 'base64').toString(); - const privateKey = await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }); + const { kyberCiphertextBase64, eccCiphertextStr } = splitHybridCiphertext( + encryptedMessageInBase64, + WORDS_HYBRID_BUCKET_KEY_IN_BASE64, + ); - const message = await openpgp.readMessage({ - armoredMessage: atob(eccCiphertextStr), - }); - - const { data: decryptedMessage } = await openpgp.decrypt({ - message, - decryptionKeys: privateKey, + const decryptedMessage = await decryptMessageWithPrivateKey({ + encryptedMessage: atob(eccCiphertextStr), + privateKeyInBase64, format: 'binary', }); - let result = decryptedMessage; - if (isHybridMode) { - const xored = xorUint8Arrays(result, kyberSecret); + let result = decryptedMessage as Uint8Array; + + if (kyberCiphertextBase64) { + const sharedSecret = await kyberDecapsulate(kyberCiphertextBase64, privateKyberKeyInBase64); + const xored = xorUint8Arrays(result, sharedSecret); result = xored; } return result; }; -export function isBucketKeyCiphertext(encryptedMessageInBase64: string): boolean { +export const isBucketKeyCiphertext = (encryptedMessageInBase64: string): boolean => { return encryptedMessageInBase64.split('$')[0] === WORDS_HYBRID_BUCKET_KEY_IN_BASE64; -} +}; diff --git a/test/unit/services/pgp.service.test.ts b/test/unit/services/pgp.service.test.ts index 674802618..e4ec6fb7e 100644 --- a/test/unit/services/pgp.service.test.ts +++ b/test/unit/services/pgp.service.test.ts @@ -20,6 +20,10 @@ export async function getOpenpgp(): Promise { return import('openpgp'); } +const toHex = (buffer: Uint8Array): string => { + return Buffer.from(buffer).toString('hex'); +}; + describe('Encryption and Decryption', () => { it('should generate new keys', async () => { const keys = await generateNewKeys(); @@ -53,7 +57,7 @@ describe('Encryption and Decryption', () => { expect(() => { XORhex(messageHex, secretHex); - }).toThrow('Can XOR only strings with identical length'); + }).toThrow('Can XOR only identical lengths'); }); it('XOR should work for the given fixed example', () => { @@ -63,7 +67,7 @@ describe('Encryption and Decryption', () => { const xoredMessage = XORhex(firstHex, secondHex); - expect(xoredMessage).toEqual(resultHex); + expect(toHex(xoredMessage)).toEqual(resultHex); }); it('XOR of two identical strings should result in zero string', () => { @@ -72,17 +76,19 @@ describe('Encryption and Decryption', () => { const xoredMessage = XORhex(strHex, strHex); - expect(xoredMessage).toEqual(resultHex); + expect(toHex(xoredMessage)).toEqual(resultHex); }); it('XOR of str1, str2 and str1 should result in str2', () => { const str1 = '74686973206973207468652074657374206d657373616765'; const str2 = '7468697320697320746865207365636f6e64206d65737361'; - const str3 = XORhex(str1, str2); + const xored = XORhex(str1, str2); + + const str3 = toHex(xored); const should_be_str2 = XORhex(str3, str1); - expect(should_be_str2).toEqual(str2); + expect(toHex(should_be_str2)).toEqual(str2); }); it('should generate keys, encrypt and decrypt a message using hybrid encryption', async () => { @@ -126,7 +132,7 @@ describe('Encryption and Decryption', () => { encryptedMessageInBase64, privateKeyInBase64: Buffer.from(keys.privateKeyArmored).toString('base64'), }), - ).rejects.toThrowError('Attempted to decrypt hybrid ciphertex without Kyber key'); + ).rejects.toThrow('Attempted to decrypt hybrid ciphertex without Kyber key'); }); it('hybrid decryption should decrypt old ciphertexts', async () => { From a58db07cfa4141717aade508b3a168f50edc1a4c Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 20 Aug 2026 10:02:51 +0200 Subject: [PATCH 11/22] up the version --- src/app/store/slices/sharedLinks/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/store/slices/sharedLinks/index.ts b/src/app/store/slices/sharedLinks/index.ts index b23cc734f..6b285ae87 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -13,8 +13,7 @@ import { t } from 'i18next'; import userService from 'services/user.service'; import { encryptBucketKey } from 'app/share/services/share.crypto'; -export const HYBRID_ALGORITHM = 'hybrid'; -export const HYBRID_ALGORITHM_WITH_BUCKET_KEY = 'hybrid'; +export const HYBRID_ALGORITHM_WITH_BUCKET_KEY = 'hybrid-v2'; export const STANDARD_ALGORITHM = 'ed25519'; export interface ShareLinksState { From fd0fa68735e28fd536bd2652167dccf84739357f Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 20 Aug 2026 14:44:22 +0200 Subject: [PATCH 12/22] bring files from master --- src/app/share/services/share.crypto.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/share/services/share.crypto.ts b/src/app/share/services/share.crypto.ts index 6c1d5a990..9936fc1c3 100644 --- a/src/app/share/services/share.crypto.ts +++ b/src/app/share/services/share.crypto.ts @@ -13,7 +13,7 @@ import { generateFileBucketKey } from 'app/network/crypto'; import { FileKey } from 'app/network/types/helper-types'; export const decryptMnemonic = async (encryptionKey: string): Promise => { - const user = encryptedStorageService.getUser(); + const user = await encryptedStorageService.getUser(); if (user) { let decryptedKey; try { @@ -25,6 +25,7 @@ export const decryptMnemonic = async (encryptionKey: string): Promise => { return hybridEncryptMessageWithPublicKey({ message: mnemonic, @@ -70,7 +71,7 @@ const handleError = (err: unknown, keyLabel: string) => { }; export const decryptBucketKey = async (encryptionKey: string): Promise => { - const user = encryptedStorageService.getUser(); + const user = await encryptedStorageService.getUser(); if (user) { let decryptedKey; try { From b9959c827b8d401ae74b61b831d14bdaa2b0f46b Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 11:50:57 +0200 Subject: [PATCH 13/22] reduce code duplication --- src/app/network/NetworkFacade.ts | 145 ++++++++++++------------------- 1 file changed, 57 insertions(+), 88 deletions(-) diff --git a/src/app/network/NetworkFacade.ts b/src/app/network/NetworkFacade.ts index 9bab43316..6191f480e 100644 --- a/src/app/network/NetworkFacade.ts +++ b/src/app/network/NetworkFacade.ts @@ -275,6 +275,7 @@ export class NetworkFacade { private async downloadInternal( execute: (onDownloadables: DownloadFileFunction, onDecrypt: DecryptFileFunction) => Promise, options?: DownloadOptions, + fetchOptions?: { range?: { start: number; end: number } }, ): Promise { const encryptedContentStreams: ReadableStream[] = []; const sha256Hashes: string[] = []; @@ -284,14 +285,34 @@ export class NetworkFacade { const onDownloadables: DownloadFileFunction = async (downloadables, fileInfo) => { fileInfoRef = fileInfo; for (const downloadable of downloadables) { - if (options?.abortController?.signal.aborted) throw new Error('Download aborted'); - const res = await fetch(downloadable.url, { signal: options?.abortController?.signal }); - if (!res.body) throw new Error('No content received'); - encryptedContentStreams.push(await createSha256HashingStream(res.body, (h) => sha256Hashes.push(h))); + if (options?.abortController?.signal.aborted) throw new DownloadAbortedByUserError(); + + const headers = fetchOptions?.range + ? { Range: `bytes=${fetchOptions.range.start}-${fetchOptions.range.end}`, Connection: 'keep-alive' } + : undefined; + + const res = await fetch(downloadable.url, { + signal: options?.abortController?.signal, + headers, + keepalive: !!fetchOptions?.range, + }); + + if (fetchOptions?.range) { + if (res.status !== 206 && res.status !== 200) throw new DownloadFailedWithUnknownError(res.status); + } + if (!res.body) throw new NoContentReceivedError(); + + encryptedContentStreams.push( + fetchOptions?.range ? res.body : await createSha256HashingStream(res.body, (h) => sha256Hashes.push(h)), + ); } }; const onDecrypt: DecryptFileFunction = async (_algorithm, key, iv, fileSize) => { + if (fetchOptions?.range) { + fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, fetchOptions.range.start); + return; + } fileStream = buildProgressStream( getDecryptedStream( encryptedContentStreams, @@ -376,94 +397,42 @@ export class NetworkFacade { chunkEnd, options, }: DownloadChunkPayload): Promise> { - const encryptedContentStreams: ReadableStream[] = []; - let fileStream: ReadableStream; - + console.log('TEST', mnemonic, bucketKey); if (mnemonic) { - await downloadFile( - fileId, - bucketId, - mnemonic, - this.network, - this.cryptoLib, - Buffer.from, - async (downloadables) => { - for (const downloadable of downloadables) { - if (options?.abortController?.signal.aborted) { - throw new DownloadAbortedByUserError(); - } - - const response = await fetch(downloadable.url, { - signal: options?.abortController?.signal, - headers: { - Range: `bytes=${chunkStart}-${chunkEnd}`, - Connection: 'keep-alive', - }, - keepalive: true, - }); - - const statusCode = response.status; - - if (statusCode !== 206 && statusCode !== 200) { - throw new DownloadFailedWithUnknownError(statusCode); - } - - if (!response.body) { - throw new NoContentReceivedError(); - } - - encryptedContentStreams.push(response.body); - } - }, - async (algorithm, key, iv, fileSize) => { - fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, chunkStart); - }, - (options?.token && { token: options.token }) || undefined, + return this.downloadInternal( + (onDownloadables, onDecrypt) => + downloadFile( + fileId, + bucketId, + mnemonic, + this.network, + this.cryptoLib, + Buffer.from, + onDownloadables, + onDecrypt, + options?.token ? { token: options.token } : undefined, + ), + options, + { range: { start: chunkStart, end: chunkEnd } }, ); } if (bucketKey) { - await downloadFileWithBucketKey( - fileId, - bucketId, - bucketKey, - this.network, - this.cryptoLibBucketKey, - Buffer.from, - async (downloadables) => { - for (const downloadable of downloadables) { - if (options?.abortController?.signal.aborted) { - throw new DownloadAbortedByUserError(); - } - - const response = await fetch(downloadable.url, { - signal: options?.abortController?.signal, - headers: { - Range: `bytes=${chunkStart}-${chunkEnd}`, - Connection: 'keep-alive', - }, - keepalive: true, - }); - - const statusCode = response.status; - - if (statusCode !== 206 && statusCode !== 200) { - throw new DownloadFailedWithUnknownError(statusCode); - } - - if (!response.body) { - throw new NoContentReceivedError(); - } - - encryptedContentStreams.push(response.body); - } - }, - async (algorithm, key, iv, fileSize) => { - fileStream = decryptStream(encryptedContentStreams, key as Buffer, iv as Buffer, chunkStart); - }, - (options?.token && { token: options.token }) || undefined, + return this.downloadInternal( + (onDownloadables, onDecrypt) => + downloadFileWithBucketKey( + fileId, + bucketId, + bucketKey, + this.network, + this.cryptoLibBucketKey, + Buffer.from, + onDownloadables, + onDecrypt, + options?.token ? { token: options.token } : undefined, + ), + options, + { range: { start: chunkStart, end: chunkEnd } }, ); - } - - return fileStream!; + } else throw new Error('No bucket key or mnemonic is given'); } } From f0cd8bdf1e8a42118693fb5f155ce7bddad961a0 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 12:46:01 +0200 Subject: [PATCH 14/22] remove log, add test --- src/app/network/NetworkFacade.test.ts | 6 ++++++ src/app/network/NetworkFacade.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/network/NetworkFacade.test.ts b/src/app/network/NetworkFacade.test.ts index 3a2015f96..cf2afe721 100644 --- a/src/app/network/NetworkFacade.test.ts +++ b/src/app/network/NetworkFacade.test.ts @@ -502,4 +502,10 @@ describe('NetworkFacade', () => { ).rejects.toThrow('Download aborted'); }); }); + + test('When neither mnemonic nor bucketKey is provided, throws', async () => { + await expect( + networkFacade.downloadChunk({ bucketId, fileId, key: {}, chunkStart: 2, chunkEnd: 5 } as any), + ).rejects.toThrow('No bucket key or mnemonic is given'); + }); }); diff --git a/src/app/network/NetworkFacade.ts b/src/app/network/NetworkFacade.ts index 6191f480e..0aaa35149 100644 --- a/src/app/network/NetworkFacade.ts +++ b/src/app/network/NetworkFacade.ts @@ -397,7 +397,6 @@ export class NetworkFacade { chunkEnd, options, }: DownloadChunkPayload): Promise> { - console.log('TEST', mnemonic, bucketKey); if (mnemonic) { return this.downloadInternal( (onDownloadables, onDecrypt) => From 4339497d1f307a8539b1fea50bb1a84848725161 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 13:21:53 +0200 Subject: [PATCH 15/22] add more tests --- src/app/network/download/v2.test.ts | 91 ++++++++++++++++++----------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/src/app/network/download/v2.test.ts b/src/app/network/download/v2.test.ts index ed11c5d57..3b3607a1f 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -5,7 +5,6 @@ import envService from 'services/env.service'; import { Network } from '@internxt/sdk/dist/network'; import { NetworkFacade } from '../NetworkFacade'; import downloadFile from './v2'; -import { FileKey } from '../types/helper-types'; vi.mock('../../crypto/services/utils'); @@ -19,6 +18,11 @@ describe('Download V2', () => { const mockHashedPassword = 'hashed-password'; const mockBridgeUrl = 'https://bridge.internxt.com'; const mockNetworkClient = {}; + const mnemonic = 'test mnemonic'; + const bucketId = 'test-bucket-id'; + const fileId = 'test-file-id'; + const bucketKey = Buffer.alloc(32, 0x03); + const token = 'shared-token'; beforeEach(async () => { vi.clearAllMocks(); @@ -33,12 +37,10 @@ describe('Download V2', () => { const abortController = new AbortController(); const progressCallback = vi.fn(); const params = { - bucketId: 'test-bucket', - fileId: 'test-file', + bucketId, + fileId, creds: mockCredentials, - key: { - mnemonic: 'test mnemonic', - }, + key: { mnemonic }, fileSize: 1024, options: { notifyProgress: progressCallback, @@ -66,7 +68,7 @@ describe('Download V2', () => { expect(downloadFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - key: { mnemonic: params.key.mnemonic }, + key: params.key, fileSize: params.fileSize, options: { downloadingCallback: progressCallback, @@ -82,12 +84,10 @@ describe('Download V2', () => { const abortController = new AbortController(); const progressCallback = vi.fn(); const params = { - bucketId: 'test-bucket', - fileId: 'test-file', + bucketId, + fileId, creds: mockCredentials, - key: { - mnemonic: 'test mnemonic', - }, + key: { mnemonic }, fileSize: 1024, chunkStart: 0, chunkEnd: 1024, @@ -117,9 +117,7 @@ describe('Download V2', () => { expect(downloadSingleFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - key: { - mnemonic: params.key.mnemonic, - }, + key: params.key, chunkStart: params.chunkStart, chunkEnd: params.chunkEnd, options: { @@ -132,19 +130,16 @@ describe('Download V2', () => { }); describe('downloadFile dispatcher', () => { - test('When token and encryptionKey are provided, downloadSharedFile is called', async () => { + test('When token and mnemonic are provided, downloadSharedFile is called', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); - const encryptionKey = 'aabbccdd'; - const token = 'shared-token'; - const downloadSpy = vi.spyOn(NetworkFacade.prototype, 'download').mockResolvedValue(mockStream); const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); const result = await downloadFile({ - bucketId: 'test-bucket', - fileId: 'test-file', - key: { mnemonic: encryptionKey } as FileKey, + bucketId, + fileId, + key: { mnemonic }, token, options: { notifyProgress: progressCallback, abortController }, } as any); @@ -154,7 +149,37 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: '', userId: '' }, ); - expect(downloadSpy).toHaveBeenCalledWith('test-bucket', 'test-file', encryptionKey, { + expect(downloadSpy).toHaveBeenCalledWith(bucketId, fileId, mnemonic, { + token, + downloadingCallback: progressCallback, + abortController, + }); + expect(result).toStrictEqual(mockStream); + }); + + test('When token and key.bucketKey are provided, downloadSharedFile is called with the bucket key', async () => { + const abortController = new AbortController(); + const progressCallback = vi.fn(); + + const downloadWithBucketKeySpy = vi + .spyOn(NetworkFacade.prototype, 'downloadWithBucketKey') + .mockResolvedValue(mockStream); + const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); + + const result = await downloadFile({ + bucketId, + fileId, + key: { bucketKey }, + token, + options: { notifyProgress: progressCallback, abortController }, + } as any); + + expect(networkClientSpy).toHaveBeenCalledWith( + mockBridgeUrl, + { clientName: 'drive-web', clientVersion: '1.0' }, + { bridgeUser: '', userId: '' }, + ); + expect(downloadWithBucketKeySpy).toHaveBeenCalledWith(bucketId, fileId, bucketKey, { token, downloadingCallback: progressCallback, abortController, @@ -170,10 +195,10 @@ describe('Download V2', () => { const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); const result = await downloadFile({ - bucketId: 'test-bucket', - fileId: 'test-file', + bucketId, + fileId, creds: mockCredentials, - key: { mnemonic: 'test mnemonic' }, + key: { mnemonic }, options: { notifyProgress: progressCallback, abortController }, } as any); @@ -182,7 +207,7 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadSpy).toHaveBeenCalledWith('test-bucket', 'test-file', 'test mnemonic', { + expect(downloadSpy).toHaveBeenCalledWith(bucketId, fileId, 'test mnemonic', { downloadingCallback: progressCallback, abortController, }); @@ -192,16 +217,14 @@ describe('Download V2', () => { test('When creds and key.bucketKey are provided, downloadOwnFileWithBucketKey is called', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); - const bucketKey = Buffer.alloc(32, 0x05); - const downloadWithBucketKeySpy = vi .spyOn(NetworkFacade.prototype, 'downloadWithBucketKey') .mockResolvedValue(mockStream); const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); const result = await downloadFile({ - bucketId: 'test-bucket', - fileId: 'test-file', + bucketId, + fileId, creds: mockCredentials, key: { bucketKey }, options: { notifyProgress: progressCallback, abortController }, @@ -212,7 +235,7 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadWithBucketKeySpy).toHaveBeenCalledWith('test-bucket', 'test-file', bucketKey, { + expect(downloadWithBucketKeySpy).toHaveBeenCalledWith(bucketId, fileId, bucketKey, { downloadingCallback: progressCallback, abortController, }); @@ -222,8 +245,8 @@ describe('Download V2', () => { test('When no valid combination is provided, an error is thrown', () => { expect(() => downloadFile({ - bucketId: 'test-bucket', - fileId: 'test-file', + bucketId, + fileId, } as any), ).toThrow('DOWNLOAD ERRNO. 0'); }); From d82f2897d09d46c15488347f9067668bf5d6b512 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 15:10:53 +0200 Subject: [PATCH 16/22] add createNetworkFacade --- src/app/network/download/v2.ts | 61 +++++++++------------------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index 2b26609be..791ba4530 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -31,10 +31,12 @@ interface DownloadSharedFileParams extends DownloadFileParams { type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => DownloadFileResponse; type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFile) => DownloadFileResponse; -const downloadSharedFile: DownloadSharedFileFunction = (params) => { - const { bucketId, fileId, key, token, options } = params; - - const networkFacade = new NetworkFacade( +/** + * Creates a NetworkFacade for the bridge. Pass auth for downloads of the user's own files; + * omit it for shared-link downloads, which authenticate through the share token instead. + */ +const createNetworkFacade = (auth?: { username: string; password: string }): NetworkFacade => + new NetworkFacade( Network.client( envService.getVariable('storjBridge'), { @@ -42,11 +44,16 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { clientVersion: '1.0', }, { - bridgeUser: '', - userId: '', + bridgeUser: auth?.username ?? '', + userId: auth?.password ?? '', }, ), ); + +const downloadSharedFile: DownloadSharedFileFunction = (params) => { + const { bucketId, fileId, key, token, options } = params; + + const networkFacade = createNetworkFacade(); if (key.mnemonic) { return networkFacade.download(bucketId, fileId, key.mnemonic, { token, @@ -76,19 +83,7 @@ const downloadOwnFile = async (params: DownloadOwnFile) => { const { bucketId, fileId, key, options } = params; const auth = await getAuthFromCredentials(params.creds); - const networkFacade = new NetworkFacade( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ); + const networkFacade = createNetworkFacade(auth); if (key.mnemonic) { return networkFacade.download(bucketId, fileId, key.mnemonic, { @@ -110,19 +105,7 @@ export async function multipartDownload(params: DownloadOwnFile & { fileSize: nu const { bucketId, fileId, key, fileSize, options } = params; const auth = await getAuthFromCredentials(params.creds); - const networkFacade = new NetworkFacade( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ); + const networkFacade = createNetworkFacade(auth); const multipartDownload = new MultipartDownload(networkFacade); @@ -144,19 +127,7 @@ export async function downloadChunkFile( const { bucketId, fileId, key, chunkStart, chunkEnd, options } = params; const auth = await getAuthFromCredentials(params.creds); - const networkFacade = new NetworkFacade( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ); + const networkFacade = createNetworkFacade(auth); return networkFacade.downloadChunk({ bucketId, From fe2995a8fb19901d27a3e3a7d4a6ea5de7ae1ee3 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 15:26:36 +0200 Subject: [PATCH 17/22] add multipart download --- src/app/network/download/v2.ts | 39 +++++++++++++++++++++--- src/app/network/index.ts | 4 +-- src/views/PublicShared/ShareFileView.tsx | 11 +++++-- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index 791ba4530..965c0a6aa 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -101,20 +101,37 @@ const downloadOwnFile = async (params: DownloadOwnFile) => { } }; -export async function multipartDownload(params: DownloadOwnFile & { fileSize: number }): Promise { +async function multipartDownloadOwnFile(params: DownloadOwnFile & { fileSize: number }): Promise { const { bucketId, fileId, key, fileSize, options } = params; const auth = await getAuthFromCredentials(params.creds); - const networkFacade = createNetworkFacade(auth); - const multipartDownload = new MultipartDownload(networkFacade); + return new MultipartDownload(networkFacade).downloadFile({ + bucketId, + fileId, + key, + fileSize, + options: { + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }, + }); +} - return multipartDownload.downloadFile({ +async function multipartDownloadSharedFile( + params: DownloadSharedFileParams & { fileSize: number }, +): Promise { + const { bucketId, fileId, key, token, fileSize, options } = params; + + const networkFacade = createNetworkFacade(); + + return new MultipartDownload(networkFacade).downloadFile({ bucketId, fileId, key, fileSize, options: { + token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, }, @@ -142,7 +159,7 @@ export async function downloadChunkFile( }); } -const downloadFile: DownloadFileFunction = (params) => { +export const downloadFile: DownloadFileFunction = (params) => { if (params.token) { return downloadSharedFile(params as DownloadSharedFileParams); } else if (params.creds) { @@ -153,3 +170,15 @@ const downloadFile: DownloadFileFunction = (params) => { }; export default downloadFile; + +export const multipartDownload = async ( + params: (DownloadOwnFile | DownloadSharedFileParams) & { fileSize: number }, +): Promise => { + if (params.token) { + return multipartDownloadSharedFile(params as DownloadSharedFileParams & { fileSize: number }); + } else if (params.creds) { + return multipartDownloadOwnFile(params as DownloadOwnFile & { fileSize: number }); + } else { + throw new Error('DOWNLOAD ERRNO. 0'); + } +}; diff --git a/src/app/network/index.ts b/src/app/network/index.ts index 4236e7f45..c31cb659a 100644 --- a/src/app/network/index.ts +++ b/src/app/network/index.ts @@ -1,4 +1,4 @@ import { uploadFile } from './upload'; -import { downloadFile } from './download'; +import { downloadFile, multipartDownloadFile } from './download'; -export default { uploadFile, downloadFile }; +export default { uploadFile, downloadFile, multipartDownloadFile }; diff --git a/src/views/PublicShared/ShareFileView.tsx b/src/views/PublicShared/ShareFileView.tsx index 83cc12dea..503ef66df 100644 --- a/src/views/PublicShared/ShareFileView.tsx +++ b/src/views/PublicShared/ShareFileView.tsx @@ -31,6 +31,7 @@ import useBeforeUnload from 'hooks/useBeforeUnload'; import { isFileSizePreviewable } from 'services'; import { IDownloadParams } from 'app/network/download'; import { isBucketKeyCiphertext } from 'app/crypto/services/pgp.service'; +import { MIN_DOWNLOAD_MULTIPART_SIZE } from 'app/network/networkConstants'; export interface ShareViewProps extends ShareViewState { match: match<{ @@ -202,9 +203,10 @@ export default function ShareFileView(props: Readonly): JSX.Elem const MIN_PROGRESS = 0; if (fileInfo) { + const fileSize = fileInfo.item.size; setProgress(MIN_PROGRESS); setIsDownloading(true); - const readable = await network.downloadFile({ + const downloadParams: IDownloadParams = { bucketId: fileInfo.item.bucket, fileId: fileInfo.item.fileId, ...getSharedFileKeyParams(fileInfo as SharingMeta), @@ -218,7 +220,12 @@ export default function ShareFileView(props: Readonly): JSX.Elem } }, }, - }); + }; + + const shouldUseMultipart = fileSize >= MIN_DOWNLOAD_MULTIPART_SIZE; + const readable = shouldUseMultipart + ? await network.multipartDownloadFile({ ...downloadParams, fileSize }) + : await network.downloadFile(downloadParams); const fileBlob = await binaryStreamToBlob(readable); await downloadService.downloadFileFromBlob(fileBlob, getFormatFileName()); From 375deed91beb71b1ee6bd9ab9f4f5b8b1fb3e1c3 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 15:43:24 +0200 Subject: [PATCH 18/22] add multipart tests --- src/app/network/download/v2.test.ts | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/app/network/download/v2.test.ts b/src/app/network/download/v2.test.ts index 3b3607a1f..2e418e118 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -214,6 +214,84 @@ describe('Download V2', () => { expect(result).toStrictEqual(mockStream); }); + test('When token and key.mnemonic are provided, multipartDownloadSharedFile is called with the mnemonic', async () => { + const abortController = new AbortController(); + const progressCallback = vi.fn(); + const params = { + bucketId, + fileId, + key: { mnemonic }, + token, + fileSize: 1024, + options: { + notifyProgress: progressCallback, + abortController, + }, + }; + + const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); + const downloadFileSpy = vi.spyOn(MultipartDownload.prototype, 'downloadFile').mockResolvedValue(mockStream); + + const result = await multipartDownload(params as any); + + expect(networkClientSpy).toHaveBeenCalledWith( + mockBridgeUrl, + { clientName: 'drive-web', clientVersion: '1.0' }, + { bridgeUser: '', userId: '' }, + ); + expect(downloadFileSpy).toHaveBeenCalledWith({ + bucketId: params.bucketId, + fileId: params.fileId, + key: params.key, + fileSize: params.fileSize, + options: { + token, + downloadingCallback: progressCallback, + abortController, + }, + }); + expect(result).toStrictEqual(mockStream); + }); + + test('When token and key.bucketKey are provided, multipartDownloadSharedFile is called with the bucket key', async () => { + const abortController = new AbortController(); + const progressCallback = vi.fn(); + const params = { + bucketId, + fileId, + key: { bucketKey }, + token, + fileSize: 1024, + options: { + notifyProgress: progressCallback, + abortController, + }, + }; + + const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); + const downloadFileSpy = vi.spyOn(MultipartDownload.prototype, 'downloadFile').mockResolvedValue(mockStream); + + const result = await multipartDownload(params as any); + + expect(networkClientSpy).toHaveBeenCalledWith( + mockBridgeUrl, + { clientName: 'drive-web', clientVersion: '1.0' }, + { bridgeUser: '', userId: '' }, + ); + expect(downloadFileSpy).toHaveBeenCalledWith({ + bucketId: params.bucketId, + fileId: params.fileId, + key: params.key, + fileSize: params.fileSize, + options: { + token, + downloadingCallback: progressCallback, + abortController, + }, + }); + expect(result).toStrictEqual(mockStream); + }); + test('When creds and key.bucketKey are provided, downloadOwnFileWithBucketKey is called', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); From 8cbf76d3df17daf014859666e26f7f90c9fd348d Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 16:40:38 +0200 Subject: [PATCH 19/22] switch to decryptSharingKey in shared view --- src/views/Shared/SharedView.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index 6bb09ffca..e810b522e 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -7,7 +7,7 @@ import { DriveItemData } from '../../app/drive/types'; import { useTranslationContext } from '../../app/i18n/provider/TranslationProvider'; import notificationsService, { ToastType } from '../../app/notifications/services/notifications.service'; import shareService from '../../app/share/services/share.service'; -import { decryptMnemonic } from '../../app/share/services/share.crypto'; +import { decryptMnemonic, decryptSharingKey } 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'; @@ -69,6 +69,7 @@ import { import { ShareDialogWrapper } from 'app/drive/components/ShareDialog/ShareDialogWrapper'; import encryptedStorageService from 'services/encrypted-storage.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { FileKey } from 'app/network/types/helper-types'; export const MAX_SHARED_NAME_LENGTH = 32; @@ -359,10 +360,11 @@ function SharedView({ }; try { - const mnemonic = - selectedWorkspace?.workspaceUser.key ?? - (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : clickedShareItemEncryptionKey)); - handleOpenItemPreview(true, { ...previewItem, key: { mnemonic } }); + const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key + ? { mnemonic: selectedWorkspace.workspaceUser.key } + : await decryptSharingKey(shareItem.encryptionKey ? shareItem.encryptionKey : clickedShareItemEncryptionKey); + + handleOpenItemPreview(true, { ...previewItem, key }); } catch (err) { const error = errorService.castError(err); errorService.reportError(error); From a60c38feb9ee30395db2f08ca35d1b9446f41479 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 18:00:21 +0200 Subject: [PATCH 20/22] fix sonar erorr and video streaming --- .../FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx | 2 +- src/views/Shared/SharedView.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx b/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx index 52610fdbe..285734900 100644 --- a/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx +++ b/src/app/drive/components/FileViewer/viewers/FileVideoViewer/FileVideoViewer.tsx @@ -90,7 +90,7 @@ const FileVideoViewer = ({ bucketId: file.bucket, fileSize: file.size, fileType: file.type, - key: { mnemonic: file.key?.mnemonic ?? mnemonic, bucketKey: file.key?.bucketKey }, + key: file.key ?? { mnemonic }, credentials: file.credentials ? { user: file.credentials?.user, pass: file.credentials?.pass } : { user: bridgeUser, pass: userId }, diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index e810b522e..1e1dafd3c 100644 --- a/src/views/Shared/SharedView.tsx +++ b/src/views/Shared/SharedView.tsx @@ -360,9 +360,10 @@ function SharedView({ }; try { + const encryptionKey = shareItem.encryptionKey || clickedShareItemEncryptionKey; const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key ? { mnemonic: selectedWorkspace.workspaceUser.key } - : await decryptSharingKey(shareItem.encryptionKey ? shareItem.encryptionKey : clickedShareItemEncryptionKey); + : await decryptSharingKey(encryptionKey); handleOpenItemPreview(true, { ...previewItem, key }); } catch (err) { From 164f05402311b576c547d19f7c84b3d584c8fc99 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 21 Aug 2026 18:33:25 +0200 Subject: [PATCH 21/22] remove code duplicate in download --- src/app/network/NetworkFacade.test.ts | 34 ++++---- src/app/network/NetworkFacade.ts | 109 +++++++++----------------- src/app/network/download/v2.test.ts | 70 ++++++++++------- src/app/network/download/v2.ts | 39 +++------ 4 files changed, 105 insertions(+), 147 deletions(-) diff --git a/src/app/network/NetworkFacade.test.ts b/src/app/network/NetworkFacade.test.ts index cf2afe721..50a59ca37 100644 --- a/src/app/network/NetworkFacade.test.ts +++ b/src/app/network/NetworkFacade.test.ts @@ -98,7 +98,7 @@ describe('NetworkFacade', () => { mockDownloadFile(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - const result = await networkFacade.download(bucketId, fileId, mnemonic); + const result = await networkFacade.download(bucketId, fileId, { mnemonic }); expect(result).toBeDefined(); expect(buildProgressStream).toHaveBeenCalledOnce(); @@ -115,7 +115,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.download(bucketId, fileId, mnemonic); + await networkFacade.download(bucketId, fileId, { mnemonic }); await expect(capturedOnFinished?.()).resolves.toBeUndefined(); }); @@ -130,7 +130,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.download(bucketId, fileId, mnemonic); + await networkFacade.download(bucketId, fileId, { mnemonic }); await expect(capturedOnFinished?.()).rejects.toThrow('File integrity check failed'); }); @@ -139,7 +139,7 @@ describe('NetworkFacade', () => { mockDownloadFile(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - await networkFacade.download(bucketId, fileId, mnemonic); + await networkFacade.download(bucketId, fileId, { mnemonic }); expect(createSha256HashingStream).toHaveBeenCalledOnce(); }); @@ -154,7 +154,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.download(bucketId, fileId, mnemonic); + await networkFacade.download(bucketId, fileId, { mnemonic }); await capturedOnFinished?.(); expect(getFileHmacFromShardHashes).toHaveBeenCalledWith(fakeKey, [fakeRipemd160Hash]); @@ -338,7 +338,7 @@ describe('NetworkFacade', () => { mockDownloadFileWithBucketKey(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - const result = await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + const result = await networkFacade.download(bucketId, fileId, { bucketKey }); expect(result).toBeDefined(); expect(downloadFileWithBucketKey).toHaveBeenCalledWith( @@ -362,7 +362,7 @@ describe('NetworkFacade', () => { mockDownloadFileWithBucketKey(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - const result = await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + const result = await networkFacade.download(bucketId, fileId, { bucketKey }); expect(result).toBeDefined(); expect(buildProgressStream).toHaveBeenCalledOnce(); @@ -379,7 +379,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + await networkFacade.download(bucketId, fileId, { bucketKey }); await expect(capturedOnFinished?.()).resolves.toBeUndefined(); }); @@ -394,7 +394,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + await networkFacade.download(bucketId, fileId, { bucketKey }); await expect(capturedOnFinished?.()).rejects.toThrow('File integrity check failed'); }); @@ -403,7 +403,7 @@ describe('NetworkFacade', () => { mockDownloadFileWithBucketKey(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + await networkFacade.download(bucketId, fileId, { bucketKey }); expect(createSha256HashingStream).toHaveBeenCalledOnce(); }); @@ -418,7 +418,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + await networkFacade.download(bucketId, fileId, { bucketKey }); await capturedOnFinished?.(); expect(getFileHmacFromShardHashes).toHaveBeenCalledWith(fakeKey, [fakeRipemd160Hash]); @@ -435,7 +435,7 @@ describe('NetworkFacade', () => { return new ReadableStream({ start: (c) => c.close() }); }); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey, { key: overrideKey }); + await networkFacade.download(bucketId, fileId, { bucketKey }, { key: overrideKey }); await capturedOnFinished?.(); expect(getFileHmacFromShardHashes).toHaveBeenCalledWith(overrideKey, [fakeRipemd160Hash]); @@ -445,7 +445,7 @@ describe('NetworkFacade', () => { mockDownloadFileWithBucketKey(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey, { token: 'test-token' }); + await networkFacade.download(bucketId, fileId, { bucketKey }, { token: 'test-token' }); expect(downloadFileWithBucketKey).toHaveBeenCalledWith( fileId, @@ -464,7 +464,7 @@ describe('NetworkFacade', () => { mockDownloadFileWithBucketKey(); vi.mocked(getFileHmacFromShardHashes).mockResolvedValue('any-hmac'); - await networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey); + await networkFacade.download(bucketId, fileId, { bucketKey }); expect(downloadFileWithBucketKey).toHaveBeenCalledWith( fileId, @@ -497,9 +497,9 @@ describe('NetworkFacade', () => { return { body: new ReadableStream() }; }); - await expect( - networkFacade.downloadWithBucketKey(bucketId, fileId, bucketKey, { abortController }), - ).rejects.toThrow('Download aborted'); + await expect(networkFacade.download(bucketId, fileId, { bucketKey }, { abortController })).rejects.toThrow( + 'Download aborted', + ); }); }); diff --git a/src/app/network/NetworkFacade.ts b/src/app/network/NetworkFacade.ts index 0aaa35149..654c767af 100644 --- a/src/app/network/NetworkFacade.ts +++ b/src/app/network/NetworkFacade.ts @@ -32,6 +32,7 @@ import { DownloadChunkPayload } from './types/index'; import { uploadFileUint8Array, UploadProgressCallback } from './upload-utils'; import { getFileHmacFromShardHashes, getRipemd160FromHex } from 'app/crypto/services/utils'; import { DecryptFileFunction, DownloadFileFunction } from '@internxt/sdk/dist/network/types'; +import { FileKey } from './types/helper-types'; interface UploadOptions { uploadingCallback: UploadProgressCallback; @@ -336,46 +337,43 @@ export class NetworkFacade { async download( bucketId: string, fileId: string, - mnemonic: string, - options?: DownloadOptions, - ): Promise { - return this.downloadInternal( - (onDownloadables, onDecrypt) => - downloadFile( - fileId, - bucketId, - mnemonic, - this.network, - this.cryptoLib, - Buffer.from, - onDownloadables, - onDecrypt, - options?.token ? { token: options.token } : undefined, - ), - options, - ); - } - - async downloadWithBucketKey( - bucketId: string, - fileId: string, - bucketKey: Buffer, + key: FileKey, options?: DownloadOptions, + fetchOptions?: { range?: { start: number; end: number } }, ): Promise { + const sdkOptions = options?.token ? { token: options.token } : undefined; return this.downloadInternal( - (onDownloadables, onDecrypt) => - downloadFileWithBucketKey( - fileId, - bucketId, - bucketKey, - this.network, - this.cryptoLibBucketKey, - Buffer.from, - onDownloadables, - onDecrypt, - options?.token ? { token: options.token } : undefined, - ), + (onDownloadables, onDecrypt) => { + if (key.mnemonic) { + return downloadFile( + fileId, + bucketId, + key.mnemonic, + this.network, + this.cryptoLib, + Buffer.from, + onDownloadables, + onDecrypt, + sdkOptions, + ); + } + if (key.bucketKey) { + return downloadFileWithBucketKey( + fileId, + bucketId, + key.bucketKey, + this.network, + this.cryptoLibBucketKey, + Buffer.from, + onDownloadables, + onDecrypt, + sdkOptions, + ); + } + throw new Error('No bucket key or mnemonic is given'); + }, options, + fetchOptions, ); } @@ -392,46 +390,11 @@ export class NetworkFacade { async downloadChunk({ bucketId, fileId, - key: { mnemonic, bucketKey }, + key, chunkStart, chunkEnd, options, }: DownloadChunkPayload): Promise> { - if (mnemonic) { - return this.downloadInternal( - (onDownloadables, onDecrypt) => - downloadFile( - fileId, - bucketId, - mnemonic, - this.network, - this.cryptoLib, - Buffer.from, - onDownloadables, - onDecrypt, - options?.token ? { token: options.token } : undefined, - ), - options, - { range: { start: chunkStart, end: chunkEnd } }, - ); - } - if (bucketKey) { - return this.downloadInternal( - (onDownloadables, onDecrypt) => - downloadFileWithBucketKey( - fileId, - bucketId, - bucketKey, - this.network, - this.cryptoLibBucketKey, - Buffer.from, - onDownloadables, - onDecrypt, - options?.token ? { token: options.token } : undefined, - ), - options, - { range: { start: chunkStart, end: chunkEnd } }, - ); - } else throw new Error('No bucket key or mnemonic is given'); + return this.download(bucketId, fileId, key, options, { range: { start: chunkStart, end: chunkEnd } }); } } diff --git a/src/app/network/download/v2.test.ts b/src/app/network/download/v2.test.ts index 2e418e118..14d567fb5 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -130,7 +130,7 @@ describe('Download V2', () => { }); describe('downloadFile dispatcher', () => { - test('When token and mnemonic are provided, downloadSharedFile is called', async () => { + test('When token and mnemonic are provided, downloadSharedFile forwards the mnemonic key', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); const downloadSpy = vi.spyOn(NetworkFacade.prototype, 'download').mockResolvedValue(mockStream); @@ -149,21 +149,24 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: '', userId: '' }, ); - expect(downloadSpy).toHaveBeenCalledWith(bucketId, fileId, mnemonic, { - token, - downloadingCallback: progressCallback, - abortController, - }); + expect(downloadSpy).toHaveBeenCalledWith( + bucketId, + fileId, + { mnemonic }, + { + token, + downloadingCallback: progressCallback, + abortController, + }, + ); expect(result).toStrictEqual(mockStream); }); - test('When token and key.bucketKey are provided, downloadSharedFile is called with the bucket key', async () => { + test('When token and key.bucketKey are provided, downloadSharedFile forwards the bucket key', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); - const downloadWithBucketKeySpy = vi - .spyOn(NetworkFacade.prototype, 'downloadWithBucketKey') - .mockResolvedValue(mockStream); + const downloadWithBucketKeySpy = vi.spyOn(NetworkFacade.prototype, 'download').mockResolvedValue(mockStream); const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); const result = await downloadFile({ @@ -179,11 +182,16 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: '', userId: '' }, ); - expect(downloadWithBucketKeySpy).toHaveBeenCalledWith(bucketId, fileId, bucketKey, { - token, - downloadingCallback: progressCallback, - abortController, - }); + expect(downloadWithBucketKeySpy).toHaveBeenCalledWith( + bucketId, + fileId, + { bucketKey }, + { + token, + downloadingCallback: progressCallback, + abortController, + }, + ); expect(result).toStrictEqual(mockStream); }); @@ -207,10 +215,15 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadSpy).toHaveBeenCalledWith(bucketId, fileId, 'test mnemonic', { - downloadingCallback: progressCallback, - abortController, - }); + expect(downloadSpy).toHaveBeenCalledWith( + bucketId, + fileId, + { mnemonic }, + { + downloadingCallback: progressCallback, + abortController, + }, + ); expect(result).toStrictEqual(mockStream); }); @@ -292,12 +305,10 @@ describe('Download V2', () => { expect(result).toStrictEqual(mockStream); }); - test('When creds and key.bucketKey are provided, downloadOwnFileWithBucketKey is called', async () => { + test('When creds and key.bucketKey are provided, download is called with bucket key', async () => { const abortController = new AbortController(); const progressCallback = vi.fn(); - const downloadWithBucketKeySpy = vi - .spyOn(NetworkFacade.prototype, 'downloadWithBucketKey') - .mockResolvedValue(mockStream); + const downloadWithBucketKeySpy = vi.spyOn(NetworkFacade.prototype, 'download').mockResolvedValue(mockStream); const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); const result = await downloadFile({ @@ -313,10 +324,15 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadWithBucketKeySpy).toHaveBeenCalledWith(bucketId, fileId, bucketKey, { - downloadingCallback: progressCallback, - abortController, - }); + expect(downloadWithBucketKeySpy).toHaveBeenCalledWith( + bucketId, + fileId, + { bucketKey }, + { + downloadingCallback: progressCallback, + abortController, + }, + ); expect(result).toStrictEqual(mockStream); }); diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index 965c0a6aa..cd8064e8f 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -54,22 +54,11 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { const { bucketId, fileId, key, token, options } = params; const networkFacade = createNetworkFacade(); - if (key.mnemonic) { - return networkFacade.download(bucketId, fileId, key.mnemonic, { - token, - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); - } - if (key.bucketKey) { - return networkFacade.downloadWithBucketKey(bucketId, fileId, key.bucketKey, { - token, - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); - } else { - throw new Error('DOWNLOAD ERRNO. 1'); - } + return networkFacade.download(bucketId, fileId, key, { + token, + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); }; async function getAuthFromCredentials(creds: NetworkCredentials): Promise<{ username: string; password: string }> { @@ -85,20 +74,10 @@ const downloadOwnFile = async (params: DownloadOwnFile) => { const networkFacade = createNetworkFacade(auth); - if (key.mnemonic) { - return networkFacade.download(bucketId, fileId, key.mnemonic, { - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); - } - if (key.bucketKey) { - return networkFacade.downloadWithBucketKey(bucketId, fileId, key.bucketKey, { - downloadingCallback: options?.notifyProgress, - abortController: options?.abortController, - }); - } else { - throw new Error('DOWNLOAD ERRNO. 1'); - } + return networkFacade.download(bucketId, fileId, key, { + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }); }; async function multipartDownloadOwnFile(params: DownloadOwnFile & { fileSize: number }): Promise { From 92a8e9cce77b95879f04bbfa6e900a6d44558d91 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Tue, 25 Aug 2026 12:38:04 +0200 Subject: [PATCH 22/22] user arrow function style in v2.ts --- src/app/network/download/v2.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index cd8064e8f..8e361f825 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -31,10 +31,6 @@ interface DownloadSharedFileParams extends DownloadFileParams { type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => DownloadFileResponse; type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFile) => DownloadFileResponse; -/** - * Creates a NetworkFacade for the bridge. Pass auth for downloads of the user's own files; - * omit it for shared-link downloads, which authenticate through the share token instead. - */ const createNetworkFacade = (auth?: { username: string; password: string }): NetworkFacade => new NetworkFacade( Network.client( @@ -61,12 +57,12 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { }); }; -async function getAuthFromCredentials(creds: NetworkCredentials): Promise<{ username: string; password: string }> { +const getAuthFromCredentials = async (creds: NetworkCredentials): Promise<{ username: string; password: string }> => { return { username: creds.user, password: await getSha256(creds.pass), }; -} +}; const downloadOwnFile = async (params: DownloadOwnFile) => { const { bucketId, fileId, key, options } = params; @@ -80,7 +76,7 @@ const downloadOwnFile = async (params: DownloadOwnFile) => { }); }; -async function multipartDownloadOwnFile(params: DownloadOwnFile & { fileSize: number }): Promise { +const multipartDownloadOwnFile = async (params: DownloadOwnFile & { fileSize: number }): Promise => { const { bucketId, fileId, key, fileSize, options } = params; const auth = await getAuthFromCredentials(params.creds); const networkFacade = createNetworkFacade(auth); @@ -95,11 +91,11 @@ async function multipartDownloadOwnFile(params: DownloadOwnFile & { fileSize: nu abortController: options?.abortController, }, }); -} +}; -async function multipartDownloadSharedFile( +const multipartDownloadSharedFile = async ( params: DownloadSharedFileParams & { fileSize: number }, -): Promise { +): Promise => { const { bucketId, fileId, key, token, fileSize, options } = params; const networkFacade = createNetworkFacade(); @@ -115,11 +111,11 @@ async function multipartDownloadSharedFile( abortController: options?.abortController, }, }); -} +}; -export async function downloadChunkFile( +export const downloadChunkFile = async ( params: DownloadOwnFile & { chunkStart: number; chunkEnd: number }, -): Promise { +): Promise => { const { bucketId, fileId, key, chunkStart, chunkEnd, options } = params; const auth = await getAuthFromCredentials(params.creds); @@ -136,7 +132,7 @@ export async function downloadChunkFile( abortController: options?.abortController, }, }); -} +}; export const downloadFile: DownloadFileFunction = (params) => { if (params.token) {