8000 Added Optional Local Encryption (chacha20) by 18c83fd3-25ea-4ed9-8205-2abeff9b3883 · Pull Request #430 · CorentinTh/enclosed · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Added Optional Local Encryption (chacha20) #430

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions packages/app-client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Router } from '@solidjs/router';
import { render, Suspense } from 'solid-js/web';
import { I18nProvider } from './modules/i18n/i18n.provider';
import { NoteContextProvider } from './modules/notes/notes.context';
import { ConfigProvider } from './modules/config/config.provider';
import { Toaster } from './modules/ui/components/sonner';
import { getRoutes } from './routes';
import '@unocss/reset/tailwind.css';
Expand All @@ -23,17 +24,18 @@ render(
root={props => (
<Suspense>
<I18nProvider>
<NoteContextProvider>
<ColorModeScript storageType={localStorageManager.type} storageKey={colorModeStorageKey} initialColorMode={initialColorMode} />
<ColorModeProvider
initialColorMode={initialColorMode}
storageManager={localStorageManager}
>
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div>
<Toaster />

</ColorModeProvider>
</NoteContextProvider>
<ConfigProvider>
<NoteContextProvider>
<ColorModeScript storageType={localStorageManager.type} storageKey={colorModeStorageKey} initialColorMode={initialColorMode} />
<ColorModeProvider
initialColorMode={initialColorMode}
storageManager={localStorageManager}
>
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div>
<Toaster />
</ColorModeProvider>
</NoteContextProvider>
</ConfigProvider>
</I18nProvider>
</Suspense>
)}
Expand Down
13 changes: 12 additions & 1 deletion packages/app-client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@
"support": "Support Enclosed",
"report-bug": "Report a bug",
"logout": "Logout",
"contribute-to-i18n": "Contribute to i18n"
"contribute-to-i18n": "Contribute to i18n",
"title": "Settings",
"security": "Security Settings"
},
"config": {
"encryptionAlgorithm": "Encryption Algorithm",
"aes256gcm": "AES-256-GCM",
"chacha20poly1305": "ChaCha20-Poly1305",
"default": "Default",
"httpCompatible": "HTTP Compatible",
"aes256gcmDescription": "Standard encryption algorithm with broad browser support.",
"chacha20poly1305Description": "High-performance encryption that works on HTTP pages and devices without AES hardware acceleration."
}
},
"footer": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Show } from 'solid-js';
import { useI18n } from '@/modules/i18n/i18n.provider';
import { useConfig, AES_256_GCM, CHACHA20_POLY1305, EncryptionAlgorithm } from '../config.provider';

interface EncryptionAlgorithmSelectorProps {
readonly class?: string;
}

/**
* Component that allows users to select their preferred encryption algorithm
*/
export function EncryptionAlgorithmSelector(props: EncryptionAlgorithmSelectorProps) {
const { t } = useI18n();
const { getEncryptionAlgorithm, setEncryptionAlgorithm } = useConfig();

// Handle algorithm change
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement;
const newAlgorithm = target.value as EncryptionAlgorithm;
setEncryptionAlgorithm(newAlgorithm);
};

return (
<div class={`encryption-algorithm-selector ${props.class ?? ''}`}>
<label for="encryption-algorithm" class="block text-sm font-medium mb-1">
{t('navbar.config.encryptionAlgorithm')}
</label>

<select
id="encryption-algorithm"
value={getEncryptionAlgorithm()}
>
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
>
<option value={AES_256_GCM}>
{t('navbar.config.aes256gcm')} ({t('navbar.config.default')})
</option>
<option value={CHACHA20_POLY1305}>
{t('navbar.config.chacha20poly1305')} ({t('navbar.config.httpCompatible')})
</option>
</select>

<Show
when={getEncryptionAlgorithm() === CHACHA20_POLY1305}
fallback={
<p class="mt-1 text-sm text-gray-500">
{t('navbar.config.aes256gcmDescription')}
</p>
}
>
<p class="mt-1 text-sm text-gray-500">
{t('navbar.config.chacha20poly1305Description')}
</p>
</Show>
</div>
);
}
134 changes: 122 additions & 12 deletions packages/app-client/src/modules/config/config.provider.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,128 @@
import type { Config } from './config.types';
import { get } from 'lodash-es';
import { buildTimeConfig } from './config.constants';
import { createContext, createEffect, useContext, createSignal, ParentComponent, createMemo } from 'solid-js';
import { makePersisted } from '@solid-primitives/storage';

export {
getConfig,
};
// Define encryption algorithm constants
export const AES_256_GCM = 'aes-256-gcm';
export const CHACHA20_POLY1305 = 'chacha20-poly1305';

// Configuration object type
export interface Config {
viewNotePathPrefix: string;
isAuthenticationRequired: boolean;
preferredEncryptionAlgorithm: EncryptionAlgorithm;
}

function getConfig(): Config {
const ru 6D47 ntimeConfig: Partial<Config> = get(window, '__CONFIG__', {});
// Default configuration
const defaultConfig: Config = {
viewNotePathPrefix: 'n',
isAuthenticationRequired: false,
preferredEncryptionAlgorithm: AES_256_GCM,
};

const config: Config = {
...buildTimeConfig,
...runtimeConfig,
// Get the application configuration
export function getConfig(): Config {
// We don't need to catch exceptions here since useContext doesn't throw
// and we're just checking if we're in a component context
const context = useContext(ConfigContext);
if (context) {
return context.config;
}

// Fallback: Try to get the preferred encryption algorithm from localStorage
let preferredEncryptionAlgorithm = AES_256_GCM;

try {
const storedAlgorithm = localStorage.getItem('enclosed_encryption_algorithm');
if (storedAlgorithm && (storedAlgorithm === AES_256_GCM || storedAlgorithm === CHACHA20_POLY1305)) {
preferredEncryptionAlgorithm = storedAlgorithm as EncryptionAlgorithm;
}
} catch (error) {
// This can happen in environments where localStorage is not available or restricted
// (e.g., incognito mode, some browser settings, or server-side rendering)
console.error('Failed to read encryption algorithm from localStorage:', error);

// Fall back to default encryption algorithm
preferredEncryptionAlgorithm = AES_256_GCM;
}

return {
...defaultConfig,
preferredEncryptionAlgorithm: preferredEncryptionAlgorithm as EncryptionAlgorithm,
};
}

// Define the encryption algorithm type
export type EncryptionAlgorithm = typeof AES_256_GCM | typeof CHACHA20_POLY1305;

return config;
// Define the configuration context type
interface ConfigContextType {
getEncryptionAlgorithm: () => EncryptionAlgorithm;
setEncryptionAlgorithm: (algorithm: EncryptionAlgorithm) => void;
supportedEncryptionAlgorithms: readonly EncryptionAlgorithm[];
config: Config;
}

// Create the context with a default undefined value
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);

// Hook to use the config context
export function useConfig() {
const context = useContext(ConfigContext);

if (!context) {
throw new Error('useConfig must be used within a ConfigProvider');
}

return context;
}

// Configuration provider component
export const ConfigProvider: ParentComponent = (props) => {
// Create a persisted signal for the encryption algorithm
const [getEncryptionAlgorithm, setEncryptionAlgorithm] = makePersisted(
createSignal<EncryptionAlgorithm>(AES_256_GCM),
{ name: 'enclosed_encryption_algorithm', storage: localStorage }
);

// List of supported encryption algorithms
const supportedEncryptionAlgorithms = [AES_256_GCM, CHACHA20_POLY1305] as const;

// Validate the stored algorithm on load
createEffect(() => {
const currentAlgorithm = getEncryptionAlgorithm();
const isValidAlgorithm = supportedEncryptionAlgorithms.includes(currentAlgorithm as EncryptionAlgorithm);

if (!isValidAlgorithm) {
setEncryptionAlgorithm(AES_256_GCM);
}
});

// Create a reactive config object that updates when the encryption algorithm changes
const [config, setConfig] = createSignal<Config>({
...defaultConfig,
preferredEncryptionAlgorithm: getEncryptionAlgorithm()
});

// Update config when encryption algorithm changes
createEffect(() => {
const algorithm = getEncryptionAlgorithm();
setConfig(prev => ({
...prev,
preferredEncryptionAlgorithm: algorithm
}));
});

// Create the context value with createMemo to prevent it from changing on every render
const contextValue = createMemo<ConfigContextType>(() => ({
getEncryptionAlgorithm,
setEncryptionAlgorithm,
supportedEncryptionAlgorithms,
config: config()
}));

return (
<ConfigContext.Provider value={contextValue()}>
{props.children}
</ConfigContext.Provider>
);
};
40 changes: 40 additions & 0 deletions F438 packages/app-client/src/modules/config/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { AES_256_GCM, CHACHA20_POLY1305, getConfig } from './config.provider';

// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};

return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => { store[key] = value; },
clear: () => { store = {}; },
removeItem: (key: string) => { delete store[key]; },
};
})();

// Mock the window.localStorage
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

describe('Config Provider', () => {
beforeEach(() => {
localStorageMock.clear();
});

test('getConfig returns default encryption algorithm when none is set', () => {
const config = getConfig();
expect(config.preferredEncryptionAlgorithm).toBe(AES_256_GCM);
});

test('getConfig returns ChaCha20-Poly1305 when set in localStorage', () => {
localStorageMock.setItem('enclosed_encryption_algorithm', CHACHA20_POLY1305);
const config = getConfig();
expect(config.preferredEncryptionAlgorithm).toBe(CHACHA20_POLY1305);
});

test('getConfig ignores invalid encryption algorithm in localStorage', () => {
localStorageMock.setItem('enclosed_encryption_algorithm', 'invalid-algorithm');
const config = getConfig();
expect(config.preferredEncryptionAlgorithm).toBe(AES_256_GCM);
});
});
28 changes: 28 additions & 0 deletions packages/app-client/src/modules/config/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useI18n } from '@/modules/i18n/i18n.provider';
import { EncryptionAlgorithmSelector } from '../components/EncryptionAlgorithmSelector';

/**
* Settings page component that includes configuration options
*/
export function SettingsPage() {
const { t } = useI18n();

return (
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">{t('navbar.settings.title')}</h1>

<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">{t('navbar.settings.security')}</h2>

<div class="space-y-6">
{/* Encryption Algorithm Selector */}
<EncryptionAlgorithmSelector />

{/* Other security settings can be added here */}
</div>
</div>

{/* Additional settings sections can be added here */}
</div>
);
}
5 changes: 5 additions & 0 deletions packages/app-client/src/modules/ui/layouts/app.layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ export const Navbar: Component = () => {
</DropdownMenuSub>

{/* Default items */}
<DropdownMenuItem as={A} class="flex items-center gap-2 cursor-pointer" href="/settings">
<div class="i-tabler-settings text-lg"></div>
{t('navbar.settings.title')}
</DropdownMenuItem>

<DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" href={buildDocUrl({ path: '/' })}>
<div class="i-tabler-file-text text-lg"></div>
{t('navbar.settings.documentation')}
Expand Down
5 changes: 5 additions & 0 deletions packages/app-client/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { A, type RouteDefinition } from '@solidjs/router';
import { LoginPage } from './modules/auth/pages/login.page';
import { getConfig } from './modules/config/config.provider';
import { SettingsPage } from './modules/config/pages/SettingsPage';
import { NOTE_ID_REGEX } from './modules/notes/notes.constants';
import { buildViewNotePagePath } from './modules/notes/notes.models';
import { CreateNotePage } from './modules/notes/pages/create-note.page';
Expand Down Expand Up @@ -29,6 +30,10 @@ export function getRoutes(): RouteDefinition[] {
noteId: NOTE_ID_REGEX,
},
},
{
path: '/settings',
component: SettingsPage,
},
{
path: '*404',
component: () => (
Expand Down
2 changes: 1 addition & 1 deletion packages/app-server/src/modules/app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const configDefinition = {
routeTimeoutMs: {
doc: 'The maximum time in milliseconds for a route to complete before timing out',
schema: z.coerce.number().int().positive(),
default: 5_000,
default: 30_000, // Increased from 5s to 30s to accommodate larger file uploads
env: 'SERVER_API_ROUTES_TIMEOUT_MS',
},
corsOrigins: {
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ By leveraging client-side encryption and a zero-knowledge server, Enclosed guara
## Get Started

Ready to start using Enclosed? You can [try it out online](https://enclosed.cc) or [self-host](./self-hosting/docker) your instance for maximum control. Dive into our documentation to learn more about how Enclosed works and how you can take full advantage of its features.

## Disclaimer

**Enclosed is provided "as is", without warranty of any kind, express or implied.** The creators and contributors of Enclosed are not responsible for the content of any notes created or shared using the service, or for any actions taken by users based on such content. Users are solely responsible for their use of the service and any content they create, share, or access.

If you choose to self-host an instance of Enclosed, you do so at your own risk. The creators and contributors are not responsible for any issues, security breaches, or other problems that may arise from self-hosting.

For more detailed information, please review our [Privacy Policy](./legal/privacy-policy.md) and [Terms of Use](./legal/terms-of-use.md).
Loading
0