Skip to content

Commit

Permalink
feat: use native crypto functions (#61)
Browse files Browse the repository at this point in the history
* chore: use webcrypto for pbkdf2

* Use webcrypto for aes and sha256

* Remove unnecessary type cast

* Remove export

* chore: simplify

* chore: use node crypto

* chore: refactor environment sensing

* chore: add comments

* chore: refactor

* chore: cleanup kdf interfaces

---------

Co-authored-by: Nico Flaig <[email protected]>
  • Loading branch information
wemeetagain and nflaig authored Jul 15, 2024
1 parent 20f0b33 commit 750d2e8
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 48 deletions.
10 changes: 5 additions & 5 deletions src/checksum.ts → src/checksum/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sha256 } from "ethereum-cryptography/sha256";
import { concatBytes, equalsBytes, hexToBytes } from "ethereum-cryptography/utils";

import { IChecksumModule } from "./types";
import { IChecksumModule } from "../types";
import { sha256 } from "./sha256";

// default checksum configuration

Expand All @@ -17,17 +17,17 @@ function checksumData(key: Uint8Array, ciphertext: Uint8Array): Uint8Array {
return concatBytes(key.slice(16), ciphertext);
}

export function checksum(mod: IChecksumModule, key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
export async function checksum(mod: IChecksumModule, key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
if (mod.function === "sha256") {
return Promise.resolve(sha256(checksumData(key, ciphertext)));
return await sha256(checksumData(key, ciphertext));
} else {
throw new Error("Invalid checksum type");
}
}

export async function verifyChecksum(mod: IChecksumModule, key: Uint8Array, ciphertext: Uint8Array): Promise<boolean> {
if (mod.function === "sha256") {
return equalsBytes(hexToBytes(mod.message), sha256(checksumData(key, ciphertext)));
return equalsBytes(hexToBytes(mod.message), await sha256(checksumData(key, ciphertext)));
} else {
throw new Error("Invalid checksum type");
}
Expand Down
13 changes: 13 additions & 0 deletions src/checksum/sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { sha256 as _sha256 } from "ethereum-cryptography/sha256";

import { hasWebCrypto } from "../env";

export const sha256 = hasWebCrypto ? sha256WebCrypto : sha256Js;

async function sha256WebCrypto(data: Uint8Array): Promise<Uint8Array> {
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
}

async function sha256Js(data: Uint8Array): Promise<Uint8Array> {
return _sha256(data);
}
60 changes: 60 additions & 0 deletions src/cipher/aes128Ctr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { encrypt as aesEncrypt, decrypt as aesDecrypt } from "ethereum-cryptography/aes";

import { hasWebCrypto } from "../env";

export const aes128CtrEncrypt = hasWebCrypto ? aes128CtrEncryptWebCrypto : aes128CtrEncryptJs;
export const aes128CtrDecrypt = hasWebCrypto ? aes128CtrDecryptWebCrypto : aes128CtrDecryptJs;

export async function aes128CtrEncryptJs(key: Uint8Array, iv: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
return await aesEncrypt(
data,
key,
iv,
"aes-128-ctr",
false,
);
}

async function aes128CtrEncryptWebCrypto(
key: Uint8Array,
iv: Uint8Array,
data: Uint8Array
): Promise<Uint8Array> {
const cryptoKey = await crypto.subtle.importKey(
"raw",
key,
{name: "AES-CTR"},
false,
["encrypt"]
);
return new Uint8Array(await crypto.subtle.encrypt(
{ name: "AES-CTR", counter: iv, length: 128 },
cryptoKey,
data
));
}

export async function aes128CtrDecryptJs(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
return await aesDecrypt(
ciphertext,
key,
iv,
"aes-128-ctr",
false,
);
}

async function aes128CtrDecryptWebCrypto(key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
const cryptoKey = await crypto.subtle.importKey(
"raw",
key,
{name: "AES-CTR"},
false,
["decrypt"]
);
return new Uint8Array(await crypto.subtle.decrypt(
{ name: "AES-CTR", counter: iv, length: 128 },
cryptoKey,
ciphertext
));
}
22 changes: 5 additions & 17 deletions src/cipher.ts → src/cipher/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getRandomBytesSync } from "ethereum-cryptography/random";
import { encrypt as aesEncrypt, decrypt as aesDecrypt } from "ethereum-cryptography/aes";

import { ICipherModule } from "./types";
import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils";

import { ICipherModule } from "../types";
import { aes128CtrDecrypt, aes128CtrEncrypt } from "./aes128Ctr";

export function defaultAes128CtrModule(): Pick<ICipherModule, "function" | "params"> {
return {
function: "aes-128-ctr",
Expand All @@ -16,13 +16,7 @@ export function defaultAes128CtrModule(): Pick<ICipherModule, "function" | "para
export async function cipherEncrypt(mod: ICipherModule, key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
if (mod.function === "aes-128-ctr") {
try {
return await aesEncrypt(
data,
key,
hexToBytes(mod.params.iv),
mod.function,
false,
);
return await aes128CtrEncrypt(key, hexToBytes(mod.params.iv), data);
} catch (e) {
throw new Error("Unable to encrypt");
}
Expand All @@ -34,13 +28,7 @@ export async function cipherEncrypt(mod: ICipherModule, key: Uint8Array, data: U
export async function cipherDecrypt(mod: ICipherModule, key: Uint8Array): Promise<Uint8Array> {
if (mod.function === "aes-128-ctr") {
try {
return await aesDecrypt(
hexToBytes(mod.message),
key,
hexToBytes(mod.params.iv),
mod.function,
false,
);
return await aes128CtrDecrypt(key, hexToBytes(mod.params.iv), hexToBytes(mod.message));
} catch (e) {
throw new Error("Unable to decrypt")
}
Expand Down
8 changes: 8 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Is in nodejs environment */
export const isNode =
typeof process !== "undefined" &&
process.versions != null &&
process.versions.node != null;

/** Is in environment with web crypto */
export const hasWebCrypto = globalThis?.crypto?.subtle != null;
32 changes: 7 additions & 25 deletions src/kdf.ts → src/kdf/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getRandomBytesSync } from "ethereum-cryptography/random";
import { pbkdf2 } from "ethereum-cryptography/pbkdf2";
import { scrypt } from "ethereum-cryptography/scrypt";
import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils"

import { IKdfModule, IPbkdf2KdfModule, IScryptKdfModule } from "./types";
import { IKdfModule, IPbkdf2KdfModule, IScryptKdfModule } from "../types";
import { doPbkdf2 } from "./pbkdf2";
import { doScrypt } from "./scrypt";

// default kdf configurations

Expand Down Expand Up @@ -36,30 +36,12 @@ export function defaultScryptModule(): Pick<IScryptKdfModule, "function" | "para

export async function kdf(mod: IKdfModule, password: Uint8Array): Promise<Uint8Array> {
if (mod.function === "pbkdf2") {
return await doPbkdf2(mod.params, password);
const { salt, c, dklen } = mod.params;
return await doPbkdf2(hexToBytes(salt), c, dklen, password);
} else if (mod.function === "scrypt") {
return await doScrypt(mod.params, password);
const { salt, n, p, r, dklen } = mod.params;
return await doScrypt(hexToBytes(salt), n, p, r, dklen, password);
} else {
throw new Error("Invalid kdf type");
}
}
async function doPbkdf2(params: IPbkdf2KdfModule["params"], password: Uint8Array): Promise<Uint8Array> {
return pbkdf2(
password,
hexToBytes(params.salt),
params.c,
params.dklen,
params.prf.slice(5),
);
}

async function doScrypt(params: IScryptKdfModule["params"], password: Uint8Array): Promise<Uint8Array> {
return scrypt(
password,
hexToBytes(params.salt),
params.n,
params.p,
params.r,
params.dklen,
);
}
40 changes: 40 additions & 0 deletions src/kdf/pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { pbkdf2 } from "ethereum-cryptography/pbkdf2";

import { hasWebCrypto, isNode } from "../env";

export const doPbkdf2 = isNode ? doPbkdf2Node : hasWebCrypto ? doPbkdf2WebCrypto : doPbkdf2Js;

async function doPbkdf2Js(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise<Uint8Array> {
return pbkdf2(
password,
salt,
c,
dklen,
"sha256",
);
}

async function doPbkdf2Node(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise<Uint8Array> {
const crypto = await import("crypto");
return crypto.pbkdf2Sync(password, salt, c, dklen, "sha256");
}

async function doPbkdf2WebCrypto(salt: Uint8Array, c: number, dklen: number, password: Uint8Array): Promise<Uint8Array> {
const passwordKey = await crypto.subtle.importKey(
"raw",
password,
{name: "PBKDF2"},
false,
["deriveBits"],
);
return new Uint8Array(await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: c,
hash: "SHA-256",
},
passwordKey,
dklen * 8,
));
}
30 changes: 30 additions & 0 deletions src/kdf/scrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { scrypt } from "ethereum-cryptography/scrypt";

import { isNode } from "../env";


export const doScrypt = isNode ? doScryptNode : doScryptJs;

async function doScryptJs(salt: Uint8Array, n: number, p: number, r: number, dklen: number, password: Uint8Array): Promise<Uint8Array> {
return scrypt(
password,
salt,
n,
p,
r,
dklen,
);
}

async function doScryptNode(salt: Uint8Array, n: number, p: number, r: number, dklen: number, password: Uint8Array): Promise<Uint8Array> {
const crypto = await import("crypto");
return crypto.scryptSync(
password,
salt,
dklen, {
N: n,
r,
p,
maxmem: n * r * 256,
});
}
5 changes: 4 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ module.exports = {
},
devtool: "source-map",
resolve: {
extensions: [".ts", ".js"]
extensions: [".ts", ".js"],
fallback: {
"crypto": false,
},
},
module: {
rules: [
Expand Down

0 comments on commit 750d2e8

Please sign in to comment.