How the Encryption Works

SG/Send uses AES-256-GCM symmetric encryption, performed entirely in your browser. The server never sees your plaintext data, your file names, or your encryption keys. This page explains exactly how.

Symmetric Encryption: One Key, Two Operations

SG/Send uses symmetric encryption — the same key encrypts and decrypts the file. This is the simplest and fastest encryption model, and when done correctly, the most secure for file transfer.

What is AES-256-GCM?

AES (Advanced Encryption Standard) is the encryption algorithm used by governments, banks, and militaries worldwide. 256 is the key size in bits — there are 2256 possible keys, making brute force computationally impossible. GCM (Galois/Counter Mode) adds authenticated encryption — it doesn't just encrypt, it also detects if anyone has tampered with the ciphertext.

Why Symmetric?

Symmetric encryption uses one key for both encryption and decryption. This is different from asymmetric encryption (like RSA) which uses a key pair. For file transfer, symmetric is the right choice: it's fast, well-understood, and the Web Crypto API provides a battle-tested implementation in every modern browser.

The key insight: if you control where the key is generated and where it travels, symmetric encryption gives you perfect confidentiality. SG/Send generates the key in your browser, and the key never touches the server.

The Encryption Flow: Step by Step

Here's exactly what happens when you send a file through SG/Send.

1

Key Generation

When you select a file, your browser generates a random 256-bit AES key using the Web Crypto API (crypto.subtle.generateKey). This uses the operating system's cryptographically secure random number generator. The key exists only in your browser's memory.

Browser Server | | | crypto.subtle.generateKey( | | { name: "AES-GCM", length: 256 }, | | true, | | ["encrypt", "decrypt"] | | ) | | | | Key generated: 3a7f...b2c1 (256 bits) | | Key stays here. Server knows nothing. | | |
2

IV Generation

A 12-byte Initialization Vector (IV) is generated randomly (crypto.getRandomValues). The IV ensures that encrypting the same file twice produces different ciphertext. The IV is not secret — it is prepended to the encrypted output and sent with the ciphertext.

3

Encryption

The file is encrypted in the browser using crypto.subtle.encrypt with the AES-GCM algorithm, the generated key, and the IV. The output is ciphertext + a 128-bit authentication tag. The authentication tag is GCM's integrity guarantee — if anyone modifies even one bit of the ciphertext, decryption will fail.

Browser Server | | | plaintext ──► AES-256-GCM ──► ciphertext | | ▲ | | | | | key + IV | | | | Output = IV (12 bytes) | | + ciphertext (same size as plaintext) | | + auth tag (16 bytes) | | |
4

Upload

Only the encrypted output (IV + ciphertext + auth tag) is uploaded to the server. The server receives a blob of random-looking bytes. It assigns a Transfer ID (12 random characters) and stores the ciphertext. No file name, no content type hint, no key — just encrypted bytes.

Browser Server | | | POST /transfers/create | | ──────────────────────────────────────────► | | | Stores: | PUT /transfers/{id}/upload | transfer_id: "a7x9k2m4p1" | Body: [IV + ciphertext + auth tag] | data: [encrypted bytes] | ──────────────────────────────────────────► | ip_hash: SHA256(ip + salt) | | timestamp: 2026-02-28T... | Key NEVER sent. File name NEVER sent. | | |
5

Link Sharing

The sender gets a download link. The decryption key is placed in the URL fragment (the part after #). URL fragments are never sent to the server by the browser — this is defined in RFC 3986. The sender shares this link through any channel they choose (email, chat, in person).

Link format: https://send.sgraph.ai/download/a7x9k2m4p1#3a7f...b2c1 ├──────────── domain ────────────┤├─ transfer id ─┤├─ key ─┤ ▲ │ URL fragment (#) Never sent to server Stays in the browser
6

Decryption

The recipient opens the link. Their browser downloads the encrypted bytes from the server, extracts the key from the URL fragment, separates the IV from the ciphertext, and decrypts using crypto.subtle.decrypt. If the auth tag doesn't match (tampering detected), decryption fails. The server never participates in decryption.

Recipient's Browser Server | | | GET /transfers/{id}/download | | ──────────────────────────────────────────► | | ◄────────────────────────────────────────── | | Response: [IV + ciphertext + auth tag] | | | | Key extracted from URL fragment (#) | | crypto.subtle.decrypt( | | { name: "AES-GCM", iv: IV }, | | key, | | ciphertext | | ) | | | | ──► plaintext file restored | | Server never saw the key or the plaintext. |

What the Server Sees vs. What It Doesn't

This is the zero-knowledge guarantee. Here is exactly what data exists on the server.

Server stores

Transfer ID — random 12 characters
Encrypted bytes — indistinguishable from random data
IP hash — SHA-256 of IP + daily rotating salt
Timestamp — when the transfer was created
File size — size of the encrypted blob (bytes)

Server never sees

Encryption key — stays in browser, shared via URL fragment
File name — never sent to server
File type — no content-type metadata stored
Plaintext content — only ciphertext ever uploaded
Raw IP address — hashed before storage

A complete server compromise — full database access, all backups, all logs — reveals nothing about the content of transferred files. The ciphertext is computationally indistinguishable from random noise without the key.

Technology Stack

Every layer is chosen for simplicity, security, and zero-dependency deployment.

Layer Technology Purpose
Runtime Python 3.12 / arm64 Application server language
Web Framework FastAPI via osbot-fast-api HTTP routing and request handling
Compute AWS Lambda + Mangum Serverless execution, pay-per-use
Storage Memory-FS (Storage_FS) Pluggable backend: memory, disk, or S3
Encryption Web Crypto API (AES-256-GCM) Client-side encryption in the browser
Type System Type_Safe (osbot-utils) Schema definitions without Pydantic
Frontend Vanilla JS + Web Components Zero framework dependencies (IFD)
Testing pytest, in-memory stack No mocks, no patches, real implementations
CI/CD GitHub Actions Test, tag, deploy pipeline

System Architecture

Two Lambda functions, one storage layer, one CDN. Simple by design.

User Lambda (Public)

Public-facing function that handles file transfers, health checks, presigned uploads, personal vaults, and MCP integration. Accessed via Lambda Function URL behind CloudFront.

18 endpoints

Admin Lambda (Authenticated)

Admin-only function for token management, analytics, server stats, and MCP admin tools. Requires authentication. Separate Lambda = separate security boundary.

55 endpoints

Memory-FS (Storage_FS)

All storage goes through an abstraction layer. Application code never knows if the backend is in-memory, on disk, or S3. Same codebase across all 7 deployment targets.

Pluggable Backend

CloudFront CDN

Static assets, caching, SSL termination, and WAF. Lambda URLs provide the HTTPS endpoints directly — no API Gateway needed.

Edge Network

Deployment Targets

One codebase, seven deployment targets grouped into four patterns.

Lambda

Primary deployment. Two Lambda functions behind Lambda Function URLs. Direct HTTPS endpoints, no API Gateway needed.

Production

Container

Docker, AWS Fargate, and GCP Cloud Run. The same application packaged as a container.

Docker / Fargate / GCP

Server

EC2 instances and AMI builds. For teams that need full control of the runtime environment.

EC2 / AMI

CLI

Command-line interface for scripted transfers, CI/CD pipelines, and power users.

Terminal
18
AI Agents
73
HTTP Endpoints
393
Passing Tests
7
Deploy Targets