diff --git a/src/app/drive/components/FileViewer/FileViewerWrapper.tsx b/src/app/drive/components/FileViewer/FileViewerWrapper.tsx index 6a44559d5a..064a900038 100644 --- a/src/app/drive/components/FileViewer/FileViewerWrapper.tsx +++ b/src/app/drive/components/FileViewer/FileViewerWrapper.tsx @@ -219,7 +219,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 b12d5124d8..2857349008 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, - mnemonic: file.mnemonic ?? mnemonic, + key: file.key ?? { mnemonic }, 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 b5859a0d0c..ff0984bdef 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,7 +14,7 @@ export default async function fetchFileBlob( item: Downloadable, options: FetchFileBlobOptions, credentials?: NetworkCredentials, - mnemonic?: string, + key?: FileKey, ): Promise { const { bridgeUser, bridgePass, encryptionKey } = await getEnvironmentConfig(!!options.isWorkspace); @@ -24,9 +24,7 @@ export default async function fetchFileBlob( bucketId: item.bucketId, fileId: item.fileId, creds, - key: { - mnemonic: mnemonic ? mnemonic : encryptionKey, - }, + key: key ?? { mnemonic: encryptionKey }, options: { notifyProgress: (totalBytes, downloadedBytes) => { options.updateProgressCallback(downloadedBytes / totalBytes); diff --git a/src/app/drive/services/downloadManager.service.ts b/src/app/drive/services/downloadManager.service.ts index 016ad4d2ea..e2b86d1026 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/drive/services/video-streaming.service/VideoStreamingSession.test.ts b/src/app/drive/services/video-streaming.service/VideoStreamingSession.test.ts index 11b8dfbc8a..fbd437ad7b 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 8def1f5d1b..e30a9f1c9a 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 33e963b9ed..7235b924c8 100644 --- a/src/app/drive/services/video-streaming.service/index.ts +++ b/src/app/drive/services/video-streaming.service/index.ts @@ -52,7 +52,7 @@ export class VideoStreamingService { 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 d327a6f2c5..0784477047 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 b2fc115e7f..50a59ca37d 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]); @@ -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: { @@ -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,15 @@ 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', + ); }); }); + + 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 00bc91f634..654c767af3 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; @@ -275,6 +276,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 +286,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, @@ -315,46 +337,43 @@ export class NetworkFacade { async download( bucketId: string, fileId: string, - mnemonic: string, + key: FileKey, options?: DownloadOptions, + fetchOptions?: { range?: { start: number; end: number } }, ): Promise { + const sdkOptions = 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, - ); - } - - async downloadWithBucketKey( - bucketId: string, - fileId: string, - bucketKey: Buffer, - options?: DownloadOptions, - ): Promise { - 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, ); } @@ -371,55 +390,11 @@ export class NetworkFacade { async downloadChunk({ bucketId, fileId, - mnemonic, + key, chunkStart, chunkEnd, options, }: DownloadChunkPayload): Promise> { - 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(); - } - - 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!; + return this.download(bucketId, fileId, key, options, { range: { start: chunkStart, end: chunkEnd } }); } } diff --git a/src/app/network/download.test.ts b/src/app/network/download.test.ts index beb0789f7f..47f818bd31 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 8c57cdd492..dca7c961b5 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 238b5c170c..14d567fb50 100644 --- a/src/app/network/download/v2.test.ts +++ b/src/app/network/download/v2.test.ts @@ -18,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(); @@ -32,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, @@ -65,7 +68,7 @@ describe('Download V2', () => { expect(downloadFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - mnemonic: params.key.mnemonic, + key: params.key, fileSize: params.fileSize, options: { downloadingCallback: progressCallback, @@ -81,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, @@ -116,7 +117,7 @@ describe('Download V2', () => { expect(downloadSingleFileSpy).toHaveBeenCalledWith({ bucketId: params.bucketId, fileId: params.fileId, - mnemonic: params.key.mnemonic, + key: params.key, chunkStart: params.chunkStart, chunkEnd: params.chunkEnd, options: { @@ -129,21 +130,17 @@ 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 forwards the mnemonic key', 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: { fileEncryptionKey: 'unused' } as any, + bucketId, + fileId, + key: { mnemonic }, token, - encryptionKey, options: { notifyProgress: progressCallback, abortController }, } as any); @@ -152,12 +149,49 @@ 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( + bucketId, + fileId, + { mnemonic }, + { + token, + downloadingCallback: progressCallback, + abortController, + }, + ); + expect(result).toStrictEqual(mockStream); + }); + + 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, 'download').mockResolvedValue(mockStream); + const networkClientSpy = vi.spyOn(Network, 'client').mockReturnValue(mockNetworkClient as any); + + const result = await downloadFile({ + bucketId, + fileId, + key: { bucketKey }, token, - downloadingCallback: progressCallback, - abortController, - }); + 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, + }, + ); expect(result).toStrictEqual(mockStream); }); @@ -169,10 +203,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); @@ -181,26 +215,105 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadSpy).toHaveBeenCalledWith('test-bucket', 'test-file', 'test mnemonic', { - downloadingCallback: progressCallback, - abortController, + expect(downloadSpy).toHaveBeenCalledWith( + bucketId, + fileId, + { mnemonic }, + { + downloadingCallback: progressCallback, + abortController, + }, + ); + 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 creds and key.bucketKey are provided, downloadOwnFileWithBucketKey is called', async () => { + 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 bucketKey = Buffer.alloc(32, 0x05); + const params = { + bucketId, + fileId, + key: { bucketKey }, + token, + fileSize: 1024, + options: { + notifyProgress: progressCallback, + abortController, + }, + }; - const downloadWithBucketKeySpy = vi - .spyOn(NetworkFacade.prototype, 'downloadWithBucketKey') - .mockResolvedValue(mockStream); + 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, download is called with bucket key', async () => { + const abortController = new AbortController(); + const progressCallback = vi.fn(); + const downloadWithBucketKeySpy = 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', + bucketId, + fileId, creds: mockCredentials, key: { bucketKey }, options: { notifyProgress: progressCallback, abortController }, @@ -211,18 +324,23 @@ describe('Download V2', () => { { clientName: 'drive-web', clientVersion: '1.0' }, { bridgeUser: mockCredentials.user, userId: mockHashedPassword }, ); - expect(downloadWithBucketKeySpy).toHaveBeenCalledWith('test-bucket', 'test-file', bucketKey, { - downloadingCallback: progressCallback, - abortController, - }); + expect(downloadWithBucketKeySpy).toHaveBeenCalledWith( + bucketId, + fileId, + { bucketKey }, + { + downloadingCallback: progressCallback, + abortController, + }, + ); expect(result).toStrictEqual(mockStream); }); 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'); }); diff --git a/src/app/network/download/v2.ts b/src/app/network/download/v2.ts index d9b5a2bae4..8e361f825e 100644 --- a/src/app/network/download/v2.ts +++ b/src/app/network/download/v2.ts @@ -16,36 +16,23 @@ 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; - - return new NetworkFacade( +const createNetworkFacade = (auth?: { username: string; password: string }): NetworkFacade => + new NetworkFacade( Network.client( envService.getVariable('storjBridge'), { @@ -53,148 +40,91 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { clientVersion: '1.0', }, { - bridgeUser: '', - userId: '', + bridgeUser: auth?.username ?? '', + userId: auth?.password ?? '', }, ), - ).download(bucketId, fileId, '', { - key: Buffer.from(encryptionKey, 'hex'), + ); + +const downloadSharedFile: DownloadSharedFileFunction = (params) => { + const { bucketId, fileId, key, token, options } = params; + + const networkFacade = createNetworkFacade(); + return networkFacade.download(bucketId, fileId, key, { token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, }); }; -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: 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( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ).download(bucketId, fileId, mnemonic, { + const networkFacade = createNetworkFacade(auth); + + return networkFacade.download(bucketId, fileId, key, { downloadingCallback: options?.notifyProgress, abortController: options?.abortController, }); }; -const downloadOwnFileWithBucketKey = async (params: DownloadOwnFileWithBucketKeyParams) => { - const { - bucketId, - fileId, - key: { bucketKey }, - options, - } = params; +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); - 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, - }); -}; - -export async function multipartDownload( - params: DownloadOwnFileWithMnemonicParams & { fileSize: number }, -): Promise { - const { + return new MultipartDownload(networkFacade).downloadFile({ bucketId, fileId, - key: { mnemonic }, + key, fileSize, - options, - } = params; - const auth = await getAuthFromCredentials(params.creds); + options: { + downloadingCallback: options?.notifyProgress, + abortController: options?.abortController, + }, + }); +}; - const networkFacade = new NetworkFacade( - Network.client( - envService.getVariable('storjBridge'), - { - clientName: 'drive-web', - clientVersion: '1.0', - }, - { - bridgeUser: auth.username, - userId: auth.password, - }, - ), - ); +const multipartDownloadSharedFile = async ( + params: DownloadSharedFileParams & { fileSize: number }, +): Promise => { + const { bucketId, fileId, key, token, fileSize, options } = params; - const multipartDownload = new MultipartDownload(networkFacade); + const networkFacade = createNetworkFacade(); - return multipartDownload.downloadFile({ + return new MultipartDownload(networkFacade).downloadFile({ bucketId, fileId, - mnemonic, + key, fileSize, options: { + token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, }, }); -} +}; -export async function downloadChunkFile( - params: DownloadOwnFileWithMnemonicParams & { chunkStart: number; chunkEnd: number }, -): Promise { - const { - bucketId, - fileId, - key: { mnemonic }, - chunkStart, - chunkEnd, - options, - } = params; +export const downloadChunkFile = async ( + params: DownloadOwnFile & { chunkStart: number; chunkEnd: number }, +): Promise => { + const { bucketId, fileId, key, chunkStart, chunkEnd, 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, - }, - ), - ).downloadChunk({ + const networkFacade = createNetworkFacade(auth); + + return networkFacade.downloadChunk({ bucketId, fileId, - mnemonic, + key, chunkStart, chunkEnd, options: { @@ -202,18 +132,28 @@ export async function downloadChunkFile( abortController: options?.abortController, }, }); -} +}; -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); +export const downloadFile: DownloadFileFunction = (params) => { + if (params.token) { + return downloadSharedFile(params as DownloadSharedFileParams); + } else if (params.creds) { + return downloadOwnFile(params as DownloadOwnFile); } else { throw new Error('DOWNLOAD ERRNO. 0'); } }; 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 4236e7f459..c31cb659a0 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/app/network/types/index.ts b/src/app/network/types/index.ts index 6ee586af99..95ec5a6e55 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/services/share.service.ts b/src/app/share/services/share.service.ts index d47562aeec..20172c2b50 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; @@ -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) { @@ -650,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/app/share/types/index.ts b/src/app/share/types/index.ts index fffb628c74..ed4fd711b7 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 & @@ -17,11 +17,12 @@ export type SharedNetworkCredentials = { networkUser: string; networkPass: string; mnemonic?: string; + bucketKey?: Buffer; }; export type PreviewFileItem = DriveFileData & { credentials?: NetworkCredentials; - mnemonic?: string; + key?: FileKey; folderUuid: string; }; diff --git a/src/app/store/slices/sharedLinks/index.test.ts b/src/app/store/slices/sharedLinks/index.test.ts index 1fb6573801..165ac4be59 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 { 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,6 +18,7 @@ import { stopSharingItem, } from './index'; import notificationsService from 'app/notifications/services/notifications.service'; +import { generateFileBucketKey } from 'app/network/crypto'; const { shareItemWithUser } = sharedThunks; vi.mock('services/navigation.service', () => ({ @@ -58,13 +55,23 @@ vi.mock('services/error.service', () => ({ reportError: vi.fn(), }, })); - -describe('Encryption and Decryption', () => { +describe('Encryption and Decryption', async () => { beforeEach(() => { vi.clearAllMocks(); 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 = { @@ -77,11 +84,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 }, }; @@ -114,13 +116,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, @@ -129,7 +131,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, }), @@ -148,11 +150,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 }, }; @@ -186,12 +183,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, @@ -219,11 +217,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 }, }; @@ -259,13 +252,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, @@ -274,7 +267,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, }), @@ -293,11 +286,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 }, }; @@ -331,13 +319,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, @@ -365,11 +353,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 }, }; @@ -403,13 +386,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, @@ -418,7 +401,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, }), @@ -437,11 +420,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 }, }; @@ -474,13 +452,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, @@ -507,11 +485,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 500145465d..6662c0d59d 100644 --- a/src/app/store/slices/sharedLinks/index.ts +++ b/src/app/store/slices/sharedLinks/index.ts @@ -11,9 +11,9 @@ 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 +53,16 @@ const shareItemWithUser = createAsyncThunk): JSX.Elem } }; - function getBlob(abortController: AbortController): Promise { - const fileInfo = info as unknown as ShareTypes.ShareLink; + const getSharedFileKeyParams = (fileInfo: SharingMeta): Pick => { + if (isBucketKeyCiphertext(fileInfo.encryptionKey)) { + return { key: { bucketKey: Buffer.from(fileInfo.encryptionKey, 'hex') } }; + } + + 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 +203,13 @@ export default function ShareFileView(props: Readonly): JSX.Elem const MIN_PROGRESS = 0; if (fileInfo) { - const encryptionKey = fileInfo.encryptionKey; - + 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, - encryptionKey: Buffer.from(encryptionKey, 'hex'), + ...getSharedFileKeyParams(fileInfo as SharingMeta), token: fileInfo.itemToken, options: { notifyProgress: (totalProgress, downloadedBytes) => { @@ -213,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()); diff --git a/src/views/Shared/SharedView.tsx b/src/views/Shared/SharedView.tsx index 813f0d491e..1e1dafd3ca 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,12 @@ function SharedView({ }; try { - const mnemonic = - selectedWorkspace?.workspaceUser.key ?? - (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : clickedShareItemEncryptionKey)); - handleOpenItemPreview(true, { ...previewItem, mnemonic }); + const encryptionKey = shareItem.encryptionKey || clickedShareItemEncryptionKey; + const key: FileKey | undefined = selectedWorkspace?.workspaceUser?.key + ? { mnemonic: selectedWorkspace.workspaceUser.key } + : await decryptSharingKey(encryptionKey); + + handleOpenItemPreview(true, { ...previewItem, key }); } 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 7d5d25fdb3..c29118001f 100644 --- a/src/views/Shared/components/SharedItemListContainer.tsx +++ b/src/views/Shared/components/SharedItemListContainer.tsx @@ -13,13 +13,14 @@ 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'; import { isItemsOwnedByCurrentUser, sortSharedItems } from '../utils/sharedViewUtils'; import encryptedStorageService from 'services/encrypted-storage.service'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { FileKey } from 'app/network/types/helper-types'; +import { decryptSharingKey } from 'app/share/services/share.crypto'; type ShareItemListContainerProps = { disableKeyboardShortcuts: boolean; @@ -104,13 +105,15 @@ const SharedItemListContainer = ({ const downloadItem = async (shareItem: AdvancedSharedItem): Promise => { try { if (shareItem.isRootLink) { - 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, @@ -141,13 +144,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, @@ -181,10 +186,11 @@ const SharedItemListContainer = ({ }; try { - const mnemonic = - selectedWorkspace?.workspaceUser.key ?? - (await decryptMnemonic(shareItem.encryptionKey ? shareItem.encryptionKey : sharedItemEncryptionKey)); - onOpenItemPreview({ ...previewItem, 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);