/
docs

Documentación de la API

easysecret.dev es una API REST para guardar y compartir secretos cifrados de extremo a extremo. El servidor almacena únicamente datos ya cifrados — nunca la clave de descifrado. Cada secreto se destruye en el momento de su primera lectura.

Base URL
https://easysecret.dev
Entorno de desarrollo: los ejemplos de esta documentación usan http://localhost:3001. Sustituye por la URL de producción antes de desplegar.

Autenticación

Los endpoints de secretos aceptan peticiones sin autenticar (plan Free: 10 req/día · 300/mes por IP) o con una API key en la cabecera Authorization (planes con cuota mensual).

Cabecera para peticiones autenticadas
Authorization: Bearer es_live_aBcDeFgHiJkLmNoPqRsTuVwX
La API key se muestra una sola vez al crear la cuenta mediante POST /api/signup. Si la pierdes, contacta con soporte — no existe endpoint de recuperación.

Formato de la key

Las keys tienen el prefijo es_live_ seguido de 32 caracteres en base64url. El servidor las almacena en claro en la base de datos local SQLite.

Planes y límites

El plan determina la cuota mensual de peticiones y el TTL máximo permitido para los secretos. Las peticiones sin API key se cuentan por IP con una ventana diaria.

Plan Autenticación Peticiones TTL máximo
Free (sin key) Por IP 10/día · 300/mes 24 h
Basic (con key) Bearer token 3.000 / mes 24 h
Pro (con key) Bearer token 50.000 / mes 30 días
Los contadores de uso se almacenan en Redis con TTL de 30 días (planes con key) o 24 horas (Hobby por IP).

Rate Limiting

El endpoint de signup tiene un límite propio por IP para frenar altas masivas. El resto de endpoints usan los límites del plan descrito arriba.

RutaLímiteVentana
POST /api/signup 5 peticiones 1 hora por IP
Endpoints de secretos (sin key, diario) 10 peticiones 24 h por IP
Endpoints de secretos (sin key, mensual) 300 peticiones 30 días por IP
Endpoints de secretos (con key) Según plan 30 días por key

Al superar el límite el servidor devuelve 429 con el mensaje de error correspondiente.

Errores

Todos los errores devuelven JSON con el campo error y el código HTTP correspondiente.

Formato de error
{ "error": "descripción del error" }
400Cuerpo JSON inválido o campos obligatorios ausentes.
401API key ausente o inválida.
404Secreto no existe o ya fue leído/caducó.
409Email ya registrado.
429Rate limit superado (Free, cuota mensual o límite de signup).
5xxError del servidor. Reintenta con backoff exponencial.
POST /api/signup

Crea una cuenta nueva y devuelve la API key. No requiere autenticación. La key se muestra una única vez en la respuesta — guárdala antes de cerrar.

Cuerpo de la petición

CampoTipoDescripción
email string requerido Dirección de correo. Debe ser única en el sistema.

Ejemplo

curl
curl -X POST https://easysecret.dev/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"tu@ejemplo.com"}'
Response · 201 Created
{
  "email":   "tu@ejemplo.com",
  "api_key": "es_live_aBcDeFgHiJkLmNoPqRsTuVwX",  // ⚠ solo aquí
  "plan":    "basic",
  "message": "Guarda tu api_key, no se mostrará de nuevo."
}

Respuestas de error

400Email inválido o ausente.
409Ya existe una cuenta con ese email.
4295 intentos de signup en la última hora desde esta IP.
POST /api/secret

Almacena un secreto ya cifrado en Redis con un TTL. El servidor guarda el texto cifrado y el IV — nunca la clave de descifrado, que el cliente pasa solo en el fragmento de URL (#).

Autenticación

Opcional. Sin Authorization el límite del plan Free es 10 peticiones/día · 300/mes por IP. Con API key se usa la cuota del plan.

Cuerpo de la petición

CampoTipoDescripción
ivBase64 string requerido Vector de inicialización AES-GCM codificado en base64 (12 bytes → 16 chars).
cipherBase64 string requerido Texto cifrado codificado en base64. El servidor lo almacena opaco.
ttl number opcional Segundos de vida. Máximo: 86400 (free) / 2592000 (pro). Por defecto 86400.

Ejemplo — cifrar en el navegador y guardar

JavaScript — cifrar con Web Crypto API
// 1. Generar clave e IV aleatorios
const iv  = crypto.getRandomValues(new Uint8Array(12));
const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]
);

// 2. Cifrar
const encoded = new TextEncoder().encode("mi secreto");
const cipher  = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);

// 3. Exportar clave (para el fragmento #) y IV (para la API)
const rawKey     = await crypto.subtle.exportKey("raw", key);
const ivBase64   = btoa(String.fromCharCode(...iv));
const cipherB64  = btoa(String.fromCharCode(...new Uint8Array(cipher)));
const keyBase64  = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
                     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

// 4. Guardar en la API
const res  = await fetch("/api/secret", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ivBase64, cipherBase64: cipherB64 })
});
const { id } = await res.json();

// 5. Construir enlace — la clave NUNCA va al servidor
const link = `https://easysecret.dev/ver/${id}#${keyBase64}`;
curl (con API key)
curl -X POST https://easysecret.dev/api/secret \
  -H "Authorization: Bearer es_live_aBcDeFgHiJkLmNoPqRsTuVwX" \
  -H "Content-Type: application/json" \
  -d '{"ivBase64":"aXZfdGVzdA==","cipherBase64":"Y2lwaGVy","ttl":3600}'
Response · 200 OK
{
  "id":                 "550e8400-e29b-41d4-a716-446655440000",
  "expires_in_seconds": 3600
}

Respuestas de error

400Faltan ivBase64 o cipherBase64.
401API key inválida.
429Límite de peticiones superado.
GET /api/secret/:id

Lee y destruye atómicamente el secreto. Tras esta llamada el secreto desaparece de Redis para siempre — no se puede volver a leer. El descifrado ocurre en el cliente usando la clave que va en el fragmento # de la URL.

Parámetros de ruta

ParámetroTipoDescripción
id string (UUID) requerido UUID del secreto, devuelto por POST /api/secret.

Ejemplo — leer y descifrar en el navegador

curl
curl https://easysecret.dev/api/secret/550e8400-e29b-41d4-a716-446655440000
Response · 200 OK
{
  "ivBase64":     "aXZfdGVzdA==",
  "cipherBase64": "Y2lwaGVy..."
}
JavaScript — descifrar tras leer
// La clave viene del fragmento # de la URL (nunca del servidor)
const keyB64UrlSafe = window.location.hash.substring(1);
let keyB64 = keyB64UrlSafe.replace(/-/g, '+').replace(/_/g, '/');
while (keyB64.length % 4) keyB64 += '=';

const base64ToBuf = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0)).buffer;

const key = await crypto.subtle.importKey(
  "raw", base64ToBuf(keyB64), { name: "AES-GCM" }, false, ["decrypt"]
);

const res  = await fetch(`/api/secret/${secretId}`);
const data = await res.json();

const decrypted = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv: base64ToBuf(data.ivBase64) },
  key,
  base64ToBuf(data.cipherBase64)
);
const plainText = new TextDecoder().decode(decrypted);

Respuestas de error

404El secreto no existe, ya fue leído o caducó su TTL.

Zero-Knowledge

El diseño de easysecret garantiza que el servidor nunca puede acceder al contenido de un secreto, incluso con acceso completo a la base de datos y a Redis.

Flujo completo:
1. El cliente genera una clave AES-GCM aleatoria en el navegador.
2. Cifra el texto con esa clave. Solo el texto cifrado y el IV van al servidor.
3. La clave se codifica en base64url y se añade al fragmento de URL (#).
4. El fragmento nunca se envía al servidor en ninguna petición HTTP.
5. El destinatario abre la URL completa: su navegador descifra localmente con la clave del fragmento.

Esto significa que incluso si el servidor fuera comprometido, el atacante solo obtendrá texto cifrado sin la clave — ilegible sin el fragmento de URL.

TTL y caducidad

Cada secreto tiene un tiempo de vida (TTL) configurable. Transcurrido ese tiempo Redis expira la entrada automáticamente y el secreto desaparece sin necesidad de limpiezas manuales.

PlanTTL máximoTTL por defecto
Free / Basic 86.400 s (24 h) 86.400 s
Pro 2.592.000 s (30 días) 86.400 s

Si pasas un ttl mayor al máximo del plan, el servidor lo trunca silenciosamente al máximo permitido. El campo expires_in_seconds de la respuesta indica el TTL que se aplicó realmente.

Destrucción atómica

La lectura del secreto usa la operación atómica GETDEL de Redis, que lee y borra en una sola instrucción. Esto garantiza que no hay ventana de tiempo entre la lectura y la eliminación — incluso bajo carga alta.

Garantía: si dos clientes abren el enlace simultáneamente, exactamente uno recibirá el secreto y el otro recibirá 404. No es posible que ambos lo lean.

Una vez que GET /api/secret/:id devuelve 200, el secreto ya no existe en Redis. Reintentar la petición devolverá 404.