From 2ef8248c501d797426a6bf1544fb365637d76f34 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Mon, 4 Aug 2025 16:05:51 -0700 Subject: [PATCH] Replace react-native-quick-crypto with @noble/hashes (#841) * Add tests for ethers polyfills * Add crypto utils * Inline crypto polyfills into ethers util * sort and update gemfile lock * update lock --- app/Gemfile.lock | 8 ++--- app/ios/Podfile.lock | 27 -------------- app/package.json | 2 +- app/src/utils/ethers.ts | 64 ++++++++++++++++++++++++---------- app/tests/utils/ethers.test.ts | 55 +++++++++++++++++++++++++++++ yarn.lock | 56 +++-------------------------- 6 files changed, 111 insertions(+), 101 deletions(-) create mode 100644 app/tests/utils/ethers.test.ts diff --git a/app/Gemfile.lock b/app/Gemfile.lock index c1a8ed4de..ae7be5f11 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -25,8 +25,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1140.0) - aws-sdk-core (3.228.0) + aws-partitions (1.1141.0) + aws-sdk-core (3.229.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -34,10 +34,10 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.109.0) + aws-sdk-kms (1.110.0) aws-sdk-core (~> 3, >= 3.228.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.195.0) + aws-sdk-s3 (1.196.0) aws-sdk-core (~> 3, >= 3.228.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index ce28c40b6..6ddbbf684 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -1448,29 +1448,6 @@ PODS: - React-Core - react-native-nfc-manager (3.16.1): - React-Core - - react-native-quick-crypto (0.7.14): - - DoubleConversion - - glog - - hermes-engine - - OpenSSL-Universal - - RCT-Folly (= 2024.01.01.00) - - RCTRequired - - RCTTypeSafety - - React - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - react-native-safe-area-context (5.5.1): - React-Core - react-native-sqlite-storage (6.0.1): @@ -1930,7 +1907,6 @@ DEPENDENCIES: - react-native-get-random-values (from `../../node_modules/react-native-get-random-values`) - "react-native-netinfo (from `../../node_modules/@react-native-community/netinfo`)" - react-native-nfc-manager (from `../../node_modules/react-native-nfc-manager`) - - react-native-quick-crypto (from `../../node_modules/react-native-quick-crypto`) - react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`) - react-native-sqlite-storage (from `../../node_modules/react-native-sqlite-storage`) - React-nativeconfig (from `../../node_modules/react-native/ReactCommon`) @@ -2094,8 +2070,6 @@ EXTERNAL SOURCES: :path: "../../node_modules/@react-native-community/netinfo" react-native-nfc-manager: :path: "../../node_modules/react-native-nfc-manager" - react-native-quick-crypto: - :path: "../../node_modules/react-native-quick-crypto" react-native-safe-area-context: :path: "../../node_modules/react-native-safe-area-context" react-native-sqlite-storage: @@ -2261,7 +2235,6 @@ SPEC CHECKSUMS: react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-nfc-manager: a280ef94cd4871a471b052f0dc70381cf1223049 - react-native-quick-crypto: 8c04031798c1902ec3c6f4def538911a8f744d03 react-native-safe-area-context: 827032edf27079702cbd006f11dc79451a2d744b react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed React-nativeconfig: 31072ab0146e643594f6959c7f970a04b6c9ddd0 diff --git a/app/package.json b/app/package.json index 746c57cbe..3437de7f9 100644 --- a/app/package.json +++ b/app/package.json @@ -66,6 +66,7 @@ "dependencies": { "@babel/runtime": "^7.27.4", "@ethersproject/shims": "^5.7.0", + "@noble/hashes": "^1.5.0", "@openpassport/zk-kit-lean-imt": "^0.0.6", "@openpassport/zk-kit-smt": "^0.0.1", "@peculiar/x509": "^1.12.3", @@ -124,7 +125,6 @@ "react-native-localize": "^3.4.1", "react-native-nfc-manager": "^3.15.1", "react-native-passport-reader": "^1.0.3", - "react-native-quick-crypto": "^0.7.12", "react-native-safe-area-context": "^5.5.1", "react-native-screens": "4.9.0", "react-native-sqlite-storage": "^6.0.1", diff --git a/app/src/utils/ethers.ts b/app/src/utils/ethers.ts index e20af75e8..22c477b0c 100644 --- a/app/src/utils/ethers.ts +++ b/app/src/utils/ethers.ts @@ -1,27 +1,55 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 // https://docs.ethers.org/v6/cookbook/react-native/ +import { hmac } from '@noble/hashes/hmac'; +import { pbkdf2 as noblePbkdf2 } from '@noble/hashes/pbkdf2'; +import { sha256 as nobleSha256 } from '@noble/hashes/sha256'; +import { sha512 as nobleSha512 } from '@noble/hashes/sha512'; import { ethers } from 'ethers'; -import crypto from 'react-native-quick-crypto'; -ethers.randomBytes.register(length => { - return new Uint8Array(crypto.randomBytes(length)); -}); +function randomBytes(length: number): Uint8Array { + if (typeof globalThis.crypto?.getRandomValues !== 'function') { + throw new Error('globalThis.crypto.getRandomValues is not available'); + } + return globalThis.crypto.getRandomValues(new Uint8Array(length)); +} -ethers.computeHmac.register((algo, key, data) => { - return crypto.createHmac(algo, key).update(data).digest(); -}); +function computeHmac( + algo: 'sha256' | 'sha512', + key: Uint8Array, + data: Uint8Array, +): Uint8Array { + const hash = algo === 'sha256' ? nobleSha256 : nobleSha512; + return hmac(hash, key, data); +} -ethers.pbkdf2.register((passwd, salt, iter, keylen, algo) => { - return crypto.pbkdf2Sync(passwd, salt, iter, keylen, algo); -}); +function pbkdf2( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen: number, + algo: 'sha256' | 'sha512', +): Uint8Array { + const hash = algo === 'sha256' ? nobleSha256 : nobleSha512; + return noblePbkdf2(hash, password, salt, { c: iterations, dkLen: keylen }); +} -ethers.sha256.register(data => { - // @ts-expect-error - return crypto.createHash('sha256').update(data).digest(); -}); +function sha256(data: Uint8Array): Uint8Array { + return nobleSha256.create().update(data).digest(); +} -ethers.sha512.register(data => { - // @ts-expect-error - return crypto.createHash('sha512').update(data).digest(); -}); +function sha512(data: Uint8Array): Uint8Array { + return nobleSha512.create().update(data).digest(); +} + +ethers.randomBytes.register(randomBytes); + +ethers.computeHmac.register(computeHmac); + +ethers.pbkdf2.register(pbkdf2); + +ethers.sha256.register(sha256); + +ethers.sha512.register(sha512); + +export { computeHmac, pbkdf2, randomBytes, sha256, sha512 }; diff --git a/app/tests/utils/ethers.test.ts b/app/tests/utils/ethers.test.ts new file mode 100644 index 000000000..a8e8865bb --- /dev/null +++ b/app/tests/utils/ethers.test.ts @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +// Register crypto polyfills +import '../../src/utils/ethers'; + +import { ethers } from 'ethers'; + +describe('ethers crypto polyfills', () => { + it('randomBytes returns requested length and unique values', () => { + const a = ethers.randomBytes(16); + const b = ethers.randomBytes(16); + + expect(a).toHaveLength(16); + expect(b).toHaveLength(16); + expect(ethers.hexlify(a)).not.toBe(ethers.hexlify(b)); + }); + + it('computeHmac matches known vector', () => { + const result = ethers.computeHmac( + 'sha256', + ethers.toUtf8Bytes('key'), + ethers.toUtf8Bytes('data'), + ); + expect(ethers.hexlify(result)).toBe( + '0x5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0', + ); + }); + + it('pbkdf2 derives expected key', () => { + const derived = ethers.pbkdf2( + ethers.toUtf8Bytes('password'), + ethers.toUtf8Bytes('salt'), + 1000, + 32, + 'sha256', + ); + expect(ethers.hexlify(derived)).toBe( + '0x632c2812e46d4604102ba7618e9d6d7d2f8128f6266b4a03264d2a0460b7dcb3', + ); + }); + + it('sha256 hashes data correctly', () => { + const digest = ethers.sha256(ethers.toUtf8Bytes('hello')); + expect(ethers.hexlify(digest)).toBe( + '0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', + ); + }); + + it('sha512 hashes data correctly', () => { + const digest = ethers.sha512(ethers.toUtf8Bytes('hello')); + expect(ethers.hexlify(digest)).toBe( + '0x9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043', + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index c319a2b31..e8f63c239 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1099,16 +1099,6 @@ __metadata: languageName: node linkType: hard -"@craftzdog/react-native-buffer@npm:^6.0.5": - version: 6.1.0 - resolution: "@craftzdog/react-native-buffer@npm:6.1.0" - dependencies: - ieee754: "npm:^1.2.1" - react-native-quick-base64: "npm:^2.0.5" - checksum: 10c0/ec6115d5ae1924a329b5ce7bebe147b7e2464e8529088dc1be75d8de67837976f0ecc555b486684a81b7a4e9d779335cf9c9a68afd03849f971e47456b55eafc - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -2570,7 +2560,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.4.0": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 @@ -4623,6 +4613,7 @@ __metadata: "@babel/plugin-transform-private-methods": "npm:^7.23.3" "@babel/runtime": "npm:^7.27.4" "@ethersproject/shims": "npm:^5.7.0" + "@noble/hashes": "npm:^1.5.0" "@openpassport/zk-kit-lean-imt": "npm:^0.0.6" "@openpassport/zk-kit-smt": "npm:^0.0.1" "@peculiar/x509": "npm:^1.12.3" @@ -4716,7 +4707,6 @@ __metadata: react-native-localize: "npm:^3.4.1" react-native-nfc-manager: "npm:^3.15.1" react-native-passport-reader: "npm:^1.0.3" - react-native-quick-crypto: "npm:^0.7.12" react-native-safe-area-context: "npm:^5.5.1" react-native-screens: "npm:4.9.0" react-native-sqlite-storage: "npm:^6.0.1" @@ -14605,7 +14595,7 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.2.0, events@npm:^3.3.0": +"events@npm:^3.2.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 @@ -20586,7 +20576,7 @@ __metadata: languageName: node linkType: hard -"process@npm:^0.11.1, process@npm:^0.11.10": +"process@npm:^0.11.1": version: 0.11.10 resolution: "process@npm:0.11.10" checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 @@ -21140,29 +21130,6 @@ __metadata: languageName: node linkType: hard -"react-native-quick-base64@npm:^2.0.5": - version: 2.2.0 - resolution: "react-native-quick-base64@npm:2.2.0" - peerDependencies: - react: "*" - react-native: "*" - checksum: 10c0/f4c800aa3b4932228c406de86d70c8b4183e2522e941736d7edb4fbe7d9b6158e375cd197869a344f551fff8c5e609b4d6eb4d2ce6e8ef61dfb2aef42aadebff - languageName: node - linkType: hard - -"react-native-quick-crypto@npm:^0.7.12": - version: 0.7.14 - resolution: "react-native-quick-crypto@npm:0.7.14" - dependencies: - "@craftzdog/react-native-buffer": "npm:^6.0.5" - events: "npm:^3.3.0" - readable-stream: "npm:^4.5.2" - string_decoder: "npm:^1.3.0" - util: "npm:^0.12.5" - checksum: 10c0/7074fb108f2205a521394128dfe44805fcb56145972ba7e6247e679c51a8b90b7114a400c80a2d9317f3ab317e36787fa89e419fe45549a32db76a41333ca921 - languageName: node - linkType: hard - "react-native-safe-area-context@npm:^5.5.1": version: 5.5.1 resolution: "react-native-safe-area-context@npm:5.5.1" @@ -21537,19 +21504,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.5.2": - version: 4.7.0 - resolution: "readable-stream@npm:4.7.0" - dependencies: - abort-controller: "npm:^3.0.0" - buffer: "npm:^6.0.3" - events: "npm:^3.3.0" - process: "npm:^0.11.10" - string_decoder: "npm:^1.3.0" - checksum: 10c0/fd86d068da21cfdb10f7a4479f2e47d9c0a9b0c862fc0c840a7e5360201580a55ac399c764b12a4f6fa291f8cee74d9c4b7562e0d53b3c4b2769f2c98155d957 - languageName: node - linkType: hard - "readdirp@npm:^4.0.1": version: 4.1.2 resolution: "readdirp@npm:4.1.2" @@ -23182,7 +23136,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": +"string_decoder@npm:^1.1.1": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: