Capítulo 8

Mensajes privados

Sobres dentro de sobres: cómo NIP-17 oculta quién habla con quién.

¡Psst! Acércate. Este capítulo va de secretos, así que traje sobres. Muchos sobres.

Todo lo que has visto hasta ahora es público a propósito: notas, likes, listas de seguidos. Pero a veces Alice solo quiere decirle a Bob dónde verse. Nostr no tiene servidores privados donde esconderse. Cada mensaje directo sigue siendo un evento guardado en un relay que cualquiera podría leer. Así que la privacidad tiene que vivir dentro del propio evento.

Es más difícil de lo que parece, porque un secreto tiene dos partes:

  • Qué dijiste: las palabras.
  • Todo lo que lo rodea: quién lo envió, para quién es, cuándo y más o menos cuánto ocupaba.

Esa segunda parte se llama metadatos, y puede revelar lo mismo o más. Si un relay ve que Alice escribe a un abogado de divorcios a las 2 de la madrugada todas las noches, no necesita leer ni una sola palabra.

Tres generaciones de notas secretas

Los mensajes privados de Nostr evolucionaron en tres pasos. Piénsalos así:

  1. NIP-04: una postal con un candado sobre el texto. Cualquiera puede seguir leyendo la dirección, el remitente y el matasellos.
  2. NIP-44: un candado mucho mejor. Sigue siendo una postal.
  3. NIP-17: la postal va dentro de un sobre firmado, y ese sobre va dentro de un paquete marrón sin marcas enviado por un desconocido.

Aquí tienes el mismo mensaje enviado de las tres formas, con cifrado real ejecutándose en tu navegador. Fíjate en lo que puede ver el relay y luego intenta abrir cada uno como Bob, como Carol la entrometida y como el propio relay.

El laboratorio de sobres

Alice le envía a Bob el mismo mensaje de tres formas. Compara lo que puede leer un relay y luego intenta abrir cada sobre con distintas claves.

¿Cómo debería enviarlo Alice?

Lo que ve el relay

Esto es todo lo que hay por fuera del evento, legible por cualquier relay o fisgón.

5 datos de metadatos se filtran a la vista de todos.

  • Remitente Filtrado Firmado por Alice: todo el mundo sabe quién lo escribió.
  • Destinatario Filtrado La etiqueta p nombra a Bob, para que el relay pueda entregarlo.
  • Hora de envío Filtrado Momento exacto: 3 oct 2026, 19:56.
  • Tipo de mensaje Filtrado Kind 4: grita «¡esto es un DM!».
  • Tamaño del mensaje Filtrado 64 bytes de texto cifrado para un mensaje de 61 bytes: el tamaño lo delata.

Intenta abrirlo

¿De quién es la clave que probamos?
  1. DM cifrado kind 4 Cerrado

    Remitente, destinatario y hora van por fuera.

    JSON
      { "kind": 4 ,"created_at": 1791057408 , "tags": [   [  ]  ] ,"content": "AxiMAWecYqFbohrMMvszmtmkNoZeqhAGnoVKln7a4I/LpCN7vdKRMjLKbFp+ofHM9bS9AkbhCU+Hb8vHonvIaQ==?iv=CWwoYdZwDhfv2EZQzNlV/w==" ,"pubkey": "e550c6e92808f358382b7b8c14345344f5ef9a299529834d1c26d57b6ace44cc" ,"id": "4074679fdba6a17f0d26fb64c9d1f5f63c43b961029943709e692cfc58c4a0b7" ,"sig": "fbda8485aec78662b3348fcbec3bf08e25c3d73b6ae80cc6fe32575270cac75fcb6009df2aae5e0330f717b1f27f4e8f660f7dd452112e0b196db26f3cc8a9dd"  } 

Aleatoriedad nueva: nonce nuevo, marcas de tiempo nuevas y, en NIP-17, una clave desechable recién creada.

Cosas que puedes probar:

  • Compara las etiquetas rojas en la vista del relay al pasar de NIP-04 a NIP-17.
  • Elige a Carol e intenta abrir el sobre. Su clave es perfectamente válida, solo que no es la correcta.
  • En NIP-17, presiona Enviar de nuevo varias veces y mira cómo cambia el remitente exterior cada vez.

Paso 1: NIP-04, la postal con fugas

NIP-04 fue el primer intento de Nostr. Alice y Bob combinan cada uno su propia clave secreta con la clave pública del otro y llegan al mismo secreto compartido sin enviarlo nunca. Ese truco se llama ECDH y es la base de todos los esquemas de este capítulo.

Después Alice revuelve su texto con ese secreto y publica un evento kind 4. Las palabras quedan ocultas, pero mira lo que queda por fuera:

  • pubkey: la clave pública real de Alice.
  • una etiqueta p: la clave pública de Bob, para que el relay sepa a quién entregarlo.
  • created_at: el segundo exacto en que presionó enviar.
  • la longitud del texto cifrado, que sigue la longitud del mensaje con un margen de 16 bytes.

Cualquiera puede dibujar un mapa de quién habla con quién, cuándo y cuánto. Por eso NIP-04 está oficialmente obsoleto: la propia especificación advierte que filtra metadatos y que está muy lejos del estado del arte.

Clave equivocada, sin alarma

NIP-04 no tiene comprobación de integridad. Si Carol prueba su clave, normalmente obtiene un error, pero a veces obtiene basura aleatoria que parece un descifrado correcto. Un buen esquema siempre debería decir “esto no es para ti”.

Bajo el capó: un evento NIP-04 Mostrar detalles

Un evento kind 4. Solo content va cifrado; todo lo demás es público:

{
  "kind": 4,
  "pubkey": "<pubkey de alice>",
  "created_at": 1735689600,
  "tags": [["p", "<pubkey de bob>"]],
  "content": "J7d9…texto cifrado en base64…==?iv=AQEBAQEBAQEBAQEBAQEBAQ==",
  "id": "…",
  "sig": "…"
}
  • Clave: la coordenada x en bruto del punto compartido ECDH, usada directamente como clave AES-256 (sin hash ni derivación de claves).
  • Cifrado: AES-256-CBC con un IV aleatorio de 16 bytes y sin MAC, así que las manipulaciones y las claves equivocadas no se detectan de forma fiable.
  • Formato: base64(ciphertext) + "?iv=" + base64(iv).

Especificación: NIP-04 (obsoleto en favor de NIP-17).

import * as nip04 from "nostr-tools/nip04";

// Solo para lo antiguo: lee DMs viejos, no envíes nuevos.
const content = nip04.encrypt(aliceSecretKey, bobPubkey, "Nos vemos a las 9");
const text = nip04.decrypt(bobSecretKey, alicePubkey, content);

Paso 2: NIP-44, un candado de confianza

NIP-44 sustituye el candado por un diseño moderno y auditado. No define ningún tipo de mensaje: es solo una forma de convertir texto en un bloque sellado que solo tú y la otra persona pueden abrir. Importan tres mejoras:

  • A prueba de manipulaciones: cada bloque lleva un código de autenticación. Cambia un solo bit, o usa la clave equivocada, y el descifrado se niega rotundamente en vez de devolverte basura.
  • Claves nuevas en cada mensaje: un nonce aleatorio hace que cada mensaje use su propio material de claves, así que enviar el mismo texto dos veces nunca da el mismo bloque.
  • Relleno: los mensajes se redondean hacia arriba en tramos de tamaño, así que “sí” y “¡ni hablar!” parecen idénticos.

Pruébalo: escribe algo corto, luego algo un poco más largo, y mira qué tamaños se mantienen iguales.

¿Puede un relay adivinar tu mensaje por su tamaño?

Escribe un mensaje y mira cuántos bytes revela cada esquema. NIP-44 redondea los tamaños hacia arriba en tramos para que los mensajes cortos se parezcan.

Bytes revelados para el mensaje actual
EsquemaTamaño visible
Mensaje real3 bytes
Texto cifrado NIP-0416 bytes
NIP-44 con relleno32 bytes

Con NIP-44, cualquier mensaje de 1 a 32 bytes ocupa exactamente esto.

Pero fíjate en lo que no cambió en el laboratorio al pasar de NIP-04 a NIP-44: si metes un bloque NIP-44 en el mismo tipo de evento, la clave de Alice, la etiqueta de Bob y la marca de tiempo siguen por fuera. Un candado mejor no arregla un sobre transparente.

Bajo el capó: NIP-44 v2, paso a paso Mostrar detalles
  1. Secreto compartido: ECDH entre la clave secreta de Alice y la clave pública de Bob (solo la coordenada x).
  2. Clave de conversación: HKDF-extract(SHA-256, ikm = shared_x, salt = "nip44-v2"). Es la misma en ambas direcciones.
  3. Claves del mensaje: elige un nonce aleatorio de 32 bytes, luego HKDF-expand(conversation_key, info = nonce, 76) y divídelo en una clave ChaCha20 (32 bytes), un nonce ChaCha20 (12) y una clave HMAC (32).
  4. Relleno: antepón al texto UTF-8 su longitud (u16 big-endian) y rellena con ceros hasta el tamaño del tramo (mínimo 32 bytes, luego potencias de dos divididas en fragmentos).
  5. Cifra los bytes rellenados con ChaCha20.
  6. Autentica: HMAC-SHA256(hmac_key, nonce ‖ ciphertext).
  7. Payload: base64(0x02 ‖ nonce ‖ ciphertext ‖ mac); el 0x02 inicial es la versión.

Lo que no te da: secreto hacia adelante (forward secrecy; una clave secreta robada abre todos los mensajes pasados) ni ninguna protección de metadatos. Especificación: NIP-44.

import * as nip44 from "nostr-tools/nip44";

const conversationKey = nip44.getConversationKey(aliceSecretKey, bobPubkey);
const payload = nip44.encrypt("Nos vemos a las 9", conversationKey);
// Bob deriva exactamente la misma clave desde su lado:
const text = nip44.decrypt(payload, nip44.getConversationKey(bobSecretKey, alicePubkey));

Paso 3: NIP-17, sobres hasta el fondo

NIP-17 es la forma recomendada hoy para enviar mensajes privados. Toma el candado de NIP-44 y le añade un embalaje ingenioso de NIP-59, llamado gift wrap (envoltorio de regalo). Imagina tres capas, de dentro hacia fuera:

  1. El rumor (kind 14): el mensaje de chat real, escrito por Alice. Es un evento normal con id, pero sin firma. Es a propósito: si alguna vez se filtra, nadie puede demostrar que lo escribió Alice. Un rumor, literalmente.
  2. El sello (kind 13): el rumor, cifrado para Bob y firmado por Alice. Así es como Bob sabe que de verdad es ella. El sello no tiene ninguna etiqueta, así que no dice para quién es.
  3. El gift wrap (kind 1059): el sello, cifrado otra vez y firmado por una clave aleatoria recién creada que se usa una vez y se descarta. Lleva una única etiqueta p para que el relay pueda entregárselo a Bob.

El relay solo ve la capa 3. El remitente es un desconocido que nunca volverá a publicar, la marca de tiempo está alterada a propósito (hasta dos días en el pasado), el kind real está oculto y el tamaño va rellenado.

Vuelve al laboratorio, elige NIP-17 y ábrelo capa a capa como Bob: wrap, luego sello, luego rumor. Cuando se abre el sello, tu navegador comprueba que quien firma el sello es la misma clave que el autor del rumor; si no, cualquiera podría falsificar un mensaje “de Alice”.

Lo que todavía se filtra

El wrap tiene que decir para quién es, así que el relay se entera de que Bob recibió algo. NIP-17 pide a los relays que solo entreguen los gift wraps a la persona a la que van dirigidos, después de que Bob demuestre quién es con la autenticación NIP-42. Además, los clientes publican una lista kind 10050 indicando en qué relays quieren recibir DMs.

Bajo el capó: las tres capas en JSON Mostrar detalles

Rumor, kind 14, sin firmar (sin sig):

{
  "id": "…",
  "pubkey": "<pubkey de alice>",
  "created_at": 1735689600,
  "kind": 14,
  "tags": [["p", "<pubkey de bob>"]],
  "content": "Nos vemos a las 9"
}

Sello, kind 13, firmado por Alice, las etiquetas deben estar vacías:

{
  "kind": 13,
  "pubkey": "<pubkey de alice>",
  "created_at": 1735601234,
  "tags": [],
  "content": "<NIP-44(JSON del rumor), Alice → Bob>",
  "id": "…",
  "sig": "…"
}

Gift wrap, kind 1059, firmado por una clave aleatoria de un solo uso:

{
  "kind": 1059,
  "pubkey": "<pubkey desechable aleatoria>",
  "created_at": 1735555555,
  "tags": [["p", "<pubkey de bob>"]],
  "content": "<NIP-44(JSON del sello), clave desechable → Bob>",
  "id": "…",
  "sig": "…"
}

Reglas que conviene conocer:

  • Los receptores deben comprobar que la pubkey del sello es igual a la pubkey del rumor.
  • El created_at del sello y del wrap debería aleatorizarse hasta dos días en el pasado.
  • El remitente también envuelve una copia para sí mismo, para poder recuperar sus propios mensajes enviados en cualquier dispositivo. Los chats de grupo envuelven una copia por participante.
  • El kind 15 es el hermano del kind 14 para mensajes con archivos; las etiquetas subject dan nombre a una conversación.
  • Los DMs van a los relays de la lista kind 10050 del destinatario.

Especificaciones: NIP-17 (DMs privados) y NIP-59 (gift wrap).

import * as nip17 from "nostr-tools/nip17";

// Una sola llamada construye rumor → sello → wrap con una clave desechable nueva.
const wrap = nip17.wrapEvent(aliceSecretKey, { publicKey: bobPubkey }, "Nos vemos a las 9");

// Bob abre ambas capas y recupera el rumor kind 14.
const rumor = nip17.unwrapEvent(wrap, bobSecretKey);
console.log(rumor.kind, rumor.content); // 14 "Nos vemos a las 9"
Privado, no perfecto

NIP-17 oculta mucho, pero no es un mensajero seguro completo: no hay secreto hacia adelante, así que si tu nsec se filtra, también se filtran tus mensajes antiguos. Guarda tu clave secreta en un firmante adecuado y usa un mensajero dedicado para los secretos de vida o muerte.

Comprobación rápida

Revisa tus sobres

Con NIP-17, ¿qué puede leer un relay en el gift wrap exterior?

Elige una respuesta

¿Por qué el rumor (kind 14) se deja sin firmar?

Elige una respuesta

¿Cuál es el principal problema de los DMs con NIP-04?

Elige una respuesta

Lo esencial

  • Cifrar el texto no basta: quién, cuándo y cuánto ocupa pueden decir tanto como el qué.
  • NIP-04 está obsoleto: oculta las palabras, pero deja a la vista remitente, destinatario, hora y tamaño.
  • NIP-44 es el candado moderno (ChaCha20 + HMAC + relleno), pero por sí solo no oculta los metadatos.
  • NIP-17 mete un rumor kind 14 dentro de un sello kind 13 dentro de un gift wrap kind 1059 firmado por una clave desechable.
  • El relay sigue sabiendo quién es el destinatario, por eso los relays de DM solo deberían entregar los wraps a su dueño después de AUTH.