From 33d4574583a2f85c2c30a23b13cfe5748ef0bbfe Mon Sep 17 00:00:00 2001 From: rosetta Date: Fri, 31 Jul 2026 04:32:34 -0700 Subject: [PATCH] Refine documentation and support experience --- known-issues.json | 44 ++ public/sitemap.xml | 6 + scripts/generate-proto-docs.mjs | 2 +- src/App.tsx | 4 + src/components/LegalPage/LegalPage.module.css | 18 +- src/components/LegalPage/LegalPage.tsx | 6 +- src/components/SiteFooter/SiteFooter.tsx | 2 +- .../SiteHeader/SiteHeader.module.css | 6 +- src/components/SiteHeader/SiteHeader.tsx | 2 +- src/generated/protoDocs.ts | 2 +- src/pages/EncryptionPage.module.css | 172 +---- src/pages/EncryptionPage.tsx | 679 ++++++------------ src/pages/LandingPage.module.css | 162 ++--- src/pages/LandingPage.tsx | 116 +-- src/pages/ProtoPage.module.css | 149 ++-- src/pages/ProtoPage.tsx | 399 +++++++--- src/pages/SupportPage.module.css | 100 ++- src/pages/SupportPage.tsx | 351 +++------ 18 files changed, 924 insertions(+), 1296 deletions(-) create mode 100644 known-issues.json diff --git a/known-issues.json b/known-issues.json new file mode 100644 index 0000000..ce5f5e6 --- /dev/null +++ b/known-issues.json @@ -0,0 +1,44 @@ +[ + { + "id": "lost-recovery-phrase", + "title": "Lost recovery phrase", + "summary": "The recovery phrase cannot be restored by Rosetta.", + "solution": "Rosetta does not keep a copy of your recovery phrase or private key. If another device is still signed in, secure any data you need and create a new account before access is lost. Never send a recovery phrase to support.", + "keywords": ["lost", "recovery", "phrase", "seed", "restore", "account", "login"] + }, + { + "id": "new-device-confirmation", + "title": "New device requires confirmation", + "summary": "A restored account may require approval before the new device is trusted.", + "solution": "Keep the existing Rosetta device online and approve the new device when prompted. Check that the recovery phrase was entered in the original order. If no trusted device remains available, contact support with the client version and operating system, but never include private keys.", + "keywords": ["new device", "confirmation", "approve", "trusted", "restore", "handshake", "login"] + }, + { + "id": "attachment-stuck", + "title": "Attachment remains in sending state", + "summary": "Large media can remain pending when the upload or connection is interrupted.", + "solution": "Keep Rosetta open and connected until the transfer finishes. Confirm that the file is still available locally, retry the message, and verify that the current client version is installed. Include the attachment type and approximate size when contacting support.", + "keywords": ["attachment", "file", "photo", "video", "upload", "sending", "stuck", "pending", "media"] + }, + { + "id": "missing-media-permission", + "title": "Photos, files, or microphone are unavailable", + "summary": "The operating system may have denied a required permission.", + "solution": "Open the operating-system privacy settings for Rosetta and enable access to Photos, Files, or Microphone as needed. Restart Rosetta after changing permissions and select the attachment again.", + "keywords": ["permission", "microphone", "photo", "camera", "file", "voice", "denied", "unavailable"] + }, + { + "id": "message-not-delivered", + "title": "Message is not delivered", + "summary": "Delivery can be delayed by connectivity, client state, or an unavailable recipient.", + "solution": "Check the network connection and confirm that Rosetta reports a connected state. Leave the application open briefly and retry once. If the problem continues, provide support with the sender platform, recipient platform, approximate time, and client version. Do not include message content or encryption material.", + "keywords": ["message", "deliver", "delivered", "waiting", "error", "offline", "connection", "send"] + }, + { + "id": "security-report", + "title": "Report abuse or a security issue", + "summary": "Security reports should contain reproducible technical details without secrets.", + "solution": "Email support@rosetta.im with the affected platform, client version, expected behavior, actual behavior, and reproduction steps. Never include a recovery phrase, private key, decrypted private messages, or another person's personal information.", + "keywords": ["security", "abuse", "report", "vulnerability", "privacy", "spam", "scam"] + } +] diff --git a/public/sitemap.xml b/public/sitemap.xml index ae7b312..ed012af 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -12,6 +12,12 @@ weekly 0.9 + + https://rosetta.im/proto/docs/ + 2026-07-31 + weekly + 0.8 + https://rosetta.im/encryption/ 2026-07-30 diff --git a/scripts/generate-proto-docs.mjs b/scripts/generate-proto-docs.mjs index f3284aa..664bdba 100644 --- a/scripts/generate-proto-docs.mjs +++ b/scripts/generate-proto-docs.mjs @@ -178,5 +178,5 @@ const totals = layers.reduce( ); console.log( - `Generated Proto docs from ${layers.length} layers: ${totals.packets} packets, ${totals.types} objects, ${totals.enums} enums.`, + `Generated Proto docs from ${layers.length} layers: ${totals.packets} packets, ${totals.types} types, ${totals.enums} enums.`, ); diff --git a/src/App.tsx b/src/App.tsx index a354fe9..cd69c24 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,9 @@ const EncryptionPage = lazy(() => const ProtoPage = lazy(() => import('./pages/ProtoPage').then((module) => ({ default: module.ProtoPage })), ); +const ProtoDocsPage = lazy(() => + import('./pages/ProtoPage').then((module) => ({ default: module.ProtoDocsPage })), +); const SupportPage = lazy(() => import('./pages/SupportPage').then((module) => ({ default: module.SupportPage })), ); @@ -51,6 +54,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/LegalPage/LegalPage.module.css b/src/components/LegalPage/LegalPage.module.css index c51d235..8a8f27c 100644 --- a/src/components/LegalPage/LegalPage.module.css +++ b/src/components/LegalPage/LegalPage.module.css @@ -15,21 +15,21 @@ width: 100%; max-width: 900px; margin-inline: auto; - padding: clamp(32px, 6vw, 72px); + padding: clamp(30px, 5vw, 60px); 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: 760px; - padding-bottom: 48px; + padding-bottom: 36px; 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-size: clamp(38px, 6vw, 56px); + font-size: clamp(28px, 4vw, 36px); font-weight: 600; letter-spacing: -0.03em; line-height: 1.08; @@ -39,8 +39,8 @@ max-width: 780px; margin: 40px 0 0; color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); - font-size: 20px; - line-height: 1.6; + font-size: 17px; + line-height: 1.72; } .text, @@ -65,7 +65,7 @@ } .list li::marker { - color: var(--mantine-color-blue-5); + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); } .section { @@ -77,7 +77,7 @@ .sectionTitle { margin: 0; color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white)); - font-size: clamp(21px, 3vw, 26px); + font-size: clamp(19px, 3vw, 23px); font-weight: 650; line-height: 1.25; } @@ -95,8 +95,8 @@ .address { margin: var(--mantine-spacing-md) 0; - border-left: 3px solid var(--mantine-color-blue-5); - background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6)); + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); } .address p { diff --git a/src/components/LegalPage/LegalPage.tsx b/src/components/LegalPage/LegalPage.tsx index b5580c1..92d3b0d 100644 --- a/src/components/LegalPage/LegalPage.tsx +++ b/src/components/LegalPage/LegalPage.tsx @@ -33,9 +33,9 @@ export function LegalPage({ - + - + {eyebrow} @@ -93,7 +93,7 @@ export function LegalList({ children }: { children: ReactNode }) { export function LegalAddress({ children }: { children: ReactNode }) { return ( - <Paper radius="md" p="md" className={classes.address}> + <Paper radius="sm" p="md" className={classes.address}> <Text component="p">{children}</Text> </Paper> ); diff --git a/src/components/SiteFooter/SiteFooter.tsx b/src/components/SiteFooter/SiteFooter.tsx index 8d489b7..36f2628 100644 --- a/src/components/SiteFooter/SiteFooter.tsx +++ b/src/components/SiteFooter/SiteFooter.tsx @@ -55,7 +55,7 @@ export function SiteFooter() { Terms of Use </Anchor> <Anchor href="mailto:support@rosetta.im" c="dimmed" size="sm"> - Support@rosetta.im + support@rosetta.im </Anchor> </Stack> </SimpleGrid> diff --git a/src/components/SiteHeader/SiteHeader.module.css b/src/components/SiteHeader/SiteHeader.module.css index 393001a..49a6666 100644 --- a/src/components/SiteHeader/SiteHeader.module.css +++ b/src/components/SiteHeader/SiteHeader.module.css @@ -19,7 +19,11 @@ align-items: center; gap: 10px; color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white)); - text-decoration: none; + text-decoration: none !important; +} + +.brand:hover { + text-decoration: none !important; } .brand img { diff --git a/src/components/SiteHeader/SiteHeader.tsx b/src/components/SiteHeader/SiteHeader.tsx index 972d330..78292eb 100644 --- a/src/components/SiteHeader/SiteHeader.tsx +++ b/src/components/SiteHeader/SiteHeader.tsx @@ -26,7 +26,7 @@ export function SiteHeader() { const links = NAVIGATION.map(({ label, to }) => { const active = - (to === '/proto' && pathname === '/proto') || + (to === '/proto' && pathname.startsWith('/proto')) || (to === '/encryption' && pathname === '/encryption') || (to === '/support' && pathname === '/support'); diff --git a/src/generated/protoDocs.ts b/src/generated/protoDocs.ts index fec7b19..4d8d987 100644 --- a/src/generated/protoDocs.ts +++ b/src/generated/protoDocs.ts @@ -71,7 +71,7 @@ export const protoLayers = [ { "name": "HandshakeStage", "type": "int8", - "description": "", + "description": "Handshake Stage (need for set device confirm view after pass seed)", "yaml": "HandshakeStage:\n type: int8\n values:\n - id: 0\n name: COMPLETED\n - id: 1\n name: NEED_DEVICE_VERIFICATION", "values": [ { diff --git a/src/pages/EncryptionPage.module.css b/src/pages/EncryptionPage.module.css index 37b8ccf..4137378 100644 --- a/src/pages/EncryptionPage.module.css +++ b/src/pages/EncryptionPage.module.css @@ -1,162 +1,30 @@ -.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; +.subheading { + margin: 28px 0 10px; + color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-dark-0)); + font-size: 17px; 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; +.code { + margin: 18px 0; + padding: 16px; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); + border-radius: var(--mantine-radius-xs); + background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); + color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0)); + font-size: 12px; + line-height: 1.65; overflow-x: auto; white-space: pre; } -.list { +.table { + margin: 18px 0 24px; + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); +} + +.references { color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: var(--mantine-font-size-sm); 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; - } -} diff --git a/src/pages/EncryptionPage.tsx b/src/pages/EncryptionPage.tsx index cc44e8f..20e9020 100644 --- a/src/pages/EncryptionPage.tsx +++ b/src/pages/EncryptionPage.tsx @@ -1,101 +1,25 @@ +import { Anchor, Code, List, Table, Text, Title } from '@mantine/core'; 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'; + LegalList, + LegalPage, + LegalSection, + LegalText, +} from '../components/LegalPage/LegalPage'; 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'], + ['IV', '12 bytes', 'Random for every encrypted chunk'], + ['Ciphertext + tag', 'Variable', 'AES-256-GCM output and authentication tag'], ]; -function TechnicalSection({ - id, - eyebrow, - title, - children, -}: { - id: string; - eyebrow: string; - title: string; - children: React.ReactNode; -}) { +function TechnicalCode({ children }: { children: string }) { 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} - {children} - - ); -} - -function Term({ - term, - children, -}: { - term: string; - children: React.ReactNode; -}) { - return ( - - - {term} - - - {children} - - + + {children} + ); } @@ -108,404 +32,213 @@ export function EncryptionPage() { canonical="https://rosetta.im/encryption/" keywords="Rosetta encryption, XChaCha20-Poly1305, AES-256-GCM, secp256k1, encrypted attachments" /> - -
- - - Rosetta - - - {' '} - / Encryption - + + + This document describes the current Rosetta desktop implementation. It covers + direct messages, per-message keys, recipient key envelopes, group messages, + local storage, and encrypted attachments. It is an implementation reference, + not a claim of formal verification or an independent security audit. + - - - - - - Implementation reference - - Rosetta Encryption - - 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. - - + + + Rosetta does not encrypt an entire message or file with public-key + cryptography. Public-key operations are comparatively slow and are not + designed for large payloads. They are used only to establish and protect + the small secret required by the content cipher. + + + The message text itself is encrypted with a symmetric AEAD cipher because + symmetric cryptography is fast and appropriate for arbitrary amounts of + data. In other words, the asymmetric part protects the key; the unique + symmetric key protects the message. + + + Every direct message receives its own fresh 32-byte content key and its own + fresh 24-byte nonce. A key is not reused as the general content key for the + next message. + + - - Scope of this document - - 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. - - + + + Content cipher + + + The client generates the content key and nonce with the platform + cryptographic random number generator. XChaCha20-Poly1305 encrypts the + UTF-8 message and produces an authentication tag. A modified ciphertext or + an incorrect key causes decryption to fail. + + {`contentKey = randomBytes(32) # 256 bits +nonce = randomBytes(24) # 192 bits +ciphertext = XChaCha20-Poly1305(contentKey, nonce, UTF8(message))`} - - {[ - ['Direct messages', '#direct-messages'], - ['Attachments', '#attachments'], - ['Groups', '#groups'], - ['Key material', '#key-material'], - ['Security properties', '#security-properties'], - ['Terminology', '#terminology'], - ].map(([label, href]) => ( - - {label} - - ))} - - - - - 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. - - - - {MESSAGE_FLOW.map(({ number, title, description }) => ( - - - {number} - - - {title} - - - {description} - - - ))} - - - Content cipher - - {`contentKey = randomBytes(32) # 256 bits -nonce = randomBytes(24) # 192 bits -ciphertext = XChaCha20-Poly1305( - key = contentKey, - nonce = nonce, - plaintext = UTF8(message) -)`} - - - 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. - - - Recipient key envelope - - - The client generates an ephemeral secp256k1 key pair for the - message. - - - ECDH combines the ephemeral private key with the recipient's - long-term public key. - - - The x-coordinate of the shared point becomes a 32-byte AES key. - - - 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. - - - The envelope carries the IV, encrypted material, and uncompressed - ephemeral public key. The recipient reconstructs the same shared - secret with their private key. - - - - - {`messageSecret = contentKey || nonce + + Recipient key envelope + + + + The client creates an ephemeral secp256k1 key pair for the message. + + + ECDH combines the ephemeral private key with the recipient's + long-term public key. + + + The x-coordinate of the shared point is used as a 32-byte AES wrapping + key. + + + The 56-byte message secret — content key followed by nonce — is wrapped + with AES-CBC and PKCS#7 padding using a random 16-byte IV. + + + The envelope contains the IV, encrypted message secret, and ephemeral + public key. The content key is never sent in plaintext. + + + {`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)`} - +keyEnvelope = Base64(iv || AES-CBC(wrappingKey, messageSecret) || ephemeral.public)`} - What crosses the service boundary - - 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. - - + + The packet sent to the service contains routing identifiers, timestamp, + message ID, encrypted content, recipient key envelope, and attachment + descriptors. The service does not receive the direct-message content key + or plaintext message. + + - - - 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. - - - - {[ - ['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 ( - - - - - - {title} - - - {description} - - - ); - })} - - - RAC2 encrypted chunk format - - RAC2 || salt[16] || iv[12] || AES-256-GCM-ciphertext-and-tag - - - - - - Segment - Length - Purpose + + + Files, images, audio, and video are encrypted before upload. Binary data is + split into independent chunks so that large files can be streamed, retried, + cached, and decrypted by range without loading the complete file into + memory. + + + The attachment password is derived from the secret of the message that + carries the attachment. PBKDF2-SHA-256 with 100,000 iterations and a random + 16-byte salt derives an AES-256-GCM key. Each chunk receives a new random + 12-byte IV. AES-GCM authenticates the encrypted chunk and rejects modified + data before plaintext is returned. + + + RAC2 || salt[16] || iv[12] || AES-256-GCM-ciphertext-and-tag + + +
+ + + Segment + Length + Purpose + + + + {ATTACHMENT_LAYOUT.map(([segment, length, purpose]) => ( + + {segment} + {length} + {purpose} - - - {ATTACHMENT_LAYOUT.map(([segment, length, purpose]) => ( - - - {segment} - - {length} - - - {purpose} - - - - ))} - -
-
+ ))} + + + + + The CDN stores ciphertext and transfer metadata such as object ID, size, + and chunk count. It does not receive the attachment decryption secret. + + - - 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. - -
+ + + A group uses a shared group secret available to its members. Group messages + do not carry a new recipient envelope for every member. The current group + and inline-payload compatibility codec compresses the input with DEFLATE, + derives an AES key with PBKDF2, and encrypts it with AES-CBC using a random + IV. + + {`input + → DEFLATE + → PBKDF2-SHA-256(password, "rosetta", 1,000 iterations) + → AES-CBC with a random 16-byte IV + → Base64(iv) : Base64(ciphertext)`} + + This compatibility format provides confidentiality but does not include an + AEAD authentication tag. Binary attachments use the newer RAC2 AES-GCM + format described above. + + - - - 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. - - - {`input - → DEFLATE compression - → PBKDF2(password, "rosetta", 1,000 iterations) - → AES-CBC with random 16-byte IV - → Base64(iv) : Base64(ciphertext)`} - - - This compatibility format provides confidentiality but does not include - an AEAD authentication tag. Binary attachments use the newer RAC2 - AES-GCM format described above. - - + + + 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 the + compressed public key used as the account identity. + + + The private key, recovery phrase, readable message cache, and sender-side + copy of the 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. + + - - - - - - - Account identity - - 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. - - - - - - - Local protection - - 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. - - - - + + + End-to-end encryption protects direct-message content and binary attachment + contents. It does not hide sender and recipient identifiers, timestamps, + message IDs, delivery state, attachment size, object IDs, network address, + or request timing. + + + The 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 key envelopes, so the + current design does not claim long-term forward secrecy. Group keys also + require rotation when membership changes. + + - - - - - Protected - - - Direct-message plaintext and per-message secret - Binary attachment contents and integrity - Locally stored private key and recovery phrase - Sender-side readable message cache - - - - - Not hidden by end-to-end encryption - - - Sender and recipient or group identifiers - Message timestamps, IDs, and delivery metadata - Attachment object IDs, sizes, and transfer metadata - Network information such as IP addresses and timing - - - - - - - - - -
- Important implementation boundaries - - 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. - -
-
-
-
- - - - - Authenticated Encryption with Associated Data. A cipher mode that - protects confidentiality and detects unauthorized modification. - - - A stream-cipher-based AEAD construction with a 192-bit nonce, - allowing securely generated random nonces with a very low collision - probability. - - - An AES mode that combines counter-mode encryption with a Galois - authentication tag. Rosetta uses it for RAC2 attachment chunks. - - - 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. - - - The elliptic curve used for Rosetta account keys and ephemeral - direct-message key envelopes. - - - A password-based key derivation function that repeatedly applies a - pseudorandom function with a salt to derive key material. - - - 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. - - - 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. - - - - - Standards and implementation references - - NIST SP 800-38D — GCM and GMAC - - - RFC 8018 — PBKDF2 and PKCS #5 - - - SEC 2 — secp256k1 domain parameters - - - noble-ciphers — XChaCha20-Poly1305 implementation - - - -
-
- + + + + AEAD: authenticated encryption that provides + confidentiality and detects modification. + + + ECDH: elliptic-curve key agreement used to calculate a + shared secret without transmitting it. + + + Nonce / IV: a value that must not repeat for a given + key and encryption mode. + + + Forward secrecy: protection of old session keys after a + long-term key is compromised. + + + + References:{' '} + + NIST SP 800-38D + + ,{' '} + RFC 8018,{' '} + SEC 2, and{' '} + + noble-ciphers + + . + + + ); } diff --git a/src/pages/LandingPage.module.css b/src/pages/LandingPage.module.css index 61ead0f..004aa89 100644 --- a/src/pages/LandingPage.module.css +++ b/src/pages/LandingPage.module.css @@ -1,103 +1,41 @@ .hero { - overflow: hidden; background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); } -.heroGrid { +.heroContainer { display: grid; - grid-template-columns: minmax(0, 1.08fr) minmax(340px, 0.92fr); - gap: 72px; - min-height: calc(100vh - 72px); - padding-top: 72px; - padding-bottom: 72px; + place-items: center; + min-height: 620px; + padding-top: 88px; + padding-bottom: 96px; +} + +.heroCopy { + width: 100%; + max-width: 760px; } .heroTitle { - max-width: 720px; + margin-top: 30px; color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); - font-size: clamp(52px, 7vw, 76px); + font-size: clamp(44px, 6vw, 64px); font-weight: 600; - letter-spacing: -0.045em; - line-height: 1.02; + letter-spacing: -0.04em; + line-height: 1.06; + text-align: center; } .heroDescription { - max-width: 680px; - margin-top: 28px; - font-size: 20px; + max-width: 650px; + margin-top: 24px; + font-size: 19px; line-height: 1.65; } -.preview { - position: relative; - display: grid; - place-items: center; - min-height: 640px; -} - -.preview::before { - position: absolute; - width: 520px; - height: 520px; - border-radius: 50%; - background: light-dark(rgba(34, 139, 230, 0.11), rgba(34, 139, 230, 0.16)); - content: ''; - filter: blur(2px); -} - -.phone { - position: relative; - z-index: 1; - width: min(100%, 380px); - padding: 20px; - background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); -} - -.phoneHeader { - padding-bottom: var(--mantine-spacing-md); - border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); -} - -.messages { - padding: 32px 0 24px; -} - -.messageIncoming, -.messageOutgoing { - max-width: 84%; -} - -.messageIncoming { - background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); -} - -.messageOutgoing { - margin-left: auto; - background: light-dark(var(--mantine-color-blue-0), rgba(34, 139, 230, 0.18)); -} - -.encryptionNote { - color: var(--mantine-color-blue-6); - padding-top: var(--mantine-spacing-md); - border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); -} - -.floatingCard { - position: absolute; - z-index: 2; - right: -24px; - bottom: 84px; - display: flex; - align-items: center; - gap: var(--mantine-spacing-sm); - padding: var(--mantine-spacing-md); - background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); -} - .featuresSection, .appsSection, .encryptionSection { - padding: 112px 0; + padding: 104px 0; } .featuresSection { @@ -115,10 +53,10 @@ .sectionHeading h2 { margin-top: var(--mantine-spacing-xs); color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); - font-size: clamp(36px, 5vw, 52px); + font-size: clamp(34px, 5vw, 48px); font-weight: 600; letter-spacing: -0.035em; - line-height: 1.08; + line-height: 1.1; } .sectionHeading > p:last-child { @@ -135,11 +73,15 @@ } .appCard { - min-height: 320px; + min-height: 280px; 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-7)); } +.downloadButton { + min-height: 42px; +} + .encryptionSection { padding-top: 0; background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)); @@ -150,65 +92,47 @@ align-items: center; justify-content: space-between; gap: 48px; - padding: 56px; + padding: 52px; background: linear-gradient(135deg, var(--mantine-color-blue-8), var(--mantine-color-blue-5)); } .encryptionCard h2 { - font-size: clamp(32px, 4vw, 46px); + font-size: clamp(30px, 4vw, 42px); font-weight: 600; letter-spacing: -0.03em; - line-height: 1.1; + line-height: 1.12; } .encryptionButton { flex: 0 0 auto; + color: var(--mantine-color-blue-8) !important; } @media (max-width: 62em) { - .heroGrid { - grid-template-columns: 1fr; - gap: 28px; - padding-top: 88px; - } - - .preview { + .heroContainer { min-height: 560px; } - - .floatingCard { - right: 8%; - } } @media (max-width: 48em) { - .heroGrid { - min-height: auto; + .heroContainer { + min-height: 500px; padding-top: 64px; padding-bottom: 64px; } .heroTitle { - font-size: clamp(44px, 13vw, 62px); + font-size: clamp(40px, 12vw, 54px); } .heroDescription { font-size: 18px; } - .preview { - min-height: 520px; - } - - .floatingCard { - right: 0; - bottom: 40px; - } - .featuresSection, .appsSection, .encryptionSection { - padding: 80px 0; + padding: 76px 0; } .encryptionSection { @@ -218,24 +142,18 @@ .encryptionCard { align-items: flex-start; flex-direction: column; - padding: 36px 28px; + padding: 34px 28px; } } @media (max-width: 36em) { - .heroCopy :global(.mantine-Group-root) { + .heroActions { align-items: stretch; - } - - .heroCopy :global(.mantine-Button-root) { + flex-direction: column; width: 100%; } - .preview { - min-height: 480px; - } - - .floatingCard { - display: none; + .heroActions :global(.mantine-Button-root) { + width: 100%; } } diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index fa48e03..ab01e3f 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -1,5 +1,4 @@ import { - Badge, Box, Button, Card, @@ -18,14 +17,11 @@ import { IconBrandAndroid, IconBrandApple, IconBrandWindows, - IconCheck, IconCode, IconDatabaseOff, IconDeviceDesktop, IconFileTextShield, - IconKey, IconLock, - IconMessageCircle, IconPhoneOff, type TablerIcon, } from '@tabler/icons-react'; @@ -73,7 +69,7 @@ const FEATURES: Array<{ icon: TablerIcon; title: string; description: string }> icon: IconCode, title: 'Documented protocol', description: - 'Every packet, object, enum, and field is published from the protocol source schemas.', + 'Every packet, type, enum, and field is published from the protocol source schemas.', }, ]; @@ -175,7 +171,7 @@ function DownloadApps() {
- + {PLATFORMS.map(({ key, title, description, icon: Icon }) => { const platformReleases = groupedReleases[key] || []; @@ -200,6 +196,7 @@ function DownloadApps() { variant="light" justify="space-between" rightSection={} + className={classes.downloadButton} > {architectureName(release.arch)} · {release.version} @@ -239,32 +236,18 @@ export function LandingPage() {
- - - - - - Private messaging - - + + + - A new era of{' '} - <Text - component="span" - variant="gradient" - gradient={{ from: 'blue', to: 'cyan' }} - inherit - > - private - </Text>{' '} - messaging. + Private messaging, simply. - + Rosetta keeps identity, conversations, and media under your control — without requiring a phone number or building a readable message archive on the server. - + @@ -278,86 +261,7 @@ export function LandingPage() { Explore Proto - - - {[ - ['No phone', 'Identity without personal contact data'], - ['Encrypted', 'Messages and media protected by default'], - ['Local', 'Readable history stays on your devices'], - ].map(([value, label]) => ( -
- {value} - - {label} - -
- ))} -
- - - - - - -
- - Rosetta - - - Encrypted - -
-
- - - -
- - - - Your recovery phrase never leaves your device. - - 10:42 - - - - And the server cannot read this conversation. - - - 10:43 - - - - - - Exactly. Privacy is the default. - - 10:43 - - - - - - - End-to-end encrypted session - -
- - - - - -
- - Delivered privately - - - No readable server history - -
-
-
@@ -413,7 +317,7 @@ export function LandingPage() {