diff --git a/packages/crypto/README.md b/packages/crypto/README.md new file mode 100644 index 00000000..b8e710b3 --- /dev/null +++ b/packages/crypto/README.md @@ -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). diff --git a/packages/crypto/build.config.ts b/packages/crypto/build.config.ts new file mode 100644 index 00000000..e302e960 --- /dev/null +++ b/packages/crypto/build.config.ts @@ -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, + }, +}); diff --git a/packages/crypto/eslint.config.js b/packages/crypto/eslint.config.js new file mode 100644 index 00000000..e0175daf --- /dev/null +++ b/packages/crypto/eslint.config.js @@ -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: '^_', + }], + }, +}); diff --git a/packages/crypto/package.json b/packages/crypto/package.json new file mode 100644 index 00000000..060dbacc --- /dev/null +++ b/packages/crypto/package.json @@ -0,0 +1,93 @@ +{ + "name": "@enclosed/crypto", + "type": "module", + "version": "1.6.2", + "packageManager": "pnpm@9.10.0", + "description": "Enclosed cross-env crypto primitives", + "author": "Corentin Thomasset (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" + } +} diff --git a/packages/crypto/src/api-definition.ts b/packages/crypto/src/api-definition.ts new file mode 100644 index 00000000..ee091ad8 --- /dev/null +++ b/packages/crypto/src/api-definition.ts @@ -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, + }; +} diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.constants.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.constants.ts new file mode 100644 index 00000000..2bcb48b6 --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.constants.ts @@ -0,0 +1,3 @@ +export const AES_256_GCM = 'aes-256-gcm'; + +export const ENCRYPTION_ALGORITHMS = [AES_256_GCM] as const; diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.models.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.models.ts new file mode 100644 index 00000000..608bc356 --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.models.ts @@ -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; +} diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.test.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.test.ts new file mode 100644 index 00000000..1b3fecc6 --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.test.ts @@ -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'); + }); + }); + }); +}); diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.ts new file mode 100644 index 00000000..f96c1145 --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.registry.ts @@ -0,0 +1,42 @@ +import type { EncryptionAlgorithm, EncryptionMethodsDefinition } from './encryption-algorithms.types'; +import { keys } from 'lodash-es'; + +export { createEncryptionAlgorithmsRegistry }; + +function createEncryptionAlgorithmsRegistry({ + encryptionMethodDefinitions, +}: { + encryptionMethodDefinitions: Record; +}) { + 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 }; + }, + + }; +}; diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.test-utils.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.test-utils.ts new file mode 100644 index 00000000..9f88848d --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.test-utils.ts @@ -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); + }); + }); +} diff --git a/packages/crypto/src/encryption-algorithms/encryption-algorithms.types.ts b/packages/crypto/src/encryption-algorithms/encryption-algorithms.types.ts new file mode 100644 index 00000000..20b0d440 --- /dev/null +++ b/packages/crypto/src/encryption-algorithms/encryption-algorithms.types.ts @@ -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; \ No newline at end of file diff --git a/packages/crypto/src/index.node.ts b/packages/crypto/src/index.node.ts new file mode 100644 index 00000000..ac20664c --- /dev/null +++ b/packages/crypto/src/index.node.ts @@ -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, + }, +}); diff --git a/packages/crypto/src/index.test.ts b/packages/crypto/src/index.test.ts new file mode 100644 index 00000000..31e69004 --- /dev/null +++ b/packages/crypto/src/index.test.ts @@ -0,0 +1,62 @@ +import { keys, times } from 'lodash-es'; +import { describe, expect, test } from 'vitest'; +import * as nodeLib from './index.node'; +import * as webLib from './index.web'; + +describe('lib api', () => { + test('the web lib exports the same functions as the node lib', () => { + expect( + keys(nodeLib), + ).to.eql( + keys(webLib), + ); + }); + + test('both lib have the same encryption algorithm implementations', () => { + expect( + nodeLib.encryptionAlgorithms, + ).to.eql( + webLib.encryptionAlgorithms, + ); + }); + + [ + { lib: nodeLib, name: 'node api' }, + { lib: webLib, name: 'web api' }, + ].forEach(({ lib, name }) => { + describe(name, () => { + describe('generateBaseKey', () => { + const { generateBaseKey } = lib; + + test('the base key is a random 32 bytes buffer', () => { + const { baseKey } = generateBaseKey(); + + expect(baseKey.length).toBe(32); + }); + }); + + describe('deriveMasterKey', () => { + const { deriveMasterKey } = lib; + + test('the master key is a random 32 bytes buffer', async () => { + const { masterKey } = await deriveMasterKey({ + baseKey: new Uint8Array(32), + password: 'password', + }); + + expect(masterKey.length).toBe(32); + }); + + test('the derivation is deterministic, the same password and base key will always generate the same master key', async () => { + const password = 'password'; + const baseKey = new Uint8Array(times(32, i => i)); + + const { masterKey: masterKey1 } = await deriveMasterKey({ baseKey, password }); + const { masterKey: masterKey2 } = await deriveMasterKey({ baseKey, password }); + + expect(masterKey1).to.eql(masterKey2); + }); + }); + }); + }); +}); diff --git a/packages/crypto/src/index.web.ts b/packages/crypto/src/index.web.ts new file mode 100644 index 00000000..bb520880 --- /dev/null +++ b/packages/crypto/src/index.web.ts @@ -0,0 +1,21 @@ +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 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, + }, +}); diff --git a/packages/lib/src/crypto/node/crypto.node.models.test.ts b/packages/crypto/src/node/crypto.node.usecases.test.ts similarity index 94% rename from packages/lib/src/crypto/node/crypto.node.models.test.ts rename to packages/crypto/src/node/crypto.node.usecases.test.ts index 71060393..3b8c3465 100644 --- a/packages/lib/src/crypto/node/crypto.node.models.test.ts +++ b/packages/crypto/src/node/crypto.node.usecases.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { base64UrlToBuffer, bufferToBase64Url } from './crypto.node.models'; +import { base64UrlToBuffer, bufferToBase64Url, generateBaseKey } from './crypto.node.usecases'; describe('crypto node models', () => { describe('bufferToBase64Url', () => { diff --git a/packages/lib/src/crypto/node/crypto.node.usecases.ts b/packages/crypto/src/node/crypto.node.usecases.ts similarity index 63% rename from packages/lib/src/crypto/node/crypto.node.usecases.ts rename to packages/crypto/src/node/crypto.node.usecases.ts index c1e0b38d..c4921875 100644 --- a/packages/lib/src/crypto/node/crypto.node.usecases.ts +++ b/packages/crypto/src/node/crypto.node.usecases.ts @@ -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 }) }; } diff --git a/packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.test.ts b/packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.test.ts new file mode 100644 index 00000000..942892ed --- /dev/null +++ b/packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; +import { runCommonEncryptionAlgorithmTest } from '../../encryption-algorithms/encryption-algorithms.test-utils'; +import { aes256GcmEncryptionAlgorithmDefinition } from './crypto.node.aes-256-gcm'; + +describe('crypto node aes-256-gcm', () => { + describe('aes256GcmEncryptionAlgorithmDefinition', () => { + runCommonEncryptionAlgorithmTest({ + encryptionMethodDefinition: aes256GcmEncryptionAlgorithmDefinition, + }); + + describe('decryptString', () => { + test('when the encrypted string does not contain the IV or the encrypted content, an error is thrown', async () => { + const { decryptString } = aes256GcmEncryptionAlgorithmDefinition; + const encryptionKey = new Uint8Array(32); + const encryptedString = 'invalid encrypted string'; + + await expect(decryptString({ encryptedString, encryptionKey })).rejects.toThrow('Invalid encrypted content'); + }); + }); + }); +}); diff --git a/packages/lib/src/crypto/node/encryption-algorithms/crypto.node.aes-256-gcm.ts b/packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.ts similarity index 90% rename from packages/lib/src/crypto/node/encryption-algorithms/crypto.node.aes-256-gcm.ts rename to packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.ts index 6d64cf42..3a2828be 100644 --- a/packages/lib/src/crypto/node/encryption-algorithms/crypto.node.aes-256-gcm.ts +++ b/packages/crypto/src/node/encryption-algorithms/crypto.node.aes-256-gcm.ts @@ -1,10 +1,8 @@ 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', encryptBuffer: async ({ buffer, encryptionKey }) => { const iv = createRandomBuffer({ length: 12 }); diff --git a/packages/crypto/src/web/crypto.web.models.ts b/packages/crypto/src/web/crypto.web.models.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/lib/src/crypto/web/crypto.web.models.test.ts b/packages/crypto/src/web/crypto.web.usecases.test.ts similarity index 99% rename from packages/lib/src/crypto/web/crypto.web.models.test.ts rename to packages/crypto/src/web/crypto.web.usecases.test.ts index d78b1ab2..ae65f51a 100644 --- a/packages/lib/src/crypto/web/crypto.web.models.test.ts +++ b/packages/crypto/src/web/crypto.web.usecases.test.ts @@ -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', () => { diff --git a/packages/lib/src/crypto/web/crypto.web.usecases.ts b/packages/crypto/src/web/crypto.web.usecases.ts similarity index 53% rename from packages/lib/src/crypto/web/crypto.web.usecases.ts rename to packages/crypto/src/web/crypto.web.usecases.ts index a6635d18..76d02e7c 100644 --- a/packages/lib/src/crypto/web/crypto.web.usecases.ts +++ b/packages/crypto/src/web/crypto.web.usecases.ts @@ -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); diff --git a/packages/crypto/src/web/encryption-algorithms/crypto.node.aes-256-gcm.test.ts b/packages/crypto/src/web/encryption-algorithms/crypto.node.aes-256-gcm.test.ts new file mode 100644 index 00000000..3debc9c6 --- /dev/null +++ b/packages/crypto/src/web/encryption-algorithms/crypto.node.aes-256-gcm.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; +import { runCommonEncryptionAlgorithmTest } from '../../encryption-algorithms/encryption-algorithms.test-utils'; +import { aes256GcmEncryptionAlgorithmDefinition } from './crypto.web.aes-256-gcm'; + +describe('crypto node aes-256-gcm', () => { + describe('aes256GcmEncryptionAlgorithmDefinition', () => { + runCommonEncryptionAlgorithmTest({ + encryptionMethodDefinition: aes256GcmEncryptionAlgorithmDefinition, + }); + + describe('decryptString', () => { + test('when the encrypted string does not contain the IV or the encrypted content, an error is thrown', async () => { + const { decryptString } = aes256GcmEncryptionAlgorithmDefinition; + const encryptionKey = new Uint8Array(32); + const encryptedString = 'invalid encrypted string'; + + await expect(decryptString({ encryptedString, encryptionKey })).rejects.toThrow('Invalid encrypted content'); + }); + }); + }); +}); diff --git a/packages/lib/src/crypto/web/encryption-algorithms/crypto.web.aes-256-gcm.ts b/packages/crypto/src/web/encryption-algorithms/crypto.web.aes-256-gcm.ts similarity index 87% rename from packages/lib/src/crypto/web/encryption-algorithms/crypto.web.aes-256-gcm.ts rename to packages/crypto/src/web/encryption-algorithms/crypto.web.aes-256-gcm.ts index eb6f8a46..744b0ad0 100644 --- a/packages/lib/src/crypto/web/encryption-algorithms/crypto.web.aes-256-gcm.ts +++ b/packages/crypto/src/web/encryption-algorithms/crypto.web.aes-256-gcm.ts @@ -1,9 +1,7 @@ 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', encryptBuffer: async ({ buffer, encryptionKey }) => { const iv = createRandomBuffer({ length: 12 }); @@ -25,7 +23,7 @@ export const aes256GcmEncryptionAlgorithmDefinition = defineEncryptionMethods({ const [ivString, encryptedContentString] = encryptedString.split(':').map(part => part.trim()); if (!ivString || !encryptedContentString) { - throw new Error('Invalid data'); + throw new Error('Invalid encrypted content'); } const iv = base64UrlToBuffer({ base64Url: ivString }); diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json new file mode 100644 index 00000000..3fd43123 --- /dev/null +++ b/packages/crypto/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": [ + "src" + ] +} diff --git a/packages/lib/build.config.ts b/packages/lib/build.config.ts index e302e960..da11c9fc 100644 --- a/packages/lib/build.config.ts +++ b/packages/lib/build.config.ts @@ -2,8 +2,7 @@ import { defineBuildConfig } from 'unbuild'; export default defineBuildConfig({ entries: [ - 'src/index.node', - 'src/index.web', + 'src/index', ], clean: true, declaration: true, diff --git a/packages/lib/package.json b/packages/lib/package.json index 88103c10..54b66e09 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -11,56 +11,14 @@ "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" - } + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" } }, - "main": "./dist/index.node.cjs", - "module": "./dist/index.web.mjs", - "types": "./dist/index.web.d.ts", - "files": [ - "dist" - ], + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "files": ["dist"], "engines": { "node": ">=22.0.0" }, @@ -77,6 +35,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { + "@enclosed/crypto": "workspace:*", "cbor-x": "^1.6.0", "lodash-es": "^4.17.21", "msgpackr": "^1.11.0", diff --git a/packages/lib/src/crypto/crypto.usecases.test.ts b/packages/lib/src/crypto/crypto.usecases.test.ts deleted file mode 100644 index b7742e3a..00000000 --- a/packages/lib/src/crypto/crypto.usecases.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { createDecryptUsecase, createEncryptUsecase } from './crypto.usecases'; -import * as nodeCryptoLib from './node/crypto.node.usecases'; -import * as webCryptoLib from './web/crypto.web.usecases'; - -export { runCommonCryptoUsecasesTests }; - -function runCommonCryptoUsecasesTests({ - encryptNote, - decryptNote, -}: { - encryptNote: (args: { content: string; password?: string }) => Promise<{ encryptedContent: string; encryptionKey: string }>; - decryptNote: (args: { encryptedContent: string; password?: string; encryptionKey: string }) => Promise<{ decryptedContent: string }>; -}) { - describe('encryption and decryption', () => { - describe('without password', () => { - test('a note can be decrypted with the same key used for encryption', async () => { - const content = 'Hello, world!'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - }); - - const { decryptedContent } = await decryptNote({ - encryptedContent, - encryptionKey, - }); - - expect(decryptedContent).toBe(content); - }); - - test('a slight variation in the encryption key results in an impossible decryption', async () => { - const content = 'Hello, world!'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - }); - - expect( - decryptNote({ - encryptedContent, - encryptionKey: `${encryptionKey}a`, - }), - ).rejects.toThrow(); - - expect( - decryptNote({ - encryptedContent, - encryptionKey: encryptionKey.slice(0, -1), - }), - ).rejects.toThrow(); - }); - - test('a slight variation in the encrypted content results in an impossible decryption', async () => { - const content = 'Hello, world!'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - }); - - expect( - decryptNote({ - encryptedContent: `${encryptedContent}a`, - encryptionKey, - }), - ).rejects.toThrow(); - - expect( - decryptNote({ - encryptedContent: encryptedContent.slice(0, -1), - encryptionKey, - }), - ).rejects.toThrow(); - }); - - test('an empty string as a password is the same as no password', async () => { - const content = 'Hello, world!'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password: '', - }); - - const { decryptedContent } = await decryptNote({ - encryptedContent, - encryptionKey, - password: '', - }); - - expect(decryptedContent).toBe(content); - }); - }); - - describe('with password', () => { - test('a note can be decrypted with the same base key and password used for encryption', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password, - }); - - const { decryptedContent } = await decryptNote({ - encryptedContent, - encryptionKey, - password, - }); - - expect(decryptedContent).toBe(content); - }); - - test('a slight variation in the encryption key results in an impossible decryption', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password, - }); - - expect( - decryptNote({ - encryptedContent, - encryptionKey: `${encryptionKey}a`, - password, - }), - ).rejects.toThrow(); - - expect( - decryptNote({ - encryptedContent, - encryptionKey: encryptionKey.slice(0, -1), - password, - }), - ).rejects.toThrow(); - }); - - test('a slight variation in the encrypted content results in an impossible decryption', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password, - }); - - expect( - decryptNote({ - encryptedContent: `${encryptedContent}a`, - encryptionKey, - password, - }), - ).rejects.toThrow(); - - expect( - decryptNote({ - encryptedContent: encryptedContent.slice(0, -1), - encryptionKey, - password, - }), - ).rejects.toThrow(); - }); - - test('if the password used for decryption is different from the one used for encryption, the note cannot be decrypted', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password, - }); - - expect( - decryptNote({ - encryptedContent, - encryptionKey, - password: 'different password', - }), - ).rejects.toThrow(); - }); - }); - - test('if the encrypted content does not include the iv, the note cannot be decrypted', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptedContent, encryptionKey } = await encryptNote({ - content, - password, - }); - - const [_ivString, encryptedStringWithAuthTag] = encryptedContent.split(':').map(part => part.trim()); - - const encryptedContentWithoutIv = encryptedStringWithAuthTag; - - expect( - decryptNote({ - encryptedContent: encryptedContentWithoutIv, - encryptionKey, - password, - }), - ).rejects.toThrow(); - }); - }); -} - -describe('cross-environment encryption and decryption', () => { - test('a note encrypted in the web environment can be decrypted in the node environment', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptNote } = createEncryptUsecase(webCryptoLib); - const { decryptNote } = createDecryptUsecase(nodeCryptoLib); - - const { encryptedPayload, encryptionKey } = await encryptNote({ - content, - password, - }); - - const { note } = await decryptNote({ - encryptedPayload, - encryptionKey, - password, - }); - - expect(note).to.eql({ - content: 'Hello, world!', - assets: [], - }); - }); - - test('a note encrypted in the node environment can be decrypted in the web environment', async () => { - const content = 'Hello, world!'; - const password = 'password'; - - const { encryptNote } = createEncryptUsecase(nodeCryptoLib); - const { decryptNote } = createDecryptUsecase(webCryptoLib); - - const { encryptedPayload, encryptionKey } = await encryptNote({ - content, - password, - }); - - const { note } = await decryptNote({ - encryptedPayload, - encryptionKey, - password, - }); - - expect(note).to.eql({ - content: 'Hello, world!', - assets: [], - }); - }); -}); diff --git a/packages/lib/src/crypto/crypto.usecases.ts b/packages/lib/src/crypto/crypto.usecases.ts index 4a4d3dd9..90ff1749 100644 --- a/packages/lib/src/crypto/crypto.usecases.ts +++ b/packages/lib/src/crypto/crypto.usecases.ts @@ -1,85 +1,63 @@ import type { NoteAsset } from '../notes/notes.types'; import type { EncryptionAlgorithm } from './crypto.types'; import type { SerializationFormat } from './serialization/serialization.types'; +import { base64UrlToBuffer, bufferToBase64Url, deriveMasterKey, generateBaseKey, getDecryptionMethod, getEncryptionMethod } from '@enclosed/crypto'; import { getParsingMethod, getSerializationMethod } from './serialization/serialization.registry'; -import { base64UrlToBuffer, bufferToBase64Url } from './web/crypto.web.models'; -export { createDecryptUsecase, createEncryptUsecase }; +export { decryptNote, encryptNote }; -function createEncryptUsecase({ - generateBaseKey, - deriveMasterKey, - getEncryptionMethod, +async function encryptNote({ + content, + password, + assets = [], + encryptionAlgorithm = 'aes-256-gcm', + serializationFormat = 'cbor-array', }: { - generateBaseKey: () => { baseKey: Uint8Array }; - deriveMasterKey: ({ baseKey, password }: { baseKey: Uint8Array; password?: string }) => Promise<{ masterKey: Uint8Array }>; - getEncryptionMethod: (args: { encryptionAlgorithm: string }) => { encryptBuffer: (args: { buffer: Uint8Array; encryptionKey: Uint8Array }) => Promise<{ encryptedString: string }> }; + content: string; + password?: string; + assets?: NoteAsset[]; + encryptionAlgorithm?: EncryptionAlgorithm; + serializationFormat?: SerializationFormat; }) { - return { - encryptNote: async ({ - content, - password, - assets = [], - encryptionAlgorithm = 'aes-256-gcm', - serializationFormat = 'cbor-array', - }: { - content: string; - password?: string; - assets?: NoteAsset[]; - encryptionAlgorithm?: EncryptionAlgorithm; - serializationFormat?: SerializationFormat; - }) => { - const { serializeNote } = getSerializationMethod({ serializationFormat }); - const { encryptBuffer } = getEncryptionMethod({ encryptionAlgorithm }); + const { serializeNote } = getSerializationMethod({ serializationFormat }); + const { encryptBuffer } = getEncryptionMethod({ encryptionAlgorithm }); - const { baseKey } = generateBaseKey(); + const { baseKey } = generateBaseKey(); - const { masterKey } = await deriveMasterKey({ baseKey, password }); + const { masterKey } = await deriveMasterKey({ baseKey, password }); - const { noteBuffer } = await serializeNote({ note: { content, assets } }); + const { noteBuffer } = await serializeNote({ note: { content, assets } }); - const { encryptedString: encryptedPayload } = await encryptBuffer({ buffer: noteBuffer, encryptionKey: masterKey }); + const { encryptedString: encryptedPayload } = await encryptBuffer({ buffer: noteBuffer, encryptionKey: masterKey }); - const encryptionKey = bufferToBase64Url({ buffer: baseKey }); + const encryptionKey = bufferToBase64Url({ buffer: baseKey }); - return { encryptedPayload, encryptionKey }; - }, - }; + return { encryptedPayload, encryptionKey }; } -function createDecryptUsecase({ - deriveMasterKey, - getDecryptionMethod, +async function decryptNote({ + encryptedPayload, + password, + encryptionKey, + serializationFormat = 'cbor-array', + encryptionAlgorithm = 'aes-256-gcm', }: { - deriveMasterKey: ({ baseKey, password }: { baseKey: Uint8Array; password?: string }) => Promise<{ masterKey: Uint8Array }>; - getDecryptionMethod: (args: { encryptionAlgorithm: string }) => { decryptString: (args: { encryptedString: string;encryptionKey: Uint8Array }) => Promise<{ decryptedBuffer: Uint8Array }> }; + encryptedPayload: string; + password?: string; + encryptionKey: string; + serializationFormat?: SerializationFormat; + encryptionAlgorithm?: EncryptionAlgorithm; }) { - return { - decryptNote: async ({ - encryptedPayload, - password, - encryptionKey, - serializationFormat = 'cbor-array', - encryptionAlgorithm = 'aes-256-gcm', - }: { - encryptedPayload: string; - password?: string; - encryptionKey: string; - serializationFormat?: SerializationFormat; - encryptionAlgorithm?: EncryptionAlgorithm; - }) => { - const { parseNote } = getParsingMethod({ serializationFormat }); - const { decryptString } = getDecryptionMethod({ encryptionAlgorithm }); + const { parseNote } = getParsingMethod({ serializationFormat }); + const { decryptString } = getDecryptionMethod({ encryptionAlgorithm }); - const baseKey = base64UrlToBuffer({ base64Url: encryptionKey }); + const baseKey = base64UrlToBuffer({ base64Url: encryptionKey }); - const { masterKey } = await deriveMasterKey({ baseKey, password }); + const { masterKey } = await deriveMasterKey({ baseKey, password }); - const { decryptedBuffer } = await decryptString({ encryptedString: encryptedPayload, encryptionKey: masterKey }); + const { decryptedBuffer } = await decryptString({ encryptedString: encryptedPayload, encryptionKey: masterKey }); - const { note } = await parseNote({ noteBuffer: decryptedBuffer }); + const { note } = await parseNote({ noteBuffer: decryptedBuffer }); - return { note }; - }, - }; + return { note }; } diff --git a/packages/lib/src/crypto/node/crypto.node.models.ts b/packages/lib/src/crypto/node/crypto.node.models.ts deleted file mode 100644 index d26b1f6c..00000000 --- a/packages/lib/src/crypto/node/crypto.node.models.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Buffer } from 'node:buffer'; - -export { base64UrlToBuffer, bufferToBase64Url }; - -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); -} diff --git a/packages/lib/src/crypto/node/encryption-algorithms/encryption-algorithms.registry.ts b/packages/lib/src/crypto/node/encryption-algorithms/encryption-algorithms.registry.ts deleted file mode 100644 index 14422475..00000000 --- a/packages/lib/src/crypto/node/encryption-algorithms/encryption-algorithms.registry.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createEncryptionAlgorithmsRegistry } from '../../encryption-algorithms/encryption-algorithms.registry'; -import { aes256GcmEncryptionAlgorithmDefinition } from './crypto.node.aes-256-gcm'; - -const encryptionMethodDefinitions = [ - aes256GcmEncryptionAlgorithmDefinition, -]; - -export const { - encryptionAlgorithms, - encryptionMethodDefinitionsByName, - getDecryptionMethod, - getEncryptionMethod, -} = createEncryptionAlgorithmsRegistry({ encryptionMethodDefinitions }); diff --git a/packages/lib/src/crypto/web/crypto.web.models.ts b/packages/lib/src/crypto/web/crypto.web.models.ts deleted file mode 100644 index 8b565b3e..00000000 --- a/packages/lib/src/crypto/web/crypto.web.models.ts +++ /dev/null @@ -1,29 +0,0 @@ -export { base64UrlToBuffer, bufferToBase64Url }; - -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; -} diff --git a/packages/lib/src/crypto/web/encryption-algorithms/encryption-algorithms.registry.ts b/packages/lib/src/crypto/web/encryption-algorithms/encryption-algorithms.registry.ts deleted file mode 100644 index d6563003..00000000 --- a/packages/lib/src/crypto/web/encryption-algorithms/encryption-algorithms.registry.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createEncryptionAlgorithmsRegistry } from '../../encryption-algorithms/encryption-algorithms.registry'; -import { aes256GcmEncryptionAlgorithmDefinition } from './crypto.web.aes-256-gcm'; - -const encryptionMethodDefinitions = [ - aes256GcmEncryptionAlgorithmDefinition, -]; - -export const { - encryptionAlgorithms, - encryptionMethodDefinitionsByName, - getDecryptionMethod, - getEncryptionMethod, -} = createEncryptionAlgorithmsRegistry({ encryptionMethodDefinitions }); diff --git a/packages/lib/src/index.node.ts b/packages/lib/src/index.node.ts deleted file mode 100644 index c9f2c233..00000000 --- a/packages/lib/src/index.node.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api/api.models'; -import { createDecryptUsecase, createEncryptUsecase } from './crypto/crypto.usecases'; -import { deriveMasterKey, generateBaseKey } from './crypto/node/crypto.node.usecases'; -import { encryptionAlgorithms, getDecryptionMethod, getEncryptionMethod } from './crypto/node/encryption-algorithms/encryption-algorithms.registry'; -import { serializationFormats } from './crypto/serialization/serialization.registry'; -import { filesToNoteAssets, fileToNoteAsset, noteAssetsToFiles, noteAssetToFile } from './files/files.models'; -import { createNoteUrlHashFragment, parseNoteUrlHashFragment } from './notes/notes.models'; -import { fetchNote, storeNote } from './notes/notes.services'; -import { createEnclosedLib } from './notes/notes.usecases'; - -const { encryptNote } = createEncryptUsecase({ generateBaseKey, deriveMasterKey, getEncryptionMethod }); -const { decryptNote } = createDecryptUsecase({ deriveMasterKey, getDecryptionMethod }); - -const { createNote, createNoteUrl, parseNoteUrl } = createEnclosedLib({ encryptNote, storeNote }); - -export { - createNote, - createNoteUrl, - createNoteUrlHashFragment, - decryptNote, - encryptionAlgorithms, - encryptNote, - fetchNote, - filesToNoteAssets, - fileToNoteAsset, - isApiClientErrorWithCode, - isApiClientErrorWithStatusCode, - noteAssetsToFiles, - noteAssetToFile, - parseNoteUrl, - parseNoteUrlHashFragment, - serializationFormats, - storeNote, -}; diff --git a/packages/lib/src/index.test.ts b/packages/lib/src/index.test.ts deleted file mode 100644 index 856dffed..00000000 --- a/packages/lib/src/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import * as nodeLib from './index.node'; -import * as webLib from './index.web'; - -describe('lib api', () => { - test('the web lib exports the same functions as the node lib', () => { - expect(Object.keys(nodeLib)).to.eql(Object.keys(webLib)); - }); -}); diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts new file mode 100644 index 00000000..4f0add8d --- /dev/null +++ b/packages/lib/src/index.ts @@ -0,0 +1,28 @@ +import { encryptionAlgorithms } from '@enclosed/crypto'; +import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api/api.models'; +import { decryptNote, encryptNote } from './crypto/crypto.usecases'; +import { serializationFormats } from './crypto/serialization/serialization.registry'; +import { filesToNoteAssets, fileToNoteAsset, noteAssetsToFiles, noteAssetToFile } from './files/files.models'; +import { createNoteUrl, createNoteUrlHashFragment, parseNoteUrl, parseNoteUrlHashFragment } from './notes/notes.models'; +import { fetchNote, storeNote } from './notes/notes.services'; +import { createNote } from './notes/notes.usecases'; + +export { + createNote, + createNoteUrl, + createNoteUrlHashFragment, + decryptNote, + encryptionAlgorithms, + encryptNote, + fetchNote, + filesToNoteAssets, + fileToNoteAsset, + isApiClientErrorWithCode, + isApiClientErrorWithStatusCode, + noteAssetsToFiles, + noteAssetToFile, + parseNoteUrl, + parseNoteUrlHashFragment, + serializationFormats, + storeNote, +}; diff --git a/packages/lib/src/index.web.ts b/packages/lib/src/index.web.ts deleted file mode 100644 index d8d286b2..00000000 --- a/packages/lib/src/index.web.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isApiClientErrorWithCode, isApiClientErrorWithStatusCode } from './api/api.models'; -import { createDecryptUsecase, createEncryptUsecase } from './crypto/crypto.usecases'; -import { serializationFormats } from './crypto/serialization/serialization.registry'; -import { deriveMasterKey, generateBaseKey } from './crypto/web/crypto.web.usecases'; -import { encryptionAlgorithms, getDecryptionMethod, getEncryptionMethod } from './crypto/web/encryption-algorithms/encryption-algorithms.registry'; -import { filesToNoteAssets, fileToNoteAsset, noteAssetsToFiles, noteAssetToFile } from './files/files.models'; -import { createNoteUrlHashFragment, parseNoteUrlHashFragment } from './notes/notes.models'; -import { fetchNote, storeNote } from './notes/notes.services'; -import { createEnclosedLib } from './notes/notes.usecases'; - -const { encryptNote } = createEncryptUsecase({ generateBaseKey, deriveMasterKey, getEncryptionMethod }); -const { decryptNote } = createDecryptUsecase({ deriveMasterKey, getDecryptionMethod }); - -const { createNote, createNoteUrl, parseNoteUrl } = createEnclosedLib({ encryptNote, storeNote }); - -export { - createNote, - createNoteUrl, - createNoteUrlHashFragment, - decryptNote, - encryptionAlgorithms, - encryptNote, - fetchNote, - filesToNoteAssets, - fileToNoteAsset, - isApiClientErrorWithCode, - isApiClientErrorWithStatusCode, - noteAssetsToFiles, - noteAssetToFile, - parseNoteUrl, - parseNoteUrlHashFragment, - serializationFormats, - storeNote, -}; diff --git a/packages/lib/src/notes/notes.usecases.ts b/packages/lib/src/notes/notes.usecases.ts index 7c83697b..abf43351 100644 --- a/packages/lib/src/notes/notes.usecases.ts +++ b/packages/lib/src/notes/notes.usecases.ts @@ -1,107 +1,77 @@ import type { EncryptionAlgorithm } from '../crypto/crypto.types'; import type { SerializationFormat } from '../crypto/serialization/serialization.types'; import type { NoteAsset } from './notes.types'; +import { encryptNote } from '../crypto/crypto.usecases'; import { createNoteUrl as createNoteUrlImpl, parseNoteUrl } from './notes.models'; +import { storeNote as storeNoteImpl } from './notes.services'; -export { createEnclosedLib }; +export { createNote }; const ONE_HOUR_IN_SECONDS = 60 * 60; const BASE_URL = 'https://enclosed.cc'; -function createEnclosedLib({ - encryptNote, - // decryptNote, - storeNote: storeNoteImpl, - // fetchNote: fetchNoteImpl, +async function createNote({ + content, + password, + ttlInSeconds = ONE_HOUR_IN_SECONDS, + deleteAfterReading = false, + clientBaseUrl = BASE_URL, + apiBaseUrl = clientBaseUrl, + createNoteUrl = createNoteUrlImpl, + storeNote = params => storeNoteImpl({ ...params, apiBaseUrl }), + assets = [], + encryptionAlgorithm = 'aes-256-gcm', + serializationFormat = 'cbor-array', + isPublic = true, }: { - encryptNote: (args: { - content: string; - password?: string; - assets?: NoteAsset[]; - encryptionAlgorithm?: EncryptionAlgorithm; - serializationFormat?: SerializationFormat; - }) => Promise<{ - encryptedPayload: string; + content: string; + password?: string; + ttlInSeconds?: number; + deleteAfterReading?: boolean; + clientBaseUrl?: string; + apiBaseUrl?: string; + assets?: NoteAsset[]; + encryptionAlgorithm?: EncryptionAlgorithm; + serializationFormat?: SerializationFormat; + isPublic?: boolean; + createNoteUrl?: (args: { + noteId: string; encryptionKey: string; - }>; - // decryptNote: (args: { encryptedContent: string; encryptionKey: string }) => Promise<{ content: string }>; - storeNote: (params: { + clientBaseUrl: string; + isPasswordProtected: boolean; + }) => { noteUrl: string }; + storeNote?: (params: { payload: string; ttlInSeconds: number; deleteAfterReading: boolean; - apiBaseUrl?: string; + encryptionAlgorithm: EncryptionAlgorithm; + serializationFormat: SerializationFormat; isPublic?: boolean; }) => Promise<{ noteId: string }>; - // fetchNote: (params: { noteId: string; apiBaseUrl?: string }) => Promise<{ content: string; isPasswordProtected: boolean }>; }) { - return { - parseNoteUrl, - createNoteUrl: createNoteUrlImpl, - - createNote: async ({ - content, - password, - ttlInSeconds = ONE_HOUR_IN_SECONDS, - deleteAfterReading = false, - clientBaseUrl = BASE_URL, - apiBaseUrl = clientBaseUrl, - createNoteUrl = createNoteUrlImpl, - storeNote = params => storeNoteImpl({ ...params, apiBaseUrl }), - assets = [], - encryptionAlgorithm = 'aes-256-gcm', - serializationFormat = 'cbor-array', - isPublic = true, - }: { - content: string; - password?: string; - ttlInSeconds?: number; - deleteAfterReading?: boolean; - clientBaseUrl?: string; - apiBaseUrl?: string; - assets?: NoteAsset[]; - encryptionAlgorithm?: EncryptionAlgorithm; - serializationFormat?: SerializationFormat; - isPublic?: boolean; - createNoteUrl?: (args: { - noteId: string; - encryptionKey: string; - clientBaseUrl: string; - isPasswordProtected: boolean; - }) => { noteUrl: string }; - storeNote?: (params: { - payload: string; - ttlInSeconds: number; - deleteAfterReading: boolean; - encryptionAlgorithm: EncryptionAlgorithm; - serializationFormat: SerializationFormat; - isPublic?: boolean; - }) => Promise<{ noteId: string }>; - }) => { - const { encryptedPayload, encryptionKey } = await encryptNote({ content, password, assets, encryptionAlgorithm, serializationFormat }); - const isPasswordProtected = Boolean(password); + const { encryptedPayload, encryptionKey } = await encryptNote({ content, password, assets, encryptionAlgorithm, serializationFormat }); + const isPasswordProtected = Boolean(password); - const { noteId } = await storeNote({ - payload: encryptedPayload, - ttlInSeconds, - deleteAfterReading, - encryptionAlgorithm, - serializationFormat, - isPublic, - }); + const { noteId } = await storeNote({ + payload: encryptedPayload, + ttlInSeconds, + deleteAfterReading, + encryptionAlgorithm, + serializationFormat, + isPublic, + }); - const { noteUrl } = createNoteUrl({ - noteId, - encryptionKey, - clientBaseUrl, - isPasswordProtected, - }); + const { noteUrl } = createNoteUrl({ + noteId, + encryptionKey, + clientBaseUrl, + isPasswordProtected, + }); - return { - encryptedPayload, - encryptionKey, - noteId, - noteUrl, - }; - }, + return { + encryptedPayload, + encryptionKey, + noteId, + noteUrl, }; -}; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c4b66b6..fb0cc288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,7 +59,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.0.0 - version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) '@iconify-json/tabler': specifier: ^1.1.120 version: 1.2.3 @@ -132,7 +132,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.0.0 - version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) '@cloudflare/workers-types': specifier: ^4.20240821.1 version: 4.20240909.0 @@ -211,7 +211,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.0.0 - version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -234,6 +234,40 @@ importers: specifier: ^2.0.5 version: 2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0) + packages/crypto: + dependencies: + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + devDependencies: + '@antfu/eslint-config': + specifier: ^3.0.0 + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^22.5.4 + version: 22.5.5 + '@vitest/coverage-v8': + specifier: ^2.0.5 + version: 2.1.1(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + eslint: + specifier: ^9.10.0 + version: 9.10.0(jiti@1.21.6) + tsx: + specifier: ^4.17.0 + version: 4.19.1 + typescript: + specifier: ^5.5.4 + version: 5.6.2 + unbuild: + specifier: ^2.0.0 + version: 2.0.0(typescript@5.6.2) + vitest: + specifier: ^2.0.5 + version: 2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0) + packages/deploy-cloudflare: {} packages/docs: @@ -244,7 +278,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.0.0 - version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) '@types/bcryptjs': specifier: ^2.4.6 version: 2.4.6 @@ -272,6 +306,9 @@ importers: packages/lib: dependencies: + '@enclosed/crypto': + specifier: workspace:* + version: link:../crypto cbor-x: specifier: ^1.6.0 version: 1.6.0 @@ -287,7 +324,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.0.0 - version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + version: 3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -5347,7 +5384,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0))': + '@antfu/eslint-config@3.6.2(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.4)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0))': dependencies: '@antfu/install-pkg': 0.4.1 '@clack/prompts': 0.7.0 @@ -5356,7 +5393,7 @@ snapshots: '@stylistic/eslint-plugin': 2.8.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/eslint-plugin': 8.5.0(@typescript-eslint/parser@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/parser': 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) - '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0)) + '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0)) eslint: 9.10.0(jiti@1.21.6) eslint-config-flat-gitignore: 0.3.0(eslint@9.10.0(jiti@1.21.6)) eslint-flat-config-utils: 0.4.0 @@ -6836,7 +6873,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(jsdom@25.0.0)(less@4.2.0)(terser@5.32.0))': + '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.5)(less@4.2.0)(terser@5.32.0))': dependencies: eslint: 9.10.0(jiti@1.21.6) optionalDependencies: