Skip to content

Commit

Permalink
refactor(packages): mutualised encryption in a lib package (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh authored Aug 31, 2024
1 parent 07de2eb commit 10645e0
Show file tree
Hide file tree
Showing 32 changed files with 1,906 additions and 220 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/ci-lib.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI - Lib

on:
pull_request:
push:
branches:
- main

jobs:
ci-lib:
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/lib

steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 22
corepack: true
cache: 'pnpm'

- name: Install dependencies
run: pnpm i
working-directory: ./

- name: Run linters
run: pnpm lint

# - name: Type check
# run: pnpm typecheck

- name: Run unit test
run: pnpm test

- name: Build the lib
run: pnpm build
1 change: 0 additions & 1 deletion packages/app-client/.nvmrc

This file was deleted.

1 change: 1 addition & 0 deletions packages/app-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@enclosed/lib": "workspace:*",
"@kobalte/core": "^0.13.4",
"@solidjs/router": "^0.14.3",
"@unocss/reset": "^0.62.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { type Component, createSignal } from 'solid-js';
import { createRandomPassword } from '../notes.models';
import { TextField } from '@/modules/ui/components/textfield';
import { Button } from '@/modules/ui/components/button';
import { createRandomString } from '@/modules/shared/random/random';

export const NotePasswordField: Component<{ getPassword: () => string; setPassword: (value: string) => void }> = (props) => {
const [getShowPassword, setShowPassword] = createSignal(false);

const generateRandomPassword = () => {
const password = createRandomString({ length: 16 });
const password = createRandomPassword({ length: 16 });

setShowPassword(true);
props.setPassword(password);
Expand Down
44 changes: 8 additions & 36 deletions packages/app-client/src/modules/notes/notes.models.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,13 @@
import { createBuffer, mergeBuffers } from '../shared/crypto/buffer';
import { aesDecrypt, aesEncrypt, deriveKey } from '../shared/crypto/encryption';
import _ from 'lodash-es';

export { encryptNoteContent, decryptNoteContent, createNoteUrl, deriveMasterKey };
export { createRandomPassword };

function createBufferFromPassword({ password }: { password?: string }) {
if (!password) {
return new Uint8Array(0);
}
function createRandomPassword({ length = 16 }: { length?: number } = {}): string {
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const numbers = '0123456789';
const specialChars = '!@#$%^&*()_+';

return new TextEncoder().encode(password);
}

async function encryptNoteContent({ content, masterKey }: { content: string; masterKey: Uint8Array }) {
const contentBuffer = createBuffer({ value: content });
const encryptedContent = await aesEncrypt({ data: contentBuffer, key: masterKey });

return encryptedContent;
}

async function decryptNoteContent({ encryptedContent, masterKey }: { encryptedContent: string; masterKey: Uint8Array }) {
const decryptedContent = await aesDecrypt({ data: encryptedContent, key: masterKey });

return decryptedContent;
}

function createNoteUrl({ noteId, encryptionKey }: { noteId: string; encryptionKey: string }): { noteUrl: string } {
const url = new URL(`/${noteId}`, window.location.origin);
url.hash = encryptionKey;

const noteUrl = url.toString();

return { noteUrl };
}

function deriveMasterKey({ baseKey, password }: { baseKey: Uint8Array; password?: string }) {
const passwordBuffer = createBufferFromPassword({ password });
const mergedBuffers = mergeBuffers(baseKey, passwordBuffer);
const corpus = alphabet + alphabet.toUpperCase() + numbers + specialChars;

return deriveKey({ key: mergedBuffers });
return _.times(length, () => _.sample(corpus)).join('');
}
4 changes: 2 additions & 2 deletions packages/app-client/src/modules/notes/notes.services.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiClient } from '../shared/http/http-client';

export { createNote, fetchNoteById };
export { storeNote, fetchNoteById };

async function createNote({ content, isPasswordProtected, ttlInSeconds, deleteAfterReading }: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean }) {
async function storeNote({ content, isPasswordProtected, ttlInSeconds, deleteAfterReading }: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean }) {
const { noteId } = await apiClient<{ noteId: string }>({
path: '/api/notes',
method: 'POST',
Expand Down
55 changes: 9 additions & 46 deletions packages/app-client/src/modules/notes/notes.usecases.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,17 @@
import { base64UrlToBuffer, bufferToBase64Url } from '../shared/crypto/buffer';
import { createRandomBuffer } from '../shared/random/random';
import { createNoteUrl, decryptNoteContent, deriveMasterKey, encryptNoteContent } from './notes.models';
import { createNote } from './notes.services';
import { createNote } from '@enclosed/lib';
import { storeNote } from './notes.services';

export { encryptAndCreateNote, decryptNote, encryptNote };
export { encryptAndCreateNote };

async function encryptAndCreateNote({
content,
password,
ttlInSeconds,
deleteAfterReading,
storeNote = createNote,
}: {
async function encryptAndCreateNote(args: {
content: string;
password?: string;
ttlInSeconds: number;
deleteAfterReading: boolean;
storeNote?: (params: { content: string; isPasswordProtected: boolean; ttlInSeconds: number; deleteAfterReading: boolean }) => Promise<{ noteId: string }>;
}) {
const { encryptedContent, encryptionKey } = await encryptNote({ content, password });

// Send the encrypted note to the server for storage, the server has no knowledge of the encryption key
const { noteId } = await storeNote({ content: encryptedContent, isPasswordProtected: Boolean(password), ttlInSeconds, deleteAfterReading });

// The base key is stored in the URL hash fragment
const { noteUrl } = createNoteUrl({ noteId, encryptionKey });

return { encryptedContent, noteId, encryptionKey, noteUrl };
}

async function encryptNote({ content, password }: { content: string; password?: string }) {
// The base key ensure e2e encryption even if the user does not provide a password
const baseKey = createRandomBuffer({ length: 32 });

// If the user provides a password, we derive a master key from the base key and the password using PBKDF2
const masterKey = await deriveMasterKey({ baseKey, password });

const encryptedContent = await encryptNoteContent({ content, masterKey });

const encryptionKey = bufferToBase64Url({ buffer: baseKey });

return { encryptedContent, encryptionKey };
}

async function decryptNote({ encryptedContent, password, encryptionKey }: { encryptedContent: string; password?: string; encryptionKey: string }) {
const baseKey = base64UrlToBuffer({ base64Url: encryptionKey });

const masterKey = await deriveMasterKey({ baseKey, password });

const decryptedContent = await decryptNoteContent({ encryptedContent, masterKey });

return { decryptedContent };
return createNote({
...args,
storeNote,
clientBaseUrl: window.location.origin,
});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocation, useParams } from '@solidjs/router';
import { type Component, Match, Show, Switch, createSignal, onMount } from 'solid-js';
import { decryptNote } from '@enclosed/lib';
import { fetchNoteById } from '../notes.services';
import { decryptNote } from '../notes.usecases';
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
import { Card, CardContent, CardDescription, CardHeader } from '@/modules/ui/components/card';
import { Button } from '@/modules/ui/components/button';
Expand Down
47 changes: 0 additions & 47 deletions packages/app-client/src/modules/shared/crypto/buffer.ts

This file was deleted.

56 changes: 0 additions & 56 deletions packages/app-client/src/modules/shared/crypto/encryption.ts

This file was deleted.

16 changes: 0 additions & 16 deletions packages/app-client/src/modules/shared/random/random.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/lib/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',
],

declaration: true,
sourcemap: true,
rollup: {
emitCJS: true,
},
});
21 changes: 21 additions & 0 deletions packages/lib/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: '^_',
}],
},
});
Loading

0 comments on commit 10645e0

Please sign in to comment.