Skip to content

Commit

Permalink
feat: make crypto package web and mobile compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Feb 10, 2025
1 parent 3a62a83 commit 3987c4c
Show file tree
Hide file tree
Showing 20 changed files with 86 additions and 69 deletions.
2 changes: 2 additions & 0 deletions apps/mobile/index.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
5 changes: 0 additions & 5 deletions apps/mobile/screens/ImportWallet/components/MnemonicGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,3 @@ export const MnemonicGrid = <T extends FieldArrayWithId<FormValues>>({
</YStack>
);
};

const MnemonicGridItem = styled(XStack, {
flex: 1,
maxWidth: 100 / 3 + "%",
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const ONBOARDING_MODE = "mnemonic";

export const MnemonicPasswordModal = ({ mnemonic }: MnemonicPasswordModalProps) => {
const { hideModal } = useModal();

const form = useForm({
mode: "onBlur",
defaultValues: {
Expand Down
8 changes: 8 additions & 0 deletions packages/crypto/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ module.exports = {
parser: "@typescript-eslint/parser",
tsconfigRootDir: __dirname,
},
overrides: [
{
files: ["*.ts", "*.tsx"],
rules: {
"import/no-unused-modules": "off",
},
},
],
};
5 changes: 5 additions & 0 deletions packages/crypto/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AsyncStorageStatic } from "@react-native-async-storage/async-storage";

declare global {
var AsyncStorage: AsyncStorageStatic;
}
9 changes: 7 additions & 2 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -50,7 +55,8 @@
},
"tsup": {
"entry": [
"src/index.ts"
"src/index.ts",
"src/mobile/index.ts"
],
"clean": true,
"format": [
Expand All @@ -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"
Expand Down
23 changes: 3 additions & 20 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
45 changes: 21 additions & 24 deletions packages/crypto/src/mobile/AES.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,15 +46,15 @@ export const decrypt = async (
): Promise<string> => {
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))
Expand All @@ -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));
9 changes: 4 additions & 5 deletions packages/crypto/src/mobile/KDF.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 4 additions & 0 deletions packages/crypto/src/mobile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./KDF";
export * from "./AES";
export * from "../types";
export * from "../AES_MODE";
2 changes: 1 addition & 1 deletion packages/crypto/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 1 addition & 1 deletion packages/state/src/hooks/mnemonic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const testPublicKeys = {
};

beforeEach(() => {
jest.mocked(generateHash).mockResolvedValue("mockFingerPrint");
jest.mocked(generateHash).mockReturnValue("mockFingerPrint");
});

describe.each(["ed25519", "secp256k1", "p256"] as const)("with %s curve", curve => {
Expand Down
2 changes: 1 addition & 1 deletion packages/state/src/hooks/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/state/src/hooks/setAccountData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe.each(["ed25519", "secp256k1", "p256"] as const)(

describe("useRestoreFromMnemonic", () => {
beforeEach(() => {
jest.mocked(generateHash).mockResolvedValue(MOCK_FINGERPRINT);
jest.mocked(generateHash).mockReturnValue(MOCK_FINGERPRINT);
jest.mocked(encrypt).mockReturnValue(Promise.resolve(MOCK_ENCRYPTED));
});

Expand Down
4 changes: 2 additions & 2 deletions packages/state/src/thunks/changeMnemonicPassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ let fingerPrint1: string;
let fingerPrint2: string;

beforeAll(async () => {
fingerPrint1 = await generateHash();
fingerPrint1 = generateHash();
await new Promise(resolve => setTimeout(resolve, 1000)); // make sure the fingerprints are different
fingerPrint2 = await generateHash();
fingerPrint2 = generateHash();
});

beforeEach(async () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/tezos/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ module.exports = {
parser: "@typescript-eslint/parser",
tsconfigRootDir: __dirname,
},
overrides: [
{
files: ["*.ts", "*.tsx"],
rules: {
"import/no-unused-modules": "off",
},
},
],
};
4 changes: 2 additions & 2 deletions packages/tezos/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ describe("helpers", () => {
});
});

test("generateHash", async () => {
test("generateHash", () => {
MockDate.set("2022-12-27T14:15:49.760Z");
expect(await generateHash()).toEqual("a321a5e0");
expect(generateHash()).toEqual("a321a5e0");
});

describe("curveToDerivationPath", () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/tezos/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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);
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3987c4c

Please sign in to comment.