Mobile: Resolves #15287: Refactor (native) Crypto module to improve encryption performance on mobile - #15288
Conversation
Add `Crypto.bufferToString()` as a wrapper to `QuickCrypto.ab2str()` on mobile Replace `Buffer.from()`/`toString()` with `QuickCrypto.binaryLikeToArrayBuffer()`/`Crypto.bufferToString()` on mobile Replace the interface of `CryptoBuffer` from `Uint8Array`+`toString()` to `Uint8Array<ArrayBuffer>` Unify the input/output data type of `Crypto` module to `CryptoBuffer` Add `CryptoBufferEncoding`
|
For 16MB resource file:
Performance tests in
Log of fd72ee8, with e2ee profiling patchLog of b1bb66a, with e2ee profiling patch |
|
Did you do any testing on Android? There are some tests which run at startup, when you start the dev build of the Android app |
|
Note: For future reference, this pull request is related to upstream contributions by @wh201906 to |
Yes. #15288 (comment) is based on the release build (for an accurate result) running on a real Android device, with some modifications to force enable the Core diff of the profiling patchdiff --git a/packages/app-mobile/utils/buildStartupTasks.ts b/packages/app-mobile/utils/buildStartupTasks.ts
index 07bd80740..0e84c4823 100644
--- a/packages/app-mobile/utils/buildStartupTasks.ts
+++ b/packages/app-mobile/utils/buildStartupTasks.ts
@@ -481,9 +481,10 @@ const buildStartupTasks = (
// ----------------------------------------------------------------------------
if (Setting.value('env') === 'dev') {
await runRsaIntegrationTests();
- await runCryptoIntegrationTests();
+ // await runCryptoIntegrationTests();
await runOnDeviceFsDriverTests();
}
+ await runCryptoIntegrationTests(false, true);
// ----------------------------------------------------------------------------
// Keep this below to test react-native-rsa-native
diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts
index ca21d98ca..14c4d4d3e 100644
--- a/packages/lib/services/e2ee/EncryptionService.ts
+++ b/packages/lib/services/e2ee/EncryptionService.ts
@@ -602,7 +622,10 @@ export default class EncryptionService {
let doneSize = 0;
while (true) {
+ const totalStart = timingNow();
+ const readStart = totalStart;
const block = await source.read(chunkSize);
+ const readMs = timingNow() - readStart;
if (!block) break;
doneSize += chunkSize;
@@ -610,13 +633,31 @@ export default class EncryptionService {
// Wait for a frame so that the app remains responsive in mobile.
// https://corbt.com/posts/2015/12/22/breaking-up-heavy-processing-in-react-native.html
+ const waitStart = timingNow();
await shim.waitForFrame();
+ const waitMs = timingNow() - waitStart;
+ const cryptoStart = timingNow();
const encrypted = await this.encrypt(method, masterKeyPlainText, block);
+ const cryptoMs = timingNow() - cryptoStart;
+
+ const nonceStart = timingNow();
await crypto.increaseNonce(this.encryptionNonce_);
+ const nonceMs = timingNow() - nonceStart;
+ const appendStart = timingNow();
await destination.append(padLeft(encrypted.length.toString(16), 6, '0'));
await destination.append(encrypted);
+ const appendMs = timingNow() - appendStart;
+
+ const totalMs = timingNow() - totalStart;
}
}
@@ -631,7 +672,10 @@ export default class EncryptionService {
let doneSize = 0;
while (true) {
+ const totalStart = timingNow();
+ const lengthReadStart = totalStart;
const lengthHex = await source.read(6);
+ const lengthReadMs = timingNow() - lengthReadStart;
if (!lengthHex) break;
if (lengthHex.length !== 6) throw new Error(`Invalid block size: ${lengthHex}`);
const length = parseInt(lengthHex, 16);
@@ -640,12 +684,30 @@ export default class EncryptionService {
doneSize += length;
if (options.onProgress) options.onProgress({ doneSize: doneSize });
+ const waitStart = timingNow();
await shim.waitForFrame();
+ const waitMs = timingNow() - waitStart;
+ const blockReadStart = timingNow();
const block = await source.read(length);
+ const blockReadMs = timingNow() - blockReadStart;
+ const cryptoStart = timingNow();
const plainText = await this.decrypt(header.encryptionMethod, masterKeyPlainText, block);
+ const cryptoMs = timingNow() - cryptoStart;
+
+ const appendStart = timingNow();
await destination.append(plainText);
+ const appendMs = timingNow() - appendStart;
+
+ const totalMs = timingNow() - totalStart;
}
}
diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts
index 43333f225..43c931f66 100644
--- a/packages/lib/services/e2ee/cryptoTestUtils.ts
+++ b/packages/lib/services/e2ee/cryptoTestUtils.ts
@@ -264,30 +264,31 @@ export const runIntegrationTests = async (silent = false, testPerformance = fals
log('Testing performance...');
if (shim.mobilePlatform() === '') {
await testStringPerformance(EncryptionMethod.StringV1, 100, 1000);
- await testStringPerformance(EncryptionMethod.StringV1, 1000000, 10);
- await testStringPerformance(EncryptionMethod.StringV1, 5000000, 10);
- await testStringPerformance(EncryptionMethod.SJCL1a, 100, 1000);
- await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 10);
- await testStringPerformance(EncryptionMethod.SJCL1a, 5000000, 10);
- await testFilePerformance(EncryptionMethod.FileV1, 100, 1000);
- await testFilePerformance(EncryptionMethod.FileV1, 1000000, 3);
- await testFilePerformance(EncryptionMethod.FileV1, 5000000, 3);
- await testFilePerformance(EncryptionMethod.SJCL1a, 100, 1000);
- await testFilePerformance(EncryptionMethod.SJCL1a, 1000000, 3);
- await testFilePerformance(EncryptionMethod.SJCL1a, 5000000, 3);
+ // await testStringPerformance(EncryptionMethod.StringV1, 1000000, 10);
+ // await testStringPerformance(EncryptionMethod.StringV1, 5000000, 10);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 100, 1000);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 10);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 5000000, 10);
+ // await testFilePerformance(EncryptionMethod.FileV1, 100, 1000);
+ // await testFilePerformance(EncryptionMethod.FileV1, 1000000, 3);
+ // await testFilePerformance(EncryptionMethod.FileV1, 5000000, 3);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 100, 1000);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 1000000, 3);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 5000000, 3);
} else {
await testStringPerformance(EncryptionMethod.StringV1, 100, 100);
- await testStringPerformance(EncryptionMethod.StringV1, 500000, 3);
- await testStringPerformance(EncryptionMethod.StringV1, 1000000, 3);
- await testStringPerformance(EncryptionMethod.SJCL1a, 100, 100);
- await testStringPerformance(EncryptionMethod.SJCL1a, 500000, 3);
- await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 3);
+ await testStringPerformance(EncryptionMethod.StringV1, 500000, 5);
+ await testStringPerformance(EncryptionMethod.StringV1, 1000000, 5);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 100, 100);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 500000, 3);
+ // await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 3);
await testFilePerformance(EncryptionMethod.FileV1, 100, 100);
- await testFilePerformance(EncryptionMethod.FileV1, 100000, 3);
- await testFilePerformance(EncryptionMethod.FileV1, 500000, 3);
- await testFilePerformance(EncryptionMethod.SJCL1a, 100, 100);
- await testFilePerformance(EncryptionMethod.SJCL1a, 100000, 3);
- await testFilePerformance(EncryptionMethod.SJCL1a, 500000, 3);
+ await testFilePerformance(EncryptionMethod.FileV1, 100000, 5);
+ await testFilePerformance(EncryptionMethod.FileV1, 500000, 5);
+ await testFilePerformance(EncryptionMethod.FileV1, 16000000, 5);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 100, 100);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 100000, 3);
+ // await testFilePerformance(EncryptionMethod.SJCL1a, 500000, 3);
}
}
I also tested the dev build on Android and found no errors during startup (b1bb66a) |
It's also related to margelo/react-native-quick-crypto@9a30242 from boorad. I cannot integrate |
| }; | ||
|
|
||
| const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise<CryptoBuffer> => { | ||
| const pbkdf2Raw = (password: string, salt: CryptoBuffer | ArrayBuffer, iterations: number, keylen: number, digest: Digest): Promise<CryptoBuffer> => { |
There was a problem hiding this comment.
There is a fast path for QuickCrypto.pbkdf2(string, salt, ...) when the type of salt is ArrayBuffer (v1.1.2), so I didn't unify the input to CryptoBuffer(Uint8Array) first.
https://github.com/margelo/react-native-quick-crypto/blob/v1.1.2/packages/react-native-quick-crypto/src/pbkdf2.ts#L83
https://github.com/margelo/react-native-quick-crypto/blob/v1.1.2/packages/react-native-quick-crypto/src/pbkdf2.ts#L63
https://github.com/margelo/react-native-quick-crypto/blob/v1.1.2/packages/react-native-quick-crypto/src/utils/conversion.ts#L195-L197
|
I added some comment in the PR, hope this helps. |
|
Looks good to me and the performance gain seems impressive, thanks for working on this @wh201906. Just waiting for confirmation from @personalizedrefrigerator since he knows the native crypto part better than me |
|
This isn't building for me on MacOS (x86_64 Mac, building for iOS simulator): I'm currently cleaning cached files and plan to try a full rebuild. |
personalizedrefrigerator
left a comment
There was a problem hiding this comment.
I'm currently cleaning cached files and plan to try a full rebuild.
The full rebuild (or an XCode update) seems to have resolved the issue! I've verified that the new startup tests pass successfully both on a simulator and a physical device.
This PR refactors
Cryptomodule to use the fast buffer/string conversion feature inreact-native-quick-crypto. Specifically:react-native-quick-cryptoto 1.1.2 to use the newab2str()APICrypto.bufferToString(buffer: CryptoBuffer, encoding: CryptoBufferEncoding)as a wrapper toQuickCrypto.ab2str(buf: ArrayBuffer, ...)on mobileCrypto.bufferToString()tries to convertCryptoBufferto string without copy. IfCryptoBufferis aBuffer, callBuffer.toString()directly. Otherwise, create aBufferas a view (no copy) to theCryptoBuffer(Uint8array) then call.toString()Buffer.from()/toString()withQuickCrypto.binaryLikeToArrayBuffer()/Crypto.bufferToString()on mobilehex/utf16le/base64encoding and decoding performance are significantly improved.CryptoBufferfromUint8Array+toString()toUint8Array<ArrayBuffer>.toString()is not required anymore.Bufferin Node.js and other JS-only implementations is a subclass ofUin8Array, so it's safe to use Uint8Array hereUint8Array<SharedArrayBuffer>. Inspired by fix: reject SharedArrayBuffer in WebCrypto and getRandomValues margelo/react-native-quick-crypto#1019Cryptomodule toCryptoBuffer.CryptoBufferEncodingQuickCryptodoesn't accept allBufferEncodingbecause it doesn't normalize encoding name. Declare the names we need for better type checking.Crypto.bufferToString()