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 18, 2024
1 parent 9522dab commit 6acd186
Show file tree
Hide file tree
Showing 33 changed files with 518 additions and 609 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const CreateNotePage: Component = () => {
isPublic: getIsPublic(),
}));

console.log(error);

Check failure on line 84 in packages/app-client/src/modules/notes/pages/create-note.page.tsx

View workflow job for this annotation

GitHub Actions / ci-app-client

Unexpected console statement

setIsNoteCreating(false);

if (!error) {
Expand Down
39 changes: 39 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
### Enclosed lib

This package contains the core functionalities of [Enclosed](https://enclosed.cc/), an open-source project that aims to provide a simple and secure way to share e2e encrypted notes.

## Installation

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

# with yarn
yarn add @enclosed/lib

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

## Usage

```javascript
import { createNote } from '@enclosed/lib';

const { noteUrl } = await createNote({
content: 'Hello, World!',
password: 'password',
ttlInSeconds: 3600,
deleteAfterReading: true,
});

console.log(noteUrl);
```

## 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,9 @@
export { defineEncryptionMethods };

function defineEncryptionMethods<Name extends string>(args: {
name: Name;
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,38 @@
import type { EncryptionMethodsDefinition } from './encryption-algorithms.types';
import { keys } from 'lodash-es';

export { createEncryptionAlgorithmsRegistry };

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

return {
encryptionMethodDefinitions,
encryptionAlgorithms,

getEncryptionMethod: ({ encryptionAlgorithm }: { encryptionAlgorithm: string }) => {
const encryptionMethods = 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(`Encryption algorithm "${encryptionAlgorithm}" not found`);
}

const { decryptString } = encryptionMethods;

return { decryptString };
},

};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type EncryptionMethodsDefinition = {
encryptBuffer: (args: { buffer: Uint8Array; encryptionKey: Uint8Array }) => Promise<{ encryptedString: string }>;
decryptString: (args: { encryptedString: string; encryptionKey: Uint8Array }) => Promise<{ decryptedBuffer: Uint8Array }>;
};
19 changes: 19 additions & 0 deletions packages/crypto/src/index.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 const {
deriveMasterKey,
generateBaseKey,
encryptionAlgorithms,
encryptionMethodDefinitions,
getDecryptionMethod,
getEncryptionMethod,
base64UrlToBuffer,
bufferToBase64Url,
} = createEnclosedCryptoApi({
...webCryptoApi,
encryptionMethodDefinitions: {
'aes-256-gcm': aes256GcmEncryptionAlgorithmDefinition,
},
});
File renamed without changes.
19 changes: 19 additions & 0 deletions packages/crypto/src/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createEnclosedCryptoApi } from './api-definition';
import * as webCryptoApi from './web/crypto.web.usecases';
import { aes256GcmEncryptionAlgorithmDefinition } from './web/encryption-algorithms/crypto.web.aes-256-gcm';

export const {
deriveMasterKey,
generateBaseKey,
encryptionAlgorithms,
encryptionMethodDefinitions,
getDecryptionMethod,
getEncryptionMethod,
base64UrlToBuffer,
bufferToBase64Url,
} = createEnclosedCryptoApi({
...webCryptoApi,
encryptionMethodDefinitions: {
'aes-256-gcm': aes256GcmEncryptionAlgorithmDefinition,
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { base64UrlToBuffer, bufferToBase64Url } from './crypto.node.models';
import { base64UrlToBuffer, bufferToBase64Url } from './crypto.node.usecases';

describe('crypto node models', () => {
describe('bufferToBase64Url', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Buffer } from 'node:buffer';
import { pbkdf2, randomBytes } from 'node:crypto';
import { promisify, TextEncoder } from 'node:util';

export { getDecryptionMethod, getEncryptionMethod } from './encryption-algorithms/encryption-algorithms.registry';

export { createRandomBuffer, deriveMasterKey, generateBaseKey };
export { base64UrlToBuffer, bufferToBase64Url, createRandomBuffer, deriveMasterKey, generateBaseKey };

const deriveWithPbkdf2 = promisify(pbkdf2);

function bufferToBase64Url({ buffer }: { buffer: Uint8Array }): string {
const base64Url = Buffer.from(buffer).toString('base64url');

return base64Url;
}

function base64UrlToBuffer({ base64Url }: { base64Url: string }): Uint8Array {
const buffer = Buffer.from(base64Url, 'base64url');

return new Uint8Array(buffer);
}

function generateBaseKey(): { baseKey: Uint8Array } {
return { baseKey: createRandomBuffer({ length: 32 }) };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createCipheriv, createDecipheriv } from 'node:crypto';
import { defineEncryptionMethods } from '../../encryption-algorithms/encryption-algorithms.models';
import { base64UrlToBuffer, bufferToBase64Url } from '../crypto.node.models';
import { createRandomBuffer } from '../crypto.node.usecases';
import { base64UrlToBuffer, bufferToBase64Url, createRandomBuffer } from '../crypto.node.usecases';

export const aes256GcmEncryptionAlgorithmDefinition = defineEncryptionMethods({
name: 'aes-256-gcm',
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { base64UrlToBuffer, bufferToBase64Url } from './crypto.web.models';
import { base64UrlToBuffer, bufferToBase64Url } from './crypto.web.usecases';

describe('crypto models', () => {
describe('bufferToBase64Url', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
export { createRandomBuffer, deriveMasterKey, generateBaseKey };
export { base64UrlToBuffer, bufferToBase64Url, createRandomBuffer, deriveMasterKey, generateBaseKey };

export { getDecryptionMethod, getEncryptionMethod } from './encryption-algorithms/encryption-algorithms.registry';
function bufferToBase64Url({ buffer }: { buffer: Uint8Array }): string {
let binaryString = '';
const chunkSize = 0x8000; // 32KB chunks to avoid stack overflow
for (let i = 0; i < buffer.length; i += chunkSize) {
const chunk = buffer.subarray(i, i + chunkSize);
binaryString += String.fromCharCode(...chunk);
}

const base64 = btoa(binaryString);
const base64Url = base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');

return base64Url;
}

function base64UrlToBuffer({ base64Url }: { base64Url: string }): Uint8Array {
const base64 = base64Url
.padEnd(base64Url.length + (4 - base64Url.length % 4) % 4, '=')
.replace(/-/g, '+')
.replace(/_/g, '/');

const buffer = new Uint8Array(atob(base64).split('').map(char => char.charCodeAt(0)));

return buffer;
}

function createRandomBuffer({ length = 16 }: { length?: number } = {}): Uint8Array {
const randomValues = new Uint8Array(length);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defineEncryptionMethods } from '../../encryption-algorithms/encryption-algorithms.models';
import { base64UrlToBuffer, bufferToBase64Url } from '../crypto.web.models';
import { createRandomBuffer } from '../crypto.web.usecases';
import { base64UrlToBuffer, bufferToBase64Url, createRandomBuffer } from '../crypto.web.usecases';

export const aes256GcmEncryptionAlgorithmDefinition = defineEncryptionMethods({
name: 'aes-256-gcm',
Expand Down
Loading

0 comments on commit 6acd186

Please sign in to comment.