Wrapped

Анонимный сервис одноразовой передачи зашифрованных данных (текст, изображения, файлы). Шифрование выполняется в браузере (Web Crypto), на сервере хранится только ciphertext. После первого успешного открытия пакет удаляется из хранилища.

Возможности

  • zero-knowledge шифрование на клиенте (AES-GCM)
  • одноразовая расшифровка — ciphertext удаляется с сервера после открытия
  • опциональный пароль: Argon2-проверка до выдачи ciphertext, лимит попыток (по умолчанию 3), пустой пароль попытку не тратит
  • подсветка синтаксиса (highlight.js), ручной выбор языка
  • вложения: drag-and-drop, выбор файлов, вставка скриншота из буфера
  • спиннер с логотипом при создании и расшифровке
  • RU / EN, светлая и тёмная тема
  • CAPTCHA: Cloudflare Turnstile и/или hCaptcha
  • админка: статистика, настройки, audit (пагинация + очистка), danger purge
  • Docker Compose и Helm; образ на Docker Hub: inecs/wrapped

Ссылка для получателя: /w/<id>#<key> или токен wrapped_v1.<id>.<key>. Ключ шифрования в URL-фрагменте (#...) на сервер не уходит.

Скриншоты

Интерфейс на русском и английском, светлая и тёмная тема.

Создание wrap

Светлая тема Тёмная тема
Создание — светлая Создание — тёмная

Создание с текстом и файлами

Текст с подсветкой синтаксиса, вложения, TTL и опциональный пароль.

Светлая тема Тёмная тема
С контентом — светлая С контентом — тёмная

Токен выдан

После создания — ссылка /w/<id>#<key> и wrapped_v1-токен. Ключ только во фрагменте URL.

Светлая тема Тёмная тема
Токен выдан — светлая Токен выдан — тёмная

Расшифровка

Одноразовое открытие: ciphertext удаляется на сервере, превью — только в текущей сессии браузера.

Светлая тема Тёмная тема
Расшифровка — светлая Расшифровка — тёмная

Быстрый старт (разработка)

Нужны Docker и Docker Compose.

make env          # создаёт .env из .env.example
make up           # app + Postgres + MinIO + nginx
Сервис URL
Приложение http://localhost:8000
Админка (статистика) http://localhost:8000/admin
MinIO Console http://localhost:9001 (wrappedminio / wrappedminio123)
Postgres (опционально с хоста) 127.0.0.1:5433
make logs         # логи app
make down         # остановить
make clean        # остановить и удалить volumes
make migrate      # alembic upgrade head (в контейнере app)

Локальный docker-compose.yml собирает образ из Dockerfile и монтирует код с --reload — режим разработки. Для сервера используйте готовый образ с Docker Hub (см. ниже).

При старте контейнер app сам выполняет alembic upgrade head (миграции, в т.ч. лимит попыток пароля).


Запуск на сервере (Docker Hub + Compose)

Образ: inecs/wrapped. На сервере не нужно собирать — достаточно docker compose pull / up.

1. Подготовка

mkdir -p /opt/wrapped && cd /opt/wrapped

Создайте .env (обязательно смените секреты):

# Приложение
APP_NAME=Wrapped
APP_ENV=production
APP_SECRET_KEY=замените-на-длинную-случайную-строку
APP_BASE_URL=https://wrapped.example.com
LOG_LEVEL=INFO
DOCS_ENABLED=false

# Админ
ADMIN_USERNAME=admin
ADMIN_PASSWORD=замените-пароль

# БД и S3 задаются в compose (см. ниже). Для внешнего Postgres/MinIO
# переопределите DATABASE_URL и S3_* здесь или в environment сервиса app.

# CAPTCHA (опционально; site keys также в админке)
TURNSTILE_SECRET_KEY=
HCAPTCHA_SECRET_KEY=

# Доверие к прокси для реального IP клиента
TRUSTED_PROXIES=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

APP_PORT=8000

2. docker-compose.yml для продакшена

services:
  proxy:
    image: nginx:1.27-alpine
    container_name: wrapped-proxy
    restart: unless-stopped
    ports:
      - "${APP_PORT:-8000}:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

  app:
    image: inecs/wrapped:0.1.0
    container_name: wrapped-app
    restart: unless-stopped
    expose:
      - "8000"
    env_file:
      - .env
    environment:
      DATABASE_URL: postgresql+asyncpg://wrapped:CHANGE_DB_PASSWORD@postgres:5432/wrapped
      S3_ENDPOINT_URL: http://minio:9000
      S3_ACCESS_KEY: CHANGE_MINIO_USER
      S3_SECRET_KEY: CHANGE_MINIO_PASSWORD
      S3_BUCKET: wrapped
      S3_USE_SSL: "false"
      S3_CREATE_BUCKET: "true"
      APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8000}
      APP_ENV: production
      DOCS_ENABLED: "false"
      TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started
      minio-init:
        condition: service_completed_successfully

  postgres:
    image: postgres:16-alpine
    container_name: wrapped-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: wrapped
      POSTGRES_PASSWORD: CHANGE_DB_PASSWORD
      POSTGRES_DB: wrapped
    volumes:
      - wrapped_pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U wrapped -d wrapped"]
      interval: 5s
      timeout: 5s
      retries: 10

  minio:
    image: minio/minio:latest
    container_name: wrapped-minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: CHANGE_MINIO_USER
      MINIO_ROOT_PASSWORD: CHANGE_MINIO_PASSWORD
    volumes:
      - wrapped_minio_data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 5

  minio-init:
    image: minio/mc:latest
    container_name: wrapped-minio-init
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio:9000 CHANGE_MINIO_USER CHANGE_MINIO_PASSWORD &&
      mc mb --ignore-existing local/wrapped &&
      exit 0
      "

volumes:
  wrapped_pg_data:
  wrapped_minio_data:

Рядом — nginx.conf (пример также в репозитории: deploy/nginx.conf):

server {
    listen 80;
    server_name _;

    client_max_body_size 64m;

    location / {
        proxy_pass http://app:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_read_timeout 120s;
    }
}

3. Запуск и обновление

docker compose pull
docker compose up -d
curl -fsS http://127.0.0.1:8000/health
# новая версия образа, например 0.2.0
docker compose pull app
docker compose up -d app

HTTPS лучше завернуть снаружи (Caddy, Traefik, nginx на хосте, Cloudflare Tunnel). В APP_BASE_URL укажите публичный https://....

Внешний Postgres и S3/MinIO

Уберите сервисы postgres, minio, minio-init и задайте:

Переменная Пример
DATABASE_URL postgresql+asyncpg://user:pass@db-host:5432/wrapped
S3_ENDPOINT_URL https://s3.example.com
S3_ACCESS_KEY / S3_SECRET_KEY ключи
S3_BUCKET wrapped
S3_USE_SSL true
S3_CREATE_BUCKET false (бакет уже есть)
S3_REGION us-east-1

Минимальный compose — только app (+ опционально proxy):

services:
  app:
    image: inecs/wrapped:0.1.0
    restart: unless-stopped
    ports:
      - "8000:8000"
    env_file:
      - .env
    environment:
      APP_ENV: production
      DOCS_ENABLED: "false"

Переменные окружения

Переменная Описание По умолчанию
APP_NAME Имя сервиса Wrapped
APP_ENV development / production development
APP_SECRET_KEY Секрет сессий — смените
APP_BASE_URL Публичный URL http://localhost:8000
DOCS_ENABLED /docs, /redoc, /openapi.json в prod — false
LOG_LEVEL Уровень логов INFO
ADMIN_USERNAME Логин админки admin
ADMIN_PASSWORD Пароль админки — смените
DATABASE_URL Postgres (asyncpg)
S3_ENDPOINT_URL Endpoint MinIO/S3
S3_ACCESS_KEY / S3_SECRET_KEY Ключи S3
S3_BUCKET Имя бакета wrapped
S3_REGION Регион us-east-1
S3_USE_SSL TLS к S3 false
S3_CREATE_BUCKET Создавать бакет при старте true
TURNSTILE_SECRET_KEY Секрет Turnstile пусто
HCAPTCHA_SECRET_KEY Секрет hCaptcha пусто
TRUSTED_PROXIES IP/CIDR прокси для X-Forwarded-* loopback + RFC1918

Лимиты загрузки, TTL, MIME, CAPTCHA, режим пароля и число попыток пароля задаются в админке (таблица app_settings), не через env. Полный шаблон env: .env.example.


Модель безопасности

  • Zero-knowledge. Шифрование AES-GCM в браузере. Сервер видит только ciphertext и метаданные (TTL, MIME, размер).
  • Одноразовая расшифровка. После успешного открытия объект удаляется из S3/MinIO, статус → consumed. Превью остаётся только в сессии браузера.
  • Пароль. При создании с паролем сервер сохраняет Argon2-хеш и проверяет его до выдачи ciphertext.
    • пустой пароль → password_required, попытка не списывается;
    • неверный пароль → bad_password, UI показывает оставшиеся попытки;
    • лимит: password_max_attempts в админке (по умолчанию 3); при исчерпании wrap уничтожается (password_locked);
    • в client_only пароль также участвует в шифровании на клиенте.
  • CAPTCHA. Включается в админке; секреты — через env, site keys — в UI.
  • IP в audit. Реальный IP — только из заголовков от TRUSTED_PROXIES.

Ключ в #fragment не уходит на сервер в запросе страницы.


Админка

Логин: /admin/login. После входа — Статистика (/admin/stats).

Раздел URL Что делает
Статистика /admin/stats MinIO: объём и число объектов нерасшифрованного ciphertext; расшифровано / pending; загрузки и items за всё время; таблица по статусам wraps; с паролем; создано за 24ч/7д; счётчики audit (create/unwrap ok/fail)
Настройки /admin/settings Limits (upload, TTL, retention audit), rate limits, MIME allowlist, пароль (режим + лимит попыток), CAPTCHA
Аудит /admin/audit Фильтры, пагинация (10/25/50/100), номера страниц, кнопка Очистить (с подтверждением; пишется событие admin.audit_clear)
Опасная зона /admin/danger Полная очистка wraps и объектов в MinIO (PURGE)

JSON Admin API: HTTP Basic (ADMIN_USERNAME / ADMIN_PASSWORD), тег OpenAPI Admin.


API

Тот же ZK-протокол, что в UI: шифруете локально, затем:

Метод Путь Назначение
GET /api/v1/settings Публичные лимиты, CAPTCHA, режим пароля, password_max_attempts
POST /api/v1/wraps Загрузить ciphertext + метаданные
POST /api/v1/wraps/{id}/unwrap Одноразово получить ciphertext
GET /health Healthcheck

Серверного «зашифруй за меня» нет.

Ошибки пароля при unwrap — 403:

detail.code Когда
password_required пароль не введён (попытка не списывается)
bad_password неверный пароль (attempts_remaining / attempts_max)
password_locked попытки исчерпаны, wrap уничтожен

Пример:

{
  "detail": {
    "code": "bad_password",
    "attempts_remaining": 2,
    "attempts_max": 3
  }
}

При DOCS_ENABLED=true — Swagger /docs и OpenAPI /openapi.json.


Сборка образа и git

Docker Hub (make release)

docker login   # пользователь inecs

make release IMAGE_TAG=0.1.0              # build + push inecs/wrapped:0.1.0
make release IMAGE_TAG=0.1.0 PUSH=0       # только локальный тег

Эквивалент:

docker build -t inecs/wrapped:0.1.0 .
docker push inecs/wrapped:0.1.0

Образ при старте: alembic upgrade head + uvicorn :8000. Healthcheck: GET /health.

Git (make push)

Как в proxmox_api_simulator:

make push
  1. git add .
  2. ввод сообщения коммита (можно несколько строк)
  3. Ctrl-D — конец ввода
  4. git push origin HEAD

Kubernetes (Helm)

helm upgrade --install wrapped ./helm/wrapped \
  -f my-values.yaml
image:
  repository: inecs/wrapped
  tag: "0.1.0"
  pullPolicy: IfNotPresent

app:
  env: production
  baseUrl: "https://wrapped.example.com"
  secretKey: "..."
  adminUsername: admin
  adminPassword: "..."
  docsEnabled: false
  trustedProxies: "*"   # если до пода достучаться может только Ingress

external:
  databaseUrl: "postgresql+asyncpg://..."
  s3:
    endpointUrl: "https://..."
    accessKey: "..."
    secretKey: "..."
    bucket: wrapped
    createBucket: false

Упаковка чарта: make helm-package IMAGE_TAG=0.1.0.


Make-цели

Цель Описание
make env .env из .env.example
make up / make down / make clean Dev-стек
make logs / make ps / make restart Логи, статус, рестарт app
make migrate alembic upgrade head
make revision m="msg" Новая alembic-ревизия
make release Образ на Docker Hub (+ Helm package)
make push git add → многострочный commit (Ctrl-D) → push
make helm-lint / make helm-package Helm

Требования

  • Python 3.12+ (без Docker)
  • PostgreSQL 16+
  • S3-совместимое хранилище (MinIO и т.п.)
  • Docker Compose v2

Лицензия

Proprietary / на ваше усмотрение.

S
Description
Анонимный сервис одноразовой передачи зашифрованных данных (текст, изображения, файлы). Шифрование выполняется в браузере (Web Crypto), на сервере хранится только ciphertext. После первого успешного открытия пакет удаляется из хранилища.
Readme 789 KiB
Languages
Python 35.1%
JavaScript 28.9%
HTML 17.5%
CSS 15.9%
Makefile 1.8%
Other 0.8%