Skip to content

Commit

Permalink
Add minimal anti-bruteforce measures
Browse files Browse the repository at this point in the history
  • Loading branch information
serjonya-trili committed Oct 21, 2024
1 parent ae08e86 commit ce6af87
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 9 deletions.
2 changes: 1 addition & 1 deletion packages/crypto/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { Config } from "jest";
const config: Config = {
...baseConfig,

testEnvironment: "node",
testTimeout: 10000,
rootDir: "./",
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
};
export default config;
3 changes: 2 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
]
},
"dependencies": {
"@taquito/utils": "^20.0.1"
"@taquito/utils": "^20.0.1",
"date-fns": "^4.1.0"
}
}
16 changes: 12 additions & 4 deletions packages/crypto/src/AES.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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];
Expand All @@ -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);
});
});
25 changes: 24 additions & 1 deletion packages/crypto/src/AES.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -33,13 +34,25 @@ export const encrypt = async (data: string, password: string): Promise<Encrypted

type DecryptMode = "V1" | "V2";

export const TOO_MANY_ATTEMPTS_ERROR =
"Too many unsuccessful attempts. Please wait a few minutes before trying again.";

export const decrypt = async (
data: EncryptedData,
password: string,
mode: DecryptMode = "V2"
): Promise<string> => {
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))
Expand All @@ -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));
25 changes: 25 additions & 0 deletions packages/crypto/src/setupTests.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
});
7 changes: 5 additions & 2 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 ce6af87

Please sign in to comment.