# secret.hackallcode.ru

> One-time secret sharing between humans and AI agents. Use it instead of pasting passwords, tokens or keys into chat, logs, tickets or commit history. A link works once: reading the secret deletes it. Unopened secrets expire after the chosen TTL (max 7 days).

Сервис одноразовых секретов: агент может и передать секрет человеку, и получить секрет от человека.

A link looks like `https://secret.hackallcode.ru/s/<id>#<key>`: `<id>` identifies the stored ciphertext, `<key>` (after `#`) is the AES-256-GCM key, base64url without padding. Browsers never send the part after `#` to the server.

## Send a secret to a human

Never print the secret itself in your reply; reply with the link only and tell the human that it works once and when it expires.

All sending goes to one endpoint: `POST https://secret.hackallcode.ru/api/secrets`.

### Option A (preferred): encrypt locally, the server never sees the secret

Response `201`: `{"id": "<id>", "expiresAt": <unix seconds>}`; build the link yourself.

```python
import base64, json, os, urllib.request
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def b64(data):
    return base64.urlsafe_b64encode(data).decode().rstrip("=")

def share(secret, ttl=86400):
    key = AESGCM.generate_key(bit_length=256)
    iv = os.urandom(12)
    body = json.dumps({
        "ciphertext": b64(AESGCM(key).encrypt(iv, secret.encode(), None)),
        "iv": b64(iv),
        "ttl": ttl,
    }).encode()
    req = urllib.request.Request(
        "https://secret.hackallcode.ru/api/secrets", body, {"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
    return f"https://secret.hackallcode.ru/s/{data['id']}#{b64(key)}"
```

Format: AES-256-GCM, 12-byte IV, ciphertext includes the 16-byte tag, all values base64url without padding.

### Option B (simple): send plain text, the server encrypts

Use when you cannot run crypto code. The secret travels over TLS and is encrypted on the server immediately; the server does not store the key, only the returned link contains it.

```bash
curl -s https://secret.hackallcode.ru/api/secrets \
  -d '{"secret": "the text", "ttl": 86400}'
```

Response `201`: `{"id": "<id>", "expiresAt": <unix seconds>, "url": "https://secret.hackallcode.ru/s/<id>#<key>"}`

## Receive a secret from a human

When a human gives you a link, read it through the API, not by rendering the page. Reading deletes the secret, so read it exactly once, keep the value in memory only for the task, and never echo it back in chat or logs.

Errors: `404` already opened or expired, `400` the key does not fit (the secret is kept).

### Option A (preferred): decrypt locally, the server never sees the key

Call `POST https://secret.hackallcode.ru/api/secrets/<id>/open` without a body; response `200`: `{"ciphertext": "...", "iv": "..."}`. The secret is deleted on the server at this moment.

```python
import base64, json, urllib.request
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def unb64(text):
    return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))

def receive(link):
    path, key = link.split("#", 1)
    secret_id = path.rstrip("/").rsplit("/", 1)[1]
    req = urllib.request.Request(
        f"https://secret.hackallcode.ru/api/secrets/{secret_id}/open", method="POST",
    )
    with urllib.request.urlopen(req) as res:
        data = json.load(res)
    return AESGCM(unb64(key)).decrypt(unb64(data["iv"]), unb64(data["ciphertext"]), None).decode()
```

### Option B (simple): POST to the link, send the key in the body

Split the link at `#`: POST to the part before it and send the part after it as the request body. The server sees the key only during this request; if the key does not fit, the secret is not deleted.

```bash
curl -s -X POST https://secret.hackallcode.ru/s/<id> -d '<key>'
```

Response `200`: `{"secret": "the text"}`. A POST without a key returns `400` and does not delete anything. The body may also be JSON `{"key": "<key>"}`.

## Limits

- `ttl`: 3600 (1 hour), 86400 (1 day, default), 259200 (3 days) or 604800 (7 days).
- Text only, up to 10000 characters.
- Up to 100 secrets per day from one IP (IPv6 counted per /64); bursts are rate limited.
- Errors are JSON `{"error": "..."}`: 400 bad input, 404 not found or already opened, 413 too large, 429 limit reached, 503 storage full.

## Do not

- Do not open a link you created yourself: opening burns the secret.
- Do not put secrets or links into public places (issues, public chats, logs).
- Do not reuse a link; create a new one per recipient.
