Skip to content

Commit

Permalink
fix: esm support for keystore package and crypto ops (#1049)
Browse files Browse the repository at this point in the history
  • Loading branch information
arboleya authored and nedsalk committed Jul 25, 2023
1 parent beda8f2 commit 59f1144
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 133 deletions.
6 changes: 6 additions & 0 deletions .changeset/neat-tips-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"fuels": patch
"@fuel-ts/keystore": patch
---

Fixing ESM support for NodeJS, using individual builds for Browser
7 changes: 7 additions & 0 deletions apps/demo-nodejs-esm/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { encrypt, decrypt } from "fuels";

/**
* Will throw if ESM support for NodeJS is broken:
* - https://github.com/FuelLabs/fuels-ts/issues/909
*/
export { encrypt, decrypt };
15 changes: 15 additions & 0 deletions apps/demo-nodejs-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"private": true,
"name": "demo-node-esm",
"description": "Simple NodeJS demo using ESM modules",
"author": "Fuel Labs <[email protected]> (https://fuel.network/)",
"type": "module",
"license": "Apache-2.0",
"scripts": {
"build": "pnpm test",
"test": "node ./index.mjs"
},
"dependencies": {
"fuels": "workspace:*"
}
}
3 changes: 3 additions & 0 deletions packages/keystore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"browser": {
"./dist/index.mjs": "./dist/index.browser.mjs"
},
"exports": {
".": {
"require": "./dist/index.js",
Expand Down
49 changes: 0 additions & 49 deletions packages/keystore/src/aes-ctr.ts

This file was deleted.

16 changes: 16 additions & 0 deletions packages/keystore/src/browser/aes-ctr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
describe.skip('Keystore', () => {
// TODO: mimick tests from `../node/aes-ctr.ts`
/**
* Testing the web version for this implementation requires us to run
* this test inside the browser, which we currently do not do, and,
* because of this, this file is just a stub-reminder of what needs
* to be done when we get there.
*
* The necessary tests should be similar or identical the ones we
* have on `../node/aes-ctr.ts`, but that can only be confirmed
* when we get to this point.
*/
test('Encrypt and Decrypt', () => {
expect(true).toBeTruthy;
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
import type { Keystore } from './aes-ctr';
import { bufferFromString, stringFromBuffer, keyFromPassword } from './aes-ctr';
import { arrayify } from '@ethersproject/bytes';
import { pbkdf2 } from '@ethersproject/pbkdf2';

import type { Keystore } from '../types';

import { btoa } from './crypto';
import { randomBytes } from './randomBytes';
import { crypto } from './universal-crypto';

const ALGORITHM = 'AES-CTR';

export function bufferFromString(
string: string,
encoding: 'utf-8' | 'base64' = 'base64'
): Uint8Array {
if (encoding === 'utf-8') {
return new TextEncoder().encode(string);
}

return new Uint8Array(
atob(string)
.split('')
.map((c) => c.charCodeAt(0))
);
}

export function stringFromBuffer(
buffer: Uint8Array,
_encoding: 'utf-8' | 'base64' = 'base64'
): string {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]));
}

/**
* Generate a pbkdf2 key from a password and random salt
*/
export function keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array {
const passBuffer = bufferFromString(String(password).normalize('NFKC'), 'utf-8');
const key = pbkdf2(passBuffer, saltBuffer, 100000, 32, 'sha256');

return arrayify(key);
}

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
Expand Down
11 changes: 11 additions & 0 deletions packages/keystore/src/browser/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { crypto, btoa } = globalThis;

if (!crypto) {
throw new Error(`Could not found 'crypto' in current browser environment`);
}

if (!btoa) {
throw new Error(`Could not found 'btoa' in current browser environment`);
}

export { crypto, btoa };
6 changes: 6 additions & 0 deletions packages/keystore/src/browser/randomBytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { crypto } from './crypto';

export const randomBytes = (length: number) => {
const randomValues = crypto.getRandomValues(new Uint8Array(length));
return randomValues;
};
3 changes: 3 additions & 0 deletions packages/keystore/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './browser/aes-ctr';
export * from './browser/randomBytes';
export * from './types';
4 changes: 3 additions & 1 deletion packages/keystore/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './keystore';
export * from './node/aes-ctr';
export * from './node/randomBytes';
export * from './types';
28 changes: 0 additions & 28 deletions packages/keystore/src/keystore.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as keystore from './keystore';
import { encrypt, decrypt } from './aes-ctr';

describe('Keystore', () => {
test('Encrypt and Decrypt', async () => {
const password = '0b540281-f87b-49ca-be37-2264c7f260f7';
const data = {
name: 'test',
};
const encryptedResult = await keystore.encrypt(password, data);
const encryptedResult = await encrypt(password, data);

expect(encryptedResult.data).toBeTruthy();
expect(encryptedResult.iv).toBeTruthy();
expect(encryptedResult.salt).toBeTruthy();

const decryptedResult = await keystore.decrypt(password, encryptedResult);
const decryptedResult = await decrypt(password, encryptedResult);

expect(decryptedResult).toEqual(data);
});
Expand All @@ -22,11 +22,9 @@ describe('Keystore', () => {
const data = {
name: 'test',
};
const encryptedResult = await keystore.encrypt(password, data);
const encryptedResult = await encrypt(password, data);

await expect(keystore.decrypt(`${password}123`, encryptedResult)).rejects.toThrow(
'Invalid credentials'
);
await expect(decrypt(`${password}123`, encryptedResult)).rejects.toThrow('Invalid credentials');
});

test('Decrypt Loop', async () => {
Expand All @@ -53,7 +51,7 @@ describe('Keystore', () => {
};

for (let i = 0; i < INPUTS.length; i += 1) {
const decryptedResult = await keystore.decrypt(password, INPUTS[i]);
const decryptedResult = await decrypt(password, INPUTS[i]);

expect(decryptedResult).toEqual(data);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import type { Keystore } from './aes-ctr';
import { bufferFromString, stringFromBuffer, keyFromPassword } from './aes-ctr';
import { arrayify } from '@ethersproject/bytes';
import { pbkdf2 } from '@ethersproject/pbkdf2';
import crypto from 'crypto';

import type { Keystore } from '../types';

import { randomBytes } from './randomBytes';
import { crypto } from './universal-crypto';

const ALGORITHM = 'aes-256-ctr';

export function bufferFromString(
string: string,
encoding: 'utf-8' | 'base64' = 'base64'
): Uint8Array {
return Buffer.from(string, encoding);
}

export function stringFromBuffer(
buffer: Uint8Array,
encoding: 'utf-8' | 'base64' = 'base64'
): string {
return Buffer.from(buffer).toString(encoding);
}

/**
* Generate a pbkdf2 key from a password and random salt
*/
export function keyFromPassword(password: string, saltBuffer: Uint8Array): Uint8Array {
const passBuffer = bufferFromString(String(password).normalize('NFKC'), 'utf-8');
const key = pbkdf2(passBuffer, saltBuffer, 100000, 32, 'sha256');

return arrayify(key);
}

/**
* Encrypts a data object that can be any serializable value using
* a provided password.
Expand Down
6 changes: 6 additions & 0 deletions packages/keystore/src/node/randomBytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import crypto from 'crypto';

export const randomBytes = (length: number) => {
const randomValues = crypto.randomBytes(length);
return randomValues;
};
6 changes: 0 additions & 6 deletions packages/keystore/src/randomBytes.ts

This file was deleted.

5 changes: 5 additions & 0 deletions packages/keystore/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Keystore {
data: string;
iv: string;
salt: string;
}
34 changes: 0 additions & 34 deletions packages/keystore/src/universal-crypto.ts

This file was deleted.

16 changes: 15 additions & 1 deletion packages/keystore/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { index } from '@internal/tsup';
import type { Options } from 'tsup';

export default index;
export const keystoreOptions: Options[] = [
{
...index,
entry: ['src/index.ts'],
format: ['cjs'],
},
{
...index,
entry: ['src/index.ts', 'src/index.browser.ts'],
format: ['esm'],
},
];

export default keystoreOptions;
6 changes: 6 additions & 0 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 59f1144

Please sign in to comment.