Skip to content

Commit

Permalink
refactor(lib): simplified lib by externalizing crypto lib
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh committed Sep 19, 2024
1 parent 9522dab commit ed223f7
Show file tree
Hide file tree
Showing 38 changed files with 730 additions and 621 deletions.
24 changes: 24 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Enclosed crypto library

This package contains the natives crypto primitives used by the [Enclosed lib and project](https://enclosed.cc). It is a standalone package built to provide compatibility between different environments by exposing the same API for node:crypto and web SubtleCrypto.

## Installation

```bash
# with npm
npm install @enclosed/crypto

# with yarn
yarn add @enclosed/crypto

# with pnpm
pnpm add @enclosed/crypto
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for more information.

## Credits and Acknowledgements

This project is crafted with ❤️ by [Corentin Thomasset](https://corentin.tech).
14 changes: 14 additions & 0 deletions packages/crypto/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
entries: [
'src/index.node',
'src/index.web',
],
clean: true,
declaration: true,
sourcemap: true,
rollup: {
emitCJS: true,
},
});
21 changes: 21 additions & 0 deletions packages/crypto/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import antfu from '@antfu/eslint-config';

export default antfu({
stylistic: {
semi: true,
},

rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'curly': ['error', 'all'],
'vitest/consistent-test-it': ['error', { fn: 'test' }],
'ts/consistent-type-definitions': ['error', 'type'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'unused-imports/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
});
93 changes: 93 additions & 0 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"name": "@enclosed/crypto",
"type": "module",
"version": "1.6.2",
"packageManager": "[email protected]",
"description": "Enclosed cross-env crypto primitives",
"author": "Corentin Thomasset <[email protected]> (https://corentin.tech)",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/CorentinTh/enclosed"
},
"exports": {
"./package.json": "./package.json",
".": {
"browser": "./dist/index.web.mjs",
"bun": "./dist/index.web.mjs",
"deno": "./dist/index.web.mjs",
"edge-light": "./dist/index.web.mjs",
"edge-routine": "./dist/index.web.mjs",
"netlify": "./dist/index.web.mjs",
"react-native": "./dist/index.web.mjs",
"wintercg": "./dist/index.web.mjs",
"worker": "./dist/index.web.mjs",
"workerd": "./dist/index.web.mjs",
"node": {
"import": {
"types": "./dist/index.node.d.mts",
"default": "./dist/index.node.mjs"
},
"require": {
"types": "./dist/index.node.d.cts",
"default": "./dist/index.node.cjs"
}
},
"types": "./dist/index.web.d.mts",
"import": {
"types": "./dist/index.web.d.mts",
"default": "./dist/index.web.mjs"
},
"require": {
"types": "./dist/index.node.d.cts",
"default": "./dist/index.node.cjs"
},
"default": "./dist/index.web.mjs"
},
"./node": {
"import": {
"types": "./dist/index.node.d.mts",
"default": "./dist/index.node.mjs"
},
"require": {
"types": "./dist/index.node.d.cts",
"default": "./dist/index.node.cjs"
}
}
},
"main": "./dist/index.node.cjs",
"module": "./dist/index.web.mjs",
"types": "./dist/index.web.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=22.0.0"
},
"react-native": "./dist/index.web.mjs",
"scripts": {
"prepare": "pnpm run build",
"build": "unbuild",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "pnpm run test:unit",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"typecheck": "tsc --noEmit",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@antfu/eslint-config": "^3.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.4",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.10.0",
"tsx": "^4.17.0",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
"vitest": "^2.0.5"
}
}
27 changes: 27 additions & 0 deletions packages/crypto/src/api-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { EncryptionMethodsDefinition } from './encryption-algorithms/encryption-algorithms.types';
import { createEncryptionAlgorithmsRegistry } from './encryption-algorithms/encryption-algorithms.registry';

export { createEnclosedCryptoApi };

function createEnclosedCryptoApi({
encryptionMethodDefinitions,
...api
}: {
generateBaseKey: () => { baseKey: Uint8Array };
deriveMasterKey: ({ baseKey, password }: { baseKey: Uint8Array; password?: string }) => Promise<{ masterKey: Uint8Array }>;
base64UrlToBuffer: ({ base64Url }: { base64Url: string }) => Uint8Array;
bufferToBase64Url: ({ buffer }: { buffer: Uint8Array }) => string;
encryptionMethodDefinitions: {
'aes-256-gcm': EncryptionMethodsDefinition;
};
}) {
const { encryptionAlgorithms, getDecryptionMethod, getEncryptionMethod } = createEncryptionAlgorithmsRegistry({ encryptionMethodDefinitions });

return {
...api,
encryptionAlgorithms,
encryptionMethodDefinitions,
getDecryptionMethod,
getEncryptionMethod,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const AES_256_GCM = 'aes-256-gcm';

export const ENCRYPTION_ALGORITHMS = [AES_256_GCM] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { defineEncryptionMethods };

function defineEncryptionMethods(args: {
encryptBuffer: (args: { buffer: Uint8Array; encryptionKey: Uint8Array }) => Promise<{ encryptedString: string }>;
decryptString: (args: { encryptedString: string; encryptionKey: Uint8Array }) => Promise<{ decryptedBuffer: Uint8Array }>;
}) {
return args;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { EncryptionAlgorithmDefinitions } from './encryption-algorithms.types';
import { describe, expect, test } from 'vitest';
import { createEncryptionAlgorithmsRegistry } from './encryption-algorithms.registry';

describe('encryption-algorithms registry', () => {
describe('createEncryptionAlgorithmsRegistry', () => {
const dummyEncryptionAlgorithmDefinition = {
encryptBuffer: async () => ({ encryptedString: '' }),
decryptString: async () => ({ decryptedBuffer: new Uint8Array() }),
};

describe('the encryption algorithms registry exposed methods to manage multiples encryption algorithms', () => {
test('when creating the registry, it exposes the available encryption algorithms names and definition', () => {
const { encryptionAlgorithms, encryptionMethodDefinitions } = createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions: {
'aes-256-gcm': dummyEncryptionAlgorithmDefinition,
'foo': dummyEncryptionAlgorithmDefinition,
} as EncryptionAlgorithmDefinitions,
});

expect(encryptionAlgorithms).to.eql(['aes-256-gcm', 'foo']);
expect(encryptionMethodDefinitions).to.eql({
'aes-256-gcm': dummyEncryptionAlgorithmDefinition,
'foo': dummyEncryptionAlgorithmDefinition,
});
});

test('you can get the encryption method definition by its name', () => {
const { getEncryptionMethod } = createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions: {
'aes-256-gcm': dummyEncryptionAlgorithmDefinition,
'foo': dummyEncryptionAlgorithmDefinition,
} as EncryptionAlgorithmDefinitions,
});

const { encryptBuffer } = getEncryptionMethod({ encryptionAlgorithm: 'aes-256-gcm' });

expect(encryptBuffer).to.be.a('function');
});

test('if the encryption method does not exist, an error is thrown', () => {
const { getEncryptionMethod } = createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions: {
'aes-256-gcm': dummyEncryptionAlgorithmDefinition,
},
});

expect(() => getEncryptionMethod({ encryptionAlgorithm: 'foo' })).to.throw('Encryption algorithm "foo" not found');
});

test('you can get the decryption method definition by its name', () => {
const { getDecryptionMethod } = createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions: {
'aes-256-gcm': {
encryptBuffer: async () => ({ encryptedString: 'encrypted using aes-256-gcm' }),
decryptString: async () => ({ decryptedBuffer: new Uint8Array([1]) }),
},
'foo': {
encryptBuffer: async () => ({ encryptedString: 'encrypted using foo' }),
decryptString: async () => ({ decryptedBuffer: new Uint8Array([2]) }),
},
} as EncryptionAlgorithmDefinitions,
});

const { decryptString } = getDecryptionMethod({ encryptionAlgorithm: 'foo' });

expect(decryptString).to.be.a('function');
});

test('if the decryption method does not exist, an error is thrown', () => {
const { getDecryptionMethod } = createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions: {
'aes-256-gcm': dummyEncryptionAlgorithmDefinition,
},
});

expect(() => getDecryptionMethod({ encryptionAlgorithm: 'foo' })).to.throw('Decryption algorithm "foo" not found');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { EncryptionAlgorithm, EncryptionMethodsDefinition } from './encryption-algorithms.types';
import { keys } from 'lodash-es';

export { createEncryptionAlgorithmsRegistry };

function createEncryptionAlgorithmsRegistry({
encryptionMethodDefinitions,
}: {
encryptionMethodDefinitions: Record<EncryptionAlgorithm, EncryptionMethodsDefinition>;
}) {
const encryptionAlgorithms = keys(encryptionMethodDefinitions);

return {
encryptionMethodDefinitions,
encryptionAlgorithms,

getEncryptionMethod: ({ encryptionAlgorithm }: { encryptionAlgorithm: string }) => {
const encryptionMethods: EncryptionMethodsDefinition | undefined = encryptionMethodDefinitions[encryptionAlgorithm];

if (!encryptionMethods) {
throw new Error(`Encryption algorithm "${encryptionAlgorithm}" not found`);
}

const { encryptBuffer } = encryptionMethods;

return { encryptBuffer };
},

getDecryptionMethod: ({ encryptionAlgorithm }: { encryptionAlgorithm: string }) => {
const encryptionMethods = encryptionMethodDefinitions[encryptionAlgorithm];

if (!encryptionMethods) {
throw new Error(`Decryption algorithm "${encryptionAlgorithm}" not found`);
}

const { decryptString } = encryptionMethods;

return { decryptString };
},

};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { EncryptionMethodsDefinition } from './encryption-algorithms.types';
import { times } from 'lodash-es';
import { describe, expect, test } from 'vitest';

export {
runCommonEncryptionAlgorithmTest,
};

function runCommonEncryptionAlgorithmTest({
encryptionMethodDefinition,
}: {
encryptionMethodDefinition: EncryptionMethodsDefinition;
}) {
const { encryptBuffer, decryptString } = encryptionMethodDefinition;

describe('encryptBuffer and decryptString', () => {
test('an encrypted buffer can be decrypted', async () => {
const encryptionKey = new Uint8Array(times(32, i => i));
const buffer = new Uint8Array([11, 22, 33, 44, 55, 66, 77, 88]);

const { encryptedString } = await encryptBuffer({ buffer, encryptionKey });
const { decryptedBuffer } = await decryptString({ encryptedString, encryptionKey });

expect(decryptedBuffer).to.eql(buffer);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ENCRYPTION_ALGORITHMS } from './encryption-algorithms.constants';

export type EncryptionMethodsDefinition = {
encryptBuffer: (args: { buffer: Uint8Array; encryptionKey: Uint8Array }) => Promise<{ encryptedString: string }>;
decryptString: (args: { encryptedString: string; encryptionKey: Uint8Array }) => Promise<{ decryptedBuffer: Uint8Array }>;
};

export type EncryptionAlgorithm = typeof ENCRYPTION_ALGORITHMS[number];

export type EncryptionAlgorithmDefinitions = Record<EncryptionAlgorithm, EncryptionMethodsDefinition>;
21 changes: 21 additions & 0 deletions packages/crypto/src/index.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createEnclosedCryptoApi } from './api-definition';
import * as webCryptoApi from './node/crypto.node.usecases';
import { aes256GcmEncryptionAlgorithmDefinition } from './node/encryption-algorithms/crypto.node.aes-256-gcm';

export type { EncryptionAlgorithm, EncryptionAlgorithmDefinitions, EncryptionMethodsDefinition } from './encryption-algorithms/encryption-algorithms.types';

export const {
deriveMasterKey,
generateBaseKey,
encryptionAlgorithms,
encryptionMethodDefinitions,
getDecryptionMethod,
getEncryptionMethod,
base64UrlToBuffer,
bufferToBase64Url,
} = createEnclosedCryptoApi({
...webCryptoApi,
encryptionMethodDefinitions: {
'aes-256-gcm': aes256GcmEncryptionAlgorithmDefinition,
},
});
Loading

0 comments on commit ed223f7

Please sign in to comment.