Add encryption reference and refine documentation

This commit is contained in:
rosetta
2026-07-30 18:16:03 -07:00
parent 5d26ef6d89
commit 179e2e65c6
19 changed files with 957 additions and 124 deletions

View File

@@ -12,6 +12,12 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
</url> </url>
<url>
<loc>https://rosetta.im/encryption/</loc>
<lastmod>2026-07-30</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url> <url>
<loc>https://rosetta.im/support/</loc> <loc>https://rosetta.im/support/</loc>
<lastmod>2026-07-30</lastmod> <lastmod>2026-07-30</lastmod>

View File

@@ -1,7 +1,7 @@
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path'; import { basename, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; 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 projectRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
const sourceDirectory = resolve( const sourceDirectory = resolve(
@@ -74,6 +74,10 @@ function mapNamedCollection(node, kind) {
pair.value?.commentBefore, pair.value?.commentBefore,
pair.value?.comment, pair.value?.comment,
); );
const yaml = stringify(
{ [name]: pair.value.toJSON() },
{ indent: 2, lineWidth: 0 },
).trimEnd();
if (kind === 'enum') { if (kind === 'enum') {
const values = pair.value.get('values', true); const values = pair.value.get('values', true);
@@ -81,6 +85,7 @@ function mapNamedCollection(node, kind) {
name, name,
type: String(pair.value.get('type')), type: String(pair.value.get('type')),
description, description,
yaml,
values: isSeq(values) values: isSeq(values)
? values.items.map((value) => { ? values.items.map((value) => {
const json = value.toJSON(); const json = value.toJSON();
@@ -105,6 +110,7 @@ function mapNamedCollection(node, kind) {
name, name,
id: kind === 'packet' ? Number(pair.value.get('id')) : undefined, id: kind === 'packet' ? Number(pair.value.get('id')) : undefined,
description, description,
yaml,
fields, fields,
}; };
}); });

View File

@@ -9,6 +9,9 @@ import './style.css';
const LandingPage = lazy(() => const LandingPage = lazy(() =>
import('./pages/LandingPage').then((module) => ({ default: module.LandingPage })), import('./pages/LandingPage').then((module) => ({ default: module.LandingPage })),
); );
const EncryptionPage = lazy(() =>
import('./pages/EncryptionPage').then((module) => ({ default: module.EncryptionPage })),
);
const ProtoPage = lazy(() => const ProtoPage = lazy(() =>
import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })), import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })),
); );
@@ -46,6 +49,7 @@ export default function App() {
> >
<Routes> <Routes>
<Route path="/" element={<LandingPage />} /> <Route path="/" element={<LandingPage />} />
<Route path="/encryption" element={<EncryptionPage />} />
<Route path="/proto" element={<ProtoPage />} /> <Route path="/proto" element={<ProtoPage />} />
<Route path="/support" element={<SupportPage />} /> <Route path="/support" element={<SupportPage />} />
<Route path="/privacy" element={<PrivacyPage />} /> <Route path="/privacy" element={<PrivacyPage />} />

View File

@@ -2,36 +2,42 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; 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 { .shell {
flex: 1; flex: 1;
padding-top: 72px;
padding-bottom: 96px;
} }
.main { .main {
width: 100%; 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 { .intro {
max-width: 900px; max-width: 760px;
padding-bottom: 64px; padding-bottom: 48px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
} }
.title { .title {
margin-top: var(--mantine-spacing-sm); margin-top: var(--mantine-spacing-sm);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
font-family: Outfit, var(--mantine-font-family); font-size: clamp(38px, 6vw, 56px);
font-size: clamp(40px, 7vw, 62px); font-weight: 600;
font-weight: 500;
letter-spacing: -0.03em; letter-spacing: -0.03em;
line-height: 1.08; line-height: 1.08;
} }
.lead { .lead {
max-width: 780px; max-width: 780px;
margin: 0; margin: 40px 0 0;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
font-size: 20px; font-size: 20px;
line-height: 1.6; line-height: 1.6;
@@ -63,25 +69,22 @@
} }
.section { .section {
display: grid; margin-top: 44px;
grid-template-columns: minmax(220px, 0.42fr) minmax(0, 1fr); padding-top: 36px;
gap: 52px;
margin-top: 56px;
padding-top: 48px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
} }
.sectionTitle { .sectionTitle {
margin: 0; margin: 0;
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white)); color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-family: Outfit, var(--mantine-font-family); font-size: clamp(21px, 3vw, 26px);
font-size: clamp(26px, 3vw, 34px); font-weight: 650;
font-weight: 500;
line-height: 1.25; line-height: 1.25;
} }
.sectionContent { .sectionContent {
min-width: 0; min-width: 0;
margin-top: var(--mantine-spacing-md);
} }
.document a { .document a {
@@ -104,24 +107,27 @@
} }
@media (max-width: 48em) { @media (max-width: 48em) {
.main { .shell {
padding: 72px 0 88px; padding-top: 40px;
padding-bottom: 64px;
} }
.intro { .intro {
padding-bottom: 48px; padding-bottom: 36px;
} }
.section { .section {
grid-template-columns: 1fr; margin-top: 36px;
gap: var(--mantine-spacing-md); padding-top: 30px;
margin-top: 44px;
padding-top: 36px;
} }
} }
@media (max-width: 36em) { @media (max-width: 36em) {
.shell {
padding-inline: var(--mantine-spacing-sm);
}
.main { .main {
padding-top: 56px; padding: 28px 22px;
} }
} }

View File

@@ -33,7 +33,7 @@ export function LegalPage({
<Box className={classes.page}> <Box className={classes.page}>
<SiteHeader /> <SiteHeader />
<Container className={classes.shell}> <Container className={classes.shell}>
<Box component="main" className={classes.main}> <Paper component="main" withBorder radius="lg" className={classes.main}>
<Box className={classes.intro}> <Box className={classes.intro}>
<Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em"> <Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em">
{eyebrow} {eyebrow}
@@ -51,7 +51,7 @@ export function LegalPage({
<Box component="article" className={classes.document}> <Box component="article" className={classes.document}>
{children} {children}
</Box> </Box>
</Box> </Paper>
</Container> </Container>
<SiteFooter /> <SiteFooter />
</Box> </Box>

View File

@@ -33,6 +33,9 @@ export function SiteFooter() {
<Anchor component={Link} to="/#apps" c="dimmed" size="sm"> <Anchor component={Link} to="/#apps" c="dimmed" size="sm">
Apps Apps
</Anchor> </Anchor>
<Anchor component={Link} to="/encryption" c="dimmed" size="sm">
Encryption
</Anchor>
<Anchor component={Link} to="/proto" c="dimmed" size="sm"> <Anchor component={Link} to="/proto" c="dimmed" size="sm">
Proto Proto
</Anchor> </Anchor>
@@ -51,8 +54,8 @@ export function SiteFooter() {
<Anchor component={Link} to="/terms" c="dimmed" size="sm"> <Anchor component={Link} to="/terms" c="dimmed" size="sm">
Terms of Use Terms of Use
</Anchor> </Anchor>
<Anchor href="mailto:admin@rosetta.im" c="dimmed" size="sm"> <Anchor href="mailto:support@rosetta.im" c="dimmed" size="sm">
Admin@rosetta.im Support@rosetta.im
</Anchor> </Anchor>
</Stack> </Stack>
</SimpleGrid> </SimpleGrid>

View File

@@ -30,13 +30,14 @@
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none !important;
transition: color 120ms ease; transition: color 120ms ease;
} }
.link:hover, .link:hover,
.link[data-active] { .link[data-active] {
color: var(--mantine-color-blue-6); color: var(--mantine-color-blue-6);
text-decoration: none !important;
} }
.burger { .burger {

View File

@@ -15,6 +15,7 @@ import classes from './SiteHeader.module.css';
const NAVIGATION = [ const NAVIGATION = [
{ label: 'Features', to: '/#features' }, { label: 'Features', to: '/#features' },
{ label: 'Apps', to: '/#apps' }, { label: 'Apps', to: '/#apps' },
{ label: 'Encryption', to: '/encryption' },
{ label: 'Proto', to: '/proto' }, { label: 'Proto', to: '/proto' },
{ label: 'Support', to: '/support' }, { label: 'Support', to: '/support' },
]; ];
@@ -26,6 +27,7 @@ export function SiteHeader() {
const links = NAVIGATION.map(({ label, to }) => { const links = NAVIGATION.map(({ label, to }) => {
const active = const active =
(to === '/proto' && pathname === '/proto') || (to === '/proto' && pathname === '/proto') ||
(to === '/encryption' && pathname === '/encryption') ||
(to === '/support' && pathname === '/support'); (to === '/support' && pathname === '/support');
return ( return (

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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 />
</>
);
}

View File

@@ -96,7 +96,7 @@
.featuresSection, .featuresSection,
.appsSection, .appsSection,
.protocolSection { .encryptionSection {
padding: 112px 0; padding: 112px 0;
} }
@@ -140,12 +140,12 @@
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
} }
.protocolSection { .encryptionSection {
padding-top: 0; padding-top: 0;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
} }
.protocolCard { .encryptionCard {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -154,14 +154,14 @@
background: linear-gradient(135deg, var(--mantine-color-blue-8), var(--mantine-color-blue-5)); 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-size: clamp(32px, 4vw, 46px);
font-weight: 600; font-weight: 600;
letter-spacing: -0.03em; letter-spacing: -0.03em;
line-height: 1.1; line-height: 1.1;
} }
.protocolButton { .encryptionButton {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -207,15 +207,15 @@
.featuresSection, .featuresSection,
.appsSection, .appsSection,
.protocolSection { .encryptionSection {
padding: 80px 0; padding: 80px 0;
} }
.protocolSection { .encryptionSection {
padding-top: 0; padding-top: 0;
} }
.protocolCard { .encryptionCard {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
padding: 36px 28px; padding: 36px 28px;

View File

@@ -16,6 +16,7 @@ import {
IconArrowDown, IconArrowDown,
IconBolt, IconBolt,
IconBrandAndroid, IconBrandAndroid,
IconBrandApple,
IconBrandWindows, IconBrandWindows,
IconCheck, IconCheck,
IconCode, IconCode,
@@ -88,6 +89,12 @@ const PLATFORMS: Array<{
description: 'Private messaging for Android phones and tablets.', description: 'Private messaging for Android phones and tablets.',
icon: IconBrandAndroid, icon: IconBrandAndroid,
}, },
{
key: 'darwin',
title: 'Rosetta for macOS',
description: 'Native packages for Apple silicon and Intel-based Macs.',
icon: IconBrandApple,
},
{ {
key: 'win32', key: 'win32',
title: 'Rosetta for Windows', title: 'Rosetta for Windows',
@@ -168,7 +175,7 @@ function DownloadApps() {
</Text> </Text>
</Box> </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 }) => { {PLATFORMS.map(({ key, title, description, icon: Icon }) => {
const platformReleases = groupedReleases[key] || []; const platformReleases = groupedReleases[key] || [];
@@ -387,30 +394,31 @@ export function LandingPage() {
<DownloadApps /> <DownloadApps />
<Box component="section" className={classes.protocolSection}> <Box component="section" className={classes.encryptionSection}>
<Container> <Container>
<Paper radius="xl" className={classes.protocolCard}> <Paper radius="xl" className={classes.encryptionCard}>
<Box> <Box>
<Text c="blue.2" fw={700} size="sm"> <Text c="blue.2" fw={700} size="sm">
Rosetta Proto Rosetta Encryption
</Text> </Text>
<Title order={2} c="white" mt="xs"> <Title order={2} c="white" mt="xs">
The protocol is documented from its source. How Rosetta protects every message and attachment.
</Title> </Title>
<Text c="blue.0" mt="md" maw={650} lh={1.65}> <Text c="blue.0" mt="md" maw={650} lh={1.65}>
Browse every packet, object, enum, field ID, type reference, and Follow the complete encryption path from per-message keys and
layer change generated directly from Rosetta YAML schemas. XChaCha20-Poly1305 to recipient key envelopes and authenticated
attachment chunks.
</Text> </Text>
</Box> </Box>
<Button <Button
component={Link} component={Link}
to="/proto" to="/encryption"
color="white" color="white"
variant="white" variant="white"
size="lg" size="lg"
className={classes.protocolButton} className={classes.encryptionButton}
> >
Open Proto Documentation Read Encryption Documentation
</Button> </Button>
</Paper> </Paper>
</Container> </Container>

View File

@@ -86,21 +86,23 @@
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.signature { .yamlBlock {
margin: var(--mantine-spacing-lg) 0; 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: 1px solid light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-9));
border-radius: var(--mantine-radius-md); border-radius: var(--mantine-radius-md);
background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.1)); background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.1));
} }
.signature code { .yamlBlock code {
flex: 1; display: block;
padding: 0; padding: 0;
background: transparent; background: transparent;
color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1)); color: light-dark(var(--mantine-color-blue-9), var(--mantine-color-blue-1));
overflow-wrap: anywhere; font-size: 13px;
white-space: normal; line-height: 1.65;
overflow-x: auto;
white-space: pre;
} }
.table { .table {

View File

@@ -163,59 +163,44 @@ function FieldTable({ fields, layer }: { fields: ProtoField[]; layer: ProtoLayer
); );
} }
function packetSignature(packet: ProtoPacket) { function YamlSnippet({ value }: { value: string }) {
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 }) {
return ( return (
<Group gap="xs" wrap="nowrap" className={classes.signature}> <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 YAML'}>
<ActionIcon
onClick={copy}
variant="subtle"
color={copied ? 'teal' : 'blue'}
aria-label="Copy YAML schema"
>
{copied ? <IconCheck size={17} /> : <IconCopy size={17} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code block>{value}</Code> <Code block>{value}</Code>
<CopyButton value={value} timeout={1600}> </Box>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy signature'}>
<ActionIcon
onClick={copy}
variant="subtle"
color={copied ? 'teal' : 'blue'}
aria-label="Copy signature"
>
{copied ? <IconCheck size={17} /> : <IconCopy size={17} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
); );
} }
function PacketDefinition({ packet, layer }: { packet: ProtoPacket; layer: ProtoLayer }) { function PacketDefinition({ packet, layer }: { packet: ProtoPacket; layer: ProtoLayer }) {
return ( return (
<Box component="article" id={`packet-${slug(packet.name)}`} className={classes.definition}> <Box component="article" id={`packet-${slug(packet.name)}`} className={classes.definition}>
<Group justify="space-between" align="flex-start"> <Text c="blue" fw={700} size="xs" tt="uppercase">
<div> Packet · ID {packet.id}
<Text c="blue" fw={700} size="xs" tt="uppercase"> </Text>
Packet · ID {packet.id} <Title order={3}>{packet.name}</Title>
</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}> <Text c="dimmed" mt="sm" lh={1.65}>
{packet.description || `Packet ${packet.name} in protocol layer ${layer.version}.`} {packet.description || `Packet ${packet.name} in protocol layer ${layer.version}.`}
</Text> </Text>
<Signature value={packetSignature(packet)} /> <YamlSnippet value={packet.yaml} />
<FieldTable fields={packet.fields} layer={layer} /> <FieldTable fields={packet.fields} layer={layer} />
</Box> </Box>
); );
@@ -231,7 +216,7 @@ function TypeDefinition({ type, layer }: { type: ProtoType; layer: ProtoLayer })
<Text c="dimmed" mt="sm" lh={1.65}> <Text c="dimmed" mt="sm" lh={1.65}>
{type.description || `Object ${type.name} in protocol layer ${layer.version}.`} {type.description || `Object ${type.name} in protocol layer ${layer.version}.`}
</Text> </Text>
<Signature value={typeSignature(type)} /> <YamlSnippet value={type.yaml} />
<FieldTable fields={type.fields} layer={layer} /> <FieldTable fields={type.fields} layer={layer} />
</Box> </Box>
); );
@@ -247,7 +232,7 @@ function EnumDefinition({ item, layer }: { item: ProtoEnum; layer: ProtoLayer })
<Text c="dimmed" mt="sm" lh={1.65}> <Text c="dimmed" mt="sm" lh={1.65}>
{item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`} {item.description || `Enumeration ${item.name} in protocol layer ${layer.version}.`}
</Text> </Text>
<Signature value={`${item.name}:${item.type}`} /> <YamlSnippet value={item.yaml} />
<Table.ScrollContainer minWidth={520}> <Table.ScrollContainer minWidth={520}>
<Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}> <Table verticalSpacing="sm" horizontalSpacing="md" className={classes.table}>
<Table.Thead> <Table.Thead>
@@ -328,7 +313,7 @@ export function ProtoPage() {
<Title order={1}>Rosetta Proto</Title> <Title order={1}>Rosetta Proto</Title>
<Text c="dimmed" className={classes.heroDescription}> <Text c="dimmed" className={classes.heroDescription}>
Complete protocol reference generated from the RCC layer schemas. Browse 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. comments, and linked types.
</Text> </Text>
</Box> </Box>
@@ -429,7 +414,7 @@ export function ProtoPage() {
<Box component="section" id="packets" className={classes.schemaSection}> <Box component="section" id="packets" className={classes.schemaSection}>
<Title order={2}>Packets</Title> <Title order={2}>Packets</Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs">
Network packet constructors and their serialized fields. Network packets and their serialized YAML fields.
</Text> </Text>
{packets.map((packet) => ( {packets.map((packet) => (
<PacketDefinition <PacketDefinition

View File

@@ -26,9 +26,6 @@
} }
.section { .section {
display: grid;
grid-template-columns: minmax(220px, 0.42fr) minmax(0, 1fr);
gap: 52px;
scroll-margin-top: var(--mantine-spacing-xl); scroll-margin-top: var(--mantine-spacing-xl);
padding-top: 48px; padding-top: 48px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
@@ -37,12 +34,15 @@
.sectionTitle, .sectionTitle,
.contactTitle { .contactTitle {
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white)); color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
font-family: Outfit, var(--mantine-font-family); font-weight: 650;
font-weight: 500;
} }
.sectionTitle { .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 { .contact {
@@ -64,8 +64,6 @@
} }
.section { .section {
grid-template-columns: 1fr;
gap: var(--mantine-spacing-lg);
padding-top: 36px; padding-top: 36px;
} }

View File

@@ -107,8 +107,8 @@ const PRIVACY_FAQS = [
answer: ( answer: (
<> <>
Email{' '} Email{' '}
<Anchor href="mailto:admin@rosetta.im?subject=Rosetta%20Security%20Report"> <Anchor href="mailto:support@rosetta.im?subject=Rosetta%20Security%20Report">
Admin@rosetta.im Support@rosetta.im
</Anchor>{' '} </Anchor>{' '}
with a description of the concern. Do not include recovery phrases or private with a description of the concern. Do not include recovery phrases or private
keys. keys.
@@ -172,7 +172,7 @@ export function SupportPage() {
</Text> </Text>
<Button <Button
component="a" component="a"
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request" href="mailto:support@rosetta.im?subject=Rosetta%20Support%20Request"
leftSection={<IconMail size={18} />} leftSection={<IconMail size={18} />}
variant="default" variant="default"
mt="xl" mt="xl"
@@ -239,10 +239,10 @@ export function SupportPage() {
</Text> </Text>
</div> </div>
<Anchor <Anchor
href="mailto:admin@rosetta.im?subject=Rosetta%20Support%20Request" href="mailto:support@rosetta.im?subject=Rosetta%20Support%20Request"
fw={700} fw={700}
> >
Admin@rosetta.im Support@rosetta.im
</Anchor> </Anchor>
</Group> </Group>
</Paper> </Paper>

View File

@@ -12,7 +12,7 @@ interface UpdateItem {
downloadUrl: string; downloadUrl: string;
} }
const PLATFORM_ORDER = ['linux', 'win32', 'android']; const PLATFORM_ORDER = ['darwin', 'linux', 'win32', 'android'];
const ARCH_ORDER = ['arm64', 'x64', 'universal']; const ARCH_ORDER = ['arm64', 'x64', 'universal'];
export async function fetchUpdates(signal?: AbortSignal): Promise<DownloadRelease[]> { export async function fetchUpdates(signal?: AbortSignal): Promise<DownloadRelease[]> {

View File

@@ -10,12 +10,14 @@ export interface ProtoPacket {
name: string; name: string;
id: number; id: number;
description: string; description: string;
yaml: string;
fields: ProtoField[]; fields: ProtoField[];
} }
export interface ProtoType { export interface ProtoType {
name: string; name: string;
description: string; description: string;
yaml: string;
fields: ProtoField[]; fields: ProtoField[];
} }
@@ -29,6 +31,7 @@ export interface ProtoEnum {
name: string; name: string;
type: string; type: string;
description: string; description: string;
yaml: string;
values: ProtoEnumValue[]; values: ProtoEnumValue[];
} }