diff --git a/__mocks__/react-native-quick-crypto.ts b/__mocks__/react-native-quick-crypto.ts new file mode 100644 index 00000000000..e4c646146b6 --- /dev/null +++ b/__mocks__/react-native-quick-crypto.ts @@ -0,0 +1,2 @@ +// We can just use the actual node crypto module here +module.exports = jest.requireActual('crypto') diff --git a/android/app/build.gradle b/android/app/build.gradle index 26890ee6877..ecab7df0b54 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -261,6 +261,9 @@ android { exclude 'META-INF/-no-jdk.kotlin_module' exclude 'META-INF/androidx.exifinterface_exifinterface.version' pickFirst '**/libc++_shared.so' + // Prevents clashes with other libraries that use OpenSSL + // See https://github.com/margelo/react-native-quick-crypto/blob/de94007239bbb6dd9655f8a9bb13c14254a082eb/docs/troubleshooting.md#android-build-errors + pickFirst '**/libcrypto.so' } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 983e9109a62..3a3bc080774 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -599,6 +599,11 @@ PODS: - react-native-pager-view (6.3.4): - RCT-Folly (= 2021.07.22.00) - React-Core + - react-native-quick-crypto (0.7.3): + - OpenSSL-Universal + - RCT-Folly (= 2021.07.22.00) + - React + - React-Core - react-native-randombytes (3.6.1): - React-Core - react-native-restart (0.0.27): @@ -909,6 +914,7 @@ DEPENDENCIES: - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) + - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -1095,6 +1101,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/netinfo" react-native-pager-view: :path: "../node_modules/react-native-pager-view" + react-native-quick-crypto: + :path: "../node_modules/react-native-quick-crypto" react-native-randombytes: :path: "../node_modules/react-native-randombytes" react-native-restart: @@ -1287,6 +1295,7 @@ SPEC CHECKSUMS: react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc react-native-pager-view: e79d6c876ab1896e17ff61af6aff22a914b8e435 + react-native-quick-crypto: 303523bf21ad1aed8757dde262da12668b4c75d0 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-safe-area-context: ab8f4a3d8180913bd78ae75dd599c94cce3d5e9a diff --git a/metro.config.js b/metro.config.js index e4dbf7d534c..505f6c9ad68 100644 --- a/metro.config.js +++ b/metro.config.js @@ -34,6 +34,9 @@ const config = { ), extraNodeModules: { ...nodeLibs, + // This is the crypto module we want to use moving forward (unless something better comes up). + // It is implemented natively using OpenSSL. + crypto: require.resolve('react-native-quick-crypto'), fs: require.resolve('react-native-fs'), 'isomorphic-fetch': require.resolve('cross-fetch'), // We don't need the `net` module for now. diff --git a/package.json b/package.json index ef69751ddd4..c1fa69dd1ea 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "react-native-picker-select": "^9.1.3", "react-native-platform-touchable": "^1.1.1", "react-native-qrcode-svg": "^6.3.2", + "react-native-quick-crypto": "^0.7.3", "react-native-randombytes": "^3.6.1", "react-native-reanimated": "^3.15.0", "react-native-restart": "^0.0.27", diff --git a/src/backup/utils.test.ts b/src/backup/utils.test.ts index be7bd7433f6..2a6c57ebe18 100644 --- a/src/backup/utils.test.ts +++ b/src/backup/utils.test.ts @@ -1,4 +1,3 @@ -import * as bip39 from 'react-native-bip39' import { formatBackupPhraseOnEdit } from 'src/backup/utils' import { normalizeMnemonic, validateMnemonic } from 'src/utils/account' @@ -52,41 +51,41 @@ inner surprise invest` it('validates spanish successfully', () => { const mnemonic = normalizeMnemonic(SPANISH_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeTruthy() + expect(validateMnemonic(mnemonic)).toBeTruthy() }) it('validates spanish successfully without mnemonic accents', () => { const mnemonic = normalizeMnemonic(SPANISH_MNEMONIC_NO_ACCENTS) - expect(validateMnemonic(mnemonic, bip39)).toBeTruthy() + expect(validateMnemonic(mnemonic)).toBeTruthy() }) it('validates portuguese successfully', () => { const mnemonic = normalizeMnemonic(PORTUGUESE_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeTruthy() + expect(validateMnemonic(mnemonic)).toBeTruthy() }) it('validates english successfully', () => { const mnemonic = normalizeMnemonic(ENGLISH_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeTruthy() + expect(validateMnemonic(mnemonic)).toBeTruthy() }) it('validates english multiline successfully', () => { const mnemonic = normalizeMnemonic(MULTILINE_ENGLISH_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeTruthy() + expect(validateMnemonic(mnemonic)).toBeTruthy() }) it('does not validate bad english', () => { const mnemonic = normalizeMnemonic(BAD_ENGLISH_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeFalsy() + expect(validateMnemonic(mnemonic)).toBeFalsy() }) it('does not validate bad spanish', () => { const mnemonic = normalizeMnemonic(BAD_SPANISH_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeFalsy() + expect(validateMnemonic(mnemonic)).toBeFalsy() }) it('does not validate bad portuguese', () => { const mnemonic = normalizeMnemonic(BAD_PORTUGUESE_MNEMONIC) - expect(validateMnemonic(mnemonic, bip39)).toBeFalsy() + expect(validateMnemonic(mnemonic)).toBeFalsy() }) }) diff --git a/src/backup/utils.ts b/src/backup/utils.ts index 27b51727e48..2024a88eaa9 100644 --- a/src/backup/utils.ts +++ b/src/backup/utils.ts @@ -1,6 +1,5 @@ import CryptoJS from 'crypto-js' import { useAsync } from 'react-async-hook' -import * as bip39 from 'react-native-bip39' import { showError } from 'src/alert/actions' import AppAnalytics from 'src/analytics/AppAnalytics' import { OnboardingEvents } from 'src/analytics/Events' @@ -20,7 +19,7 @@ const MNEMONIC_STORAGE_KEY = 'mnemonic' export async function generateKeysFromMnemonic(mnemonic: string) { const wordCount = countMnemonicWords(mnemonic) const derivationPath = wordCount === 24 ? CELO_DERIVATION_PATH_BASE : ETHEREUM_DERIVATION_PATH - return generateKeys(mnemonic, undefined, undefined, undefined, bip39, derivationPath) + return generateKeys(mnemonic, undefined, undefined, undefined, derivationPath) } export async function storeMnemonic(mnemonic: string, account: string | null, password?: string) { diff --git a/src/import/saga.ts b/src/import/saga.ts index c4a008b8ff7..daf01523817 100644 --- a/src/import/saga.ts +++ b/src/import/saga.ts @@ -1,6 +1,5 @@ import { privateKeyToAddress } from '@celo/utils/lib/address' import { Task } from '@redux-saga/types' -import * as bip39 from 'react-native-bip39' import { setBackupCompleted } from 'src/account/actions' import { initializeAccountSaga } from 'src/account/saga' import { recoveringFromStoreWipeSelector } from 'src/account/selectors' @@ -52,7 +51,7 @@ export function* importBackupPhraseSaga({ phrase, useEmptyWallet }: ImportBackup Logger.debug(TAG + '@importBackupPhraseSaga', 'Importing backup phrase') try { const normalizedPhrase = normalizeMnemonic(phrase) - const phraseIsValid = validateMnemonic(normalizedPhrase, bip39) + const phraseIsValid = validateMnemonic(normalizedPhrase) const invalidWords = phraseIsValid ? [] : invalidMnemonicWords(normalizedPhrase) if (!phraseIsValid) { diff --git a/src/utils/account.ts b/src/utils/account.ts index 66aab9fc45b..3c809891cde 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -1,7 +1,10 @@ // Initially copied from https://github.com/celo-org/developer-tooling/blob/467d4e16444535d341bd2296d41c386f1dab187f/packages/sdk/cryptographic-utils/src/account.ts import { levenshteinDistance } from '@celo/utils/lib/levenshtein' import * as bip39 from '@scure/bip39' -import { randomBytes } from 'crypto' +// Provides fast and secure cryptographic functions from OpenSSL +// Note: we could also just import "crypto", since react-native-quick-crypto polyfills it +// But at least it's explicit here and we have more guarantees the expected implementation is being used +import crypto from 'react-native-quick-crypto' import { bytesToHex } from 'viem' import { HDKey, @@ -36,18 +39,9 @@ export enum MnemonicLanguages { portuguese, } -type RandomNumberGenerator = ( - size: number, - callback: (err: Error | null, buf: Buffer) => void -) => void - interface Bip39 { mnemonicToSeed: (mnemonic: string, password?: string) => Promise - generateMnemonic: ( - strength?: number, - rng?: RandomNumberGenerator, - wordlist?: string[] - ) => Promise + generateMnemonic: (strength?: number, wordlist?: string[]) => Promise validateMnemonic: (mnemonic: string, wordlist?: string[]) => boolean } @@ -63,20 +57,53 @@ const wordlists: Record = { [MnemonicLanguages.chinese_traditional]: traditionalChinese, } as const -function defaultGenerateMnemonic( +function salt(passphrase: string) { + return `mnemonic${passphrase}`.normalize('NFKD') +} + +// Note: here we don't use bip39.mnemonicToSeed because it's currently implemented in JS +// and it's slow: +// - 3+ seconds on a iPhone 13 Pro +// - 10+ seconds on a Pixel 4a +async function _mnemonicToSeed(mnemonic: string, passphrase = '') { + const mnemonicBuffer = Buffer.from(mnemonic, 'utf8') + const saltBuffer = Buffer.from(salt(passphrase), 'utf8') + return new Promise((resolve, reject) => + crypto.pbkdf2(mnemonicBuffer, saltBuffer, 2048, 64, 'SHA-512', (err, derivedKey) => { + if (err) { + reject(err) + } else { + if (!derivedKey) { + // This should never happen + reject(new Error('No derived key')) + } else { + resolve(derivedKey) + } + } + }) + ) +} + +// Note: we could use bip39.generateMnemonic +// because it uses crypto.randomBytes too +// but at least we have the guarantee that the expected implementation is being used +function _generateMnemonic( strength?: number, - rng?: RandomNumberGenerator, wordlist: string[] = wordlists[MnemonicLanguages.english] ): Promise { return new Promise((resolve, reject) => { strength = strength || 128 - rng = rng || randomBytes - rng(strength / 8, (error, randomBytesBuffer) => { + crypto.randomBytes(strength / 8, (error, randomBytesBuffer) => { if (error) { reject(error) } else { - resolve(bip39.entropyToMnemonic(randomBytesBuffer, wordlist)) + if (!randomBytesBuffer) { + // This should never happen + reject(new Error('No random bytes generated')) + } else { + resolve(bip39.entropyToMnemonic(randomBytesBuffer, wordlist)) + } } }) }) @@ -90,31 +117,26 @@ function _validateMnemonic( } const bip39Wrapper: Bip39 = { - mnemonicToSeed: bip39.mnemonicToSeed, - generateMnemonic: defaultGenerateMnemonic, + mnemonicToSeed: _mnemonicToSeed, + generateMnemonic: _generateMnemonic, validateMnemonic: _validateMnemonic, } export async function generateMnemonic( strength: MnemonicStrength = MnemonicStrength.s256_24words, - language?: MnemonicLanguages, - bip39ToUse = bip39Wrapper + language?: MnemonicLanguages ): Promise { - return bip39ToUse.generateMnemonic(strength, undefined, getWordList(language)) + return bip39Wrapper.generateMnemonic(strength, getWordList(language)) } -export function validateMnemonic( - mnemonic: string, - bip39ToUse = bip39Wrapper, - language?: MnemonicLanguages -) { +export function validateMnemonic(mnemonic: string, language?: MnemonicLanguages) { if (language !== undefined) { - return bip39ToUse.validateMnemonic(mnemonic, getWordList(language)) + return bip39Wrapper.validateMnemonic(mnemonic, getWordList(language)) } const languages = getAllLanguages() for (const guessedLanguage of languages) { - if (bip39ToUse.validateMnemonic(mnemonic, getWordList(guessedLanguage))) { + if (bip39Wrapper.validateMnemonic(mnemonic, getWordList(guessedLanguage))) { return true } } @@ -329,7 +351,7 @@ export function* suggestMnemonicCorrections( // Iterate over the generator of corrections, and return those that have a valid checksum. for (const suggestion of suggestUnvalidatedCorrections(words, lang)) { const phrase = joinMnemonic(suggestion, lang) - if (validateMnemonic(phrase, undefined, lang)) { + if (validateMnemonic(phrase, lang)) { yield phrase } } @@ -427,10 +449,9 @@ export async function generateKeys( password?: string, changeIndex: number = 0, addressIndex: number = 0, - bip39ToUse = bip39Wrapper, derivationPath: string = CELO_DERIVATION_PATH_BASE ): Promise<{ privateKey: string; publicKey: string; address: string }> { - const seed: Buffer = await generateSeed(mnemonic, password, bip39ToUse) + const seed: Buffer = await generateSeed(mnemonic, password) return generateKeysFromSeed(seed, changeIndex, addressIndex, derivationPath) } @@ -439,10 +460,9 @@ export async function generateKeys( async function generateSeed( mnemonic: string, password?: string, - bip39ToUse = bip39Wrapper, keyByteLength: number = 64 ): Promise { - let seed = Buffer.from(await bip39ToUse.mnemonicToSeed(mnemonic, password)) + let seed = Buffer.from(await bip39Wrapper.mnemonicToSeed(mnemonic, password ?? '')) if (keyByteLength > 0 && seed.byteLength > keyByteLength) { const bufAux = Buffer.allocUnsafe(keyByteLength) seed.copy(bufAux, 0, 0, keyByteLength) diff --git a/src/web3/saga.test.ts b/src/web3/saga.test.ts index 80ee235e4c6..96e65eae734 100644 --- a/src/web3/saga.test.ts +++ b/src/web3/saga.test.ts @@ -1,5 +1,4 @@ import { isValidChecksumAddress } from '@celo/utils/lib/address' -import * as bip39 from 'react-native-bip39' import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { call, select } from 'redux-saga/effects' @@ -8,15 +7,15 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import { storeMnemonic } from 'src/backup/utils' import { currentLanguageSelector } from 'src/i18n/selectors' import { getPasswordSaga, retrieveSignedMessage } from 'src/pincode/authentication' -import { generateMnemonic, MnemonicLanguages, MnemonicStrength } from 'src/utils/account' +import { MnemonicLanguages, MnemonicStrength, generateMnemonic } from 'src/utils/account' import { setAccount, setDataEncryptionKey } from 'src/web3/actions' import { + UnlockResult, getConnectedAccount, getConnectedUnlockedAccount, getOrCreateAccount, getWalletAddress, unlockAccount, - UnlockResult, } from 'src/web3/saga' import { currentAccountSelector, walletAddressSelector } from 'src/web3/selectors' import { createMockStore } from 'test/utils' @@ -101,8 +100,7 @@ describe(getOrCreateAccount, () => { .call( generateMnemonic, MnemonicStrength.s128_12words, - MnemonicLanguages[expectedMnemonicLang] as unknown as MnemonicLanguages, - bip39 + MnemonicLanguages[expectedMnemonicLang] as unknown as MnemonicLanguages ) .run() diff --git a/src/web3/saga.ts b/src/web3/saga.ts index 35f56556087..ac38ca9d397 100644 --- a/src/web3/saga.ts +++ b/src/web3/saga.ts @@ -1,21 +1,20 @@ import { privateKeyToAddress } from '@celo/utils/lib/address' import { UnlockableWallet } from '@celo/wallet-base' import { RpcWalletErrors } from '@celo/wallet-rpc/lib/rpc-wallet' -import * as bip39 from 'react-native-bip39' import { setAccountCreationTime } from 'src/account/actions' import { generateSignedMessage } from 'src/account/saga' import { ErrorMessages } from 'src/app/ErrorMessages' import { generateKeysFromMnemonic, storeMnemonic } from 'src/backup/utils' +import { clearPasswordCaches } from 'src/pincode/PasswordCache' import { CANCELLED_PIN_INPUT, getPasswordSaga, retrieveSignedMessage, } from 'src/pincode/authentication' -import { clearPasswordCaches } from 'src/pincode/PasswordCache' -import { generateMnemonic, MnemonicLanguages, MnemonicStrength } from 'src/utils/account' -import { ensureError } from 'src/utils/ensureError' import Logger from 'src/utils/Logger' -import { Actions, setAccount, SetAccountAction } from 'src/web3/actions' +import { MnemonicLanguages, MnemonicStrength, generateMnemonic } from 'src/utils/account' +import { ensureError } from 'src/utils/ensureError' +import { Actions, SetAccountAction, setAccount } from 'src/web3/actions' import { UNLOCK_DURATION } from 'src/web3/consts' import { getWallet, initContractKit } from 'src/web3/contracts' import { createAccountDek } from 'src/web3/dataEncryptionKey' @@ -45,7 +44,7 @@ export function* getOrCreateAccount() { const mnemonicBitLength = MnemonicStrength.s128_12words const mnemonicLanguage = MnemonicLanguages.english - let mnemonic: string = yield* call(generateMnemonic, mnemonicBitLength, mnemonicLanguage, bip39) + let mnemonic: string = yield* call(generateMnemonic, mnemonicBitLength, mnemonicLanguage) // Ensure no duplicates in mnemonic const checkDuplicate = (someString: string) => { @@ -54,7 +53,7 @@ export function* getOrCreateAccount() { let duplicateInMnemonic = checkDuplicate(mnemonic) while (duplicateInMnemonic) { Logger.debug(TAG + '@getOrCreateAccount', 'Regenerating mnemonic to avoid duplicates') - mnemonic = yield* call(generateMnemonic, mnemonicBitLength, mnemonicLanguage, bip39) + mnemonic = yield* call(generateMnemonic, mnemonicBitLength, mnemonicLanguage) duplicateInMnemonic = checkDuplicate(mnemonic) } diff --git a/yarn.lock b/yarn.lock index 92a76f6d71a..96011c067f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1368,6 +1368,14 @@ resolved "https://registry.yarnpkg.com/@coinbase/cbpay-js/-/cbpay-js-2.2.1.tgz#131b15e74e4780736d802b0254daba04fa3a1713" integrity sha512-u97f6RiS4XgO+GGMGc8OBMm+ZCJTtJrFym4z00jIj6uikU7af/YP+dFrlIfvBLy7Jt8xQzAoGU0Rzmz98DFWdg== +"@craftzdog/react-native-buffer@^6.0.5": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@craftzdog/react-native-buffer/-/react-native-buffer-6.0.5.tgz#0d4fbe0dd104186d2806655e3c0d25cebdae91d3" + integrity sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw== + dependencies: + ieee754 "^1.2.1" + react-native-quick-base64 "^2.0.5" + "@crowdin/ota-client@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@crowdin/ota-client/-/ota-client-0.7.0.tgz#6aef4f4be90b7f931175a274c68b669ef4044c6b" @@ -15428,6 +15436,24 @@ react-native-qrcode-svg@^6.3.2: qrcode "^1.5.1" text-encoding "^0.7.0" +react-native-quick-base64@^2.0.5: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.1.2.tgz#062b09b165c1530095fe99b94544c948318dbe99" + integrity sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA== + dependencies: + base64-js "^1.5.1" + +react-native-quick-crypto@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/react-native-quick-crypto/-/react-native-quick-crypto-0.7.3.tgz#731401a321e2e346f0933f3f47d3647425ba2537" + integrity sha512-Fw0/N5qwbGAMuzMeF6pS6P6v3R/iZEuqwc4Yim2s/hEqr5lJ+Yeh1JhXPEpDUXI1ndTzV/qUdfji6V8r/B5+KA== + dependencies: + "@craftzdog/react-native-buffer" "^6.0.5" + events "^3.3.0" + readable-stream "^4.5.2" + string_decoder "^1.3.0" + util "^0.12.5" + react-native-randombytes@^3.5.1, react-native-randombytes@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/react-native-randombytes/-/react-native-randombytes-3.6.1.tgz#cac578093b5ca38e3e085becffdc6cbcf6f0d654" @@ -15738,6 +15764,17 @@ readable-stream@^4.2.0: events "^3.3.0" process "^0.11.10" +readable-stream@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -16286,7 +16323,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -17161,14 +17198,14 @@ string_decoder@^1.0.0, string_decoder@^1.0.3, string_decoder@^1.1.1, string_deco dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - ansi-regex "^5.0.1" + safe-buffer "~5.2.0" -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==