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>
<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>

View File

@@ -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,
};
});

View File

@@ -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 />} />

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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

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,
.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;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -163,59 +163,44 @@ 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}>
<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>
<CopyButton value={value} timeout={1600}>
{({ 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>
</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="blue" fw={700} size="xs" tt="uppercase">
Packet · ID {packet.id}
</Text>
<Title order={3}>{packet.name}</Title>
<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

View File

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

View File

@@ -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>

View File

@@ -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[]> {

View File

@@ -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[];
}