Comprobación privada de credenciales comprometidas mediante índices ofuscados y filtros de Bloom (2/4)

Artículo invitado de liya Garakh. Segunda entrega de la serie iniciada en Comprobación privada de credenciales comprometidas (1/4).

En esta entrega construimos el protocolo de filtros de Bloom para contraseñas filtradas con ruido de ofuscación, paso a paso. Empezamos repasando las propiedades del filtro que hacen viable la idea, derivamos los índices reales y los índices de ruido a partir de funciones criptográficas, y explicamos por qué el ruido determinista es la diferencia entre un esquema robusto y uno que se rompe en la segunda consulta.


En la primera entrega dejamos planteado el problema: comprobar si una contraseña aparece en un corpus de filtraciones sin que el servidor que responde aprenda nada útil sobre ella. Vimos por qué el hash directo —incluso con sal, incluso con bcrypt— no resuelve la cuestión, y mencionamos que el esquema que nos ocupa toma una vía distinta: descomponer la búsqueda en muchas subconsultas pequeñas e individualmente insignificantes, y ocultar cuáles son las reales. Toca construirlo.

4. Filtros de Bloom: propiedades relevantes

Un filtro de Bloom es un vector de bits B[0…m−1] junto con k funciones hash h1, …, hk que proyectan entradas arbitrarias en [0, m−1]. Para insertar un elemento x, se establece B[hi(x)] = 1 para cada i. Para comprobar la pertenencia de y, se verifica si B[hi(y)] = 1 para todos los i.

Tres propiedades son especialmente relevantes para nuestro caso de uso:

1. Ausencia de falsos negativos. Si alguna de las k celdas de y vale cero, y definitivamente no pertenece al conjunto. Es exactamente lo que necesitamos para una respuesta de «definitivamente no comprometida».

2. Tasa de falsos positivos ajustable. Para n elementos insertados, la probabilidad de falso positivo es:

p ≈ (1 − e^(−kn/m))^k(1)

que se minimiza para k* = (m/n) ln 2, con lo que:

p ≈ (1/2) elevado a k*

y:

m/n ≈ −log2(p)/ln 2 ≈ 1,44 · |log2 p|(2)

Para un corpus de 6 × 108 hashes comprometidos y una tasa objetivo de p = 10−3, la ecuación (2) da m/n ≈ 14,4 bits por elemento, equivalente a m ≈ 8,6 × 109 bits (≈ 1,08 GB de almacenamiento en el servidor), con un número óptimo de funciones hash k* ≈ 10.

3. Proyección de muchos a uno. Un único bit B[i] = 1 no identifica ningún elemento concreto; con un factor de carga típico, cada celda acumula el residuo de millones de entradas. Esta es la propiedad que hace al esquema relevante para la privacidad: revelar el valor de una sola celda revela muy poco.

La consulta ingenua —el cliente calcula los k índices para su contraseña y los envía al servidor— no aprovecha esta propiedad: revelar el conjunto de k índices es suficiente para identificar pw en cualquier diccionario razonable. La construcción de la siguiente sección intenta recuperar la privacidad por celda añadiendo ruido denegable al conjunto de índices.

5. Índices deterministas ofuscados

5.1. Construcción del protocolo

Sean H una función hash criptográfica, F una función pseudoaleatoria (PRF) y σ un secreto a largo plazo custodiado exclusivamente por el cliente. Para comprobar una contraseña pw:

  1. Calcular el hash de la contraseña: hpw = H(pw).
  2. Índices reales: derivar las k posiciones del filtro de Bloom a las que se proyecta la contraseña:
    T = { H(i ‖ h_pw) mod m : i ∈ [1, k] }(3)
  3. Índices de ruido: derivar d posiciones adicionales de forma determinista a partir de la contraseña y el secreto del cliente:
    N = { F_σ(h_pw ‖ j) mod m : j ∈ [1, d] }(4)
  4. Formar la unión Q = TN, permutarla de forma determinista (Fisher–Yates inicializado con H(hpw ‖ σ) y dirigido por un GNPA criptográficamente seguro) y enviarla al servidor.
  5. El servidor devuelve B[i] para cada iQ.
  6. El cliente examina únicamente los bits en posiciones de T. Si alguno vale 0, la contraseña es definitivamente segura. En caso contrario, posiblemente comprometida (con probabilidad de falso positivo p del filtro).

5.2. Requisitos de implementación

Generación de índices sin sesgo. La reducción hash mod m introduce un sesgo cuando m no es potencia de dos; este sesgo se convierte en canal lateral observable por el servidor. Dos soluciones:

  • Elegir m = 2t y usar máscara de bits: índice = hash & (m − 1).
  • Si m no puede ser potencia de dos, usar la reducción de Lemire: (h · m) >> 64, con paso de rechazo para uniformidad total.

Permutación criptográficamente segura. Usar un PRNG débil (Mersenne Twister, random.shuffle) permite que un adversario con recuperación de semilla identifique los índices reales por su posición. La semilla debe derivarse como SHA-256(h_pw ‖ σ) y el GNPA debe ser ChaCha20, HMAC-DRBG o equivalente.

5.3. Por qué el ruido debe ser determinista

La decisión de diseño central es que N es función determinista de (pw, σ). La alternativa —usar aleatoriedad fresca en cada consulta— parece superficialmente equivalente, pero introduce una vulnerabilidad crítica bajo consultas repetidas.

Con ruido aleatorio, consultas sucesivas producen:

Q1 = T ∪ N1, Q2 = T ∪ N2, Q3 = T ∪ N3, …

y su intersección converge hacia T. Formalmente: si cada Ni se extrae uniformemente e independientemente de [0, m−1], la probabilidad de que un índice fijo j sobreviva en la intersección de n conjuntos de ruido es:

Pr[j ∈ N1 ∩ … ∩ Nn] = (d/m)^n(5)

El tamaño esperado del ruido superviviente vale:

E[|N1 ∩ … ∩ Nn|] = m·(d/m)^n = d^n/m^(n−1)(6)

Este valor cae por debajo de 1 —la intersección está casi con certeza vacía— cuando n > log m / log(m/d). Para los parámetros representativos d = 30, m = 8 × 109:

n > log2(8·10^9) / log2(8·10^9/30) = 32,9/27,9 ≈ 1,18″ /><span class=(7)

Con ruido determinista, cada consulta sobre la misma contraseña produce el mismo Q. La intersección de n consultas es siempre Q: el servidor no puede acotar progresivamente T por observación repetida, que es precisamente la amenaza que el diseño determinista neutraliza.

5.4. Dimensionamiento del parámetro de ruido d

Bajo el modelo de oráculo aleatorio, cada candidato cae independientemente en AS(Q) con probabilidad:

p_coinc ≈ ((k+d)/m)^k(8)

El tamaño esperado del conjunto de anonimato (excluyendo la contraseña real, que siempre es compatible) es por tanto:

E[|AS(Q)|] ≈ |D|·((k+d)/m)^k(9)

Para garantizar una ambigüedad de al menos K candidatos, se requiere:

d ≥ m·(K/|D|)^(1/k) − k(10)

La ecuación (10) revela la tensión central del esquema. Para |𝒟| = 109, m = 8 × 109, k = 10 y un objetivo de K = 103 candidatos (equivalente a la ambigüedad de k-anonimato):

d ≥ 8·10^9·(10^3/10^9)^(1/10) − 10 ≈ 2,0·10^9(11)

Una consulta de 2 × 109 índices de 4 bytes supone 8 GB de ancho de banda: nueve órdenes de magnitud por encima del punto de operación práctico de d = 30. Para K = 2 (ambigüedad mínima no trivial) el resultado es d ≈ 1,1 × 109, igualmente inviable.

El exponente 1/k es el hecho estructural clave. Los filtros de Bloom deben su eficiencia espacial a la independencia k veces de sus funciones hash; esa misma propiedad exige que, para que una comprobación de candidato compatible se cumpla aleatoriamente, los k índices elegidos independientemente deban caer en el pequeño conjunto Q. La eficiencia del esquema y su falta de resistencia a la enumeración comparten la misma causa raíz.


En la tercera entrega situaremos el esquema en el mapa: rendimiento (almacenamiento, cómputo, ancho de banda, falsos positivos) y comparación punto a punto con los enfoques existentes —el modelo de k-anonymity de HIBP, OPRF, PSI y la opción de descarga local del corpus—. Es donde se ve dónde encaja la propuesta y dónde no.

Speak Your Mind

*