diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 67a2683130e..eae118a38e2 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -32,6 +32,7 @@ import AppInformation from '../../Views/Settings/AppInformation'; import DeveloperOptions from '../../Views/Settings/DeveloperOptions'; import Contacts from '../../Views/Settings/Contacts'; import FeatureFlagOverride from '../../Views/FeatureFlagOverride'; +import LibsodiumPoc from '../../Views/LibsodiumPoc'; import Wallet from '../../Views/Wallet'; import SecurityTrustScreen from '../../UI/SecurityTrust/Views/SecurityTrustScreen'; import AddAsset from '../../Views/AddAsset/AddAsset'; @@ -1467,6 +1468,11 @@ const MainNavigator = () => { options={{ headerShown: false }} /> )} + + typeof performance !== 'undefined' && typeof performance.now === 'function' + ? performance.now() + : Date.now(); + +/** + * Builds a `check(name, fn)` helper that runs `fn` `iterations` times, records + * pass/fail into `collected`, and reports the average execution time per run. + * Timing stops at the first failing iteration. + */ +const makeCheck = + (collected: CheckResult[], iterations: number) => + (name: string, fn: () => string | void) => { + try { + let detail = 'ok'; + const start = now(); + for (let i = 0; i < iterations; i++) { + detail = fn() || 'ok'; + } + const total = now() - start; + collected.push({ + name, + ok: true, + detail, + iterations, + avgMs: total / iterations, + }); + } catch (err) { + collected.push({ + name, + ok: false, + detail: err instanceof Error ? err.message : String(err), + iterations, + avgMs: null, + }); + } + }; + +/** + * Formats an average duration in ms with resolution appropriate to its size. + */ +const formatAvg = (avgMs: number): string => { + if (avgMs >= 100) { + return `${avgMs.toFixed(0)} ms`; + } + if (avgMs >= 1) { + return `${avgMs.toFixed(2)} ms`; + } + return `${avgMs.toFixed(4)} ms`; +}; + +/** + * Proof-of-concept screen that exercises two libsodium bindings inside the + * React Native (Hermes) runtime and reports whether each cryptographic + * primitive round-trips correctly. The bindings are libsodium-wrappers + * (pure-JS / WebAssembly) and react-native-libsodium (native JSI bindings). + */ +const LibsodiumPoc = () => { + const tw = useTailwind(); + const navigation = useNavigation(); + const [running, setRunning] = useState(false); + const [results, setResults] = useState([]); + const [summary, setSummary] = useState(null); + const [engine, setEngine] = useState(null); + const [iterationsInput, setIterationsInput] = useState('1'); + + const parseIterations = useCallback(() => { + const parsed = parseInt(iterationsInput, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 1; + }, [iterationsInput]); + + /** + * libsodium-wrappers (pure-JS / WebAssembly) — full wrapper API, including + * string helpers and the secretstream construction. + */ + const runJsChecks = useCallback(async () => { + setRunning(true); + setResults([]); + setSummary(null); + setEngine(null); + + const iterations = parseIterations(); + const collected: CheckResult[] = []; + const check = makeCheck(collected, iterations); + + try { + await _sodium.ready; + const sodium = _sodium; + + setEngine( + `libsodium-wrappers ${sodium.sodium_version_string()} · ${ + typeof WebAssembly === 'undefined' + ? 'pure-JS (asm.js backup — no WebAssembly)' + : 'WebAssembly available' + }`, + ); + + check('helpers: hex round-trip', () => { + const bytes = sodium.from_hex('deadbeef'); + if (sodium.to_hex(bytes) !== 'deadbeef') { + throw new Error('hex mismatch'); + } + return 'deadbeef -> bytes -> deadbeef'; + }); + + check('helpers: utf-8 string round-trip', () => { + const s = 'héllo libsodium 🔐'; + if (sodium.to_string(sodium.from_string(s)) !== s) { + throw new Error('string mismatch'); + } + return s; + }); + + check('helpers: base64 round-trip', () => { + const b = sodium.from_string('metamask'); + const b64 = sodium.to_base64(b); + if (sodium.to_hex(sodium.from_base64(b64)) !== sodium.to_hex(b)) { + throw new Error('base64 mismatch'); + } + return b64; + }); + + check('randombytes_buf(32)', () => { + const r = sodium.randombytes_buf(32); + if (!(r instanceof Uint8Array) || r.length !== 32) { + throw new Error('bad random buffer'); + } + return `len=${r.length}`; + }); + + check('crypto_generichash (BLAKE2b) known vector', () => { + const h = sodium.crypto_generichash( + 32, + sodium.from_string('test'), + null, + ); + if (sodium.to_hex(h) !== GENERICHASH_TEST_VECTOR) { + throw new Error(`got ${sodium.to_hex(h)}`); + } + return GENERICHASH_TEST_VECTOR.slice(0, 16) + '…'; + }); + + check('crypto_secretbox: encrypt/decrypt round-trip', () => { + const key = sodium.crypto_secretbox_keygen(); + const nonce = sodium.randombytes_buf( + sodium.crypto_secretbox_NONCEBYTES, + ); + const ct = sodium.crypto_secretbox_easy( + sodium.from_string('secretbox message'), + nonce, + key, + ); + const pt = sodium.crypto_secretbox_open_easy(ct, nonce, key); + if (sodium.to_string(pt) !== 'secretbox message') { + throw new Error('decrypt mismatch'); + } + return 'decrypted ok'; + }); + + check('crypto_secretbox: tampered ciphertext rejected', () => { + const key = sodium.crypto_secretbox_keygen(); + const nonce = sodium.randombytes_buf( + sodium.crypto_secretbox_NONCEBYTES, + ); + const ct = sodium.crypto_secretbox_easy( + sodium.from_string('tamper me'), + nonce, + key, + ); + ct[0] ^= 0xff; + let threw = false; + try { + sodium.crypto_secretbox_open_easy(ct, nonce, key); + } catch { + threw = true; + } + if (!threw) { + throw new Error('tampered ciphertext was accepted'); + } + return 'rejected as expected'; + }); + + check('crypto_box (X25519): public-key encryption', () => { + const alice = sodium.crypto_box_keypair(); + const bob = sodium.crypto_box_keypair(); + const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); + const ct = sodium.crypto_box_easy( + sodium.from_string('hello bob'), + nonce, + bob.publicKey, + alice.privateKey, + ); + const pt = sodium.crypto_box_open_easy( + ct, + nonce, + alice.publicKey, + bob.privateKey, + ); + if (sodium.to_string(pt) !== 'hello bob') { + throw new Error('decrypt mismatch'); + } + return `keyType=${alice.keyType}`; + }); + + check('crypto_sign (Ed25519): sign/verify + reject bad', () => { + const kp = sodium.crypto_sign_keypair(); + const msg = sodium.from_string('sign this'); + const sig = sodium.crypto_sign_detached(msg, kp.privateKey); + if (!sodium.crypto_sign_verify_detached(sig, msg, kp.publicKey)) { + throw new Error('valid signature rejected'); + } + sig[0] ^= 0xff; + if (sodium.crypto_sign_verify_detached(sig, msg, kp.publicKey)) { + throw new Error('invalid signature accepted'); + } + return `keyType=${kp.keyType}`; + }); + + check('crypto_secretstream_xchacha20poly1305 round-trip', () => { + const key = sodium.crypto_secretstream_xchacha20poly1305_keygen(); + const { state: sOut, header } = + sodium.crypto_secretstream_xchacha20poly1305_init_push(key); + const c1 = sodium.crypto_secretstream_xchacha20poly1305_push( + sOut, + sodium.from_string('message 1'), + null, + sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE, + ); + const c2 = sodium.crypto_secretstream_xchacha20poly1305_push( + sOut, + sodium.from_string('message 2'), + null, + sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL, + ); + const sIn = sodium.crypto_secretstream_xchacha20poly1305_init_pull( + header, + key, + ); + const r1 = sodium.crypto_secretstream_xchacha20poly1305_pull( + sIn, + c1, + null, + ); + const r2 = sodium.crypto_secretstream_xchacha20poly1305_pull( + sIn, + c2, + null, + ); + if (r1 === false || r2 === false) { + throw new Error('stream pull failed (bad tag/ciphertext)'); + } + if ( + sodium.to_string(r1.message) !== 'message 1' || + sodium.to_string(r2.message) !== 'message 2' + ) { + throw new Error('stream decrypt mismatch'); + } + return '2 messages decrypted'; + }); + + const passed = collected.filter((r) => r.ok).length; + setResults(collected); + setSummary( + `${passed}/${collected.length} checks passed · ${iterations}× each`, + ); + } catch (err) { + setResults(collected); + setSummary( + `libsodium-wrappers failed to initialize: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } finally { + setRunning(false); + } + }, [parseIterations]); + + /** + * react-native-libsodium (native JSI). Its native binding exposes a curated + * subset of the API — notably no string helpers (`from_string`/`from_hex`), + * no `sodium_version_string`, and no secretstream — but it does include the + * native `crypto_pwhash` (Argon2). Checks below stick to that subset and pass + * JS strings directly where the wrapper would normally take a `from_string` + * buffer. + */ + const runNativeChecks = useCallback(async () => { + setRunning(true); + setResults([]); + setSummary(null); + setEngine(null); + + const iterations = parseIterations(); + const collected: CheckResult[] = []; + const check = makeCheck(collected, iterations); + + try { + await _rnSodium.ready; + const sodium = _rnSodium; + + setEngine('react-native-libsodium (native JSI) · libsodium 1.0.21'); + + check('helpers: base64 round-trip', () => { + const b = sodium.randombytes_buf(16); + const b64 = sodium.to_base64(b); + if (sodium.to_hex(sodium.from_base64(b64)) !== sodium.to_hex(b)) { + throw new Error('base64 mismatch'); + } + return b64; + }); + + check('randombytes_buf(32)', () => { + const r = sodium.randombytes_buf(32); + if (!(r instanceof Uint8Array) || r.length !== 32) { + throw new Error('bad random buffer'); + } + return `len=${r.length}`; + }); + + check('crypto_generichash (BLAKE2b) known vector', () => { + // Native accepts JS strings directly (UTF-8), matching from_string. + const h = sodium.crypto_generichash(32, 'test', null); + if (sodium.to_hex(h) !== GENERICHASH_TEST_VECTOR) { + throw new Error(`got ${sodium.to_hex(h)}`); + } + return GENERICHASH_TEST_VECTOR.slice(0, 16) + '…'; + }); + + check('crypto_secretbox: encrypt/decrypt round-trip', () => { + const key = sodium.crypto_secretbox_keygen(); + const nonce = sodium.randombytes_buf( + sodium.crypto_secretbox_NONCEBYTES, + ); + const ct = sodium.crypto_secretbox_easy( + 'secretbox message', + nonce, + key, + ); + const pt = sodium.crypto_secretbox_open_easy(ct, nonce, key); + if (sodium.to_string(pt) !== 'secretbox message') { + throw new Error('decrypt mismatch'); + } + return 'decrypted ok'; + }); + + check('crypto_secretbox: tampered ciphertext rejected', () => { + const key = sodium.crypto_secretbox_keygen(); + const nonce = sodium.randombytes_buf( + sodium.crypto_secretbox_NONCEBYTES, + ); + const ct = sodium.crypto_secretbox_easy('tamper me', nonce, key); + ct[0] ^= 0xff; + let threw = false; + try { + sodium.crypto_secretbox_open_easy(ct, nonce, key); + } catch { + threw = true; + } + if (!threw) { + throw new Error('tampered ciphertext was accepted'); + } + return 'rejected as expected'; + }); + + check('crypto_box (X25519): public-key encryption', () => { + const alice = sodium.crypto_box_keypair(); + const bob = sodium.crypto_box_keypair(); + const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); + const ct = sodium.crypto_box_easy( + 'hello bob', + nonce, + bob.publicKey, + alice.privateKey, + ); + const pt = sodium.crypto_box_open_easy( + ct, + nonce, + alice.publicKey, + bob.privateKey, + ); + if (sodium.to_string(pt) !== 'hello bob') { + throw new Error('decrypt mismatch'); + } + return `keyType=${alice.keyType}`; + }); + + check('crypto_sign (Ed25519): sign/verify + reject bad', () => { + const kp = sodium.crypto_sign_keypair(); + const sig = sodium.crypto_sign_detached('sign this', kp.privateKey); + if ( + !sodium.crypto_sign_verify_detached(sig, 'sign this', kp.publicKey) + ) { + throw new Error('valid signature rejected'); + } + sig[0] ^= 0xff; + if ( + sodium.crypto_sign_verify_detached(sig, 'sign this', kp.publicKey) + ) { + throw new Error('invalid signature accepted'); + } + return `keyType=${kp.keyType}`; + }); + + check('crypto_pwhash (Argon2) deterministic — native only', () => { + const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES); + const k1 = sodium.crypto_pwhash( + 32, + 'correct horse battery staple', + salt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT, + ); + const k2 = sodium.crypto_pwhash( + 32, + 'correct horse battery staple', + salt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT, + ); + if (k1.length !== 32 || sodium.to_hex(k1) !== sodium.to_hex(k2)) { + throw new Error('pwhash not deterministic'); + } + return `derived ${k1.length}-byte key`; + }); + + const passed = collected.filter((r) => r.ok).length; + setResults(collected); + setSummary( + `${passed}/${collected.length} checks passed · ${iterations}× each`, + ); + } catch (err) { + setResults(collected); + setSummary( + `react-native-libsodium failed to initialize: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } finally { + setRunning(false); + } + }, [parseIterations]); + + const handleBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + return ( + + + + + Exercises two libsodium bindings inside the React Native runtime and + reports whether each primitive round-trips correctly. + + + + + Iterations per check + + + setIterationsInput(text.replace(/[^0-9]/g, '')) + } + onEndEditing={() => { + if (!iterationsInput || parseInt(iterationsInput, 10) < 1) { + setIterationsInput('1'); + } + }} + keyboardType="number-pad" + editable={!running} + placeholder="1" + style={tw.style( + 'w-20 px-3 py-2 rounded-lg border border-border-default text-text-default text-center bg-background-default', + )} + placeholderTextColor={tw.color('text-muted')} + testID="libsodium-poc-iterations-input" + /> + + + + + + + {running && !summary ? ( + + + + ) : null} + + {engine ? ( + + + {engine} + + + ) : null} + + {summary ? ( + + + {summary} + + + ) : null} + + {results.map((r) => ( + + + + {r.ok ? '✓' : '✗'} {r.name} + + {r.avgMs !== null ? ( + + {formatAvg(r.avgMs)} + + ) : null} + + + {r.detail} + {r.avgMs !== null && r.iterations > 1 + ? ` · avg over ${r.iterations} runs` + : ''} + + + ))} + + + ); +}; + +export default LibsodiumPoc; diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index cb66f96c035..91de62502e5 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -71,6 +71,9 @@ import { BadgeWrapper, BadgeWrapperPosition, BadgeWrapperPositionAnchorShape, + Button as MMDSButton, + ButtonVariant as MMDSButtonVariant, + ButtonSize as MMDSButtonSize, ButtonIcon, ButtonIconSize, IconColor as MMDSIconColor, @@ -1039,6 +1042,19 @@ const Wallet = ({ ) : null; + const libsodiumPocButton = + process.env.METAMASK_ENVIRONMENT !== 'production' ? ( + navigation.navigate(Routes.LIBSODIUM_POC)} + testID="wallet-libsodium-poc-button" + > + libsodium POC + + ) : null; + const portfolioHeaderBase = ( {bannerContent} @@ -1049,6 +1065,7 @@ const Wallet = ({ homeGrowthBannerContent} {homepageDiscoveryPills} {isMoneyAccountVisible && } + {libsodiumPocButton} ); @@ -1064,6 +1081,7 @@ const Wallet = ({ homeGrowthBannerContent} {homepageDiscoveryPills} {isMoneyAccountVisible && } + {libsodiumPocButton} ); diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 20eec0ff886..312da2bffb2 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -583,6 +583,7 @@ const Routes = { CONFIRM: 'AgenticCliDashboardConfirmation', }, FEATURE_FLAG_OVERRIDE: 'FeatureFlagOverride', + LIBSODIUM_POC: 'LibsodiumPoc', CREATE_PRICE_ALERT: 'CreatePriceAlert', MANAGE_PRICE_ALERTS: 'ManagePriceAlerts', SECURITY_TRUST: 'SecurityTrust', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5063e98376c..b37ea582d35 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2776,6 +2776,34 @@ PODS: - Yoga - react-native-launch-arguments (4.0.1): - React + - react-native-libsodium (1.7.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-mmkv (3.3.3): - boost - DoubleConversion @@ -4552,6 +4580,7 @@ DEPENDENCIES: - react-native-in-app-review (from `../node_modules/react-native-in-app-review`) - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) + - react-native-libsodium (from `../node_modules/react-native-libsodium`) - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -4872,6 +4901,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-keyboard-controller" react-native-launch-arguments: :path: "../node_modules/react-native-launch-arguments" + react-native-libsodium: + :path: "../node_modules/react-native-libsodium" react-native-mmkv: :path: "../node_modules/react-native-mmkv" react-native-netinfo: @@ -5160,6 +5191,7 @@ SPEC CHECKSUMS: react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400 react-native-keyboard-controller: dbf568b5d029cdc5b26667a7c2323b160c25dc00 react-native-launch-arguments: 7eb321ed3f3ef19b3ec4a2eca71c4f9baee76b41 + react-native-libsodium: 9b2056828bae2ada8ca3ec52666d24e26687c357 react-native-mmkv: ac7507625cd74bac0eb5333604a7cd7b08fe9e3e react-native-netinfo: 57447b5a45c98808f8eae292cf641f3d91d13830 react-native-pager-view: d7d2aa47f54343bf55fdcee3973503dd27c2bd37 diff --git a/metro.config.js b/metro.config.js index 3da33e91e43..970f1898bb9 100644 --- a/metro.config.js +++ b/metro.config.js @@ -132,6 +132,16 @@ module.exports = function (baseConfig) { 'node:buffer': '@craftzdog/react-native-buffer', }, resolveRequest: (context, moduleName, platform) => { + // libsodium.js references `node:fs` inside a Node-only runtime + // branch (gated on `process.versions.node`) that never executes + // in React Native. Metro still resolves the require statically, + // and `node:fs` is not shimmed, so stub it to an empty module. + if ( + moduleName === 'node:fs' && + context.originModulePath?.includes('libsodium') + ) { + return { type: 'empty' }; + } // MYXProvider is intentionally excluded from @metamask/perps-controller's // published dist (extension-only). The dynamic import() uses webpackIgnore // but babel's dynamicImportToRequire rewrites it to require(), causing Metro diff --git a/package.json b/package.json index 18055f28659..0385fc766dc 100644 --- a/package.json +++ b/package.json @@ -470,6 +470,7 @@ "ip-regex": "^5.0.0", "is-url": "^1.2.4", "js-sha3": "0.9.3", + "libsodium-wrappers": "^0.8.4", "lodash": "4.18.1", "lottie-react-native": "~7.3.1", "luxon": "^3.5.0", @@ -518,6 +519,7 @@ "react-native-keyboard-controller": "1.20.7", "react-native-keychain": "^9.0.0", "react-native-level-fs": "3.0.1", + "react-native-libsodium": "^1.7.0", "react-native-linear-gradient": "^2.8.3", "react-native-material-textfield": "0.16.1", "react-native-mmkv": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index a176b51a500..d95ad9d6a9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34983,6 +34983,38 @@ __metadata: languageName: node linkType: hard +"libsodium-sumo@npm:^0.8.0": + version: 0.8.4 + resolution: "libsodium-sumo@npm:0.8.4" + checksum: 10/dcb605269232b1724fb3f19d1d261ec0c47406f7102e16e5fa02ef3fd101b93ab6fa00b1a3029fbdfc5ea82533539d3221ed004afba53788099e8701baeb0797 + languageName: node + linkType: hard + +"libsodium-wrappers-sumo@npm:^0.8.2": + version: 0.8.4 + resolution: "libsodium-wrappers-sumo@npm:0.8.4" + dependencies: + libsodium-sumo: "npm:^0.8.0" + checksum: 10/026dfca86f7497ad7572491105c1eb11a423694c5742d29f38f4cdcd8f7ca76c083fc18a7448da0dfaec2fb764086e65775157f197474416598df41f8f419c8e + languageName: node + linkType: hard + +"libsodium-wrappers@npm:^0.8.2, libsodium-wrappers@npm:^0.8.4": + version: 0.8.4 + resolution: "libsodium-wrappers@npm:0.8.4" + dependencies: + libsodium: "npm:^0.8.0" + checksum: 10/186e9004482826a086e03017efca80405b7449f085cced48702adb8bb92d66e8c983f761550883cb1c078b01098ca459053d3c7c69ec3cdee7f86ceba25bd888 + languageName: node + linkType: hard + +"libsodium@npm:^0.8.0": + version: 0.8.4 + resolution: "libsodium@npm:0.8.4" + checksum: 10/14ce69b78350e81d92d60da78608df5d019825013b3140999d820c70fb7151f5b6e71e1d7c05ca88de4b46f8deda710a023a6ecd4a8e0b8544a4d5e129d1a769 + languageName: node + linkType: hard + "lie@npm:~3.3.0": version: 3.3.0 resolution: "lie@npm:3.3.0" @@ -36272,6 +36304,7 @@ __metadata: js-sha3: "npm:0.9.3" js-yaml: "npm:^4.1.0" koa: "npm:^2.14.2" + libsodium-wrappers: "npm:^0.8.4" lint-staged: "npm:10.5.4" listr2: "npm:^8.0.2" liveline: "npm:0.0.7" @@ -36335,6 +36368,7 @@ __metadata: react-native-keychain: "npm:^9.0.0" react-native-launch-arguments: "npm:^4.0.1" react-native-level-fs: "npm:3.0.1" + react-native-libsodium: "npm:^1.7.0" react-native-linear-gradient: "npm:^2.8.3" react-native-material-textfield: "npm:0.16.1" react-native-mmkv: "npm:^3.2.0" @@ -40783,6 +40817,20 @@ __metadata: languageName: node linkType: hard +"react-native-libsodium@npm:^1.7.0": + version: 1.7.0 + resolution: "react-native-libsodium@npm:1.7.0" + dependencies: + "@noble/hashes": "npm:^1.3.2" + libsodium-wrappers: "npm:^0.8.2" + libsodium-wrappers-sumo: "npm:^0.8.2" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/e1b6991df02bed1137fcd43dc3d5eccdeca7c65b21f08346ece5e47bc23a92c0f30ed172ad7b82afb331d96a8080a0f06ddb7b5ac82568db4aa52583c5e7eaf5 + languageName: node + linkType: hard + "react-native-linear-gradient@npm:^2.8.3": version: 2.8.3 resolution: "react-native-linear-gradient@npm:2.8.3"