Skip to content

Commit 17c78f4

Browse files
authored
feat: add timingSafeEqual() utility (#860)
1 parent 52c66cc commit 17c78f4

25 files changed

Lines changed: 276 additions & 317 deletions

docs/ios-setup.md

Lines changed: 0 additions & 95 deletions
This file was deleted.

example/ios/Podfile

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
22
ENV['SODIUM_ENABLED'] = '1'
33

4-
# Fix QuickCrypto SPM framework signing for physical devices
5-
require_relative '../../packages/react-native-quick-crypto/scripts/quickcrypto_spm_fix'
6-
74
# Resolve react_native_pods.rb with node to allow for hoisting
85
require Pod::Executable.execute_command('node', ['-p',
96
'require.resolve(
@@ -62,7 +59,5 @@ target 'QuickCryptoExample' do
6259
end
6360
end
6461

65-
# Fix QuickCrypto SPM framework signing for physical devices
66-
quickcrypto_fix_spm_signing(installer)
6762
end
6863
end

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2773,7 +2773,7 @@ SPEC CHECKSUMS:
27732773
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
27742774
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
27752775
NitroModules: 1715fe0e22defd9e2cdd48fb5e0dbfd01af54bec
2776-
QuickCrypto: e1cebaf5b3be427ce291f8230795e9fd915517ab
2776+
QuickCrypto: a9f8d3f4334c080896ff38f1812fe3c676f4bbc4
27772777
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
27782778
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
27792779
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
@@ -2845,6 +2845,6 @@ SPEC CHECKSUMS:
28452845
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
28462846
Yoga: 11c9686a21e2cd82a094a723649d9f4507200fb0
28472847

2848-
PODFILE CHECKSUM: c53d893905e8bc54582367d8191b1d2814070820
2848+
PODFILE CHECKSUM: 36c6d27eaa3ccd179672014ad06e56eb12f2a223
28492849

28502850
COCOAPODS: 1.15.2

example/ios/QuickCryptoExample.xcodeproj/project.pbxproj

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@
116116
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
117117
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
118118
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
119-
218A62117F2E6018338554EF /* [QuickCrypto] Embed & Sign SPM Frameworks */,
120119
);
121120
buildRules = (
122121
);
@@ -205,24 +204,6 @@
205204
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickCryptoExample/Pods-QuickCryptoExample-frameworks.sh\"\n";
206205
showEnvVarsInLog = 0;
207206
};
208-
218A62117F2E6018338554EF /* [QuickCrypto] Embed & Sign SPM Frameworks */ = {
209-
isa = PBXShellScriptBuildPhase;
210-
buildActionMask = 2147483647;
211-
files = (
212-
);
213-
inputFileListPaths = (
214-
);
215-
inputPaths = (
216-
);
217-
name = "[QuickCrypto] Embed & Sign SPM Frameworks";
218-
outputFileListPaths = (
219-
);
220-
outputPaths = (
221-
);
222-
runOnlyForDeploymentPostprocessing = 0;
223-
shellPath = /bin/sh;
224-
shellScript = "set -euo pipefail\n\n# Embed and sign SPM frameworks (OpenSSL) from QuickCrypto\n# This phase MUST run LAST, after all other framework embedding\n# See: https://github.com/margelo/react-native-quick-crypto/issues/857\n\nFRAMEWORKS_DIR=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\n\nsign_framework() {\n local framework_path=\"$1\"\n local framework_name=$(basename \"$framework_path\")\n\n if [ ! -d \"$framework_path\" ]; then\n echo \"warning: $framework_name not found at $framework_path, skipping\"\n return 0\n fi\n\n echo \"[QuickCrypto] Processing $framework_name...\"\n\n # Copy to app bundle\n rsync -av --delete \"$framework_path\" \"$FRAMEWORKS_DIR/\"\n\n local dest_framework=\"$FRAMEWORKS_DIR/$framework_name\"\n\n # Sign if required (physical device builds only)\n if [ \"${CODE_SIGNING_REQUIRED:-NO}\" = \"YES\" ] && [ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ]; then\n echo \"[QuickCrypto] Signing $framework_name with identity: ${EXPANDED_CODE_SIGN_IDENTITY}\"\n\n # Make framework writable (rsync preserves read-only from source)\n chmod -R u+w \"$dest_framework\"\n\n # Strip existing signature and re-sign with app's identity\n # This is required for pre-signed xcframeworks from SPM\n /usr/bin/codesign --force --deep --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" \\\n --timestamp=none \\\n \"$dest_framework\"\n\n echo \"[QuickCrypto] Successfully signed $framework_name\"\n else\n echo \"[QuickCrypto] Code signing not required (simulator build)\"\n fi\n}\n\n# Sign OpenSSL.framework from SPM\nsign_framework \"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n\necho \"[QuickCrypto] SPM framework embedding complete\"\n";
225-
};
226207
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
227208
isa = PBXShellScriptBuildPhase;
228209
buildActionMask = 2147483647;

example/src/benchmarks/scrypt/scrypt.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import rnqc from 'react-native-quick-crypto';
22
import * as noble from '@noble/hashes/scrypt';
3-
// @ts-expect-error - crypto-browserify is not typed
4-
import browserify from 'crypto-browserify';
53
import type { BenchFn } from '../../types/benchmarks';
64
import { Bench } from 'tinybench';
75

@@ -42,20 +40,6 @@ const scrypt_async: BenchFn = () => {
4240
})
4341
.add('@noble/hashes/scrypt', async () => {
4442
await noble.scryptAsync('password', 'salt', { N, r, p, dkLen: keylen });
45-
})
46-
.add('browserify/scrypt', async () => {
47-
await new Promise<void>((resolve, reject) => {
48-
browserify.scrypt(
49-
'password',
50-
'salt',
51-
keylen,
52-
{ N, r, p },
53-
(err: unknown) => {
54-
if (err) reject(err);
55-
else resolve();
56-
},
57-
);
58-
});
5943
});
6044

6145
bench.warmupTime = 100;
@@ -84,9 +68,6 @@ const scrypt_sync: BenchFn = () => {
8468
})
8569
.add('@noble/hashes/scrypt', () => {
8670
noble.scrypt('password', 'salt', { N, r, p, dkLen: keylen });
87-
})
88-
.add('browserify/scrypt', () => {
89-
browserify.scryptSync('password', 'salt', keylen, { N, r, p });
9071
});
9172

9273
bench.warmupTime = 100;

example/src/hooks/useBenchmarks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import hash from '../benchmarks/hash/hash';
88
import hmac from '../benchmarks/hmac/hmac';
99
import pbkdf2 from '../benchmarks/pbkdf2/pbkdf2';
1010
import random from '../benchmarks/random/randomBytes';
11+
import scrypt from '../benchmarks/scrypt/scrypt';
1112
import xsalsa20 from '../benchmarks/cipher/xsalsa20';
1213

1314
export const useBenchmarks = (): [
@@ -37,6 +38,7 @@ export const useBenchmarks = (): [
3738
'polyfilled with RNQC, so a somewhat senseless benchmark',
3839
}),
3940
);
41+
newSuites.push(new BenchmarkSuite('scrypt', scrypt));
4042
setSuites(newSuites);
4143
}, []);
4244

example/src/hooks/useTestsList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import '../tests/keys/sign_verify_streaming';
2121
import '../tests/pbkdf2/pbkdf2_tests';
2222
import '../tests/scrypt/scrypt_tests';
2323
import '../tests/random/random_tests';
24+
import '../tests/utils/timingSafeEqual_tests';
2425
import '../tests/subtle/x25519_x448';
2526
import '../tests/subtle/deriveBits';
2627
import '../tests/subtle/derive_key';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Buffer } from '@craftzdog/react-native-buffer';
2+
import { expect } from 'chai';
3+
import { test } from '../util';
4+
5+
import crypto from 'react-native-quick-crypto';
6+
7+
const SUITE = 'timingSafeEqual';
8+
9+
test(SUITE, 'should return true for equal buffers', () => {
10+
const a = Buffer.from('hello world');
11+
const b = Buffer.from('hello world');
12+
expect(crypto.timingSafeEqual(a, b)).to.equal(true);
13+
});
14+
15+
test(SUITE, 'should return false for different buffers', () => {
16+
const a = Buffer.from('hello world');
17+
const b = Buffer.from('hello worlD');
18+
expect(crypto.timingSafeEqual(a, b)).to.equal(false);
19+
});
20+
21+
test(SUITE, 'should work with Uint8Array', () => {
22+
const a = new Uint8Array([1, 2, 3, 4, 5]);
23+
const b = new Uint8Array([1, 2, 3, 4, 5]);
24+
expect(crypto.timingSafeEqual(a, b)).to.equal(true);
25+
});
26+
27+
test(SUITE, 'should return false for different Uint8Array', () => {
28+
const a = new Uint8Array([1, 2, 3, 4, 5]);
29+
const b = new Uint8Array([1, 2, 3, 4, 6]);
30+
expect(crypto.timingSafeEqual(a, b)).to.equal(false);
31+
});
32+
33+
test(SUITE, 'should work with ArrayBuffer', () => {
34+
const a = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer;
35+
const b = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer;
36+
expect(crypto.timingSafeEqual(a, b)).to.equal(true);
37+
});
38+
39+
test(SUITE, 'should throw for different length buffers', () => {
40+
const a = Buffer.from('hello');
41+
const b = Buffer.from('hello world');
42+
expect(() => crypto.timingSafeEqual(a, b)).to.throw(RangeError);
43+
});
44+
45+
test(SUITE, 'should work with empty buffers', () => {
46+
const a = Buffer.alloc(0);
47+
const b = Buffer.alloc(0);
48+
expect(crypto.timingSafeEqual(a, b)).to.equal(true);
49+
});
50+
51+
test(SUITE, 'should work with single byte buffers', () => {
52+
const a = Buffer.from([0xff]);
53+
const b = Buffer.from([0xff]);
54+
expect(crypto.timingSafeEqual(a, b)).to.equal(true);
55+
56+
const c = Buffer.from([0x00]);
57+
expect(crypto.timingSafeEqual(a, c)).to.equal(false);
58+
});
59+
60+
test(SUITE, 'should work for HMAC comparison use case', () => {
61+
const hmac1 = crypto
62+
.createHmac('sha256', 'secret')
63+
.update('message')
64+
.digest();
65+
const hmac2 = crypto
66+
.createHmac('sha256', 'secret')
67+
.update('message')
68+
.digest();
69+
const hmac3 = crypto
70+
.createHmac('sha256', 'secret')
71+
.update('different')
72+
.digest();
73+
74+
expect(crypto.timingSafeEqual(hmac1, hmac2)).to.equal(true);
75+
expect(crypto.timingSafeEqual(hmac1, hmac3)).to.equal(false);
76+
});

packages/react-native-quick-crypto/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ README.md
33

44
ios/**
55

6+
# Downloaded by CocoaPods prepare_command
7+
OpenSSL.xcframework/
8+
69
.cache/**
710
build/**
811
compile_commands.json

packages/react-native-quick-crypto/QuickCrypto.podspec

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,22 @@ Pod::Spec.new do |s|
2222
sodium_enabled = ENV['SODIUM_ENABLED'] == '1'
2323
Pod::UI.puts("[QuickCrypto] 🧂 has libsodium #{sodium_enabled ? "enabled" : "disabled"}!")
2424

25+
# OpenSSL 3.6+ vendored xcframework (not yet on CocoaPods trunk)
26+
openssl_version = "3.6.0000"
27+
openssl_url = "https://github.com/krzyzanowskim/OpenSSL/releases/download/#{openssl_version}/OpenSSL.xcframework.zip"
28+
2529
if sodium_enabled
2630
# Build libsodium from source for XSalsa20 cipher support
2731
# CocoaPods packages are outdated (1.0.12) and SPM causes module conflicts
2832
s.prepare_command = <<-CMD
2933
set -e
34+
# Download OpenSSL.xcframework
35+
if [ ! -d "OpenSSL.xcframework" ]; then
36+
curl -L -o OpenSSL.xcframework.zip #{openssl_url}
37+
unzip -o OpenSSL.xcframework.zip
38+
rm -f OpenSSL.xcframework.zip
39+
fi
40+
# Build libsodium
3041
mkdir -p ios
3142
curl -L -o ios/libsodium.tar.gz https://download.libsodium.org/libsodium/releases/libsodium-1.0.20-stable.tar.gz
3243
tar -xzf ios/libsodium.tar.gz -C ios
@@ -38,11 +49,21 @@ Pod::Spec.new do |s|
3849
CMD
3950
else
4051
s.prepare_command = <<-CMD
52+
set -e
53+
# Download OpenSSL.xcframework
54+
if [ ! -d "OpenSSL.xcframework" ]; then
55+
curl -L -o OpenSSL.xcframework.zip #{openssl_url}
56+
unzip -o OpenSSL.xcframework.zip
57+
rm -f OpenSSL.xcframework.zip
58+
fi
59+
# Clean up libsodium if previously built
4160
rm -rf ios/libsodium-stable
4261
rm -f ios/libsodium.tar.gz
4362
CMD
4463
end
4564

65+
s.vendored_frameworks = "OpenSSL.xcframework"
66+
4667
base_source_files = [
4768
# implementation (Swift)
4869
"ios/**/*.{swift}",
@@ -137,12 +158,5 @@ Pod::Spec.new do |s|
137158
s.dependency "React-jsi"
138159
s.dependency "React-callinvoker"
139160

140-
# OpenSSL 3.6+ via SPM for ML-DSA (post-quantum cryptography) support
141-
spm_dependency(s,
142-
url: 'https://github.com/krzyzanowskim/OpenSSL.git',
143-
requirement: {kind: 'upToNextMajorVersion', minimumVersion: '3.6.0'},
144-
products: ['OpenSSL']
145-
)
146-
147161
install_modules_dependencies(s)
148162
end

0 commit comments

Comments
 (0)