SEBASTIANDEV_
  • Sobre mí
  • Proyectos
  • Blog
arrow_backVolver al blog
2026 SEBASTIÁN. TERMINAL_ACTIVA
terminalmail
08_SEP_2026schedule5 min de lectura

cortex-sync 0.6.0: salt aleatorio, AAD y cortex rekey

#CLAUDE-CODE#CLI#SEGURIDAD
cortex-sync 0.6.0: salt aleatorio, AAD y cortex rekey

npm install -g cortex-sync@latest

En la auditoría de la 0.5.0 quedaron tres mejoras de cifrado anotadas para más adelante: AAD, un salt aleatorio en vez de uno fijo, y que ensureGitHubRepo chequeara si un repo ya existente era público. Dije explícitamente que venían después, en ese orden.

Esta versión cierra esas tres, más dos cosas que no estaban en la lista original.


Salt aleatorio en vez de SHA256(email)

La clave de cifrado sale de PBKDF2(passphrase, salt). Hasta la 0.5.1, el salt era SHA256(tu-email) — siempre el mismo para el mismo usuario.

PBKDF2 con 600.000 iteraciones ya era la defensa real; un salt fijo no rompe nada por sí solo, pero es una mejora de higiene obvia una vez que te pones a mirarlo, y además habilita algo que antes no existía: rotar la clave. Si el salt siempre da el mismo resultado con la misma passphrase, no hay forma de invalidar una clave vieja sin cambiar la passphrase.

Ahora el salt es aleatorio, se genera una vez por proyecto la primera vez que corres cortex sync, y se guarda sin cifrar junto al manifiesto (no es secreto — solo tiene que ser el mismo en todas tus máquinas):

 export async function getOrCreateSalt(backend: IStorageBackend, projectKey: string): Promise { \
 const path = remoteSaltPath(projectKey); \
 if (await backend.has(path)) return backend.read(path); \
 const salt = randomBytes(16); \
 await backend.write(path, salt); \
 return salt; \
 } 

AAD: el cifrado ahora sabe a qué ruta pertenece

El otro pendiente de la auditoría: GCM autentica el contenido del ciphertext, pero no dónde se supone que vive. Atar la ruta de destino como additional authenticated data cierra eso — un blob cifrado deja de ser válido si termina en una ruta distinta a la que tenía cuando se cifró:

 export function encrypt(plaintext: Buffer, derived: DerivedKey, aad?: Buffer): Buffer { \
 const iv = randomBytes(IV_LEN); \
 const cipher = createCipheriv('aes-256-gcm', derived.key, iv); \
 if (aad) cipher.setAAD(aad); \
 const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); \
 return Buffer.concat([MAGIC, Buffer.from([VERSION]), iv, cipher.getAuthTag(), ciphertext]); \
 } 

sync, pull y status ahora pasan Buffer.from(destPath) en cada encrypt()/decrypt(). Si alguien mueve un blob a otra ruta, GCM rechaza el auth tag: Unsupported state or unable to authenticate data, no un archivo corrupto silencioso.

cortex rekey: rotar la clave sin perder nada

Con salt aleatorio ya tenía sentido agregar el comando que la 0.5.0 no podía ofrecer: rotar la clave si sospechas que tu passphrase o tu backend estuvieron expuestos.

cortex rekey genera un salt nuevo, deriva una clave nueva, y re-cifra cada archivo — descifrando con la clave vieja y volviendo a cifrar los mismos bytes comprimidos con la clave nueva, sin tocar el contenido en ningún momento intermedio:

 for (const relPath of paths) { \
 const destPath = remoteFilePath(projectKey, relPath); \
 const encBlob = await backend.read(destPath); \
 const compressed = decrypt(encBlob, oldDerived, Buffer.from(destPath)); \
 uploads.push({ path: destPath, content: encrypt(compressed, newDerived, Buffer.from(destPath)) }); \
 } 

El salt nuevo y todos los archivos re-cifrados —manifiesto incluido— se suben en un solo writeMany(). Si eso no fuera atómico, una falla a mitad de camino podría dejar contenido cifrado con la clave vieja junto a un salt nuevo, y nada podría volver a descifrarse. Lo probé de punta a punta: sincronicé un proyecto, roté la clave, confirmé que la combinación vieja de clave+salt ya no descifra nada, y que otra máquina puede seguir haciendo cortex pull con la misma passphrase de siempre.

De paso: dos cosas más que no estaban en la lista

  • cortex sync --strict: hasta ahora, si el detector de secretos encontraba algo, cortex sync solo avisaba y seguía. --strict corta el sync en seco. Combinalo con --redact si querés que borre los secretos y sincronice igual.

  • ensureGitHubRepo verifica privacidad antes de asumir éxito: crear un repo devuelve 422 si ya existe uno con ese nombre en tu cuenta — el código viejo trataba cualquier 422 como "ya existe, todo bien". Si ese repo ya existente resultaba ser público (por ejemplo, lo creaste público a mano hace tiempo y después decidiste usarlo con cortex), el sync subía ahí sin decir nada. El contenido sigue cifrado, pero nombres de archivo, estructura de proyecto y timestamps quedaban expuestos. Ahora, si el 422 aparece, cortex consulta el repo existente y tira error si no es privado.

Un bug que encontré releyendo mi propio código, no con un test

cortex status nunca recibió el decompress() que agregué en la 0.5.1. sync y pull sí lo tenían; status seguía haciendo JSON.parse(decrypt(...)) directo sobre bytes gzip. Ningún test lo agarró porque ningún test de status corría contra un manifiesto remoto realmente comprimido — lo encontré releyendo el archivo mientras tocaba el resto del cifrado, no porque algo fallara. Ya está arreglado, y ahora hay un test que específicamente arma un manifiesto comprimido y corre status contra él.

Sin ruta de migración desde la 0.5.x

Igual que en las dos versiones anteriores: si tenías algo sincronizado con 0.5.0 o 0.5.1, esta versión no lo puede leer. El salt viejo era SHA256(email), sin AAD; esta versión genera un salt aleatorio nuevo la primera vez que toca el proyecto, deriva una clave distinta, y el resultado es justo lo que se espera de GCM cuando la clave o el AAD no coinciden — lo verifiqué armando un remoto con el formato viejo y corriendo el pull nuevo contra él:

 PULL THREW: Unsupported state or unable to authenticate data 

No es un mensaje amigable — es el error crudo del módulo de cripto de Node. La solución es simple: corré cortex sync desde la máquina que tenga la copia más reciente antes de hacer pull desde cualquier otra.


Cómo se hace esto

Este proyecto lo escribo trabajando con Claude Code — vibecoding, dicho sin vueltas. Yo decido qué se construye, en qué orden y por qué; una parte grande del código, los tests y este mismo texto salen de esa sesión de trabajo. Lo que reviso y decido yo: qué diff entra, qué exploit o test corro para confirmar que algo realmente pasa (los números y los mensajes de error de este post son de correr el código, no supuestos), y qué se publica.


Actualizate

 npm install -g cortex-sync@latest # → 0.6.0 

188 tests en 32 archivos, todos en verde, más la verificación real de rotación de clave (sync → rekey → el salt y la clave viejos ya no sirven → pull sigue funcionando desde otra máquina) y del error de migración de arriba.

GitHub: SebastiaWeb/cortex-cli