Saltar al contenido principal

Tecnología

Esta página es técnica. Describe la arquitectura criptográfica de ZeroKVault para quienes quieran entender exactamente cómo se mantiene la privacidad de sus datos.

Arquitectura de Conocimiento Cero

ZeroKVault está construido sobre un principio simple: nunca deberíamos poder leer tus datos. Todo el encriptado y desencriptado ocurre en tu navegador usando la Web Crypto API. Nuestros servidores almacenan solo texto encriptado y claves encriptadas — no tenemos forma de desencriptarlos, incluso si nos lo exigieran.

Tu clave maestra es una frase semilla BIP-39 de 12 palabras que nunca sale de tu dispositivo. A partir de ella, derivamos de forma determinista un par de claves ECDH P-256. Solo la clave pública se envía a nuestros servidores. La clave privada permanece en la memoria de tu navegador y se descarta cuando cierras la pestaña.

Cómo Funciona el Cifrado

1

Generación de Frase Semilla

Se genera un mnemónico BIP-39 de 12 palabras a partir de 128 bits de entropía criptográfica. Esta es tu clave maestra — anótala y guárdala en un lugar seguro.

2

Derivación de Claves

El mnemónico se convierte en una semilla de 64 bytes mediante PBKDF2 (estándar BIP-39), luego pasa por HKDF-SHA256 con separación de dominio para producir un par de claves ECDH P-256 determinista. Solo la clave pública se envía al servidor; la clave privada nunca sale de tu navegador.

3

Encriptado por Elemento

Cada elemento de la bóveda recibe una Clave de Encriptado de Datos (DEK) AES-256-GCM aleatoria. El contenido se encripta con esta DEK. Un IV aleatorio de 12 bytes se antepone al texto encriptado.

4

Envoltura de DEK

La DEK se envuelve usando la clave pública del destinatario: se genera un par de claves P-256 efímero, se realiza un acuerdo de claves ECDH entre la clave privada efímera y la clave pública del destinatario, y luego el secreto compartido pasa por HKDF para derivar una clave de envoltura AES-KW. Cada elemento recibe una clave efímera única, proporcionando secreto hacia adelante.

5

Almacenamiento en Servidor

El servidor recibe solo el contenido encriptado y la DEK envuelta. No puede desenvolver la DEK porque no tiene la clave privada del destinatario.

6

Desencriptado

El destinatario ingresa su frase semilla, deriva su par de claves, valida la huella digital, desenvuelve la DEK de cada elemento y desencripta el contenido — todo en el navegador.

Bajo el Capó

Los fragmentos de código a continuación están escritos en TypeScript, que se compila a JavaScript durante el proceso de construcción. El JavaScript resultante es lo que realmente se ejecuta en tu navegador — y podés verificarlo vos mismo. Abrí las Herramientas de Desarrollo del navegador (F12), andá a la pestaña Sources e inspeccioná el código que se está ejecutando. Lo que ves ahí debería coincidir con la lógica mostrada acá. Ese es el sentido del conocimiento cero: no tenés que confiar en nosotros, podés verificarlo.

Derivación de Claves (Semilla BIP-39 → Par de Claves P-256)

Derivación determinista usando solo la Web Crypto API. La misma semilla siempre produce el mismo par de claves.

TypeScript
async function deriveKeyPair(seed: Uint8Array): Promise<{
  publicKeyJwk: JsonWebKey;
  privateKeyJwk: JsonWebKey;
}> {
  // Step 1: Import seed as HKDF base key material
  const baseKey = await crypto.subtle.importKey(
    "raw", seed.buffer as ArrayBuffer,
    "HKDF", false, ["deriveBits"],
  );

  // Step 2: Derive 32 bytes via HKDF-SHA256
  const derivedBits = await crypto.subtle.deriveBits(
    {
      name: "HKDF",
      hash: "SHA-256",
      salt: new Uint8Array(32),
      info: new TextEncoder().encode("zerokvault-ecdh-p256-v1"),
    },
    baseKey, 256,
  );

  // Step 3: Import as P-256 private key
  const dBytes = new Uint8Array(derivedBits);
  const privateKey = await crypto.subtle.importKey(
    "jwk",
    { kty: "EC", crv: "P-256", d: uint8ArrayToBase64url(dBytes) },
    { name: "ECDH", namedCurve: "P-256" },
    true, ["deriveBits"],
  );

  // Step 4: Export both JWK forms
  const privateKeyJwk = await crypto.subtle.exportKey("jwk", privateKey);
  const publicKeyJwk = exportPublicKey(privateKeyJwk);
  return { publicKeyJwk, privateKeyJwk };
}

Cifrado de Contenido (AES-256-GCM)

Cada elemento se encripta con una DEK aleatoria. El IV se antepone al texto encriptado para cargas útiles autocontenidas.

TypeScript
async function encryptContent(
  plaintext: string,
  dek: CryptoKey,
): Promise<string> {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(plaintext);
  const ciphertext = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    dek, encoded,
  );
  // Concatenate IV (12 bytes) + ciphertext
  const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(ciphertext), iv.byteLength);
  return uint8ArrayToBase64(combined);
}

Envoltura de DEK (ECDH Efímero + AES-KW)

La DEK de cada elemento se envuelve con una clave efímera nueva, asegurando secreto hacia adelante. Solo la clave privada del destinatario puede desenvolverla.

TypeScript
async function wrapDEK(
  dek: CryptoKey,
  recipientPublicKeyJwk: JsonWebKey,
): Promise<string> {
  // Generate ephemeral ECDH key pair (forward secrecy)
  const ephemeral = await crypto.subtle.generateKey(
    { name: "ECDH", namedCurve: "P-256" },
    true, ["deriveBits"],
  );

  // Import recipient public key
  const recipientPublicKey = await crypto.subtle.importKey(
    "jwk", recipientPublicKeyJwk,
    { name: "ECDH", namedCurve: "P-256" },
    false, [],
  );

  // ECDH key agreement -> shared secret
  const sharedBits = await crypto.subtle.deriveBits(
    { name: "ECDH", public: recipientPublicKey },
    ephemeral.privateKey, 256,
  );

  // HKDF -> AES-KW wrapping key, then wrap
  const wrappingKey = await deriveWrappingKey(sharedBits);
  const wrappedKeyBuffer = await crypto.subtle.wrapKey(
    "raw", dek, wrappingKey, "AES-KW",
  );

  return JSON.stringify({
    wrappedKey: uint8ArrayToBase64(new Uint8Array(wrappedKeyBuffer)),
    ephemeralPublicKey: await crypto.subtle.exportKey("jwk", ephemeral.publicKey),
  });
}

Generación de claves sin conexión

El eslabón más débil en cualquier sistema de conocimiento cero es la máquina que genera el par de claves. Si ese dispositivo está comprometido — por malware, una extensión del navegador o spyware — un atacante podría capturar silenciosamente tu frase semilla o clave privada antes de que abandonen el dispositivo.

Para usuarios con requisitos de seguridad elevados, ZeroKVault permite importar una clave pública generada completamente sin conexión. Generás el par de claves en una máquina confiable y sin acceso a internet usando un script de Python, y luego importás solo la clave pública en la app. La clave privada y la frase semilla nunca tocan el sistema en línea.

El script es un único archivo que funciona con Python 3 y un solo paquete (pip install cryptography). No requiere acceso a internet y replica exactamente el mismo pipeline de derivación que usa la app: mnemónico BIP-39 → semilla PBKDF2-HMAC-SHA512 → HKDF-SHA256 → par de claves EC P-256. Con aproximadamente 120 líneas, es lo suficientemente corto para leerlo y auditarlo vos mismo antes de ejecutarlo.

Descargar el script offline

Revisiónalo antes de ejecutarlo. El script nunca se conecta a internet y solo genera la clave pública — la frase semilla se queda en tu máquina.

Visión General de la Arquitectura

Tu Navegador

  • Ingreso de frase semilla
  • Derivación de claves (HKDF)
  • Cifrado AES-256-GCM
  • Acuerdo de claves ECDH
  • Envoltura / desenvoltura de DEK

Servidor ZeroKVault

  • Autenticación de usuarios
  • Orquestación del Latido
  • Almacenamiento de clave pública
  • Rechaza material de clave privada

Almacenamiento Cifrado

  • Elementos encriptados de la bóveda
  • DEKs envueltas
  • Archivos adjuntos encriptados (S3)

Las claves privadas y el información sin encriptar nunca salen de tu navegador

Propiedades de Seguridad

PropertyDescription
Conocimiento CeroEl servidor nunca ve información sin encriptar, DEKs ni claves privadas.
Secreto Hacia AdelanteCada elemento usa un par de claves efímero único para la envoltura de DEK.
Claves DeterministasLa misma frase semilla siempre produce el mismo par de claves, permitiendo la recuperación.
Criptografía NativaTodas las operaciones usan la Web Crypto API del navegador — sin librerías JS externas de criptografía.
Cifrado de SobreEl contenido se encripta con una DEK aleatoria; la DEK se encripta con la clave pública del usuario.
Verificación de Huella DigitalUn hash SHA-256 de la clave pública confirma que se ingresó la semilla correcta antes del desencriptado.

Cliente (Navegador)

Todas las operaciones criptográficas: generación de claves, encriptado, desencriptado, envoltura/desenvoltura de DEK. Usa la Web Crypto API nativa del navegador.

Servidor (API)

Almacena datos encriptados, gestiona cuentas de usuario y activaciones del Latido. Nunca maneja información sin encriptar ni claves privadas. Rechaza cualquier solicitud que contenga material de clave privada.

Almacenamiento (Base de datos + S3)

PostgreSQL almacena elementos encriptados y DEKs envueltas. Almacenamiento compatible con S3 guarda archivos adjuntos encriptados. Todos los datos son texto encriptado.