Skip to content

Commit 12bafc9

Browse files
committed
Add CODEOWNERS for encryption files and implement file key generation with tests to compare with drive-web implementation
1 parent b1ae1b0 commit 12bafc9

7 files changed

Lines changed: 147 additions & 22 deletions

File tree

.github/CODEOWNERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Encryption / key derivation
2+
src/network/crypto.ts @TamaraFinogina
3+
src/network/crypto.spec.ts @TamaraFinogina
4+
src/shareExtension/services/shareEncryptionService.ts @TamaraFinogina
5+
src/shareExtension/services/shareUploadService.ts @TamaraFinogina
6+
src/network/NetworkFacade.ts @TamaraFinogina

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
"concurrently": "^9.2.1",
157157
"detox": "^19.7.0",
158158
"eslint": "^8.8.0",
159+
"hash-wasm": "^4.11.0",
159160
"husky": "^9.1.7",
160161
"jest": "^29.3.1",
161162
"jest-expo": "~54.0.16",

src/network/NetworkFacade.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import uuid from 'react-native-uuid';
1515
import drive from '@internxt-mobile/services/drive';
1616
import pLimit, { LimitFunction } from 'p-limit';
1717
import { ripemd160 } from '../@inxt-js/lib/crypto';
18-
import { generateFileKey } from '../lib/network';
18+
import { generateFileKey } from './crypto';
1919
import appService from '../services/AppService';
2020
import { driveEvents } from '../services/drive/events';
2121
import fileSystemService from '../services/FileSystemService';

src/network/crypto.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import crypto from 'crypto';
2+
import { createSHA512 } from 'hash-wasm';
3+
import { generateFileBucketKey, generateFileKey, getFileDeterministicKey } from './crypto';
4+
5+
type Bytes = { buffer: ArrayBufferLike; byteOffset: number; byteLength: number };
6+
const asUint8 = (v: Bytes): Uint8Array => new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
7+
8+
const reference = {
9+
getFileDeterministicKey(key: Bytes, data: Bytes): Buffer {
10+
return crypto.createHash('sha512').update(asUint8(key)).update(asUint8(data)).digest();
11+
},
12+
13+
async generateFileBucketKey(mnemonic: string, bucketId: string): Promise<Buffer> {
14+
const seed = crypto.pbkdf2Sync(mnemonic, 'mnemonic', 2048, 64, 'sha512');
15+
return reference.getFileDeterministicKey(seed, Buffer.from(bucketId, 'hex'));
16+
},
17+
18+
async generateFileKey(mnemonic: string, bucketId: string, index: Bytes): Promise<Buffer> {
19+
const bucketKey = await reference.generateFileBucketKey(mnemonic, bucketId);
20+
return reference.getFileDeterministicKey(bucketKey.subarray(0, 32), index).subarray(0, 32);
21+
},
22+
};
23+
24+
const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
25+
const BUCKET_ID = 'a1b2c3d4e5f6a1b2c1d2e3f4a5b6c7d8';
26+
const INDEX = Buffer.from([0, 0, 0, 1]);
27+
28+
describe('getFileDeterministicKey', () => {
29+
describe('output shape', () => {
30+
it('when called with buffer inputs, then returns a 64-byte Buffer', () => {
31+
const result = getFileDeterministicKey(Buffer.from('key'), Buffer.from('data'));
32+
expect(result).toBeInstanceOf(Buffer);
33+
expect(result.length).toBe(64);
34+
});
35+
36+
it('when called with string inputs, then returns a 64-byte Buffer', () => {
37+
const result = getFileDeterministicKey('key', 'data');
38+
expect(result).toBeInstanceOf(Buffer);
39+
expect(result.length).toBe(64);
40+
});
41+
});
42+
43+
describe('compatibility', () => {
44+
it('when compared to Node.js SHA512, then produces identical output', () => {
45+
const key = Buffer.from('test_key_bytes');
46+
const data = Buffer.from('test_data_bytes');
47+
const result = getFileDeterministicKey(key, data);
48+
const expected = reference.getFileDeterministicKey(key, data);
49+
expect(result.toString('hex')).toBe(expected.toString('hex'));
50+
});
51+
52+
it('when compared to hash-wasm SHA512 (drive-web), then produces identical output', async () => {
53+
const key = Buffer.from('test_key_bytes');
54+
const data = Buffer.from('test_data_bytes');
55+
const result = getFileDeterministicKey(key, data);
56+
const hash = await createSHA512();
57+
const expected = hash.init().update(key).update(data).digest();
58+
expect(result.toString('hex')).toBe(expected);
59+
});
60+
});
61+
});
62+
63+
describe('generateFileBucketKey', () => {
64+
describe('output shape', () => {
65+
it('when called with a valid mnemonic and bucketId, then returns a 64-byte Buffer', async () => {
66+
const result = await generateFileBucketKey(MNEMONIC, BUCKET_ID);
67+
expect(result).toBeInstanceOf(Buffer);
68+
expect(result.length).toBe(64);
69+
});
70+
});
71+
72+
describe('compatibility', () => {
73+
it('when compared to Node.js PBKDF2 + SHA512 reference, then produces identical output', async () => {
74+
const result = await generateFileBucketKey(MNEMONIC, BUCKET_ID);
75+
const expected = await reference.generateFileBucketKey(MNEMONIC, BUCKET_ID);
76+
expect(result.toString('hex')).toBe(expected.toString('hex'));
77+
});
78+
});
79+
});
80+
81+
describe('generateFileKey', () => {
82+
describe('output shape', () => {
83+
it('when called with valid inputs, then returns a 32-byte Buffer', async () => {
84+
const result = await generateFileKey(MNEMONIC, BUCKET_ID, INDEX);
85+
expect(result).toBeInstanceOf(Buffer);
86+
expect(result.length).toBe(32);
87+
});
88+
});
89+
90+
describe('compatibility', () => {
91+
it('when compared to Node.js reference, then produces identical output for Buffer index', async () => {
92+
const result = await generateFileKey(MNEMONIC, BUCKET_ID, INDEX);
93+
const expected = await reference.generateFileKey(MNEMONIC, BUCKET_ID, INDEX);
94+
expect(result.toString('hex')).toBe(expected.toString('hex'));
95+
});
96+
97+
it('when compared to Node.js reference, then produces identical output for string index', async () => {
98+
const index = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
99+
const result = await generateFileKey(MNEMONIC, BUCKET_ID, index);
100+
const expected = await reference.generateFileKey(MNEMONIC, BUCKET_ID, Buffer.from(index));
101+
expect(result.toString('hex')).toBe(expected.toString('hex'));
102+
});
103+
});
104+
});

src/network/crypto.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { sha512 } from '@noble/hashes/sha2.js';
2+
import { mnemonicToSeed } from '@scure/bip39';
3+
import { Buffer } from 'buffer';
4+
5+
/**
6+
* SHA512 — mirrors drive-web getFileDeterministicKey.
7+
*/
8+
export const getFileDeterministicKey = (key: Buffer | string, data: Buffer | string): Buffer => {
9+
const keyBuf = Buffer.isBuffer(key) ? key : Buffer.from(key as string);
10+
const dataBuf = Buffer.isBuffer(data) ? data : Buffer.from(data as string);
11+
const hash = sha512.create();
12+
hash.update(new Uint8Array(keyBuf));
13+
hash.update(new Uint8Array(dataBuf));
14+
return Buffer.from(hash.digest());
15+
};
16+
17+
export const generateFileBucketKey = async (mnemonic: string, bucketId: string): Promise<Buffer> => {
18+
const seed = Buffer.from(await mnemonicToSeed(mnemonic));
19+
return getFileDeterministicKey(seed, Buffer.from(bucketId, 'hex'));
20+
};
21+
22+
export const generateFileKey = async (mnemonic: string, bucketId: string, index: Buffer | string): Promise<Buffer> => {
23+
const bucketKey = await generateFileBucketKey(mnemonic, bucketId);
24+
return getFileDeterministicKey(bucketKey.slice(0, 32), index).slice(0, 32);
25+
};

src/shareExtension/services/shareEncryptionService.ts

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { encryptFile, encryptFileToChunks, createHash as rnCreateHash, pbkdf2 as rnPbkdf2 } from '@internxt/rn-crypto';
2-
import { HMAC } from '@internxt/rn-crypto/src/types/crypto';
1+
import { encryptFile, encryptFileToChunks } from '@internxt/rn-crypto';
32
import { ALGORITHMS } from '@internxt/sdk/dist/network';
43
import { BinaryData } from '@internxt/sdk/dist/network/types';
54
import { ripemd160 as nobleRipemd160 } from '@noble/hashes/legacy.js';
5+
import { validateMnemonic } from '@scure/bip39';
6+
import { wordlist } from '@scure/bip39/wordlists/english.js';
67
import { Buffer } from 'buffer';
8+
import { generateFileKey } from '../../network/crypto';
79

810
/** Cryptographically secure random bytes via Hermes globalThis.crypto */
911
const generateSecureRandomBytes = (size: number): Buffer => {
@@ -20,28 +22,10 @@ export const computeRipemd160Digest = (input: Buffer | string): Buffer => {
2022
return Buffer.from(nobleRipemd160(new Uint8Array(inputBuffer)));
2123
};
2224

23-
const computeHmacSha512 = async (key: Buffer | string, data: Buffer | string): Promise<Buffer> => {
24-
const hmacHasher = rnCreateHash(HMAC.sha512);
25-
hmacHasher.update(key);
26-
hmacHasher.update(data);
27-
return hmacHasher.digest() as Promise<Buffer>;
28-
};
29-
30-
/**
31-
* Derives encryption key from mnemonic + bucketId + index.
32-
* Mirrors GenerateFileKey in @inxt-js/lib/crypto/crypto.ts — reimplemented here because
33-
* that module imports react-native-crypto at the top level, which crashes in the share extension.
34-
*/
35-
const generateFileKey = async (mnemonic: string, bucketId: string, index: Buffer | string): Promise<Buffer> => {
36-
const mnemonicSeedBytes = await rnPbkdf2(mnemonic, 'mnemonic', 2048, 64);
37-
const bucketDerivedKey = await computeHmacSha512(mnemonicSeedBytes, Buffer.from(bucketId, 'hex'));
38-
const indexDerivedKey = await computeHmacSha512(bucketDerivedKey.slice(0, 32), index);
39-
return indexDerivedKey.slice(0, 32);
40-
};
4125

4226
export const buildSdkEncryptionAdapter = () => ({
4327
algorithm: ALGORITHMS.AES256CTR,
44-
validateMnemonic: (mnemonic: string) => typeof mnemonic === 'string' && mnemonic.trim().split(/\s+/).length >= 12,
28+
validateMnemonic: (mnemonic: string) => validateMnemonic(mnemonic, wordlist),
4529
generateFileKey: (mnemonic: string, bucketId: string, index: BinaryData | string) =>
4630
generateFileKey(mnemonic, bucketId, index as Buffer),
4731
randomBytes: generateSecureRandomBytes,

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5143,6 +5143,11 @@ hash-base@^3.0.0, hash-base@^3.1.2:
51435143
safe-buffer "^5.2.1"
51445144
to-buffer "^1.2.1"
51455145

5146+
hash-wasm@^4.11.0:
5147+
version "4.12.0"
5148+
resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb"
5149+
integrity sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==
5150+
51465151
hash.js@^1.0.0, hash.js@^1.0.3:
51475152
version "1.1.7"
51485153
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"

0 commit comments

Comments
 (0)