forked from Rosetta/landing
Add encryption reference and refine documentation
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
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,
|
.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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user