-
Notifications
You must be signed in to change notification settings - Fork 18
[PB-5937] feature/upload logic #386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
CandelR
merged 8 commits into
feature/PB-5318-share-extension
from
feature/PB-5937-file-upload-logic-and-encription-2
Mar 24, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
eb84654
feat: implement file upload logic and encryption in share extension
CandelR 3e3c67e
Enhance file upload error handling and response validation
CandelR 43d7078
Fix sonar cloud issues
CandelR 7a92944
Merge branch 'feature/PB-5937-file-upload-logic' into feature/PB-5937…
CandelR b1ae1b0
Merge branch 'feature/PB-5937-file-upload-logic' into feature/PB-5937…
CandelR 12bafc9
Add CODEOWNERS for encryption files and implement file key generation…
CandelR d4df71b
Fix sonar issues
CandelR c461f28
Use noble for generate random bytes and removed string type in comput…
CandelR File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Encryption / key derivation | ||
| src/network/crypto.ts @TamaraFinogina | ||
| src/network/crypto.spec.ts @TamaraFinogina | ||
| src/shareExtension/services/shareEncryptionService.ts @TamaraFinogina | ||
| src/shareExtension/services/shareUploadService.ts @TamaraFinogina | ||
| src/network/NetworkFacade.ts @TamaraFinogina |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import crypto from 'node:crypto'; | ||
| import { createSHA512 } from 'hash-wasm'; | ||
| import { generateFileBucketKey, generateFileKey, getFileDeterministicKey } from './crypto'; | ||
|
|
||
| type Bytes = { buffer: ArrayBufferLike; byteOffset: number; byteLength: number }; | ||
| const asUint8 = (v: Bytes): Uint8Array => new Uint8Array(v.buffer, v.byteOffset, v.byteLength); | ||
|
|
||
| const reference = { | ||
| getFileDeterministicKey(key: Bytes, data: Bytes): Buffer { | ||
| return crypto.createHash('sha512').update(asUint8(key)).update(asUint8(data)).digest(); | ||
| }, | ||
|
|
||
| async generateFileBucketKey(mnemonic: string, bucketId: string): Promise<Buffer> { | ||
| const seed = crypto.pbkdf2Sync(mnemonic, 'mnemonic', 2048, 64, 'sha512'); | ||
| return reference.getFileDeterministicKey(seed, Buffer.from(bucketId, 'hex')); | ||
| }, | ||
|
|
||
| async generateFileKey(mnemonic: string, bucketId: string, index: Bytes): Promise<Buffer> { | ||
| const bucketKey = await reference.generateFileBucketKey(mnemonic, bucketId); | ||
| return reference.getFileDeterministicKey(bucketKey.subarray(0, 32), index).subarray(0, 32); | ||
| }, | ||
| }; | ||
|
|
||
| const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; | ||
| const BUCKET_ID = 'a1b2c3d4e5f6a1b2c1d2e3f4a5b6c7d8'; | ||
| const INDEX = Buffer.from([0, 0, 0, 1]); | ||
|
|
||
| describe('getFileDeterministicKey', () => { | ||
| describe('output shape', () => { | ||
| it('when called with buffer inputs, then returns a 64-byte Buffer', () => { | ||
| const result = getFileDeterministicKey(Buffer.from('key'), Buffer.from('data')); | ||
| expect(result).toBeInstanceOf(Buffer); | ||
| expect(result.length).toBe(64); | ||
| }); | ||
|
|
||
| it('when called with string inputs, then returns a 64-byte Buffer', () => { | ||
| const result = getFileDeterministicKey('key', 'data'); | ||
| expect(result).toBeInstanceOf(Buffer); | ||
| expect(result.length).toBe(64); | ||
| }); | ||
| }); | ||
|
|
||
| describe('compatibility', () => { | ||
| it('when compared to Node.js SHA512, then produces identical output', () => { | ||
| const key = Buffer.from('test_key_bytes'); | ||
| const data = Buffer.from('test_data_bytes'); | ||
| const result = getFileDeterministicKey(key, data); | ||
| const expected = reference.getFileDeterministicKey(key, data); | ||
| expect(result.toString('hex')).toBe(expected.toString('hex')); | ||
| }); | ||
|
|
||
| it('when compared to hash-wasm SHA512 (drive-web), then produces identical output', async () => { | ||
| const key = Buffer.from('test_key_bytes'); | ||
| const data = Buffer.from('test_data_bytes'); | ||
| const result = getFileDeterministicKey(key, data); | ||
| const hash = await createSHA512(); | ||
| const expected = hash.init().update(key).update(data).digest(); | ||
| expect(result.toString('hex')).toBe(expected); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('generateFileBucketKey', () => { | ||
| describe('output shape', () => { | ||
| it('when called with a valid mnemonic and bucketId, then returns a 64-byte Buffer', async () => { | ||
| const result = await generateFileBucketKey(MNEMONIC, BUCKET_ID); | ||
| expect(result).toBeInstanceOf(Buffer); | ||
| expect(result.length).toBe(64); | ||
| }); | ||
| }); | ||
|
|
||
| describe('compatibility', () => { | ||
| it('when compared to Node.js PBKDF2 + SHA512 reference, then produces identical output', async () => { | ||
| const result = await generateFileBucketKey(MNEMONIC, BUCKET_ID); | ||
| const expected = await reference.generateFileBucketKey(MNEMONIC, BUCKET_ID); | ||
| expect(result.toString('hex')).toBe(expected.toString('hex')); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('generateFileKey', () => { | ||
| describe('output shape', () => { | ||
| it('when called with valid inputs, then returns a 32-byte Buffer', async () => { | ||
| const result = await generateFileKey(MNEMONIC, BUCKET_ID, INDEX); | ||
| expect(result).toBeInstanceOf(Buffer); | ||
| expect(result.length).toBe(32); | ||
| }); | ||
| }); | ||
|
|
||
| describe('compatibility', () => { | ||
| it('when compared to Node.js reference, then produces identical output for Buffer index', async () => { | ||
| const result = await generateFileKey(MNEMONIC, BUCKET_ID, INDEX); | ||
| const expected = await reference.generateFileKey(MNEMONIC, BUCKET_ID, INDEX); | ||
| expect(result.toString('hex')).toBe(expected.toString('hex')); | ||
| }); | ||
|
|
||
| it('when compared to Node.js reference, then produces identical output for string index', async () => { | ||
| const index = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; | ||
| const result = await generateFileKey(MNEMONIC, BUCKET_ID, index); | ||
| const expected = await reference.generateFileKey(MNEMONIC, BUCKET_ID, Buffer.from(index)); | ||
| expect(result.toString('hex')).toBe(expected.toString('hex')); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { sha512 } from '@noble/hashes/sha2.js'; | ||
| import { mnemonicToSeed } from '@scure/bip39'; | ||
| import { Buffer } from 'buffer'; | ||
|
|
||
| /** | ||
| * SHA512 — mirrors drive-web getFileDeterministicKey. | ||
| */ | ||
| export const getFileDeterministicKey = (key: Buffer | string, data: Buffer | string): Buffer => { | ||
| const keyBuf = Buffer.isBuffer(key) ? key : Buffer.from(key as string); | ||
| const dataBuf = Buffer.isBuffer(data) ? data : Buffer.from(data as string); | ||
| const hash = sha512.create(); | ||
| hash.update(new Uint8Array(keyBuf)); | ||
| hash.update(new Uint8Array(dataBuf)); | ||
| return Buffer.from(hash.digest()); | ||
| }; | ||
|
|
||
| export const generateFileBucketKey = async (mnemonic: string, bucketId: string): Promise<Buffer> => { | ||
| const seed = Buffer.from(await mnemonicToSeed(mnemonic)); | ||
| return getFileDeterministicKey(seed, Buffer.from(bucketId, 'hex')); | ||
| }; | ||
|
|
||
| export const generateFileKey = async (mnemonic: string, bucketId: string, index: Buffer | string): Promise<Buffer> => { | ||
| const bucketKey = await generateFileBucketKey(mnemonic, bucketId); | ||
| return getFileDeterministicKey(bucketKey.slice(0, 32), index).slice(0, 32); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { encryptFile, encryptFileToChunks } from '@internxt/rn-crypto'; | ||
| import { ALGORITHMS } from '@internxt/sdk/dist/network'; | ||
| import { BinaryData } from '@internxt/sdk/dist/network/types'; | ||
| import { ripemd160 as nobleRipemd160 } from '@noble/hashes/legacy.js'; | ||
| import { randomBytes as nobleRandomBytes } from '@noble/hashes/utils.js'; | ||
| import { validateMnemonic } from '@scure/bip39'; | ||
| import { wordlist } from '@scure/bip39/wordlists/english.js'; | ||
| import { Buffer } from 'buffer'; | ||
| import { generateFileKey } from '../../network/crypto'; | ||
|
|
||
| /** RIPEMD160 via @noble/hashes/legacy (pure-JS) — share extension can't link arbitrary native modules */ | ||
| export const computeRipemd160Digest = (input: Buffer): Buffer => Buffer.from(nobleRipemd160(new Uint8Array(input))); | ||
|
|
||
| const generateSecureRandomBytes = (size: number): Buffer => Buffer.from(nobleRandomBytes(size)); | ||
|
|
||
| export const buildSdkEncryptionAdapter = () => ({ | ||
| algorithm: ALGORITHMS.AES256CTR, | ||
| validateMnemonic: (mnemonic: string) => validateMnemonic(mnemonic, wordlist), | ||
| generateFileKey: (mnemonic: string, bucketId: string, index: BinaryData | string) => | ||
| generateFileKey(mnemonic, bucketId, index as Buffer), | ||
| randomBytes: generateSecureRandomBytes, | ||
| }); | ||
|
|
||
| export const encryptFileForUpload = ( | ||
| plainFilePath: string, | ||
| encryptedFilePath: string, | ||
| key: Buffer, | ||
| iv: Buffer, | ||
| ): Promise<void> => | ||
| new Promise((resolve, reject) => { | ||
| encryptFile(plainFilePath, encryptedFilePath, key.toString('hex'), iv.toString('hex'), (err: Error | null) => { | ||
| if (err) reject(err); | ||
| else resolve(); | ||
| }); | ||
| }); | ||
|
|
||
| export const encryptFileIntoMultipartChunks = ( | ||
| plainFilePath: string, | ||
| encryptedPaths: string[], | ||
| key: Buffer, | ||
| iv: Buffer, | ||
| partSize: number, | ||
| ): Promise<void> => | ||
| new Promise((resolve, reject) => { | ||
| encryptFileToChunks( | ||
| plainFilePath, | ||
| encryptedPaths, | ||
| key.toString('hex'), | ||
| iv.toString('hex'), | ||
| partSize, | ||
| (err: Error | null) => (err ? reject(err) : resolve()), | ||
| ); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.