forked from Rosetta/landing
Add encryption reference and refine documentation
This commit is contained in:
@@ -12,6 +12,12 @@
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://rosetta.im/encryption/</loc>
|
||||
<lastmod>2026-07-30</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://rosetta.im/support/</loc>
|
||||
<lastmod>2026-07-30</lastmod>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
||||
import { basename, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { isMap, isSeq, parseDocument } from 'yaml';
|
||||
import { isMap, isSeq, parseDocument, stringify } from 'yaml';
|
||||
|
||||
const projectRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const sourceDirectory = resolve(
|
||||
@@ -74,6 +74,10 @@ function mapNamedCollection(node, kind) {
|
||||
pair.value?.commentBefore,
|
||||
pair.value?.comment,
|
||||
);
|
||||
const yaml = stringify(
|
||||
{ [name]: pair.value.toJSON() },
|
||||
{ indent: 2, lineWidth: 0 },
|
||||
).trimEnd();
|
||||
|
||||
if (kind === 'enum') {
|
||||
const values = pair.value.get('values', true);
|
||||
@@ -81,6 +85,7 @@ function mapNamedCollection(node, kind) {
|
||||
name,
|
||||
type: String(pair.value.get('type')),
|
||||
description,
|
||||
yaml,
|
||||
values: isSeq(values)
|
||||
? values.items.map((value) => {
|
||||
const json = value.toJSON();
|
||||
@@ -105,6 +110,7 @@ function mapNamedCollection(node, kind) {
|
||||
name,
|
||||
id: kind === 'packet' ? Number(pair.value.get('id')) : undefined,
|
||||
description,
|
||||
yaml,
|
||||
fields,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ import './style.css';
|
||||
const LandingPage = lazy(() =>
|
||||
import('./pages/LandingPage').then((module) => ({ default: module.LandingPage })),
|
||||
);
|
||||
const EncryptionPage = lazy(() =>
|
||||
import('./pages/EncryptionPage').then((module) => ({ default: module.EncryptionPage })),
|
||||
);
|
||||
const ProtoPage = lazy(() =>
|
||||
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })),
|
||||
);
|
||||
@@ -46,6 +49,7 @@ export default function App() {
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/encryption" element={<EncryptionPage />} />
|
||||
<Route path="/proto" element={<ProtoPage />} />
|
||||
<Route path="/support" element={<SupportPage />} />
|
||||
<Route path="/privacy" element={<PrivacyPage />} />
|
||||
|
||||
@@ -2,36 +2,42 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
|
||||
}
|
||||
|
||||
.shell {
|
||||
flex: 1;
|
||||
padding-top: 72px;
|
||||
padding-bottom: 96px;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
padding: 96px 0 112px;
|
||||
max-width: 900px;
|
||||
margin-inline: auto;
|
||||
padding: clamp(32px, 6vw, 72px);
|
||||
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.intro {
|
||||
max-width: 900px;
|
||||
padding-bottom: 64px;
|
||||
max-width: 760px;
|
||||
padding-bottom: 48px;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family: Outfit, var(--mantine-font-family);
|
||||
font-size: clamp(40px, 7vw, 62px);
|
||||
font-weight: 500;
|
||||
font-size: clamp(38px, 6vw, 56px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 780px;
|
||||
margin: 0;
|
||||
margin: 40px 0 0;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
@@ -63,25 +69,22 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.42fr) minmax(0, 1fr);
|
||||
gap: 52px;
|
||||
margin-top: 56px;
|
||||
padding-top: 48px;
|
||||
margin-top: 44px;
|
||||
padding-top: 36px;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0;
|
||||
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
|
||||
font-family: Outfit, var(--mantine-font-family);
|
||||
font-size: clamp(26px, 3vw, 34px);
|
||||
font-weight: 500;
|
||||
font-size: clamp(21px, 3vw, 26px);
|
||||
font-weight: 650;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
min-width: 0;
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.document a {
|
||||
@@ -104,24 +107,27 @@
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.main {
|
||||
padding: 72px 0 88px;
|
||||
.shell {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
padding-bottom: 48px;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--mantine-spacing-md);
|
||||
margin-top: 44px;
|
||||
padding-top: 36px;
|
||||
margin-top: 36px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 36em) {
|
||||
.shell {
|
||||
padding-inline: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-top: 56px;
|
||||
padding: 28px 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function LegalPage({
|
||||
<Box className={classes.page}>
|
||||
<SiteHeader />
|
||||
<Container className={classes.shell}>
|
||||
<Box component="main" className={classes.main}>
|
||||
<Paper component="main" withBorder radius="lg" className={classes.main}>
|
||||
<Box className={classes.intro}>
|
||||
<Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em">
|
||||
{eyebrow}
|
||||
@@ -51,7 +51,7 @@ export function LegalPage({
|
||||
<Box component="article" className={classes.document}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
<SiteFooter />
|
||||
</Box>
|
||||
|
||||
@@ -33,6 +33,9 @@ export function SiteFooter() {
|
||||
<Anchor component={Link} to="/#apps" c="dimmed" size="sm">
|
||||
Apps
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/encryption" c="dimmed" size="sm">
|
||||
Encryption
|
||||
</Anchor>
|
||||
<Anchor component={Link} to="/proto" c="dimmed" size="sm">
|
||||
Proto
|
||||
</Anchor>
|
||||
@@ -51,8 +54,8 @@ export function SiteFooter() {
|
||||
<Anchor component={Link} to="/terms" c="dimmed" size="sm">
|
||||
Terms of Use
|
||||
</Anchor>
|
||||
<Anchor href="mailto:admin@rosetta.im" c="dimmed" size="sm">
|
||||
Admin@rosetta.im
|
||||
<Anchor href="mailto:support@rosetta.im" c="dimmed" size="sm">
|
||||
Support@rosetta.im
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -30,13 +30,14 @@
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
text-decoration: none !important;
|
||||
transition: color 120ms ease;
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.link[data-active] {
|
||||
color: var(--mantine-color-blue-6);
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.burger {
|
||||
|
||||
@@ -15,6 +15,7 @@ import classes from './SiteHeader.module.css';
|
||||
const NAVIGATION = [
|
||||
{ label: 'Features', to: '/#features' },
|
||||
{ label: 'Apps', to: '/#apps' },
|
||||
{ label: 'Encryption', to: '/encryption' },
|
||||
{ label: 'Proto', to: '/proto' },
|
||||
{ label: 'Support', to: '/support' },
|
||||
];
|
||||
@@ -26,6 +27,7 @@ export function SiteHeader() {
|
||||
const links = NAVIGATION.map(({ label, to }) => {
|
||||
const active =
|
||||
(to === '/proto' && pathname === '/proto') ||
|
||||
(to === '/encryption' && pathname === '/encryption') ||
|
||||
(to === '/support' && pathname === '/support');
|
||||
|
||||
return (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
162
src/pages/EncryptionPage.module.css
Normal file
162
src/pages/EncryptionPage.module.css
Normal file
@@ -0,0 +1,162 @@
|
||||
.page {
|
||||
padding: 48px 0 112px;
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
|
||||
}
|
||||
|
||||
.hero,
|
||||
.notice,
|
||||
.contents,
|
||||
.section {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 64px 0 48px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin-top: var(--mantine-spacing-lg);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-size: clamp(48px, 7vw, 74px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.045em;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.heroDescription {
|
||||
max-width: 790px;
|
||||
margin-top: var(--mantine-spacing-lg);
|
||||
font-size: 19px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: var(--mantine-spacing-xl);
|
||||
border-left: 4px solid var(--mantine-color-blue-5);
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.contents {
|
||||
margin-top: var(--mantine-spacing-lg);
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.contentLink {
|
||||
padding: 7px 12px;
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
border-radius: 999px;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contentLink:hover {
|
||||
border-color: var(--mantine-color-blue-4);
|
||||
color: var(--mantine-color-blue-6);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
scroll-margin-top: 104px;
|
||||
margin-top: 24px;
|
||||
padding: 64px clamp(24px, 5vw, 64px);
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.section > h2 {
|
||||
max-width: 760px;
|
||||
margin-top: var(--mantine-spacing-xs);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-size: clamp(30px, 4vw, 43px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.sectionBody {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.sectionBody > h3 {
|
||||
margin-top: 42px;
|
||||
margin-bottom: 12px;
|
||||
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
|
||||
font-size: 21px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
margin: var(--mantine-spacing-lg) 0;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-9));
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.1));
|
||||
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.list {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.list li::marker {
|
||||
color: var(--mantine-color-blue-5);
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-radius-md);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.boundary {
|
||||
background: light-dark(var(--mantine-color-orange-0), rgba(253, 126, 20, 0.08));
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 32px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.term {
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.term dd {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.page {
|
||||
padding: 32px 0 80px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 48px 0 40px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 42px 24px;
|
||||
}
|
||||
|
||||
.terms {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
511
src/pages/EncryptionPage.tsx
Normal file
511
src/pages/EncryptionPage.tsx
Normal file
@@ -0,0 +1,511 @@
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Box,
|
||||
Code,
|
||||
Container,
|
||||
Group,
|
||||
List,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBinaryTree,
|
||||
IconFileTextShield,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconRoute,
|
||||
IconShieldCheck,
|
||||
} from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SEO } from '../components/SEO/SEO';
|
||||
import { SiteFooter } from '../components/SiteFooter/SiteFooter';
|
||||
import { SiteHeader } from '../components/SiteHeader/SiteHeader';
|
||||
import classes from './EncryptionPage.module.css';
|
||||
|
||||
const MESSAGE_FLOW = [
|
||||
{
|
||||
number: '01',
|
||||
title: 'Create message material',
|
||||
description: 'Generate a fresh 32-byte content key and a fresh 24-byte nonce.',
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
title: 'Encrypt the content',
|
||||
description: 'Protect UTF-8 message content with XChaCha20-Poly1305.',
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
title: 'Wrap for the recipient',
|
||||
description: 'Encrypt the key and nonce for the recipient using ephemeral secp256k1 ECDH.',
|
||||
},
|
||||
{
|
||||
number: '04',
|
||||
title: 'Transmit ciphertext',
|
||||
description: 'Send the encrypted content, key envelope, routing data, and attachment metadata.',
|
||||
},
|
||||
];
|
||||
|
||||
const ATTACHMENT_LAYOUT = [
|
||||
['Magic', '4 bytes', 'ASCII RAC2 (52 41 43 32)'],
|
||||
['Salt', '16 bytes', 'Random per attachment encryption session'],
|
||||
['IV', '12 bytes', 'Random and unique for every encrypted chunk'],
|
||||
['Ciphertext + tag', 'Variable', 'AES-256-GCM output including its authentication tag'],
|
||||
];
|
||||
|
||||
function TechnicalSection({
|
||||
id,
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box component="section" id={id} className={classes.section}>
|
||||
<Text c="blue" fw={700} size="xs" tt="uppercase" lts="0.08em">
|
||||
{eyebrow}
|
||||
</Text>
|
||||
<Title order={2}>{title}</Title>
|
||||
<Box className={classes.sectionBody}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Term({
|
||||
term,
|
||||
children,
|
||||
}: {
|
||||
term: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box className={classes.term}>
|
||||
<Text component="dt" fw={700}>
|
||||
{term}
|
||||
</Text>
|
||||
<Text component="dd" c="dimmed" size="sm" lh={1.65}>
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function EncryptionPage() {
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title="Rosetta Encryption"
|
||||
description="Technical documentation for Rosetta message, key-envelope, group, local-storage, and attachment encryption."
|
||||
canonical="https://rosetta.im/encryption/"
|
||||
keywords="Rosetta encryption, XChaCha20-Poly1305, AES-256-GCM, secp256k1, encrypted attachments"
|
||||
/>
|
||||
<SiteHeader />
|
||||
<main className={classes.page}>
|
||||
<Container>
|
||||
<Anchor component={Link} to="/" size="sm">
|
||||
Rosetta
|
||||
</Anchor>
|
||||
<Text component="span" c="dimmed" size="sm">
|
||||
{' '}
|
||||
/ Encryption
|
||||
</Text>
|
||||
|
||||
<Box className={classes.hero}>
|
||||
<Group gap="sm">
|
||||
<ThemeIcon size={46} radius="md" variant="light">
|
||||
<IconShieldCheck size={24} />
|
||||
</ThemeIcon>
|
||||
<Badge variant="light">Implementation reference</Badge>
|
||||
</Group>
|
||||
<Title order={1}>Rosetta Encryption</Title>
|
||||
<Text c="dimmed" className={classes.heroDescription}>
|
||||
A technical description of how the current Rosetta desktop client
|
||||
derives identity keys, protects direct and group messages, encrypts
|
||||
attachments, and separates plaintext from server-visible data.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Paper withBorder radius="lg" className={classes.notice}>
|
||||
<Text fw={700}>Scope of this document</Text>
|
||||
<Text c="dimmed" size="sm" mt={6} lh={1.65}>
|
||||
This page documents the current client implementation, including its
|
||||
compatibility formats. It is an implementation reference, not a claim
|
||||
of formal verification, an independent security audit, or a frozen
|
||||
protocol specification.
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Group component="nav" gap="xs" className={classes.contents}>
|
||||
{[
|
||||
['Direct messages', '#direct-messages'],
|
||||
['Attachments', '#attachments'],
|
||||
['Groups', '#groups'],
|
||||
['Key material', '#key-material'],
|
||||
['Security properties', '#security-properties'],
|
||||
['Terminology', '#terminology'],
|
||||
].map(([label, href]) => (
|
||||
<Anchor key={href} href={href} className={classes.contentLink}>
|
||||
{label}
|
||||
</Anchor>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
<TechnicalSection
|
||||
id="direct-messages"
|
||||
eyebrow="Direct messages"
|
||||
title="A new authenticated cipher for every message."
|
||||
>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
Rosetta encrypts the text of each one-to-one message with a newly
|
||||
generated content key. The symmetric secret is never sent in plaintext:
|
||||
it is placed in a separate recipient-specific key envelope.
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" mt="xl">
|
||||
{MESSAGE_FLOW.map(({ number, title, description }) => (
|
||||
<Paper key={number} withBorder radius="md" p="lg">
|
||||
<Text c="blue" fw={700} size="xs">
|
||||
{number}
|
||||
</Text>
|
||||
<Text fw={700} mt="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm" mt={6} lh={1.6}>
|
||||
{description}
|
||||
</Text>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Title order={3}>Content cipher</Title>
|
||||
<Code block className={classes.codeBlock}>
|
||||
{`contentKey = randomBytes(32) # 256 bits
|
||||
nonce = randomBytes(24) # 192 bits
|
||||
ciphertext = XChaCha20-Poly1305(
|
||||
key = contentKey,
|
||||
nonce = nonce,
|
||||
plaintext = UTF8(message)
|
||||
)`}
|
||||
</Code>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
XChaCha20-Poly1305 is an AEAD construction: it encrypts the message and
|
||||
produces an authentication tag. Decryption fails if the protected
|
||||
ciphertext has been changed or the wrong key is supplied.
|
||||
</Text>
|
||||
|
||||
<Title order={3}>Recipient key envelope</Title>
|
||||
<List spacing="sm" className={classes.list}>
|
||||
<List.Item>
|
||||
The client generates an ephemeral secp256k1 key pair for the
|
||||
message.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
ECDH combines the ephemeral private key with the recipient's
|
||||
long-term public key.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
The x-coordinate of the shared point becomes a 32-byte AES key.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
The 56-byte message material — content key followed by nonce — is
|
||||
encrypted with AES-CBC and PKCS#7 padding using a random 16-byte IV.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
The envelope carries the IV, encrypted material, and uncompressed
|
||||
ephemeral public key. The recipient reconstructs the same shared
|
||||
secret with their private key.
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Code block className={classes.codeBlock}>
|
||||
{`messageSecret = contentKey || nonce
|
||||
ephemeral = secp256k1.randomKeyPair()
|
||||
sharedPoint = ECDH(ephemeral.private, recipient.public)
|
||||
wrappingKey = sharedPoint.x[0..31]
|
||||
keyEnvelope = Base64(iv || AES-CBC(wrappingKey, messageSecret) || ephemeral.public)`}
|
||||
</Code>
|
||||
|
||||
<Title order={3}>What crosses the service boundary</Title>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
The message packet contains sender and recipient identifiers, a
|
||||
timestamp, message ID, encrypted content, the recipient key envelope,
|
||||
and attachment descriptors. The server can route this packet, but it
|
||||
does not receive the direct-message content key or plaintext message.
|
||||
</Text>
|
||||
</TechnicalSection>
|
||||
|
||||
<TechnicalSection
|
||||
id="attachments"
|
||||
eyebrow="Attachments"
|
||||
title="Authenticated encryption, one chunk at a time."
|
||||
>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
Files, images, audio, and video are encrypted before upload. Large
|
||||
payloads are processed as independent chunks so the client can stream,
|
||||
retry, cache, and decrypt ranges without holding an entire file in
|
||||
memory.
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" mt="xl">
|
||||
{[
|
||||
['AES-256-GCM', 'Authenticated encryption for every binary chunk'],
|
||||
['PBKDF2-SHA-256', '100,000 iterations and a 16-byte random salt'],
|
||||
['Random IV', 'A fresh 12-byte IV for every chunk'],
|
||||
].map(([title, description], index) => {
|
||||
const Icon = [IconFileTextShield, IconBinaryTree, IconKey][index];
|
||||
return (
|
||||
<Paper key={title} withBorder radius="md" p="lg">
|
||||
<ThemeIcon variant="light" radius="xl">
|
||||
<Icon size={18} />
|
||||
</ThemeIcon>
|
||||
<Text fw={700} mt="md">
|
||||
{title}
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm" mt={6} lh={1.6}>
|
||||
{description}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
|
||||
<Title order={3}>RAC2 encrypted chunk format</Title>
|
||||
<Code block className={classes.codeBlock}>
|
||||
RAC2 || salt[16] || iv[12] || AES-256-GCM-ciphertext-and-tag
|
||||
</Code>
|
||||
<Table.ScrollContainer minWidth={620}>
|
||||
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Segment</Table.Th>
|
||||
<Table.Th>Length</Table.Th>
|
||||
<Table.Th>Purpose</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{ATTACHMENT_LAYOUT.map(([segment, length, purpose]) => (
|
||||
<Table.Tr key={segment}>
|
||||
<Table.Td>
|
||||
<Code>{segment}</Code>
|
||||
</Table.Td>
|
||||
<Table.Td>{length}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text c="dimmed" size="sm">
|
||||
{purpose}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
|
||||
<Text c="dimmed" mt="xl" lh={1.75}>
|
||||
One random salt and derived AES key are reused within an attachment
|
||||
encryption session; every chunk receives an independent random IV.
|
||||
AES-GCM's authentication tag lets the client reject modified or
|
||||
corrupted chunks before exposing plaintext. The CDN stores encrypted
|
||||
chunks and object metadata, not the file decryption secret.
|
||||
</Text>
|
||||
</TechnicalSection>
|
||||
|
||||
<TechnicalSection
|
||||
id="groups"
|
||||
eyebrow="Groups and compatibility"
|
||||
title="Shared group keys and inline payloads."
|
||||
>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
A Rosetta group has a shared secret available to its members. Group
|
||||
messages do not transmit a new recipient envelope with every message;
|
||||
the shared group key is used by the client's compatibility codec.
|
||||
Inline attachment data, sender-readable message copies, and protected
|
||||
local values use the same codec.
|
||||
</Text>
|
||||
<Code block className={classes.codeBlock}>
|
||||
{`input
|
||||
→ DEFLATE compression
|
||||
→ PBKDF2(password, "rosetta", 1,000 iterations)
|
||||
→ AES-CBC with random 16-byte IV
|
||||
→ Base64(iv) : Base64(ciphertext)`}
|
||||
</Code>
|
||||
<Text c="dimmed" lh={1.75}>
|
||||
This compatibility format provides confidentiality but does not include
|
||||
an AEAD authentication tag. Binary attachments use the newer RAC2
|
||||
AES-GCM format described above.
|
||||
</Text>
|
||||
</TechnicalSection>
|
||||
|
||||
<TechnicalSection
|
||||
id="key-material"
|
||||
eyebrow="Identity and storage"
|
||||
title="Long-term keys remain device-controlled."
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<ThemeIcon variant="light" radius="xl">
|
||||
<IconKey size={19} />
|
||||
</ThemeIcon>
|
||||
<Title order={3}>Account identity</Title>
|
||||
<Text c="dimmed" size="sm" mt="sm" lh={1.7}>
|
||||
The recovery phrase is expanded to a BIP39 seed. SHA-256 of that
|
||||
seed becomes the 32-byte secp256k1 private key, and the client
|
||||
derives a compressed public key for the account identity.
|
||||
</Text>
|
||||
</Paper>
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<ThemeIcon variant="light" radius="xl">
|
||||
<IconLock size={19} />
|
||||
</ThemeIcon>
|
||||
<Title order={3}>Local protection</Title>
|
||||
<Text c="dimmed" size="sm" mt="sm" lh={1.7}>
|
||||
The private key, recovery phrase, readable message cache, and
|
||||
sender-side message secret are encrypted locally with material
|
||||
derived from the user's password or account private key.
|
||||
The plaintext account password is not sent to the service.
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</TechnicalSection>
|
||||
|
||||
<TechnicalSection
|
||||
id="security-properties"
|
||||
eyebrow="Security properties"
|
||||
title="What the current design protects — and what remains visible."
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<Text c="teal" fw={700}>
|
||||
Protected
|
||||
</Text>
|
||||
<List spacing="sm" mt="md" className={classes.list}>
|
||||
<List.Item>Direct-message plaintext and per-message secret</List.Item>
|
||||
<List.Item>Binary attachment contents and integrity</List.Item>
|
||||
<List.Item>Locally stored private key and recovery phrase</List.Item>
|
||||
<List.Item>Sender-side readable message cache</List.Item>
|
||||
</List>
|
||||
</Paper>
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<Text c="orange" fw={700}>
|
||||
Not hidden by end-to-end encryption
|
||||
</Text>
|
||||
<List spacing="sm" mt="md" className={classes.list}>
|
||||
<List.Item>Sender and recipient or group identifiers</List.Item>
|
||||
<List.Item>Message timestamps, IDs, and delivery metadata</List.Item>
|
||||
<List.Item>Attachment object IDs, sizes, and transfer metadata</List.Item>
|
||||
<List.Item>Network information such as IP addresses and timing</List.Item>
|
||||
</List>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
<Paper withBorder radius="md" p="xl" mt="lg" className={classes.boundary}>
|
||||
<Group gap="md" align="flex-start" wrap="nowrap">
|
||||
<ThemeIcon variant="light" radius="xl" color="orange">
|
||||
<IconRoute size={19} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700}>Important implementation boundaries</Text>
|
||||
<Text c="dimmed" size="sm" mt={6} lh={1.7}>
|
||||
Direct-message ciphertext has AEAD integrity, but the
|
||||
current recipient key envelope is not a digital signature.
|
||||
Compromise of a recipient's long-term private key can
|
||||
expose previously captured direct-message key envelopes, so
|
||||
this design does not claim long-term forward secrecy.
|
||||
Shared group keys similarly require careful rotation when
|
||||
membership changes.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
</TechnicalSection>
|
||||
|
||||
<TechnicalSection
|
||||
id="terminology"
|
||||
eyebrow="Terminology"
|
||||
title="Cryptographic terms used by Rosetta."
|
||||
>
|
||||
<Box component="dl" className={classes.terms}>
|
||||
<Term term="AEAD">
|
||||
Authenticated Encryption with Associated Data. A cipher mode that
|
||||
protects confidentiality and detects unauthorized modification.
|
||||
</Term>
|
||||
<Term term="XChaCha20-Poly1305">
|
||||
A stream-cipher-based AEAD construction with a 192-bit nonce,
|
||||
allowing securely generated random nonces with a very low collision
|
||||
probability.
|
||||
</Term>
|
||||
<Term term="AES-GCM">
|
||||
An AES mode that combines counter-mode encryption with a Galois
|
||||
authentication tag. Rosetta uses it for RAC2 attachment chunks.
|
||||
</Term>
|
||||
<Term term="ECDH">
|
||||
Elliptic Curve Diffie–Hellman. Two parties combine a private key
|
||||
with another party's public key to calculate the same shared
|
||||
secret without transmitting that secret.
|
||||
</Term>
|
||||
<Term term="secp256k1">
|
||||
The elliptic curve used for Rosetta account keys and ephemeral
|
||||
direct-message key envelopes.
|
||||
</Term>
|
||||
<Term term="PBKDF2">
|
||||
A password-based key derivation function that repeatedly applies a
|
||||
pseudorandom function with a salt to derive key material.
|
||||
</Term>
|
||||
<Term term="Nonce / IV">
|
||||
A value that must be unique for a key and encryption operation.
|
||||
Rosetta generates random nonces and IVs using the platform
|
||||
cryptographic random number generator.
|
||||
</Term>
|
||||
<Term term="Forward secrecy">
|
||||
A property in which compromise of a long-term key does not reveal
|
||||
earlier session keys. The current Rosetta direct-message envelope
|
||||
does not claim this property.
|
||||
</Term>
|
||||
</Box>
|
||||
|
||||
<Stack gap="xs" mt="xl">
|
||||
<Text fw={700}>Standards and implementation references</Text>
|
||||
<Anchor
|
||||
href="https://csrc.nist.gov/pubs/sp/800/38/d/final"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
NIST SP 800-38D — GCM and GMAC
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="https://www.rfc-editor.org/rfc/rfc8018"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
RFC 8018 — PBKDF2 and PKCS #5
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="https://www.secg.org/sec2-v2.pdf"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
SEC 2 — secp256k1 domain parameters
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="https://github.com/paulmillr/noble-ciphers"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
noble-ciphers — XChaCha20-Poly1305 implementation
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</TechnicalSection>
|
||||
</Container>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
.featuresSection,
|
||||
.appsSection,
|
||||
.protocolSection {
|
||||
.encryptionSection {
|
||||
padding: 112px 0;
|
||||
}
|
||||
|
||||
@@ -140,12 +140,12 @@
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.protocolSection {
|
||||
.encryptionSection {
|
||||
padding-top: 0;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.protocolCard {
|
||||
.encryptionCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -154,14 +154,14 @@
|
||||
background: linear-gradient(135deg, var(--mantine-color-blue-8), var(--mantine-color-blue-5));
|
||||
}
|
||||
|
||||
.protocolCard h2 {
|
||||
.encryptionCard h2 {
|
||||
font-size: clamp(32px, 4vw, 46px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.protocolButton {
|
||||
.encryptionButton {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -207,15 +207,15 @@
|
||||
|
||||
.featuresSection,
|
||||
.appsSection,
|
||||
.protocolSection {
|
||||
.encryptionSection {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.protocolSection {
|
||||
.encryptionSection {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.protocolCard {
|
||||
.encryptionCard {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
padding: 36px 28px;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IconArrowDown,
|
||||
IconBolt,
|
||||
IconBrandAndroid,
|
||||
IconBrandApple,
|
||||
IconBrandWindows,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
@@ -88,6 +89,12 @@ const PLATFORMS: Array<{
|
||||
description: 'Private messaging for Android phones and tablets.',
|
||||
icon: IconBrandAndroid,
|
||||
},
|
||||
{
|
||||
key: 'darwin',
|
||||
title: 'Rosetta for macOS',
|
||||
description: 'Native packages for Apple silicon and Intel-based Macs.',
|
||||
icon: IconBrandApple,
|
||||
},
|
||||
{
|
||||
key: 'win32',
|
||||
title: 'Rosetta for Windows',
|
||||
@@ -168,7 +175,7 @@ function DownloadApps() {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg" mt={48}>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg" mt={48}>
|
||||
{PLATFORMS.map(({ key, title, description, icon: Icon }) => {
|
||||
const platformReleases = groupedReleases[key] || [];
|
||||
|
||||
@@ -387,30 +394,31 @@ export function LandingPage() {
|
||||
|
||||
<DownloadApps />
|
||||
|
||||
<Box component="section" className={classes.protocolSection}>
|
||||
<Box component="section" className={classes.encryptionSection}>
|
||||
<Container>
|
||||
<Paper radius="xl" className={classes.protocolCard}>
|
||||
<Paper radius="xl" className={classes.encryptionCard}>
|
||||
<Box>
|
||||
<Text c="blue.2" fw={700} size="sm">
|
||||
Rosetta Proto
|
||||
Rosetta Encryption
|
||||
</Text>
|
||||
<Title order={2} c="white" mt="xs">
|
||||
The protocol is documented from its source.
|
||||
How Rosetta protects every message and attachment.
|
||||
</Title>
|
||||
<Text c="blue.0" mt="md" maw={650} lh={1.65}>
|
||||
Browse every packet, object, enum, field ID, type reference, and
|
||||
layer change generated directly from Rosetta YAML schemas.
|
||||
Follow the complete encryption path from per-message keys and
|
||||
XChaCha20-Poly1305 to recipient key envelopes and authenticated
|
||||
attachment chunks.
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/proto"
|
||||
to="/encryption"
|
||||
color="white"
|
||||
variant="white"
|
||||
size="lg"
|
||||
className={classes.protocolButton}
|
||||
className={classes.encryptionButton}
|
||||
>
|
||||
Open Proto Documentation
|
||||
Read Encryption Documentation
|
||||
</Button>
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
@@ -86,21 +86,23 @@
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.signature {
|
||||
.yamlBlock {
|
||||
margin: var(--mantine-spacing-lg) 0;
|
||||
padding: 12px 12px 12px 18px;
|
||||
padding: 12px 12px 16px 18px;
|
||||
border: 1px solid light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-9));
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.1));
|
||||
}
|
||||
|
||||
.signature code {
|
||||
flex: 1;
|
||||
.yamlBlock code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.table {
|
||||
|
||||
@@ -163,33 +163,21 @@ function FieldTable({ fields, layer }: { fields: ProtoField[]; layer: ProtoLayer
|
||||
);
|
||||
}
|
||||
|
||||
function packetSignature(packet: ProtoPacket) {
|
||||
const constructor = packet.id.toString(16).padStart(2, '0');
|
||||
const fields = packet.fields
|
||||
.map((field) => `${field.name}${field.optional ? '?' : ''}:${field.type}`)
|
||||
.join(' ');
|
||||
return `${packet.name}#${constructor}${fields ? ` ${fields}` : ''}`;
|
||||
}
|
||||
|
||||
function typeSignature(type: ProtoType) {
|
||||
const fields = type.fields
|
||||
.map((field) => `${field.name}${field.optional ? '?' : ''}:${field.type}`)
|
||||
.join(' ');
|
||||
return `${type.name}${fields ? ` ${fields}` : ''} = ${type.name}`;
|
||||
}
|
||||
|
||||
function Signature({ value }: { value: string }) {
|
||||
function YamlSnippet({ value }: { value: string }) {
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap" className={classes.signature}>
|
||||
<Code block>{value}</Code>
|
||||
<Box className={classes.yamlBlock}>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={700} size="xs" c="dimmed" tt="uppercase">
|
||||
YAML schema
|
||||
</Text>
|
||||
<CopyButton value={value} timeout={1600}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? 'Copied' : 'Copy signature'}>
|
||||
<Tooltip label={copied ? 'Copied' : 'Copy YAML'}>
|
||||
<ActionIcon
|
||||
onClick={copy}
|
||||
variant="subtle"
|
||||
color={copied ? 'teal' : 'blue'}
|
||||
aria-label="Copy signature"
|
||||
aria-label="Copy YAML schema"
|
||||
>
|
||||
{copied ? <IconCheck size={17} /> : <IconCopy size={17} />}
|
||||
</ActionIcon>
|
||||
@@ -197,25 +185,22 @@ function Signature({ value }: { value: string }) {
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
<Code block>{value}</Code>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function PacketDefinition({ packet, layer }: { packet: ProtoPacket; layer: ProtoLayer }) {
|
||||
return (
|
||||
<Box component="article" id={`packet-${slug(packet.name)}`} className={classes.definition}>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Text c="blue" fw={700} size="xs" tt="uppercase">
|
||||
Packet · ID {packet.id}
|
||||
</Text>
|
||||
<Title order={3}>{packet.name}</Title>
|
||||
</div>
|
||||
<Badge variant="light">0x{packet.id.toString(16).padStart(2, '0')}</Badge>
|
||||
</Group>
|
||||
<Text c="dimmed" mt="sm" lh={1.65}>
|
||||
{packet.description || `Packet ${packet.name} in protocol layer ${layer.version}.`}
|
||||
</Text>
|
||||
<Signature value={packetSignature(packet)} />
|
||||
<YamlSnippet value={packet.yaml} />
|
||||
<FieldTable fields={packet.fields} layer={layer} />
|
||||
</Box>
|
||||
);
|
||||
@@ -231,7 +216,7 @@ function TypeDefinition({ type, layer }: { type: ProtoType; layer: ProtoLayer })
|
||||
<Text c="dimmed" mt="sm" lh={1.65}>
|
||||
{type.description || `Object ${type.name} in protocol layer ${layer.version}.`}
|
||||
</Text>
|
||||
<Signature value={typeSignature(type)} />
|
||||
<YamlSnippet value={type.yaml} />
|
||||
<FieldTable fields={type.fields} layer={layer} />
|
||||
</Box>
|
||||
);
|
||||
@@ -247,7 +232,7 @@ function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer })
|
||||
<Text c="dimmed" mt="sm" lh={1.65}>
|
||||
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
|
||||
</Text>
|
||||
<Signature value={`${item.name}:${item.type}`} />
|
||||
<YamlSnippet value={item.yaml} />
|
||||
<Table.ScrollContainer minWidth={520}>
|
||||
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
|
||||
<Table.Thead>
|
||||
@@ -328,7 +313,7 @@ export function ProtoPage() {
|
||||
<Title order={1}>Rosetta Proto</Title>
|
||||
<Text c="dimmed" className={classes.heroDescription}>
|
||||
Complete protocol reference generated from the RCC layer schemas. Browse
|
||||
packet constructors, objects, enumerations, field IDs, optional values,
|
||||
packet definitions, objects, enumerations, field IDs, optional values,
|
||||
comments, and linked types.
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -429,7 +414,7 @@ export function ProtoPage() {
|
||||
<Box component="section" id="packets" className={classes.schemaSection}>
|
||||
<Title order={2}>Packets</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
Network packet constructors and their serialized fields.
|
||||
Network packets and their serialized YAML fields.
|
||||
</Text>
|
||||
{packets.map((packet) => (
|
||||
<PacketDefinition
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.42fr) minmax(0, 1fr);
|
||||
gap: 52px;
|
||||
scroll-margin-top: var(--mantine-spacing-xl);
|
||||
padding-top: 48px;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
@@ -37,12 +34,15 @@
|
||||
.sectionTitle,
|
||||
.contactTitle {
|
||||
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
|
||||
font-family: Outfit, var(--mantine-font-family);
|
||||
font-weight: 500;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: clamp(26px, 3vw, 34px);
|
||||
font-size: clamp(21px, 3vw, 26px);
|
||||
}
|
||||
|
||||
.section :global(.mantine-Accordion-root) {
|
||||
margin-top: var(--mantine-spacing-lg);
|
||||
}
|
||||
|
||||
.contact {
|
||||
@@ -64,8 +64,6 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--mantine-spacing-lg);
|
||||
padding-top: 36px;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +107,8 @@ const PRIVACY_FAQS = [
|
||||
answer: (
|
||||
<>
|
||||
Email{' '}
|
||||
<Anchor href="mailto:admin@rosetta.im?subject=Rosetta%20Security%20Report">
|
||||
Admin@rosetta.im
|
||||
<Anchor href="mailto:support@rosetta.im?subject=Rosetta%20Security%20Report">
|
||||
Support@rosetta.im
|
||||
</Anchor>{' '}
|
||||
with a description of the concern. Do not include recovery phrases or private
|
||||
keys.
|
||||
@@ -172,7 +172,7 @@ export function SupportPage() {
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request"
|
||||
href="mailto:support@rosetta.im?subject=Rosetta%20Support%20Request"
|
||||
leftSection={<IconMail size={18} />}
|
||||
variant="default"
|
||||
mt="xl"
|
||||
@@ -239,10 +239,10 @@ export function SupportPage() {
|
||||
</Text>
|
||||
</div>
|
||||
<Anchor
|
||||
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request"
|
||||
href="mailto:support@rosetta.im?subject=Rosetta%20Support%20Request"
|
||||
fw={700}
|
||||
>
|
||||
Admin@rosetta.im
|
||||
Support@rosetta.im
|
||||
</Anchor>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface UpdateItem {
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
const PLATFORM_ORDER = ['linux', 'win32', 'android'];
|
||||
const PLATFORM_ORDER = ['darwin', 'linux', 'win32', 'android'];
|
||||
const ARCH_ORDER = ['arm64', 'x64', 'universal'];
|
||||
|
||||
export async function fetchUpdates(signal?: AbortSignal): Promise<DownloadRelease[]> {
|
||||
|
||||
@@ -10,12 +10,14 @@ export interface ProtoPacket {
|
||||
name: string;
|
||||
id: number;
|
||||
description: string;
|
||||
yaml: string;
|
||||
fields: ProtoField[];
|
||||
}
|
||||
|
||||
export interface ProtoType {
|
||||
name: string;
|
||||
description: string;
|
||||
yaml: string;
|
||||
fields: ProtoField[];
|
||||
}
|
||||
|
||||
@@ -29,6 +31,7 @@ export interface ProtoEnum {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
yaml: string;
|
||||
values: ProtoEnumValue[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user