Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"concurrently": "^9.2.1",
"detox": "^19.7.0",
"eslint": "^8.8.0",
"hash-wasm": "^4.11.0",
"husky": "^9.1.7",
"jest": "^29.3.1",
"jest-expo": "~54.0.16",
Expand Down
2 changes: 1 addition & 1 deletion src/network/NetworkFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import uuid from 'react-native-uuid';
import drive from '@internxt-mobile/services/drive';
import pLimit, { LimitFunction } from 'p-limit';
import { ripemd160 } from '../@inxt-js/lib/crypto';
import { generateFileKey } from '../lib/network';
import { generateFileKey } from './crypto';
import appService from '../services/AppService';
import { driveEvents } from '../services/drive/events';
import fileSystemService from '../services/FileSystemService';
Expand Down
104 changes: 104 additions & 0 deletions src/network/crypto.spec.ts
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'));
});
});
});
25 changes: 25 additions & 0 deletions src/network/crypto.ts
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);
};
53 changes: 53 additions & 0 deletions src/shareExtension/services/shareEncryptionService.ts
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';
Comment thread
CandelR marked this conversation as resolved.
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()),
);
});
Loading
Loading