Entwickler-API
Geheimnisse aus dem Code teilen.
Für Skripte, CI/CD-Pipelines und interne Tools: dieselbe Verschlüsselung wie im Browser, als REST-API und als SDK für JavaScript/TypeScript und Python, jeweils mit Kommandozeile.
Schnellstart
Die SDKs verschlüsseln lokal. Beim Server kommt nur Ciphertext an, der Schlüssel bleibt im Link.
import { OtpClient } from 'otp-giar';
const otp = new OtpClient({ baseUrl: 'https://otp.giar.digital', apiKey: process.env.OTP_API_KEY });
// Verschlüsselt lokal, der Server bekommt nur den Ciphertext.
const { link, deleteToken } = await otp.create('DB_PASSWORD=hunter2', {
expiresIn: 3600, // Sekunden, maximal 7 Tage
maxViews: 1,
password: 'optional'
});
// Öffnen (verbraucht eine Öffnung) und lokal entschlüsseln.
const { secret, viewsRemaining } = await otp.reveal(link, { password: 'optional' });- npm
- npm install otp-giar
- pip
- pip install otp-giar
Endpunkte
Basis-URL https://otp.giar.digital. JSON rein, JSON raus. CORS ist für alle Ursprünge offen, die API nutzt keine
Cookies.
- POST /api/v1/secrets Geheimnis anlegen
- GET /api/v1/secrets/{id} Metadaten lesen, verbraucht keine Öffnung
- POST /api/v1/secrets/{id}/reveal Öffnen, verbraucht eine Öffnung
- DELETE /api/v1/secrets/{id} Vorzeitig löschen
- GET /api/v1/openapi.json OpenAPI-3.1-Spezifikation
POST/api/v1/secrets
Zwei Varianten, je nachdem was im Body steht:
- Ende-zu-Ende (SDKs, Web-App): der Client verschlüsselt selbst und schickt
ciphertext,iv,salt,kdfundauthToken. Den Link baut er ausurl+#+ Schlüssel. - Klartext (schnell mit cURL):
secretund optionalpassword. Der Server verschlüsselt im Arbeitsspeicher, speichert nur den Ciphertext und gibtkeyundlinkgenau einmal zurück.
| Feld | Bedeutung |
|---|---|
| expiresIn | Lebensdauer in Sekunden, 60 bis 604800 (7 Tage). Standard 86400. |
| maxViews | Wie oft es geöffnet werden kann, 1 bis 100. Standard 1. |
| secret | Klartext-Modus: der Text, maximal 64 KB. |
| password | Klartext-Modus: optionales Passwort zum Öffnen. |
{
"version": 1,
"ciphertext": "<base64url>",
"iv": "<base64url, 12 Bytes>",
"salt": "<base64url, 16 Bytes>",
"kdf": null, // oder { "name": "pbkdf2-sha256", "iterations": 600000 }
"authToken": "<base64url, 32 Bytes>",
"expiresIn": 3600,
"maxViews": 1
}{
"id": "vN4MP8Hdk0rjKY06fWMxVVZDA-OPAkLI",
"url": "https://otp.giar.digital/s/vN4MP8Hdk0rjKY06fWMxVVZDA-OPAkLI",
"expiresAt": "2026-09-25T14:32:00.000Z",
"maxViews": 1,
"deleteToken": "lnNzBFYmszeV1pnADnKpmYuSVKJ6IIZTG2GZI3P3yxg",
// nur im Klartext-Modus:
"key": "hbEP-kstB6NdtzT_E86eOgH5iOt2oM1m-vedLUx469I",
"link": "https://otp.giar.digital/s/vN4MP8Hdk0rjKY06fWMxVVZDA-OPAkLI#hbEP-kstB6N…"
}Im Klartext-Modus sieht der Server das Geheimnis kurz im Arbeitsspeicher. Wenn das nicht in Frage kommt, nimm ein SDK oder die CLI.
GET/api/v1/secrets/{id}
Liefert expiresAt, maxViews, viewsRemaining, passwordRequired sowie salt und kdf, die ein Client zum Ableiten des authToken braucht. Verbraucht keine Öffnung.
POST/api/v1/secrets/{id}/reveal
Öffnet das Geheimnis und zieht eine Öffnung ab. Bei der letzten Öffnung wird es sofort gelöscht.
{ "authToken": "…" }liefertciphertext,iv,salt,kdf. Der Client entschlüsselt selbst.{ "key": "…", "password": "…" }liefertsecretim Klartext.
Ein falsches Passwort verbraucht keine Öffnung, zählt aber als Fehlversuch. Nach 10 Fehlversuchen wird das Geheimnis gelöscht.
DELETE/api/v1/secrets/{id}
Löscht ein Geheimnis vor Ablauf. Erwartet den deleteToken aus der Antwort beim Anlegen im Header X-Delete-Token. Antwortet mit 204.
GET/api/v1/openapi.json
Maschinenlesbare Beschreibung für Postman, Insomnia oder Codegeneratoren: openapi.json
Verschlüsselung
Jedes Geheimnis bekommt einen eigenen Schlüssel. Browser schicken den Teil nach dem # nie an einen
Server, deshalb kennt die Instanz den Schlüssel nicht.
key = 32 Zufallsbytes → steht nur im Link nach dem #
salt, iv = 16 / 12 Zufallsbytes
ikm = key ohne Passwort
= key ‖ PBKDF2-SHA256(Passwort, salt, 600 000)
encKey = HKDF-SHA256(ikm, salt, "otp-giar:v1:enc")
authToken = HKDF-SHA256(ikm, salt, "otp-giar:v1:auth")
ciphertext = AES-256-GCM(encKey, iv, Text, aad = "otp-giar:v1")- Gespeichert werden nur Ciphertext,
iv,saltund SHA-256 desauthToken. Wer nur die ID kennt, kann das Geheimnis weder lesen noch eine Öffnung verbrauchen. - IDs haben 192 Bit, Schlüssel 256 Bit, beides aus dem Zufallsgenerator des Betriebssystems. Raten ist aussichtslos; zusätzlich gibt es Rate-Limits pro IP.
- Abgelaufene und verbrauchte Geheimnisse werden sofort gelöscht, SQLite überschreibt freie Seiten.
API-Keys und Limits
Ohne Key funktioniert alles, aber mit engeren Limits. Ein Key wird als Authorization: Bearer ogk_… mitgeschickt. Keys vergibt, wer die Instanz betreibt: npm run apikey -- create "GitHub Actions".
| Aktion | Ohne Key | Mit Key |
|---|---|---|
| Anlegen | 20 pro 10 Min. und IP | 1000 pro 10 Min. |
| Öffnen | 30 pro 10 Min. und IP | gleich |
| Grösse | 64 KB Text | |
| Lebensdauer | 1 Minute bis 7 Tage | |
| Öffnungen | 1 bis 100 | |
Fehler
Immer im Format { "error": { "code": "…", "message": "…" } }, teils mit Zusatzfeldern.
| Status | code | Bedeutung |
|---|---|---|
| 400 | invalid_request | Feld fehlt oder ist ungültig, siehe field |
| 400 | password_required | Geheimnis ist passwortgeschützt, password fehlt |
| 401 | invalid_api_key | API-Key unbekannt oder widerrufen |
| 401 | invalid_credentials | Passwort oder Schlüssel falsch, siehe attemptsRemaining |
| 403 | invalid_delete_token | Lösch-Token passt nicht |
| 404 | not_found | Existiert nicht, abgelaufen oder schon geöffnet |
| 410 | destroyed | Nach 10 Fehlversuchen gelöscht |
| 413 | secret_too_large | Mehr als 64 KB |
| 415 | unsupported_media_type | Body muss application/json sein |
| 429 | rate_limited | Zu viele Anfragen, siehe Retry-After |
Beispiel: GitHub Actions
Ein Pipeline-Schritt, der ein Staging-Passwort als Einmal-Link in Slack postet, statt es im Klartext zu verschicken.
- name: Staging-Zugang an QA schicken
env:
OTP_BASE_URL: https://otp.giar.digital
OTP_API_KEY: ${{ secrets.OTP_API_KEY }}
STAGING_PASSWORD: ${{ secrets.STAGING_PASSWORD }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
LINK=$(printf '%s' "$STAGING_PASSWORD" | npx -y otp-giar create --ttl 1d --views 1)
echo "::add-mask::$LINK"
curl -s -X POST "$SLACK_WEBHOOK" -H 'Content-Type: application/json' \
-d "{\"text\": \"Staging-Zugang (einmal lesbar): $LINK\"}"