From 7dc91de86c41866f4170602f074c39dbf1609377 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 15 Oct 2026 17:08:00 +0400 Subject: [PATCH] refac --- .eslintignore | 4 + .prettierignore | 5 +- backend/open_webui/config.py | 3 + backend/open_webui/main.py | 2 + backend/open_webui/routers/auths.py | 4 + backend/open_webui/routers/channels.py | 5 + backend/open_webui/socket/encryption.py | 401 + backend/open_webui/socket/main.py | 6 + crypto/encryption/.gitignore | 1 + crypto/encryption/Cargo.lock | 2026 +++++ crypto/encryption/Cargo.toml | 28 + crypto/encryption/rust-toolchain.toml | 4 + crypto/encryption/src/lib.rs | 267 + package.json | 5 + scripts/encryption/artifact.mjs | 71 + scripts/encryption/build.py | 136 + .../components/admin/Settings/General.svelte | 8 + src/lib/components/channel/Channel.svelte | 82 +- src/lib/components/channel/ChannelMenu.svelte | 27 +- .../components/channel/MessageInput.svelte | 6 +- .../channel/Messages/Message.svelte | 41 +- src/lib/components/channel/Navbar.svelte | 79 +- .../components/chat/Messages/CodeBlock.svelte | 6 +- .../components/chat/Messages/Markdown.svelte | 22 +- .../Markdown/MarkdownInlineTokens.svelte | 3 +- .../Messages/Markdown/MarkdownTokens.svelte | 6 - src/lib/components/common/Textarea.svelte | 32 +- src/lib/encryption/client.ts | 407 + .../vendor/THIRD_PARTY_LICENSES.txt | 7011 +++++++++++++++++ src/lib/encryption/vendor/encryption.d.ts | 68 + src/lib/encryption/vendor/encryption.js | 587 ++ src/lib/encryption/vendor/encryption_bg.wasm | Bin 0 -> 2085445 bytes .../encryption/vendor/encryption_bg.wasm.d.ts | 24 + src/lib/encryption/vendor/manifest.json | 741 ++ src/lib/encryption/worker.ts | 42 + src/lib/i18n/locales/en-US/translation.json | 2 + src/lib/stores/index.ts | 2 + .../channels/[id]/encrypted/+page.svelte | 354 + vite.config.ts | 4 + 39 files changed, 12438 insertions(+), 84 deletions(-) create mode 100644 backend/open_webui/socket/encryption.py create mode 100644 crypto/encryption/.gitignore create mode 100644 crypto/encryption/Cargo.lock create mode 100644 crypto/encryption/Cargo.toml create mode 100644 crypto/encryption/rust-toolchain.toml create mode 100644 crypto/encryption/src/lib.rs create mode 100644 scripts/encryption/artifact.mjs create mode 100644 scripts/encryption/build.py create mode 100644 src/lib/encryption/client.ts create mode 100644 src/lib/encryption/vendor/THIRD_PARTY_LICENSES.txt create mode 100644 src/lib/encryption/vendor/encryption.d.ts create mode 100644 src/lib/encryption/vendor/encryption.js create mode 100644 src/lib/encryption/vendor/encryption_bg.wasm create mode 100644 src/lib/encryption/vendor/encryption_bg.wasm.d.ts create mode 100644 src/lib/encryption/vendor/manifest.json create mode 100644 src/lib/encryption/worker.ts create mode 100644 src/routes/(app)/channels/[id]/encrypted/+page.svelte diff --git a/.eslintignore b/.eslintignore index 38972655fa..888068f984 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,3 +11,7 @@ node_modules pnpm-lock.yaml package-lock.json yarn.lock + +# Generated and checksum-verified crypto distribution +src/lib/encryption/vendor/ +crypto/encryption/target/ diff --git a/.prettierignore b/.prettierignore index 83bbde598b..bb9f25f137 100644 --- a/.prettierignore +++ b/.prettierignore @@ -311,4 +311,7 @@ cypress/screenshots -/static/* \ No newline at end of file +/static/* +# Generated and checksum-verified crypto distribution +src/lib/encryption/vendor/ +crypto/encryption/target/ diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 210eafcb71..c1fc5c8815 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2055,6 +2055,8 @@ FOLDER_MAX_FILE_COUNT = os.getenv('FOLDER_MAX_FILE_COUNT', '') ENABLE_CHANNELS = os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true' +ENABLE_ENCRYPTED_CHANNELS = os.getenv('ENABLE_ENCRYPTED_CHANNELS', 'False').lower() == 'true' + ENABLE_APPS = os.getenv('ENABLE_APPS', 'False').lower() == 'true' CHANNEL_MODEL_RESPONSE_MODE = os.getenv('CHANNEL_MODEL_RESPONSE_MODE', 'thread') @@ -3179,6 +3181,7 @@ DEFAULT_CONFIG = { 'folders.enable': ENABLE_FOLDERS, 'folders.max_file_count': FOLDER_MAX_FILE_COUNT, 'channels.enable': ENABLE_CHANNELS, + 'channels.encryption.enable': ENABLE_ENCRYPTED_CHANNELS, 'apps.enable': ENABLE_APPS, 'channels.model_response_mode': CHANNEL_MODEL_RESPONSE_MODE, 'calendar.enable': ENABLE_CALENDAR, diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index c9540eeb9d..16052c494f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2338,6 +2338,7 @@ async def get_app_config(request: Request): 'folders.enable', 'folders.max_file_count', 'channels.enable', + 'channels.encryption.enable', 'apps.enable', 'calendar.enable', 'automations.enable', @@ -2434,6 +2435,7 @@ async def get_app_config(request: Request): 'enable_folders': config.get('folders.enable'), 'folder_max_file_count': config.get('folders.max_file_count'), 'enable_channels': config.get('channels.enable'), + 'enable_encrypted_channels': config.get('channels.encryption.enable'), 'enable_apps': config.get('apps.enable'), 'enable_calendar': config.get('calendar.enable'), 'enable_automations': config.get('automations.enable'), diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 9805b32b7a..b48d6b1c45 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -128,6 +128,7 @@ ADMIN_CONFIG_KEYS = { 'AUTOMATION_MIN_INTERVAL': 'automations.min_interval', 'ENABLE_AUTOMATIONS': 'automations.enable', 'ENABLE_CHANNELS': 'channels.enable', + 'ENABLE_ENCRYPTED_CHANNELS': 'channels.encryption.enable', 'ENABLE_APPS': 'apps.enable', 'CHANNEL_MODEL_RESPONSE_MODE': 'channels.model_response_mode', 'ENABLE_CALENDAR': 'calendar.enable', @@ -1178,6 +1179,7 @@ class AdminConfig(MfaConfigForm): AUTOMATION_MIN_INTERVAL: int | str | None = None ENABLE_AUTOMATIONS: bool ENABLE_CHANNELS: bool + ENABLE_ENCRYPTED_CHANNELS: bool = False ENABLE_APPS: bool = False CHANNEL_MODEL_RESPONSE_MODE: str = 'thread' ENABLE_CALENDAR: bool @@ -1225,6 +1227,8 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep updates.pop(key, None) if 'ENABLE_LOGIN_FORM' not in form_data.model_fields_set: updates.pop('ui.enable_login_form', None) + if 'ENABLE_ENCRYPTED_CHANNELS' not in form_data.model_fields_set: + updates.pop('channels.encryption.enable', None) if 'ENABLE_APPS' not in form_data.model_fields_set: updates.pop('apps.enable', None) if 'I18N' not in form_data.model_fields_set: diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 6ef96c8062..1e03a1f4f1 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -42,6 +42,7 @@ from open_webui.models.users import ( Users, ) from open_webui.socket.main import ( + encryption, emit_to_users, enter_room_for_users, get_user_ids_from_room, @@ -657,6 +658,7 @@ async def update_is_active_member_by_id_and_user_id( if not await Channels.is_user_channel_member(channel.id, user.id, db=db): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + await encryption.invalidate(id) await Channels.update_member_active_status(channel.id, user.id, form_data.is_active, db=db) await publish_event( request, @@ -753,6 +755,7 @@ async def remove_members_by_id( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: + await encryption.invalidate(id) deleted = await Channels.remove_members_from_channel(channel.id, form_data.user_ids, db=db) if channel.type == 'group': await leave_room_for_users(f'channel:{channel.id}', form_data.user_ids) @@ -803,6 +806,7 @@ async def update_channel_by_id( previous_access_grants = channel.access_grants try: + await encryption.invalidate(id) channel = await Channels.update_channel_by_id(id, form_data, db=db) # Group and DM channels use membership instead of access grants. if form_data.access_grants is not None and channel.type not in ['group', 'dm']: @@ -847,6 +851,7 @@ async def delete_channel_by_id( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: + await encryption.invalidate(id) await Channels.delete_channel_by_id(id, db=db) await sio.close_room(f'channel:{id}') await publish_event( diff --git a/backend/open_webui/socket/encryption.py b/backend/open_webui/socket/encryption.py new file mode 100644 index 0000000000..3a5bf7cb23 --- /dev/null +++ b/backend/open_webui/socket/encryption.py @@ -0,0 +1,401 @@ +"""Volatile packet relay. Only account bindings and liveness enter coordination storage.""" + +import asyncio +import base64 +import hashlib +import json +import time +from contextlib import asynccontextmanager +from contextvars import ContextVar +from uuid import uuid4 + +from open_webui.env import ( + ENABLE_WEBSOCKET_SUPPORT, + ENV, + REDIS_KEY_PREFIX, + WEBSOCKET_SERVER_ENGINEIO_LOGGING, + WEBSOCKET_SERVER_LOGGING, +) +from open_webui.models.channels import Channels +from open_webui.models.config import Config +from open_webui.models.users import Users +from open_webui.utils.access_control import has_permission + +MAX_PACKET = 1024 * 1024 +TTL = 90 + + +class Rejected(Exception): + pass + + +def require(condition): + if not condition: + raise Rejected() + + +def packet(value): + require(isinstance(value, str) and 0 < len(value) <= (MAX_PACKET + 2) // 3 * 4) + try: + decoded = base64.b64decode(value, validate=True) + except (ValueError, TypeError): + raise Rejected() from None + require(0 < len(decoded) <= MAX_PACKET) + return value + + +class Registry: + def __init__(self, redis=None): + self.redis = redis + self.values = {} + self.lock = asyncio.Lock() + self.lease = ContextVar('encryption_lease', default=None) + + def key(self, channel): + digest = hashlib.sha256(channel.encode()).hexdigest() + return f'{REDIS_KEY_PREFIX}:encryption:{{{digest}}}' + + @asynccontextmanager + async def transaction(self, channel): + key = self.key(channel) + lock = self.redis.lock(key + ':lock', timeout=10, blocking_timeout=2) if self.redis else self.lock + async with lock: + raw = await self.redis.get(key) if self.redis else self.values.get(key) + state = json.loads(raw) if raw else None + if state and state['expires'] <= time.time(): + state = None + token = self.lease.set(lock if self.redis else None) + try: + yield state + finally: + self.lease.reset(token) + + async def save(self, channel, state): + key = self.key(channel) + if state is not None: + state['expires'] = time.time() + TTL + raw = json.dumps(state, separators=(',', ':')) if state is not None else '' + if self.redis: + lease = self.lease.get() + require(lease is not None) + # A stalled worker must never overwrite a newer owner's metadata. + changed = await self.redis.eval( + "if redis.call('get', KEYS[2]) ~= ARGV[1] then return 0 end " + "if ARGV[2] == '' then redis.call('del', KEYS[1]) " + "else redis.call('set', KEYS[1], ARGV[2], 'EX', ARGV[3]) end return 1", + 2, + key, + key + ':lock', + lease.local.token, + raw, + TTL, + ) + if not changed: + raise RuntimeError('Coordination lease expired') + elif state is None: + self.values.pop(key, None) + else: + self.values[key] = raw + + +class Relay: + def __init__(self, sio, authenticate, redis=None): + self.sio, self.authenticate = sio, authenticate + self.registry = Registry(redis) + self.local = {} # sid -> channel, never payloads + self.rates = {} + for action in ('start', 'status', 'join', 'bind', 'packet', 'leave', 'ping'): + + async def handler(sid, data=None, action=action): + return await self.handle(action, sid, data) + + sio.on(f'encryption:{action}', handler) + + async def authorize(self, sid, channel): + require(await Config.get('channels.encryption.enable')) + require(ENABLE_WEBSOCKET_SUPPORT and self.sio.transport(sid) == 'websocket') + require(not WEBSOCKET_SERVER_LOGGING and not WEBSOCKET_SERVER_ENGINEIO_LOGGING) + scope = (self.sio.get_environ(sid) or {}).get('asgi.scope', {}) + require(ENV != 'prod' or scope.get('scheme') in ('https', 'wss')) + auth = await self.authenticate(sid, wait_for_disconnect=False) + require(auth and auth.get('id')) + user = await Users.get_user_by_id(auth['id']) + require(user and user.role in ('user', 'admin')) + require(await Config.get('channels.enable')) + require( + user.role == 'admin' + or await has_permission(user.id, 'features.channels', await Config.get('user.permissions')) + ) + record = await Channels.get_channel_by_id(channel) + require(record and (record.type == 'dm' or (record.type == 'group' and record.is_private is True))) + require(await Channels.is_user_channel_member(channel, user.id)) + return user + + @staticmethod + def view(state): + if not state: + return None + return { + 'channel': state['channel'], + 'session': state['session'], + 'host': state['host'], + 'bindings': [ + { + 'identity': { + 'channel': state['channel'], + 'session': state['session'], + 'user': p['user'], + 'participation': pid, + }, + 'key': p['key'], + } + for pid, p in state['members'].items() + if p.get('key') + ], + 'participants': [ + {'participation': pid, 'user': p['user'], 'name': p['name']} for pid, p in state['members'].items() + ], + } + + async def invite(self, state, channel): + await self.sio.emit( + 'encryption:available', + {'channel': channel, 'session': state['session'] if state else None}, + room=f'channel:{channel}', + ) + + async def deliver(self, state, recipients, event): + # Per-recipient sequence detects missing Pub/Sub deliveries without storing packets. + targets = [] + for pid in recipients: + member = state['members'][pid] + member['out'] += 1 + targets.append((member['sid'], member['out'])) + await self.registry.save(state['channel'], state) + for sid, sequence in targets: + await self.sio.emit('encryption:event', {**event, **self.view(state), 'sequence': sequence}, to=sid) + + async def close(self, channel, state): + await self.registry.save(channel, None) + if state: + for p in state['members'].values(): + self.local.pop(p['sid'], None) + await self.sio.emit( + 'encryption:event', {'kind': 'ended', 'channel': channel, 'session': state['session']}, to=p['sid'] + ) + await self.invite(None, channel) + + async def prune(self, state): + if not state: + return None + members = await Channels.get_members_by_channel_id(state['channel']) + active = {m.user_id for m in members if m.is_active} + users = await Users.get_users_by_user_ids([p['user'] for p in state['members'].values()]) + humans = {u.id for u in users if u.role in ('user', 'admin')} + stale = [] + for pid, p in state['members'].items(): + if p['seen'] < time.time() - 45 or p['user'] not in active or p['user'] not in humans: + stale.append(pid) + if state['host'] in stale: + await self.close(state['channel'], state) + return None + for pid in stale: + await self.remove(state, pid) + return state + + async def remove(self, state, pid): + if pid == state['host']: + await self.close(state['channel'], state) + return + p = state['members'].pop(pid) + self.local.pop(p['sid'], None) + await self.sio.emit( + 'encryption:event', {'kind': 'ended', 'channel': state['channel'], 'session': state['session']}, to=p['sid'] + ) + await self.deliver( + state, + [i for i, m in state['members'].items() if m['admitted']], + {'kind': 'departed', 'participation': pid, 'admitted': p['admitted']}, + ) + + async def limit(self, user, action, size): + now = time.monotonic() + if len(self.rates) > 10000: + self.rates = {k: v for k, v in self.rates.items() if now - v[0] < 60} + if self.registry.redis: + bucket = 'admission' if action in ('start', 'join') else 'traffic' + period, maximum = (60, 10) if bucket == 'admission' else (1, 100) + key = self.registry.key('rate:' + user + ':' + bucket) + allowed = await self.registry.redis.eval( + "local n = redis.call('hincrby', KEYS[1], 'count', 1) " + "local b = redis.call('hincrby', KEYS[1], 'bytes', ARGV[1]) " + "if n == 1 then redis.call('expire', KEYS[1], ARGV[2]) end " + 'if n > tonumber(ARGV[3]) or b > tonumber(ARGV[4]) then return 0 end return 1', + 1, + key, + size, + period, + maximum, + 4 * MAX_PACKET, + ) + require(allowed) + return + # Account-wide admission bound also applies across tabs on this worker. + key = (user, 'admission' if action in ('start', 'join') else 'traffic') + period, maximum = (60, 10) if key[1] == 'admission' else (1, 100) + start, count, total = self.rates.get(key, (now, 0, 0)) + if now - start >= period: + start, count, total = now, 0, 0 + require(count < maximum and total + size <= 4 * MAX_PACKET) + self.rates[key] = (start, count + 1, total + size) + + async def reserve(self, action, sid, user, channel, data, state): + # Another worker may have closed the session; its metadata is authoritative. + if self.local.get(sid) == channel and ( + not state or not any(p['sid'] == sid for p in state['members'].values()) + ): + self.local.pop(sid, None) + require(sid not in self.local) + if action == 'start': + require(state is None) + pid = str(uuid4()) + state = {'channel': channel, 'session': str(uuid4()), 'host': pid, 'members': {}} + else: + require(state and data.get('session') == state['session']) + require(len(state['members']) < 128 and not any(p['user'] == user.id for p in state['members'].values())) + require(state['members'][state['host']].get('key')) + pid = str(uuid4()) + state['members'][pid] = { + 'sid': sid, + 'user': user.id, + 'name': user.name, + 'key': None, + 'seen': time.time(), + 'out': 0, + 'admitted': action == 'start', + } + self.local[sid] = channel + await self.registry.save(channel, state) + return {'ok': True, **self.view(state), 'participation': pid} + + async def bind(self, state, pid, member, data): + require(member['key'] is None) + key = packet(data.get('key')) + require(len(base64.b64decode(key)) == 32) + member['key'] = key + if pid != state['host']: + kp = packet(data.get('packet')) + await self.deliver(state, [state['host']], {'kind': 'candidate', 'sender': pid, 'packet': kp}) + else: + await self.registry.save(state['channel'], state) + await self.invite(state, state['channel']) + + async def forward(self, state, pid, member, data): + require(member['key'] and member['admitted']) + body = packet(data.get('packet')) + if data.get('kind') == 'admit': + require(pid == state['host']) + target = data.get('target') + joining = state['members'].get(target) + require(joining and joining.get('key') and not joining['admitted']) + welcome = packet(data.get('welcome')) + existing = [i for i, p in state['members'].items() if i != pid and p['admitted']] + joining['admitted'] = True + await self.deliver(state, existing, {'kind': 'mls', 'sender': pid, 'packet': body}) + await self.deliver(state, [target], {'kind': 'welcome', 'sender': pid, 'packet': welcome}) + else: + require(data.get('kind') == 'mls') + recipients = [i for i, p in state['members'].items() if i != pid and p['admitted']] + await self.deliver(state, recipients, {'kind': 'mls', 'sender': pid, 'packet': body}) + + async def handle(self, action, sid, data): + channel = None + authorized = False + try: + require(isinstance(data, dict)) + channel = data.get('channel') + require(isinstance(channel, str) and 0 < len(channel) <= 128) + size = len(json.dumps(data)) + require(size <= 3 * MAX_PACKET) + user = await self.authorize(sid, channel) + authorized = True + await self.limit(user.id, action, size) + async with self.registry.transaction(channel) as state: + state = await self.prune(state) + if action == 'status': + return { + 'ok': True, + 'available': bool(state and state['members'][state['host']].get('key')), + 'session': state['session'] if state else None, + } + if action in ('start', 'join'): + return await self.reserve(action, sid, user, channel, data, state) + require(state and data.get('session') == state['session'] and self.local.get(sid) == channel) + pid = data.get('participation') + member = state['members'].get(pid) + require(member and member['sid'] == sid and member['user'] == user.id) + member['seen'] = time.time() + if action == 'leave': + await self.remove(state, pid) + return {'ok': True} + if action == 'bind': + await self.bind(state, pid, member, data) + elif action == 'ping': + await self.registry.save(channel, state) + elif action == 'packet': + await self.forward(state, pid, member, data) + else: + raise Rejected() + return {'ok': True, **self.view(state)} + except Rejected: + return {'ok': False, 'error': 'Private chat is unavailable or the session changed.'} + except Exception: + # Never log exceptions carrying packet input. Fail closed on coordinator failure. + if authorized: + await self.fail_local(channel) + return {'ok': False, 'error': 'Private chat ended.'} + + async def fail_local(self, channel): + for sid, local_channel in list(self.local.items()): + if channel is None or local_channel == channel: + self.local.pop(sid, None) + await self.sio.emit( + 'encryption:event', {'kind': 'ended', 'channel': local_channel}, to=sid, ignore_queue=True + ) + + async def disconnect(self, sid): + channel = self.local.pop(sid, None) + if not channel: + return + try: + async with self.registry.transaction(channel) as state: + if state: + for pid, p in list(state['members'].items()): + if p['sid'] == sid: + await self.remove(state, pid) + break + except Exception: + await self.fail_local(channel) + + async def invalidate(self, channel): + # Channel access mutations end the session before any further private sends. + try: + async with self.registry.transaction(channel) as state: + await self.close(channel, state) + except Exception: + await self.fail_local(channel) + + async def sweep(self): + for channel in set(self.local.values()): + try: + async with self.registry.transaction(channel) as state: + if not state: + await self.fail_local(channel) + else: + await self.prune(state) + except Exception: + await self.fail_local(channel) + if not self.registry.redis: + for key, raw in list(self.registry.values.items()): + if json.loads(raw)['expires'] <= time.time(): + self.registry.values.pop(key, None) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 5b2a826fee..7e0a51be05 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -42,6 +42,7 @@ from open_webui.models.config import Config from open_webui.models.folders import Folders from open_webui.models.notes import Notes, NoteUpdateForm from open_webui.models.users import UserNameResponse, Users +from open_webui.socket.encryption import Relay from open_webui.socket.redis_room_channels import AsyncRedisRoomChannelManager from open_webui.socket.utils import SOCKET_EVENT_LOCKS, CachedRedisDict, RedisDict, RedisLock, YdocManager from open_webui.tasks import ( @@ -355,6 +356,7 @@ LOCAL_AUTHENTICATED_SIDS: set[str] = set() async def periodic_socket_authentication(): while True: await asyncio.sleep(30) + await encryption.sweep() for sid in tuple(LOCAL_AUTHENTICATED_SIDS): await get_socket_session_user(sid) @@ -376,6 +378,9 @@ async def get_socket_session_user(sid: str, *, wait_for_disconnect: bool = True) return None +encryption = Relay(sio, get_socket_session_user, REDIS) + + def get_session_ids_from_room(room): """Get all session IDs from a specific room.""" members = get_room_sid_map(sio.manager, '/', room) @@ -1128,6 +1133,7 @@ async def yjs_awareness_update(sid, data): @sio.event async def disconnect(sid, reason=None): + await encryption.disconnect(sid) LOCAL_AUTHENTICATED_SIDS.discard(sid) if sid in SESSION_POOL: del SESSION_POOL[sid] diff --git a/crypto/encryption/.gitignore b/crypto/encryption/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/crypto/encryption/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/crypto/encryption/Cargo.lock b/crypto/encryption/Cargo.lock new file mode 100644 index 0000000000..7783d53fed --- /dev/null +++ b/crypto/encryption/Cargo.lock @@ -0,0 +1,2026 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-models" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3344d349528f449816cfa85e558c439bdca5a6d8a738cfb21e69f0b2b1952f" +dependencies = [ + "hax-lib", + "pastey", + "rand", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crabgrind" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7459e07732f6de74001fcf73a3fdfbb0f368e4fd8e2392f4a2206a065e6e95" +dependencies = [ + "bindgen", + "cc", + "pkg-config", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "curve25519-dalek-derive", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf 0.12.4", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hax-lib" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a33cb227f97ee5c419064c31d7c55a7d895f28d8019ccdadc311cc036500c4" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28da835a5f153e9f3d0cc1f42ee605a1cfa9093c93348193793d60a1b0908b51" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed327e9a28d6ee018a4a9ba842d4ee27e378c8a591d2beb6f42f32b24a02fcb" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hpke-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812de62ab573b876c6c40d7553bae9402ae3bad95b64af06f964388eecb9b7b1" +dependencies = [ + "hpke-rs-crypto", + "hpke-rs-libcrux", + "hpke-rs-rust-crypto", + "libcrux-sha3", + "log", + "serde", + "subtle", + "tls_codec", + "zeroize", +] + +[[package]] +name = "hpke-rs-crypto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c461da2e0c9c93d875b597f0c78214fb0d2d5c57834b2ef2a2fa057fa20e24" +dependencies = [ + "rand", + "zeroize", +] + +[[package]] +name = "hpke-rs-libcrux" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1ace95ef11fbbd84527eada5fc1304f66720fc4c008db30d888d62d3c52613" +dependencies = [ + "hpke-rs-crypto", + "libcrux-aead", + "libcrux-ecdh", + "libcrux-hkdf", + "libcrux-kem", + "libcrux-traits", + "rand", + "rand_chacha 0.10.0", + "rand_core 0.10.1", + "zeroize", +] + +[[package]] +name = "hpke-rs-rust-crypto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a606ac19851da841862ef5f39d26d6227bfcfb4047417801a66c1f6e6652d71" +dependencies = [ + "aes-gcm", + "chacha20poly1305", + "hkdf 0.13.0", + "hpke-rs-crypto", + "k256", + "ml-kem", + "p256", + "p384", + "rand", + "rand_chacha 0.10.0", + "rand_core 0.10.1", + "sha2 0.11.1", + "subtle", + "x-wing", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "ctutils", + "typenum", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", +] + +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libcrux-aead" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22acbe68be84b7b41aaba6e39b87036fc4324dee628d94773750bf3d7e906d69" +dependencies = [ + "libcrux-aes", + "libcrux-chacha20poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-aes" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cc95b11dbc797b1e169467b1fc4205c4e6ee2ce516a035ad7342ce5f6ae971" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537e6eee5cdc9a980014d058784b0be09614f0596df789b114f7833aa9f35d75" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f3cfd5e31cb9745e290ee061222c380ec42884654b09fc7d12a5b0ed63e028" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a227590b89ff55ce7cd97b5a7468c82d231db8f3b890bb4d4fb6d271e7524d7" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand", + "tls_codec", +] + +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66106db376ae249af86911aee048cf73ea1f394d6653026fc988dd22cf2a4ace" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc94e651dca4d47edbcdd88bb2428ce16a2bd86b7a4e7170da87b505478f9839" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500ad9b32c715161b594172ca1fb11503a1c033b393ff4c0c33505ce51c1943c" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a0c574d4eb81d0814bc2b91e4a433d7e0b35851b1d62eb5dd95c064f1f76d0" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541a7377fb35060892e0620982e224e47419f10da8c212453bf642dafe529691" +dependencies = [ + "libcrux-curve25519", + "libcrux-ecdh", + "libcrux-ml-kem", + "libcrux-p256", + "libcrux-sha3", + "libcrux-traits", + "rand", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8160f7d64fd2716b4fd05cc886a042f8dcda18d9206c0d506e2c67bdf97daa" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", + "rand", + "tls_codec", +] + +[[package]] +name = "libcrux-p256" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3400732702d578be622257b98cec85e9b0cd34a67f1f06d3ebe9ae34963cdf02" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a9144949845813a0b8787d08cbba47baabe59c83f3043e558e6e92385c40cc" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79054fc9037cb70d6a546cf094cea7d7df06af5e49d230ba24103d27ccc886f1" +dependencies = [ + "crabgrind", + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e46e1be0b77098cc6ce82137864936ecad3a1b9fd4303dd51202ca140dd1e" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09f5c39afae0528e1f70a3c1e3c6ee649ec1f19dda26fc9b6ea91108fb879ef" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa7a21e8c2e8baa8b40f0b176740f2ca4baadd79d1b00e48d6b1e363c42085a" +dependencies = [ + "libcrux-secrets", + "rand", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0", + "shake", + "signature 3.0.0", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "rand_core 0.10.1", + "sha3 0.11.0", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openmls" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ea97f8931047590bda498a9502e8fe6c9bb2bb04547a9a463618783481e0ec" +dependencies = [ + "getrandom 0.4.3", + "log", + "openmls_serialization_helpers", + "openmls_traits", + "rayon", + "serde", + "serde_bytes", + "thiserror", + "tls_codec", + "web-time", + "zeroize", +] + +[[package]] +name = "openmls_basic_credential" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd3f0c3422e7c7a8496f042b547b0c28d0793f8ba97feb401966e58196a1e40" +dependencies = [ + "ed25519-dalek", + "ml-dsa", + "openmls_traits", + "p256", + "p384", + "rand_core 0.6.4", + "serde", + "tls_codec", + "zeroize", +] + +[[package]] +name = "openmls_memory_storage" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce927945a16eaa19302ed5ccef052f01ba68f2ba7ae67e4ac4ff2fc661445364" +dependencies = [ + "log", + "openmls_traits", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "openmls_rust_crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bf3fe824ead6e81d22f49a9cd7feffca3494979fac14c1248e19516e3625f1" +dependencies = [ + "aes-gcm", + "chacha20poly1305", + "ed25519-dalek", + "hkdf 0.13.0", + "hmac 0.13.0", + "hpke-rs", + "hpke-rs-crypto", + "hpke-rs-rust-crypto", + "ml-dsa", + "openmls_memory_storage", + "openmls_traits", + "p256", + "p384", + "rand_chacha 0.3.1", + "rand_core 0.10.1", + "rand_core 0.6.4", + "sha2 0.11.1", + "thiserror", + "tls_codec", +] + +[[package]] +name = "openmls_serialization_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2cf2193541e83ce1a89aaa44c2fd4783abd54853823075a32b60ef032284c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "thiserror", +] + +[[package]] +name = "openmls_traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac13edad8698eb1917cbc1dab99591fa1594d84320904fe3fc5121854f95831c" +dependencies = [ + "serde", + "tls_codec", +] + +[[package]] +name = "encryption" +version = "0.1.0" +dependencies = [ + "base64", + "getrandom 0.2.17", + "openmls", + "openmls_basic_credential", + "openmls_rust_crypto", + "openmls_traits", + "serde", + "serde_json", + "sha2 0.10.9", + "tls_codec", + "wasm-bindgen", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tls_codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18cc98286004cea38f717e2b03d990fc774fbfd38a82720de40e5c94365067c8" +dependencies = [ + "serde", + "serde_bytes", + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "674f41dd95f76cdeb005c83f9444e20cf43daebef37cde9e49a9ca6f4e87b423" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "x-wing" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01576e56599d01d75c5efcd5b30fa5de29e776f5a225e43fbf8c92c7302d4c8b" +dependencies = [ + "kem", + "ml-kem", + "sha3 0.12.0", + "shake", + "x25519-dalek 3.0.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crypto/encryption/Cargo.toml b/crypto/encryption/Cargo.toml new file mode 100644 index 0000000000..13254268e6 --- /dev/null +++ b/crypto/encryption/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "encryption" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openmls = { version = "=0.9.1", features = ["js"] } +openmls_traits = "=0.6.0" +openmls_rust_crypto = "=0.6.0" +openmls_basic_credential = "=0.6.0" +getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen = "=0.2.126" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tls_codec = "0.5" +base64 = "0.22" +sha2 = "0.10" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +panic = "abort" diff --git a/crypto/encryption/rust-toolchain.toml b/crypto/encryption/rust-toolchain.toml new file mode 100644 index 0000000000..c5432d939a --- /dev/null +++ b/crypto/encryption/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.95.0" +targets = ["wasm32-unknown-unknown"] +profile = "minimal" diff --git a/crypto/encryption/src/lib.rs b/crypto/encryption/src/lib.rs new file mode 100644 index 0000000000..f1c7b90028 --- /dev/null +++ b/crypto/encryption/src/lib.rs @@ -0,0 +1,267 @@ +//! The browser worker is the only owner. No persistence, export, or logging API. +use base64::{engine::general_purpose::STANDARD as B64, Engine}; +use openmls::prelude::*; +use openmls_basic_credential::SignatureKeyPair; +use openmls_rust_crypto::OpenMlsRustCrypto; +use openmls_traits::OpenMlsProvider; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use tls_codec::{Deserialize as TlsDeserialize, Serialize as TlsSerialize}; +use wasm_bindgen::prelude::*; + +const SUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; +const MAX_PACKET: usize = 1024 * 1024; +fn err(_: T) -> String { "Invalid session operation".into() } +fn require(ok: bool) -> Result<(), String> { if ok { Ok(()) } else { Err(err(())) } } +fn encode(v: &T) -> Result { Ok(B64.encode(v.tls_serialize_detached().map_err(err)?)) } +fn decode(v: &str) -> Result, String> { + require(v.len() <= MAX_PACKET * 4 / 3 + 4)?; + let bytes = B64.decode(v).map_err(err)?; + require(bytes.len() <= MAX_PACKET)?; + Ok(bytes) +} +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct Identity { channel: String, session: String, user: String, participation: String } +#[derive(Clone, Deserialize)] +#[serde(deny_unknown_fields)] +struct Binding { identity: Identity, key: String } +fn identity(cred: &Credential) -> Result { + let basic = BasicCredential::try_from(cred.clone()).map_err(err)?; + serde_json::from_slice(basic.identity()).map_err(err) +} +fn check(cred: &Credential, key: &[u8], context: &Identity, roster: &[Binding]) -> Result { + let id = identity(cred)?; + require(id.channel == context.channel && id.session == context.session)?; + require(roster.iter().any(|b| b.identity == id && b.key == B64.encode(key)))?; + Ok(id) +} +fn roster(value: &str, context: &Identity) -> Result, String> { + let roster: Vec = serde_json::from_str(value).map_err(err)?; + require(!roster.is_empty() && roster.len() <= 128)?; + let mut users = std::collections::HashSet::new(); + let mut participations = std::collections::HashSet::new(); + for b in &roster { + require(b.identity.channel == context.channel && b.identity.session == context.session + && users.insert(&b.identity.user) && participations.insert(&b.identity.participation) + && decode(&b.key)?.len() == 32)?; + } + Ok(roster) +} + +#[wasm_bindgen] +pub struct Session { + provider: OpenMlsRustCrypto, + signer: SignatureKeyPair, + credential: CredentialWithKey, + id: Identity, + host: String, + group: Option, +} +#[wasm_bindgen] +impl Session { + #[wasm_bindgen(constructor)] + pub fn new(identity_json: &str, host: &str) -> Result { + let id: Identity = serde_json::from_str(identity_json).map_err(err)?; + require([&id.channel, &id.session, &id.user, &id.participation].iter() + .all(|s| !s.is_empty() && s.len() <= 128) && !host.is_empty())?; + let provider = OpenMlsRustCrypto::default(); + let signer = SignatureKeyPair::new(SignatureScheme::ED25519).map_err(err)?; + signer.store(provider.storage()).map_err(err)?; + let credential = CredentialWithKey { + credential: BasicCredential::new(serde_json::to_vec(&id).map_err(err)?).into(), + signature_key: signer.public().into(), + }; + Ok(Self { provider, signer, credential, id, host: host.into(), group: None }) + } + pub fn public_key(&self) -> String { B64.encode(self.signer.public()) } + pub fn key_package(&self) -> Result { + require(self.group.is_none())?; + let kp = KeyPackage::builder().build(SUITE, &self.provider, &self.signer, self.credential.clone()).map_err(err)?; + encode(kp.key_package()) + } + pub fn create(&mut self) -> Result<(), String> { + require(self.group.is_none() && self.id.participation == self.host)?; + self.group = Some(MlsGroup::builder().ciphersuite(SUITE) + .use_ratchet_tree_extension(true) + .set_past_epoch_deletion_policy(PastEpochDeletionPolicy::MaxEpochs(0)) + .with_group_id(GroupId::from_slice(self.id.session.as_bytes())) + .build(&self.provider, &self.signer, self.credential.clone()).map_err(err)?); + Ok(()) + } + pub fn join(&mut self, welcome: &str, bindings: &str) -> Result<(), String> { + require(self.group.is_none() && self.id.participation != self.host)?; + let roster = roster(bindings, &self.id)?; + let msg = MlsMessageIn::tls_deserialize_exact(decode(welcome)?).map_err(err)?; + let welcome = match msg.extract() { MlsMessageBodyIn::Welcome(w) => w, _ => return Err(err(())) }; + let config = MlsGroupJoinConfig::builder().use_ratchet_tree_extension(true) + .set_past_epoch_deletion_policy(PastEpochDeletionPolicy::MaxEpochs(0)).build(); + let group = StagedWelcome::new_from_welcome(&self.provider, &config, welcome, None).map_err(err)? + .into_group(&self.provider).map_err(err)?; + require(group.group_id().as_slice() == self.id.session.as_bytes() && group.ciphersuite() == SUITE)?; + let mut users = std::collections::HashSet::new(); + let mut found_host = false; + for m in group.members() { + let id = check(&m.credential, &m.signature_key, &self.id, &roster)?; + require(users.insert(id.user))?; + found_host |= id.participation == self.host; + } + require(found_host)?; + self.group = Some(group); + Ok(()) + } + pub fn add(&mut self, key_package: &str, expected: &str, bindings: &str) -> Result { + require(self.id.participation == self.host)?; + let roster = roster(bindings, &self.id)?; + let kp = KeyPackageIn::tls_deserialize_exact(decode(key_package)?).map_err(err)? + .validate(self.provider.crypto(), ProtocolVersion::Mls10).map_err(err)?; + let id = check(kp.leaf_node().credential(), kp.leaf_node().signature_key().as_slice(), &self.id, &roster)?; + require(id.participation == expected)?; + let group = self.group.as_mut().ok_or_else(|| err(()))?; + require(group.members().count() < 128 && !group.members().any(|m| identity(&m.credential).is_ok_and(|i| i.user == id.user)))?; + let (commit, welcome, _) = group.add_members(&self.provider, &self.signer, &[kp]).map_err(err)?; + let result = json!({"commit": encode(&commit)?, "welcome": encode(&welcome)?}); + group.merge_pending_commit(&self.provider).map_err(err)?; + Ok(result.to_string()) + } + pub fn remove(&mut self, participation: &str, bindings: &str) -> Result { + require(self.id.participation == self.host && participation != self.host)?; + let roster = roster(bindings, &self.id)?; + require(!roster.iter().any(|b| b.identity.participation == participation))?; + let group = self.group.as_mut().ok_or_else(|| err(()))?; + let index = group.members().find(|m| identity(&m.credential).is_ok_and(|i| i.participation == participation)) + .ok_or_else(|| err(()))?.index; + let (commit, _, _) = group.remove_members(&self.provider, &self.signer, &[index]).map_err(err)?; + let out = encode(&commit)?; + group.merge_pending_commit(&self.provider).map_err(err)?; + Ok(out) + } + pub fn update(&mut self) -> Result { + let group = self.group.as_mut().ok_or_else(|| err(()))?; + if self.id.participation == self.host { + let bundle = group.self_update(&self.provider, &self.signer, LeafNodeParameters::default()).map_err(err)?; + let out = encode(bundle.commit())?; + group.merge_pending_commit(&self.provider).map_err(err)?; + Ok(out) + } else { + let (proposal, _) = group.propose_self_update(&self.provider, &self.signer, LeafNodeParameters::default()).map_err(err)?; + encode(&proposal) + } + } + pub fn commit_updates(&mut self) -> Result { + require(self.id.participation == self.host)?; + let group = self.group.as_mut().ok_or_else(|| err(()))?; + let (commit, _, _) = group.commit_to_pending_proposals(&self.provider, &self.signer).map_err(err)?; + let out = encode(&commit)?; + group.merge_pending_commit(&self.provider).map_err(err)?; + Ok(out) + } + pub fn encrypt(&mut self, text: &str) -> Result { + require(text.len() <= 100 * 1024)?; // Includes the authenticated receipt/message envelope. + let group = self.group.as_mut().ok_or_else(|| err(()))?; + encode(&group.create_message(&self.provider, &self.signer, text.as_bytes()).map_err(err)?) + } + pub fn process(&mut self, packet: &str, expected: &str, bindings: &str) -> Result { + let roster = roster(bindings, &self.id)?; + let group = self.group.as_mut().ok_or_else(|| err(()))?; + let msg = MlsMessageIn::tls_deserialize_exact(decode(packet)?).map_err(err)?; + let processed = group.process_message(&self.provider, msg.try_into_protocol_message().map_err(err)?).map_err(err)?; + let index = match processed.sender() { Sender::Member(i) => *i, _ => return Err(err(())) }; + let member = group.member_at(index).ok_or_else(|| err(()))?; + let sender = check(processed.credential(), &member.signature_key, &self.id, &roster)?; + require(sender.participation == expected)?; + let result = match processed.into_content() { + ProcessedMessageContent::ApplicationMessage(m) => { + let bytes = m.into_bytes(); + require(bytes.len() <= 100 * 1024)?; + json!({"kind":"application", "sender":sender, "text":String::from_utf8(bytes).map_err(err)?}) + }, + ProcessedMessageContent::ProposalMessage(p) => { + match p.proposal() { + Proposal::Update(u) => { + let id = check(u.leaf_node().credential(), u.leaf_node().signature_key().as_slice(), &self.id, &roster)?; + require(id == sender)?; + }, + _ => return Err(err(())), + } + group.store_pending_proposal(self.provider.storage(), *p).map_err(err)?; + json!({"kind":"proposal"}) + }, + ProcessedMessageContent::StagedCommitMessage(c) => { + require(sender.participation == self.host)?; + if let Some(leaf) = c.update_path_leaf_node() { + require(check(leaf.credential(), leaf.signature_key().as_slice(), &self.id, &roster)? == sender)?; + } + let mut added_users = std::collections::HashSet::new(); + for p in c.queued_proposals() { + match p.proposal() { + Proposal::Add(a) => { + let leaf = a.key_package().leaf_node(); + let id = check(leaf.credential(), leaf.signature_key().as_slice(), &self.id, &roster)?; + require(added_users.insert(id.user.clone()) && !group.members().any(|m| identity(&m.credential).is_ok_and(|i| i.user == id.user)))?; + }, + Proposal::Remove(r) => { + let m = group.member_at(r.removed()).ok_or_else(|| err(()))?; + let id = identity(&m.credential)?; + require(id.participation != self.host && !roster.iter().any(|b| b.identity == id))?; + }, + Proposal::Update(u) => { + let idx = match p.sender() { Sender::Member(i) => *i, _ => return Err(err(())) }; + let old = group.member_at(idx).ok_or_else(|| err(()))?; + require(check(u.leaf_node().credential(), u.leaf_node().signature_key().as_slice(), &self.id, &roster)? == identity(&old.credential)?)?; + }, + _ => return Err(err(())), + } + } + require(group.members().count() + c.add_proposals().count() - c.remove_proposals().count() <= 128)?; + group.merge_staged_commit(&self.provider, *c).map_err(err)?; + json!({"kind":"commit"}) + }, + _ => return Err(err(())), + }; + Ok(result.to_string()) + } + pub fn members(&self) -> Result { + let group = self.group.as_ref().ok_or_else(|| err(()))?; + let mut members = Vec::::new(); + for m in group.members() { + let id = identity(&m.credential)?; + let mut hash = Sha256::new(); + hash.update(b"Open WebUI temporary chat identity v1\0"); + hash.update(serde_json::to_vec(&id).map_err(err)?); + hash.update(&m.signature_key); + members.push(json!({"identity":id,"fingerprint":format!("{:x}",hash.finalize())})); + } + Ok(json!(members).to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn client(n: usize, session: &str) -> Session { + Session::new(&json!({"channel":"c","session":session,"user":format!("u{n}"),"participation":format!("p{n}")}).to_string(), "p0").unwrap() + } + fn bindings(clients: &[&Session]) -> String { + json!(clients.iter().map(|c| json!({"identity":c.id,"key":c.public_key()})).collect::>()).to_string() + } + #[test] + fn bindings_membership_and_epochs() { + let mut a = client(0,"s"); let mut b = client(1,"s"); let c = client(2,"other-session"); + a.create().unwrap(); + let roster = bindings(&[&a,&b]); + assert!(a.add(&c.key_package().unwrap(), "p2", &roster).is_err()); + let old = a.encrypt("past epoch").unwrap(); + let added: Value = serde_json::from_str(&a.add(&b.key_package().unwrap(), "p1", &roster).unwrap()).unwrap(); + b.join(added["welcome"].as_str().unwrap(), &roster).unwrap(); + assert!(b.process(&old,"p0",&roster).is_err()); + let msg = a.encrypt("hello").unwrap(); + assert!(b.process(&msg,"p0",&roster).unwrap().contains("hello")); + assert!(b.process(&msg,"p0",&roster).is_err()); + // A valid MLS commit from an ordinary member must still be rejected. + let bundle = b.group.as_mut().unwrap().self_update(&b.provider,&b.signer,LeafNodeParameters::default()).unwrap(); + assert!(a.process(&encode(bundle.commit()).unwrap(),"p1",&roster).is_err()); + assert!(b.remove("p0", &roster).is_err()); + } +} diff --git a/package.json b/package.json index 910aa316aa..6396754d9b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "version": "0.12.2", "private": true, "scripts": { + "crypto:verify": "node scripts/encryption/artifact.mjs", + "predev": "npm run crypto:verify", + "predev:5050": "npm run crypto:verify", + "prebuild": "npm run crypto:verify", + "prebuild:watch": "npm run crypto:verify", "dev": "npm run pyodide:fetch && vite dev --host", "dev:5050": "npm run pyodide:fetch && vite dev --port 5050", "build": "npm run pyodide:fetch && vite build", diff --git a/scripts/encryption/artifact.mjs b/scripts/encryption/artifact.mjs new file mode 100644 index 0000000000..d90c339e1c --- /dev/null +++ b/scripts/encryption/artifact.mjs @@ -0,0 +1,71 @@ +// Normal builds only read committed files. No Rust or network required. +import { createHash } from 'node:crypto'; +import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +const root = new URL('../../', import.meta.url).pathname; +const source = 'crypto/encryption'; +const artifact = 'src/lib/encryption/vendor'; +const hash = (data) => createHash('sha256').update(data).digest('hex'); +function files(dir) { + return readdirSync(join(root, dir), { withFileTypes: true }) + .flatMap((e) => { + if (e.name === 'target' || e.name.startsWith('.')) return []; + const path = join(dir, e.name); + return e.isDirectory() ? files(path) : [path]; + }) + .sort(); +} +const inputs = [ + ...files(source), + 'scripts/encryption/build.py', + 'scripts/encryption/artifact.mjs' +].sort(); +const sourceFingerprint = hash( + inputs.map((p) => `${p}\0${hash(readFileSync(join(root, p)))}`).join('\n') +); +const manifestPath = join(root, artifact, 'manifest.json'); +if (process.argv.includes('--write')) { + const metadata = JSON.parse( + readFileSync(process.argv[process.argv.indexOf('--metadata') + 1], 'utf8') + ); + const checksums = Object.fromEntries( + files(artifact) + .filter((p) => !p.endsWith('/manifest.json')) + .map((p) => [relative(artifact, p), hash(readFileSync(join(root, p)))]) + ); + writeFileSync( + manifestPath, + JSON.stringify( + { + format: 1, + sourceFingerprint, + sourceRevision: process.env.CRYPTO_SOURCE_REVISION, + toolchain: '1.95.0', + wasmBindgen: '0.2.126', + dependencies: metadata.packages.map(({ name, version, license }) => ({ + name, + version, + license + })), + checksums + }, + null, + 2 + ) + '\n' + ); +} else { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + if (manifest.sourceFingerprint !== sourceFingerprint) + throw Error('Encryption source changed: rebuild and review the artifact.'); + const actual = files(artifact) + .filter((p) => !p.endsWith('/manifest.json')) + .map((p) => relative(artifact, p)) + .sort(); + if (JSON.stringify(actual) !== JSON.stringify(Object.keys(manifest.checksums).sort())) + throw Error('Unexpected crypto artifact files'); + for (const [p, digest] of Object.entries(manifest.checksums)) { + if (hash(readFileSync(join(root, artifact, p))) !== digest) + throw Error(`Crypto artifact checksum mismatch: ${p}`); + } + console.log('Encryption artifact source and checksums verified'); +} diff --git a/scripts/encryption/build.py b/scripts/encryption/build.py new file mode 100644 index 0000000000..9e081e4552 --- /dev/null +++ b/scripts/encryption/build.py @@ -0,0 +1,136 @@ +"""Manual artifact release only; never invoked by the application build.""" + +import json +import os +import subprocess +import tempfile +from functools import cache +from pathlib import Path +from urllib.parse import urlparse +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / 'crypto/encryption' +OUTPUT = ROOT / 'src/lib/encryption/vendor' + + +def run(*args, **kwargs): + return subprocess.run(args, cwd=SOURCE, check=True, **kwargs) + + +run('cargo', 'test', '--locked') +run('cargo', 'build', '--locked', '--release', '--target', 'wasm32-unknown-unknown') +run( + 'wasm-bindgen', + '--target', + 'web', + '--out-name', + 'encryption', + '--out-dir', + str(OUTPUT), + str(SOURCE / 'target/wasm32-unknown-unknown/release/encryption.wasm'), +) +# Use Cargo's actual target build graph, excluding tests and unused optional crates. +metadata = json.loads(run('cargo', 'metadata', '--locked', '--format-version', '1', capture_output=True).stdout) +tree = run( + 'cargo', + 'tree', + '--locked', + '--target', + 'wasm32-unknown-unknown', + '--edges', + 'normal,build', + '--prefix', + 'none', + '--format', + '{p}', + capture_output=True, +).stdout.decode() +used = {tuple(line.split()[:2]) for line in tree.splitlines()} +metadata['packages'] = [p for p in metadata['packages'] if (p['name'], 'v' + p['version']) in used] +assert {(p['name'], 'v' + p['version']) for p in metadata['packages']} == used + + +@cache +def download(url): + with urlopen(url, timeout=30) as response: + return response.read().decode('utf-8') + + +texts = {} +notices = [ + 'Third-Party Licenses', + '===================', + '', + 'Scope: Encryption WebAssembly artifact and its build-time dependencies.', + 'Packages are listed alphabetically with references to the full license and notice texts below.', + 'Identical texts are included once; package-specific copyright notices are preserved.', + 'Source code, including MPL-2.0 dependencies, is available at each Source URL below.', +] +for package in sorted(metadata['packages'], key=lambda p: (p['name'], p['version'])): + if package['source'] is None: + continue # The local wrapper is not a third-party dependency. + license = package['license'] or '' + choices = { + 'MIT', + 'Apache-2.0', + 'BSD-2-Clause', + 'BSD-3-Clause', + 'ISC', + 'Unicode-3.0', + 'Zlib', + 'CC0-1.0', + 'Unlicense', + 'MPL-2.0', + } + if not any(x.strip(' ()') in choices for x in license.replace('/', ' OR ').split(' OR ')): + raise RuntimeError(f'Review dependency license: {package["name"]} {license}') + directory = Path(package['manifest_path']).parent + documents = { + path.name: path.read_text() + for path in sorted(directory.iterdir()) + if path.is_file() and path.name.upper().startswith(('LICENSE', 'COPYING', 'NOTICE')) + } + if package.get('license_file'): + path = directory / package['license_file'] + documents[path.name] = path.read_text() + if not documents: + # Some published crates omit the workspace license; use their exact source revision. + if license == 'MPL-2.0': + url = 'https://www.mozilla.org/media/MPL/2.0/index.txt' + expected = 'Mozilla Public License Version 2.0' + else: + repository = urlparse(package['repository']) + if repository.netloc != 'github.com' or license not in ('MIT', 'Apache-2.0'): + raise RuntimeError(f'Missing license notice: {package["name"]}') + repo = '/'.join(repository.path.strip('/').split('/')[:2]) + revision = json.loads((directory / '.cargo_vcs_info.json').read_text())['git']['sha1'] + url = f'https://raw.githubusercontent.com/{repo}/{revision}/LICENSE' + expected = 'Permission is hereby granted' if license == 'MIT' else 'Apache License' + documents[url] = download(url) + if expected not in documents[url]: + raise RuntimeError(f'Unexpected license notice: {url}') + references = [] + for name, text in documents.items(): + text = text.strip() + if not text: + raise RuntimeError(f'Empty license notice: {package["name"]}/{name}') + number = texts.setdefault(text, len(texts) + 1) + references.append(f'{name}: [{number}]') + notices.append( + f'\nPackage: {package["name"]}\nVersion: {package["version"]}\nLicense: {license}\n' + f'Source: https://crates.io/api/v1/crates/{package["name"]}/{package["version"]}/download\n' + + '\n'.join(references) + ) +notices.extend(f'\nLicense and notice text [{number}]\n--------------------------------\n{text}' for text, number in texts.items()) +(OUTPUT / 'THIRD_PARTY_LICENSES.txt').write_text('\n'.join(notices) + '\n') +with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as temp: + json.dump(metadata, temp) + temp.flush() + env = dict( + os.environ, + CRYPTO_SOURCE_REVISION=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT).decode().strip(), + ) + subprocess.run( + ['node', str(ROOT / 'scripts/encryption/artifact.mjs'), '--write', '--metadata', temp.name], check=True, env=env + ) diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 24e865fbea..0256410e30 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -352,6 +352,14 @@ {#if adminConfig.ENABLE_CHANNELS} + + + {}; + $: watchInvitation( + $socket, + channel?.id === id ? id : null, + mounted && $config?.features?.enable_encrypted_channels + ); + + function watchInvitation( + connection: Socket | null, + channelId: string | null, + enabled: boolean | undefined + ) { + stopInvitation(); + stopInvitation = () => {}; + invitationSession = null; + if (!enabled || !connection || !channelId) return; + let alive = true; + let revision = 0; + const receive = (event: { channel: string; session: string | null }) => { + if (event.channel === channelId) { + revision++; + invitationSession = event.session; + } + }; + const clear = () => { + revision++; + invitationSession = null; + }; + const query = () => { + if (!connected(connection)) return; + const issued = revision; + void request(connection, 'status', { channel: channelId }) + .then((result) => { + if (alive && revision === issued) + invitationSession = result.available ? result.session : null; + }) + .catch(() => {}); + }; + connection.on('encryption:available', receive); + connection.on('disconnect', clear); + connection.on('connect', query); + query(); + stopInvitation = () => { + alive = false; + connection.off('encryption:available', receive); + connection.off('disconnect', clear); + connection.off('connect', query); + }; + } + let currentId = null; let scrollEnd = true; @@ -297,6 +352,7 @@ }; onMount(() => { + mounted = true; if ($chatId) { chatId.set(''); } @@ -310,6 +366,7 @@ }); onDestroy(() => { + stopInvitation(); // last read at updateLastReadAt(id); _channelId.set(null); @@ -358,7 +415,30 @@ return null; }); }} - /> + > + + {#if invitationSession} +
+ {$i18n.t('Encrypted chat in progress')} + + +
+ {/if} +
+ {#if channel && messages !== null}
diff --git a/src/lib/components/channel/ChannelMenu.svelte b/src/lib/components/channel/ChannelMenu.svelte index 993ea4381a..5317781f3d 100644 --- a/src/lib/components/channel/ChannelMenu.svelte +++ b/src/lib/components/channel/ChannelMenu.svelte @@ -1,7 +1,9 @@ - - +{#if !readOnly} + + +{/if}
diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index 53d35c9323..56831ebaf7 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -30,6 +30,7 @@ import Tooltip from '$lib/components/common/Tooltip.svelte'; const i18n = getContext>('i18n'); + const markdown = getContext<{ allowRemoteContent: boolean } | undefined>('markdown'); export let id = ''; export let edit = true; @@ -368,6 +369,7 @@ const render = async () => { onUpdate(token, id); + if (markdown?.allowRemoteContent === false) return; if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) { try { renderHTML = await renderMermaid(code); @@ -440,7 +442,7 @@ class="relative {className} flex flex-col rounded-2xl border border-gray-100/30 dark:border-gray-850/30 my-0.5 overflow-clip" dir="ltr" > - {#if ['mermaid', 'vega', 'vega-lite'].includes(lang)} + {#if markdown?.allowRemoteContent !== false && ['mermaid', 'vega', 'vega-lite'].includes(lang)} {#if renderHTML} {$i18n.t('Running')} - {:else if run} + {:else if run && markdown?.allowRemoteContent !== false} + + {/if} + + + + {#if state.status === 'ended'} +
+

+ {$i18n.t('Encrypted chat ended')} +

+ +
+ {:else} +
+
+ {#if state.messages.length === 0} +
+

+ {state.status === 'joining' + ? $i18n.t('Joining encrypted chat…') + : $i18n.t('Messages are end-to-end encrypted and aren’t saved.')} +

+

{$i18n.t('Leaving or reloading clears this device’s chat.')}

+
+ {/if} + {#each state.messages as message (message.id)} + {#if message.type === 'joined'} +

+ {$i18n.t('{{NAME}} joined', { NAME: message.user.name })} +

+ {:else} + + + + + + {#if message.sender === state.self} + {#if message.expected.length > 0 && message.received.length === message.expected.length} + + {$i18n.t('Delivered')} + + {:else if !message.relayed} + + {$i18n.t('Sending…')} + + {/if} + {/if} + + + {/if} + {/each} +
+
+
+
+
+
+ { + draft = state.status === 'active' ? md : ''; + }} + on:keydown={({ detail: { event } }) => { + if (event.isComposing || event.defaultPrevented || event.key !== 'Enter') return; + const control = event.ctrlKey || event.metaKey; + if ( + ($settings?.ctrlEnterToSend ?? false) + ? control + : !event.shiftKey && (!$mobile || control) + ) { + event.preventDefault(); + send(); + } + }} + /> +
+
+
+ {#if oversized}{$i18n.t('Message exceeds the 16 KiB limit.')}{/if} + + + +
+
+
+ {/if} + + + +
+

{$i18n.t('Participants')}

+ +
+
+ {#each state.members as member (member.identity.participation)} +
+ + + {member.name} + {verified.has(member.fingerprint) ? $i18n.t('Verified') : $i18n.t('Verify')} + + +
+

+ {$i18n.t( + 'Compare this code with this participant through another trusted channel. It changes when they rejoin.' + )} +

+ {member.fingerprint.match(/.{1,4}/g)?.join(' ')} + {#if member.identity.participation !== state.self} + + {/if} +
+
+ {/each} +
+ +
+
+
diff --git a/vite.config.ts b/vite.config.ts index 3780dbe6a9..5216d38835 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,6 +15,10 @@ export default defineConfig({ sveltekit(), viteStaticCopy({ targets: [ + { + src: 'src/lib/encryption/vendor/THIRD_PARTY_LICENSES.txt', + dest: 'licenses/encryption' + }, { src: 'node_modules/onnxruntime-web/dist/*.jsep.*',