From ce6af87720bfc636895779fb5023cbded7633643 Mon Sep 17 00:00:00 2001 From: Sergey Kintsel Date: Sat, 19 Oct 2024 11:26:45 +0100 Subject: [PATCH] Add minimal anti-bruteforce measures --- packages/crypto/jest.config.ts | 2 +- packages/crypto/package.json | 3 ++- packages/crypto/src/AES.test.ts | 16 ++++++++++++---- packages/crypto/src/AES.ts | 25 ++++++++++++++++++++++++- packages/crypto/src/setupTests.ts | 25 +++++++++++++++++++++++++ pnpm-lock.yaml | 7 +++++-- 6 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 packages/crypto/src/setupTests.ts diff --git a/packages/crypto/jest.config.ts b/packages/crypto/jest.config.ts index 0615eecd5..6b3fe30e7 100644 --- a/packages/crypto/jest.config.ts +++ b/packages/crypto/jest.config.ts @@ -4,8 +4,8 @@ import type { Config } from "jest"; const config: Config = { ...baseConfig, - testEnvironment: "node", testTimeout: 10000, rootDir: "./", + setupFilesAfterEnv: ["/src/setupTests.ts"], }; export default config; diff --git a/packages/crypto/package.json b/packages/crypto/package.json index df55e8941..c79fdb104 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -58,6 +58,7 @@ ] }, "dependencies": { - "@taquito/utils": "^20.0.1" + "@taquito/utils": "^20.0.1", + "date-fns": "^4.1.0" } } diff --git a/packages/crypto/src/AES.test.ts b/packages/crypto/src/AES.test.ts index f7bd55e92..ccf16bcd1 100644 --- a/packages/crypto/src/AES.test.ts +++ b/packages/crypto/src/AES.test.ts @@ -1,6 +1,6 @@ import { mnemonic1, recoveredPhrases, umamiBackup } from "@umami/test-utils"; -import { decrypt, encrypt } from "./AES"; +import { TOO_MANY_ATTEMPTS_ERROR, decrypt, encrypt } from "./AES"; const password = "password"; @@ -14,7 +14,7 @@ describe("AES", () => { expect(decrypted).toEqual(mnemonic1); }); - it("decryption restores mnemonic from v1 backup file", async () => { + it("decrypts from v1 backup file", async () => { for (let i = 0; i < umamiBackup.recoveryPhrases.length; i++) { const encrypted = umamiBackup.recoveryPhrases[i]; const expected = recoveredPhrases[i]; @@ -29,16 +29,24 @@ describe("AES", () => { await expect(decrypt(encrypted, password, "V1")).rejects.toThrow(DECRYPTION_ERROR_MESSAGE); }); - it("decryption fails with cyclic password", async () => { + it("fails the decryption with cyclic password", async () => { // Used to work in V1. Now it fails. const encrypted = await encrypt(mnemonic1, "abc"); await expect(decrypt(encrypted, "abcabc")).rejects.toThrow(DECRYPTION_ERROR_MESSAGE); }); - it("decryption fails with wrong password", async () => { + it("fails the decryption with wrong password", async () => { const encrypted = await encrypt(mnemonic1, password); await expect(decrypt(encrypted, `wrong ${password}`)).rejects.toThrow(DECRYPTION_ERROR_MESSAGE); }); + + it("throws too many attempts error", async () => { + const encrypted = await encrypt(mnemonic1, password); + for (let i = 0; i < 3; i++) { + await expect(decrypt(encrypted, "wrong password")).rejects.toThrow(DECRYPTION_ERROR_MESSAGE); + } + await expect(decrypt(encrypted, "wrong password")).rejects.toThrow(TOO_MANY_ATTEMPTS_ERROR); + }); }); diff --git a/packages/crypto/src/AES.ts b/packages/crypto/src/AES.ts index 3df50c73b..20eedcce9 100644 --- a/packages/crypto/src/AES.ts +++ b/packages/crypto/src/AES.ts @@ -1,4 +1,5 @@ import { buf2hex, hex2Bytes } from "@taquito/utils"; +import { differenceInMinutes } from "date-fns"; import { AES_MODE } from "./AES_MODE"; import { derivePasswordBasedKeyV1, derivePasswordBasedKeyV2 } from "./KDF"; @@ -33,6 +34,9 @@ export const encrypt = async (data: string, password: string): Promise => { const { iv, salt, data: encrypted } = data; try { + if (getAttemptsCount() >= 3) { + const minutesSinceLastAttempt = differenceInMinutes( + new Date(), + new Date(localStorage.getItem("failedDecryptTime")!) + ); + if (minutesSinceLastAttempt < 5) { + throw new Error(TOO_MANY_ATTEMPTS_ERROR); + } + } const derivedKey = mode === "V2" ? await derivePasswordBasedKeyV2(password, hex2Bytes(salt)) @@ -52,8 +65,18 @@ export const decrypt = async ( derivedKey, hex2Bytes(encrypted) ); + setAttemptsCount(0); + localStorage.removeItem("failedDecryptTime"); return Buffer.from(decrypted).toString("utf-8"); - } catch (_) { + } catch (err: any) { + if (err?.message === TOO_MANY_ATTEMPTS_ERROR) { + throw err; + } + setAttemptsCount(getAttemptsCount() + 1); + localStorage.setItem("failedDecryptTime", new Date().toISOString()); throw new Error("Error decrypting data: Invalid password"); } }; + +const getAttemptsCount = () => Number(localStorage.getItem("passwordAttempts") || 0); +const setAttemptsCount = (count: number) => localStorage.setItem("passwordAttempts", String(count)); diff --git a/packages/crypto/src/setupTests.ts b/packages/crypto/src/setupTests.ts new file mode 100644 index 000000000..76ca2042d --- /dev/null +++ b/packages/crypto/src/setupTests.ts @@ -0,0 +1,25 @@ +import { webcrypto } from "crypto"; + +Object.defineProperties(global, { + crypto: { value: webcrypto, writable: true }, +}); + +const mockLocalStorage = () => { + const store: { [key: string]: string } = {}; + + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString(); + }, + removeItem: (key: string) => { + delete store[key]; + }, + }; +}; + +beforeEach(() => { + Object.defineProperty(window, "localStorage", { + value: mockLocalStorage(), + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78404a708..ff02a6dff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ importers: devDependencies: jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.4)) + version: 29.7.0(@types/node@22.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.1.0)(typescript@5.5.4)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1149,6 +1149,9 @@ importers: '@taquito/utils': specifier: ^20.0.1 version: 20.0.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 devDependencies: '@babel/core': specifier: ^7.25.7 @@ -1185,7 +1188,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.1.0)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.4)) madge: specifier: ^8.0.0 version: 8.0.0(typescript@5.5.4)