- JavaScript 97.8%
- TypeScript 1%
- C# 0.8%
- CSS 0.3%
- PowerShell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| client/BirdieShot.Uploader | ||
| docker | ||
| docs | ||
| infrastructure | ||
| protocol | ||
| scripts | ||
| server/BirdieShot.Api | ||
| tests | ||
| web/viewer | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| BirdieShot.sln | ||
| global.json | ||
| README.md | ||
| ROADMAP.md | ||
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.
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, andmaimon 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]
- The uploader generates a random 256-bit secret and encrypts the file locally.
- The API receives an opaque encrypted container and returns a random capture ID.
- The uploader creates a URL such as
https://shot.example.com/i/abc123#secret. - URL fragments are not included in HTTP requests, so the server receives the capture ID but not the decryption secret.
- 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
- .NET 10 SDK
- Node.js 22 or newer
- MariaDB 10.11 or newer
- Git
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:
- Replace the database password, API token, and admin password in
docker/.envwith long, unique values. - Set
BIRDIESHOT_PUBLIC_URLto the externally reachable HTTPS origin. - Set storage and backup paths to durable host locations.
- 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_TYPESis 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