Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: esm support for keystore package and crypto ops #1049

Merged
merged 18 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
arboleya marked this conversation as resolved.
Show resolved Hide resolved
"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.

Loading