Self-hosted end-to-end encrypted screenshot and media sharing
  • JavaScript 97.8%
  • TypeScript 1%
  • C# 0.8%
  • CSS 0.3%
  • PowerShell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-27 08:13:22 +02:00
client/BirdieShot.Uploader Initial BirdieShot import 2026-09-27 08:13:22 +02:00
docker Initial BirdieShot import 2026-09-27 08:13:22 +02:00
docs Initial BirdieShot import 2026-09-27 08:13:22 +02:00
infrastructure Initial BirdieShot import 2026-09-27 08:13:22 +02:00
protocol Initial BirdieShot import 2026-09-27 08:13:22 +02:00
scripts Initial BirdieShot import 2026-09-27 08:13:22 +02:00
server/BirdieShot.Api Initial BirdieShot import 2026-09-27 08:13:22 +02:00
tests Initial BirdieShot import 2026-09-27 08:13:22 +02:00
web/viewer Initial BirdieShot import 2026-09-27 08:13:22 +02:00
.dockerignore Initial BirdieShot import 2026-09-27 08:13:22 +02:00
.gitattributes Initial BirdieShot import 2026-09-27 08:13:22 +02:00
.gitignore Initial BirdieShot import 2026-09-27 08:13:22 +02:00
BirdieShot.sln Initial BirdieShot import 2026-09-27 08:13:22 +02:00
global.json Initial BirdieShot import 2026-09-27 08:13:22 +02:00
README.md Initial BirdieShot import 2026-09-27 08:13:22 +02:00
ROADMAP.md Initial BirdieShot import 2026-09-27 08:13:22 +02:00

BirdieShot

Private, self-hosted media sharing with end-to-end encryption.

Capture with the tools you already use, upload an encrypted file, and share a link that only the recipient's browser can decrypt.

.NET TypeScript MariaDB Docker

BirdieShot is an end-to-end encrypted alternative to traditional screenshot and media hosting. A small cross-platform uploader encrypts each capture before it leaves the device. The ASP.NET Core server stores ciphertext and operational metadata, while the TypeScript viewer decrypts media locally using a secret carried in the URL fragment.

The result is a fast sharing workflow without giving the host access to private captures, filenames, titles, tags, OCR text, or collection names.

Important

BirdieShot is a portfolio project and self-hosted application, not an independently audited cryptographic product. Review the encryption protocol and deployment configuration before using it for sensitive data.

Highlights

  • End-to-end encrypted uploads using AES-256-GCM, HKDF-SHA-256, and a fresh secret and nonce for every capture.
  • Encrypted personal library with grid and list views, local search, filters, favorites, tags, bulk actions, and recoverable trash.
  • Flexible sharing through normal encrypted links, password-protected links, encrypted collection galleries, and optional public Open Graph/Discord embeds.
  • Local-first media tools including image editing, redaction, QR generation, downloads, and self-hosted PaddleOCR—all executed after browser-side decryption.
  • Capture-tool integration for ShareX on Windows and Flameshot, Spectacle, GNOME Screenshot, grim, and maim on Linux.
  • Operational controls for upload tokens, users, quotas, retention, audit events, privacy-safe error records, backups, and storage-integrity checks.
  • Defense in depth with authenticated encryption, strict cookies, rate-limited sign-in, a restrictive Content Security Policy, and no plaintext private-media metadata on the server.

How it works

flowchart LR
    A[Capture tool] --> B[BirdieShot uploader]
    B -->|Encrypt media and metadata| C[Encrypted container]
    C -->|Ciphertext only| D[ASP.NET Core API]
    D --> E[(MariaDB + blob storage)]
    B -->|Share URL with secret in #fragment| F[Recipient]
    F --> G[Browser viewer]
    D -->|Encrypted bytes| G
    G -->|Decrypt locally with Web Crypto| H[Image, GIF, video, or file]
  1. The uploader generates a random 256-bit secret and encrypts the file locally.
  2. The API receives an opaque encrypted container and returns a random capture ID.
  3. The uploader creates a URL such as https://shot.example.com/i/abc123#secret.
  4. URL fragments are not included in HTTP requests, so the server receives the capture ID but not the decryption secret.
  5. The viewer downloads the ciphertext and decrypts it locally with the Web Crypto API.

Signed-in owners get an encrypted library without weakening that boundary. The browser creates an RSA-OAEP vault key pair; upload secrets are wrapped to the owner's public key, and the password-encrypted private key is unlocked only in the browser.

Project scope

BirdieShot owns the encrypted hosting, organization, and sharing layer. It deliberately integrates with mature capture software instead of reimplementing operating-system capture features.

In scope Out of scope
Client-side encryption and decryption Screen or audio recording
Encrypted media hosting and library management Region/window capture
Private, protected, collection, and public sharing Global hotkey management
Browser-side editing, redaction, and OCR Tray applications and desktop notifications
Upload integrations for existing capture tools Native installers and automatic client updates
Retention, quotas, backups, and administrative tooling DRM or prevention of recipient-side copying

Public embeds are an explicit exception to the private-media model: publishing creates a separate plaintext derivative so Discord and Open Graph crawlers can render it. The encrypted original is unchanged, and the public copy can be expired or revoked, but content already downloaded or cached cannot be recalled.

Technology

Layer Technology Responsibility
Uploader .NET 10 console application File/stdin input, encryption, upload, clipboard integration
API ASP.NET Core 10 minimal API Authentication, opaque blob storage, sharing, lifecycle, administration
Viewer TypeScript, Vite, Web Crypto Decryption, library UI, editing, search, OCR, galleries
Database MariaDB Accounts and non-secret operational metadata
Storage Filesystem behind IBlobStorage Encrypted originals and optional public derivatives
Delivery Docker, Apache or Caddy Container build, TLS termination, reverse proxying
Tests xUnit, Vitest, Playwright Crypto vectors, API behavior, UI logic, critical browser flows

Quick start

Prerequisites

The commands below use PowerShell. Linux users can use the equivalent shell syntax and follow the Linux integration guide for uploader installation.

1. Clone and build the viewer

From the repository root:

cd web/viewer
npm ci
npm run build
cd ../..

The Vite build writes the viewer directly to server/BirdieShot.Api/wwwroot so the API can serve the complete application.

2. Create a development database

Create a database and a dedicated local user. For example, from the MariaDB client:

CREATE DATABASE birdieshot CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'birdieshot'@'localhost' IDENTIFIED BY 'birdieshot-dev';
GRANT ALL PRIVILEGES ON birdieshot.* TO 'birdieshot'@'localhost';
FLUSH PRIVILEGES;

BirdieShot creates and updates its tables on startup. The database itself and a user with access to it must already exist.

3. Configure and run the API

Trust the ASP.NET Core development certificate, then override the machine-specific storage and database defaults for this terminal:

dotnet dev-certs https --trust

$env:ConnectionStrings__Database = "Server=localhost;Port=3306;Database=birdieshot;User ID=birdieshot;Password=birdieshot-dev;SslMode=Disabled"
$env:Storage__Path = "$PWD\.data"
$env:PublicBaseUrl = "https://localhost:7238"

dotnet run --project server/BirdieShot.Api --launch-profile https

Open https://localhost:7238 and sign in with the development-only credentials:

Username: admin
Password: birdieshot-local-development

The first sign-in creates the encrypted owner vault. Do this before the first upload. Never reuse the bundled development credentials in a deployed instance.

4. Configure and try the uploader

Create %APPDATA%\BirdieShot\config.json:

{
  "Server": "https://localhost:7238",
  "ApiToken": "local-development-token-change-me",
  "ExpirationHours": null
}

Upload a file:

dotnet run --project client/BirdieShot.Uploader -- "C:\path\to\capture.png" --clipboard

The uploader prints the encrypted sharing URL and, with --clipboard, copies it to the clipboard. Keep the complete URL—including the #fragment—because the fragment contains the decryption secret.

Uploader usage

BirdieShot.Uploader <path|-> [--name <filename>] [--clipboard] [--config <path>] [--expires <hours|never>]
Option Purpose
<path> Upload a file from disk.
- Read capture bytes from standard input.
--name <filename> Set the encrypted filename and help determine the media type for stdin input.
--clipboard Copy the resulting URL using the platform clipboard integration.
--config <path> Use a non-default uploader configuration file.
--expires <hours|never> Override retention for this upload. Accepted hours are 1–87600.

Configuration can also be supplied with BIRDIESHOT_SERVER, BIRDIESHOT_API_TOKEN, and optional BIRDIESHOT_EXPIRATION_HOURS. The server and token variables must be set together, and environment variables take precedence over the JSON file.

Publish a standalone uploader

Windows x64:

dotnet publish client/BirdieShot.Uploader -p:PublishProfile=win-x64 -o dist/BirdieShot.Uploader

Linux x64:

dotnet publish client/BirdieShot.Uploader -p:PublishProfile=linux-x64 -o "$HOME/.local/lib/birdieshot"

For ShareX, configure an external action with the published executable and arguments "$input" --clipboard, then enable Save image to file and Perform actions. See BirdieShot on Linux for stdin-based commands that avoid unencrypted temporary files where the capture tool permits it.

Docker deployment

The included Compose configuration builds the viewer and API into one container. It intentionally connects to an existing MariaDB instance instead of starting a database container.

Copy-Item docker/.env.example docker/.env

Before starting it:

  1. Replace the database password, API token, and admin password in docker/.env with long, unique values.
  2. Set BIRDIESHOT_PUBLIC_URL to the externally reachable HTTPS origin.
  3. Set storage and backup paths to durable host locations.
  4. Ensure MariaDB is reachable with the configured TLS mode and is attached to the external Docker network named shared-database.

Then run:

cd docker
docker compose up --build -d

The container listens on 127.0.0.1:3006 for a local reverse proxy. Example configurations are included for Apache and Caddy. GET /healthz returns 200 only when the database is reachable.

After deployment, sign in once to initialize the owner vault, create a named upload token from the Tokens panel, configure the uploader, and verify an encrypted round trip before placing the service into regular use.

Important environment variables

Variable Description Default
BIRDIESHOT_PUBLIC_URL Public HTTPS origin used in generated links and previews https://shot.birdie.codes
BIRDIESHOT_API_TOKEN Bootstrap uploader credential Required
BIRDIESHOT_OWNER_ID Owner shared by bootstrap admin and token owner
BIRDIESHOT_ADMIN_USERNAME Bootstrap administrator username admin
BIRDIESHOT_ADMIN_PASSWORD Bootstrap administrator password Required
BIRDIESHOT_STORAGE_PATH Host path for persistent blobs and data-protection keys M:/BirdieShot
BIRDIESHOT_BACKUP_PATH Host path containing backup snapshots M:/BirdieShot-Backups
BIRDIESHOT_MAX_BYTES Maximum encrypted upload size 26214400 (25 MiB)
BIRDIESHOT_QUOTA_BYTES Per-owner storage quota; 0 disables it 0
BIRDIESHOT_DEFAULT_EXPIRATION_DAYS Default capture retention; 0 means no expiry 0
BIRDIESHOT_ALLOWED_MEDIA_TYPES Advisory client policy such as image/*,video/mp4 Unrestricted

See docker/.env.example for the complete deployment configuration.

Note

BIRDIESHOT_ALLOWED_MEDIA_TYPES is enforced by official clients, not by the API. Because private originals are encrypted before upload, the server cannot inspect their real media type; a modified client can bypass this policy.

Security model

What the server can see

  • Random capture and share identifiers
  • Encrypted object sizes and timestamps
  • Account, quota, retention, transfer, and lifecycle data needed to operate the service
  • Explicitly published public derivatives and their public Open Graph metadata

What remains encrypted for private captures

  • Original media bytes
  • Filename and media type
  • Title, tags, favorites, and OCR text
  • Collection names, descriptions, contents, and ordering
  • Per-capture decryption secrets

Private containers use AES-256-GCM with an authenticated header. HKDF-SHA-256 derives purpose-specific keys, and malformed headers or modified ciphertext are rejected. Password-protected shares derive a local manifest key with PBKDF2-SHA-256; the password and derived key are not sent to the server.

The durable byte-level format is documented in protocol/encryption-v1.md.

Practical limitations

  • Anyone who has a complete encrypted link has its decryption secret and can save or forward the content.
  • Revocation prevents future retrieval from this server; it cannot delete copies already downloaded by a recipient.
  • Disabling public downloads is a convenience control, not DRM.
  • Traffic analysis can still reveal timing, encrypted sizes, request patterns, and the service's network location.
  • Browser extensions, a compromised client, a compromised origin, or malicious replacement JavaScript can access content after decryption.
  • The application has not undergone an external security audit.

Testing

Run the .NET and viewer unit suites:

dotnet test BirdieShot.sln

cd web/viewer
npm ci
npm test

Run the critical browser flows after installing Playwright's Chromium runtime:

cd web/viewer
npx playwright install chromium
npm run test:e2e

The test suite covers cross-platform encryption vectors, tamper rejection, owner key wrapping, authenticated library operations, quotas and retention, protected and public sharing, storage safety, and browser accessibility flows.

Backup and recovery

BirdieShot backups must keep the database and blob storage from the same point in time. The database contains encrypted owner-vault material and wrapped upload keys, so copying ciphertext alone is not a complete backup.

The Windows-oriented scripts in scripts/ support scheduled snapshots, non-destructive restore drills, and read-only storage diagnosis:

# Create a snapshot
powershell -ExecutionPolicy Bypass -File scripts/backup.ps1

# Verify the newest completed snapshot in an isolated MariaDB container
powershell -ExecutionPolicy Bypass -File scripts/verify-backup.ps1

# Compare live database records with blob storage without modifying either
powershell -ExecutionPolicy Bypass -File scripts/diagnose-storage.ps1

Review and adapt the scripts' paths and MariaDB assumptions before using them outside the original Windows/Docker deployment. A successful restore drill is the only reliable proof that a backup is usable.

Repository structure

BirdieShot/
├── client/BirdieShot.Uploader/   # Cross-platform encrypted uploader
├── server/BirdieShot.Api/        # ASP.NET Core API and compiled viewer host
├── web/viewer/                    # TypeScript/Vite viewer and owner library
├── tests/                         # xUnit integration and cryptography tests
├── protocol/                      # Versioned encrypted-container specification
├── docs/                          # Platform integration guides
├── docker/                        # Container build and Apache example
├── infrastructure/caddy/          # Caddy service and cutover tooling
└── scripts/                       # Backup, verification, and diagnostics

Further reading