diff --git a/apps/mobile/index.js b/apps/mobile/index.js index 803fd0e80..bd42afd7f 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -1,11 +1,13 @@ import { Buffer } from "buffer"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import { registerRootComponent } from "expo"; import { ExpoRoot } from "expo-router"; import "react-native-get-random-values"; global.Buffer = Buffer; global.process = process; +global.AsyncStorage = AsyncStorage; // Must be exported or Fast Refresh won't update the context export function App() { diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index 195370bdf..554c167f6 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -10,6 +10,18 @@ const defaultConfig = getDefaultConfig(projectRoot); defaultConfig.watchFolders = [workspaceRoot]; +// Replace @umami/crypto dependency with @umami/crypto/react-native for mobile +defaultConfig.resolver.resolveRequest = (context, moduleName, platform) => { + if (moduleName === "@umami/crypto") { + return { + filePath: require.resolve("@umami/crypto/react-native"), + type: "sourceFile", + }; + } + + return context.resolveRequest(context, moduleName, platform); +}; + defaultConfig.resolver = { ...defaultConfig.resolver, sourceExts: [...defaultConfig.resolver.sourceExts, "cjs", "mjs"], diff --git a/apps/mobile/screens/ImportWallet/components/MnemonicGrid.tsx b/apps/mobile/screens/ImportWallet/components/MnemonicGrid.tsx index 856251113..8d0ac0bdb 100644 --- a/apps/mobile/screens/ImportWallet/components/MnemonicGrid.tsx +++ b/apps/mobile/screens/ImportWallet/components/MnemonicGrid.tsx @@ -82,8 +82,3 @@ export const MnemonicGrid = >({ ); }; - -const MnemonicGridItem = styled(XStack, { - flex: 1, - maxWidth: 100 / 3 + "%", -}); diff --git a/apps/mobile/screens/ImportWallet/components/MnemonicPasswordModal.tsx b/apps/mobile/screens/ImportWallet/components/MnemonicPasswordModal.tsx index 85f460b9d..4ab282508 100644 --- a/apps/mobile/screens/ImportWallet/components/MnemonicPasswordModal.tsx +++ b/apps/mobile/screens/ImportWallet/components/MnemonicPasswordModal.tsx @@ -15,6 +15,7 @@ const ONBOARDING_MODE = "mnemonic"; export const MnemonicPasswordModal = ({ mnemonic }: MnemonicPasswordModalProps) => { const { hideModal } = useModal(); + const form = useForm({ mode: "onBlur", defaultValues: { diff --git a/packages/crypto/.eslintrc.cjs b/packages/crypto/.eslintrc.cjs index effe41d41..af08845d6 100644 --- a/packages/crypto/.eslintrc.cjs +++ b/packages/crypto/.eslintrc.cjs @@ -6,4 +6,12 @@ module.exports = { parser: "@typescript-eslint/parser", tsconfigRootDir: __dirname, }, + overrides: [ + { + files: ["*.ts", "*.tsx"], + rules: { + "import/no-unused-modules": "off", + }, + }, + ], }; diff --git a/packages/crypto/global.d.ts b/packages/crypto/global.d.ts new file mode 100644 index 000000000..2ad3f0406 --- /dev/null +++ b/packages/crypto/global.d.ts @@ -0,0 +1,5 @@ +import { AsyncStorageStatic } from "@react-native-async-storage/async-storage"; + +declare global { + var AsyncStorage: AsyncStorageStatic; +} diff --git a/packages/crypto/package.json b/packages/crypto/package.json index d6e10dbd8..3d47b2f63 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -11,6 +11,11 @@ "import": "./dist/index.js", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" + }, + "./react-native": { + "import": "./dist/mobile/index.js", + "require": "./dist/mobile/index.cjs", + "types": "./dist/mobile/index.d.ts" } }, "devDependencies": { @@ -50,7 +55,8 @@ }, "tsup": { "entry": [ - "src/index.ts" + "src/index.ts", + "src/mobile/index.ts" ], "clean": true, "format": [ @@ -59,7 +65,6 @@ ] }, "dependencies": { - "@react-native-async-storage/async-storage": "^2.0.0", "@taquito/utils": "^21.0.0", "date-fns": "^4.1.0", "react-native-quick-crypto": "^0.7.11" diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 2314955c4..b1fb6dee1 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -1,21 +1,4 @@ export * from "./types"; - -import * as webKDF from "./KDF"; -import * as webAES from "./AES"; - -import * as mobileKDF from "./mobile/KDF"; -import * as mobileAES from "./mobile/AES"; - -const isMobile = () => { - try { - // Check if we're in a React Native environment - return !!require("react-native"); - } catch (e) { - return false; - } -}; - -export const { encrypt, decrypt } = isMobile() ? mobileAES : webAES; -export const { derivePasswordBasedKeyV1, derivePasswordBasedKeyV2 } = isMobile() - ? mobileKDF - : webKDF; +export * from "./KDF"; +export * from "./AES"; +export * from "./AES_MODE"; diff --git a/packages/crypto/src/mobile/AES.ts b/packages/crypto/src/mobile/AES.ts index 7d53dd992..0ac6a2349 100644 --- a/packages/crypto/src/mobile/AES.ts +++ b/packages/crypto/src/mobile/AES.ts @@ -1,14 +1,11 @@ import { buf2hex, hex2Bytes } from "@taquito/utils"; import { CustomError } from "@umami/utils"; -// import { differenceInMinutes } from "date-fns"; +import { differenceInMinutes } from "date-fns"; +import crypto from "react-native-quick-crypto"; import { AES_MODE } from "../AES_MODE"; import { derivePasswordBasedKeyV1, derivePasswordBasedKeyV2 } from "./KDF"; import { type EncryptedData } from "../types"; -// import AsyncStorage from "@react-native-async-storage/async-storage"; - -// for mobile we should use react-native-quick-crypto polifil over native crypto api -import crypto from "react-native-quick-crypto"; // NIST recommends a salt size of at least 128 bits (16 bytes) // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf @@ -49,15 +46,15 @@ export const decrypt = async ( ): Promise => { const { iv, salt, data: encrypted } = data; try { - // if ((await getAttemptsCount()) >= 3) { - // const minutesSinceLastAttempt = differenceInMinutes( - // new Date(), - // new Date((await AsyncStorage.getItem("failedDecryptTime"))!) - // ); - // if (minutesSinceLastAttempt < 5) { - // throw new CustomError(TOO_MANY_ATTEMPTS_ERROR); - // } - // } + if ((await getAttemptsCount()) >= 3) { + const minutesSinceLastAttempt = differenceInMinutes( + new Date(), + new Date((await AsyncStorage.getItem("failedDecryptTime"))!) + ); + if (minutesSinceLastAttempt < 5) { + throw new CustomError(TOO_MANY_ATTEMPTS_ERROR); + } + } const derivedKey = mode === "V2" ? await derivePasswordBasedKeyV2(password, hex2Bytes(salt)) @@ -70,19 +67,19 @@ export const decrypt = async ( derivedKey, hex2Bytes(encrypted) ); - // setAttemptsCount(0); - // await AsyncStorage.removeItem("failedDecryptTime"); + await setAttemptsCount(0); + await AsyncStorage.removeItem("failedDecryptTime"); return Buffer.from(decrypted).toString("utf-8"); } catch (err: any) { - // if (err?.message === TOO_MANY_ATTEMPTS_ERROR) { - // throw err; - // } - // setAttemptsCount((await getAttemptsCount()) + 1); - // await AsyncStorage.setItem("failedDecryptTime", new Date().toISOString()); + if (err?.message === TOO_MANY_ATTEMPTS_ERROR) { + throw err; + } + await setAttemptsCount((await getAttemptsCount()) + 1); + await AsyncStorage.setItem("failedDecryptTime", new Date().toISOString()); throw new CustomError("Error decrypting data: Invalid password"); } }; -// const getAttemptsCount = async () => Number((await AsyncStorage.getItem("passwordAttempts")) || 0); -// const setAttemptsCount = async (count: number) => -// await AsyncStorage.setItem("passwordAttempts", String(count)); +const getAttemptsCount = async () => Number((await AsyncStorage.getItem("passwordAttempts")) || 0); +const setAttemptsCount = async (count: number) => + await AsyncStorage.setItem("passwordAttempts", String(count)); diff --git a/packages/crypto/src/mobile/KDF.ts b/packages/crypto/src/mobile/KDF.ts index 2912403ca..63e102228 100644 --- a/packages/crypto/src/mobile/KDF.ts +++ b/packages/crypto/src/mobile/KDF.ts @@ -1,9 +1,8 @@ -import { CryptoKey } from "react-native-quick-crypto/lib/typescript/src/keys"; -import { AES_MODE } from "../AES_MODE"; -import { RandomTypedArrays } from "react-native-quick-crypto/lib/typescript/src/random"; - -// for mobile we should use react-native-quick-crypto polifil over native crypto api import crypto from "react-native-quick-crypto"; +import { type CryptoKey } from "react-native-quick-crypto/lib/typescript/src/keys"; +import { type RandomTypedArrays } from "react-native-quick-crypto/lib/typescript/src/random"; + +import { AES_MODE } from "../AES_MODE"; // Use the full output of the hash to maximize the derived key's strength (sha256) const KEY_SIZE = 32; diff --git a/packages/crypto/src/mobile/index.ts b/packages/crypto/src/mobile/index.ts new file mode 100644 index 000000000..1961b97f4 --- /dev/null +++ b/packages/crypto/src/mobile/index.ts @@ -0,0 +1,4 @@ +export * from "./KDF"; +export * from "./AES"; +export * from "../types"; +export * from "../AES_MODE"; diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index 42161d97f..99baaff10 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@umami/typescript-config/tsconfig.json", - "include": ["src", "jest.config.ts", ".eslintrc.cjs"] + "include": ["src", "jest.config.ts", ".eslintrc.cjs", "global.d.ts"] } diff --git a/packages/state/src/hooks/mnemonic.ts b/packages/state/src/hooks/mnemonic.ts index 71184532a..b3fe28611 100644 --- a/packages/state/src/hooks/mnemonic.ts +++ b/packages/state/src/hooks/mnemonic.ts @@ -89,7 +89,7 @@ export const useRestoreRevealedMnemonicAccounts = () => { curve, network ); - const seedFingerPrint = await generateHash(); + const seedFingerPrint = generateHash(); const accountLabels = getNextAvailableAccountLabels(label, pubKeyPairs.length); return pubKeyPairs.map(({ pk, pkh }, accountIndex) => ({ diff --git a/packages/tezos/.eslintrc.cjs b/packages/tezos/.eslintrc.cjs index effe41d41..af08845d6 100644 --- a/packages/tezos/.eslintrc.cjs +++ b/packages/tezos/.eslintrc.cjs @@ -6,4 +6,12 @@ module.exports = { parser: "@typescript-eslint/parser", tsconfigRootDir: __dirname, }, + overrides: [ + { + files: ["*.ts", "*.tsx"], + rules: { + "import/no-unused-modules": "off", + }, + }, + ], }; diff --git a/packages/tezos/src/helpers.ts b/packages/tezos/src/helpers.ts index f5f0db69a..2ba193fb0 100644 --- a/packages/tezos/src/helpers.ts +++ b/packages/tezos/src/helpers.ts @@ -1,15 +1,16 @@ +import crypto from "crypto"; + import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; import { DerivationType, LedgerSigner } from "@taquito/ledger-signer"; import { Parser } from "@taquito/michel-codec"; import { type Curves, InMemorySigner } from "@taquito/signer"; import { TezosToolkit } from "@taquito/taquito"; import { CustomError } from "@umami/utils"; -import crypto from "crypto"; import { FakeSigner } from "./fakeSigner"; import { type PublicKeyPair, type SignerConfig } from "./types"; -export const generateHash = async (): Promise => { +export const generateHash = (): string => { const utf8 = new TextEncoder().encode(Date.now().toString()); const hash = crypto.createHash("sha256").update(utf8).digest("hex"); return hash.slice(0, 8); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 583ea39a4..205d49104 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1529,9 +1529,6 @@ importers: packages/crypto: dependencies: - '@react-native-async-storage/async-storage': - specifier: ^2.0.0 - version: 2.1.0(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@15.1.3)(@types/react@18.3.12)(encoding@0.1.13)(react@18.3.1)) '@taquito/utils': specifier: ^21.0.0 version: 21.0.0