Artículo técnico

Reintentar contraseñas de PDF cifrado en Delphi con PDF Library for Delphi

PDF Library for Delphi reintenta una contraseña equivocada en un PDF cifrado descartando el TPDFDocument que acaba de fallar y creando uno completamente nuevo para el siguiente intento, controlado por un callback OnPassword (TPDFlibPasswordEvent) que se ejecuta hasta dieciséis veces antes de rendirse. Eso es una desviación deliberada del instinto al que recurre primero la mayoría de los desarrolladores Delphi: conservar el objeto de documento que ya está sentado en memoria, entregarle una contraseña corregida, y volver a cargar en el mismo lugar en lugar de empezar de nuevo desde nada. El bucle de reintento de PDF Library for Delphi, agregado en v3.245.0, toma la posición opuesta, por razones específicas a lo que deja atrás un intento de contraseña fallido. El escenario detrás de esto es lo bastante ordinario como para que la mayoría de las aplicaciones Delphi con mucho documento se topen con él eventualmente: una pantalla de admisión acepta un PDF, un trailer cifrado fuerza un diálogo de contraseña, el operador se equivoca al escribir la cadena, y el diálogo reaparece para un segundo intento. Nada en esa experiencia de usuario es inusual, así que el código detrás tiene que aceptar más de una contraseña candidata para el mismo archivo, y tiene que hacerlo con seguridad, sin filtrar estado del intento rechazado hacia el que sigue

¿Por qué no se puede simplemente reintentar en el mismo objeto de documento?

Reutilizar un TPDFDocument a través de intentos de contraseña no funciona, porque un intento fallido ya ha desmontado ese objeto internamente en lugar de dejarlo en algún estado pausado y reanudable. Abrir un PDF cifrado significa analizar la tabla de referencia cruzada, construir un lector sobre el origen subyacente, y construir un manejador de cifrado a partir de cualquiera que sea la contraseña suministrada, todo antes de que PDF Library for Delphi siquiera pueda probar si esa contraseña es correcta. Cuando la contraseña resulta ser equivocada, la rutina de carga interna del documento limpia el lector, la tabla de referencia cruzada, y el manejador de cifrado como parte de fallar, exactamente como debería, lo que significa que no hay ningún analizador a medio construir sentado ahí esperando una contraseña corregida en una segunda llamada. Conduzca ese mismo objeto a través de otro intento de carga de todos modos y el modo de fallo es exactamente del tipo miserable de depurar: un error sale a la superficie desde estado interno construido para un análisis distinto, ya fallado, sin nada en él que apunte obviamente de vuelta a la contraseña tres llamadas atrás. PDF Library for Delphi evita toda esa clase de problema nunca intentando recuperar un objeto de documento una vez que ha fallado en abrir; cada intento recibe un documento que nunca ha visto una contraseña equivocada, lector y tabla de referencia cruzada incluidos

¿Cómo pide el callback OnPassword la siguiente contraseña?

TPDFlibPasswordEvent es el tipo de callback que invoca PDF Library for Delphi a través de TPDFlib.LoadFromFile, LoadFromStream, y LoadFromString cada vez que la contraseña recién probada resulta ser equivocada, y le entrega al manejador tres cosas: qué intento está a punto de ejecutarse, un parámetro Password para sobrescribir con el siguiente candidato, y una bandera Retry que por defecto es falsa

TPDFlibPasswordEvent = procedure(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean) of object;

property OnPassword: TPDFlibPasswordEvent read FOnPassword write FOnPassword;

La contraseña pasada a la llamada original de LoadFromFile cuenta como el intento uno, así que la primera vez que se dispara OnPassword en absoluto, AttemptNumber llega como 2. Deje Retry sin establecer y la carga falla limpiamente con LastErrorCode 404; establézcalo en verdadero y PDF Library for Delphi vuelve a intentar con cualquiera que sea lo que el manejador acaba de escribir en Password

Dentro del bucle de reintento: un TPDFDocument nuevo para cada intento

Internamente, PDF Library for Delphi responde a la pregunta del ciclo de vida del objeto de la misma manera para LoadFromFile, LoadFromStream, y LoadFromString: cada intento, incluido el primero, construye un TPDFDocument nuevo, lo ejecuta a través de la secuencia de apertura completa con cualquiera que sea la contraseña que use ese intento, y solo conserva el objeto si la contraseña se verifica. El TPDFDocument de un intento rechazado se libera de inmediato, llevándose consigo su lector, tabla de referencia cruzada, y manejador de cifrado, y el siguiente intento empieza de nuevo con un objeto que no tiene ningún historial en absoluto

Diagrama de flujo de PDF Library for Delphi del bucle de reintento de contraseña donde cada intento construye un TPDFDocument nuevo, un intento rechazado libera su lector, tabla de referencias cruzadas y manejador criptográfico, y el callback OnPassword decide si se ejecuta otro intento o la carga falla con LastErrorCode 404
Un candidato fallido nunca sobrevive al bucle: su documento se libera con el estado del parser aún a medio construir. Establecer Retry escribe el siguiente candidato en Password y repite con un objeto que nunca ha visto una contraseña incorrecta
// Extracto simplificado de dentro de LoadFromFile: cada intento recibe un
// documento que nunca ha visto una contraseña previamente rechazada. FileName,
// AttemptNumber y AttemptPassword provienen del método que lo envuelve.
Var
  Doc: TPDFDocument;
  LoadResult: TPLLoadResult;
  Success: Boolean;
Begin
  Success := False;
  Repeat
    Doc := TPDFDocument.Create;
    Doc.DecodeMode := FDefaultDecodeMode;
    Try
      LoadResult := Doc.LoadFromFile(FileName, AttemptPassword);
      Success := LoadResult = lrOkay;
      if Success then
      begin
        FDocs.Add(Doc);            // entrega el documento verificado a la
        Doc := nil;                 // colección de quien llama; omite el Free de abajo
      end;
    Finally
      Doc.Free;                     // el lector, la tabla de referencia cruzada
    End;                            // y el manejador de cifrado de un intento rechazado se desmontan aquí mismo
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // éxito, o un fallo que no es de contraseña: detente
    Inc(AttemptNumber);
  Until not RequestPasswordRetry(AttemptNumber, AttemptPassword);
End;

Esa línea Doc := nil justo antes del bloque Finally es todo el contrato de ciclo de vida del objeto en una sola sentencia. Un documento que falla lleva su estado de analizador a medio construir a la tumba con él, por diseño, y un documento que tiene éxito es el único que jamás se agrega a FDocs, la colección que TPDFlib mantiene para cada documento que quien llama tiene abierto. Nada de un intento rechazado es visible desde fuera del bucle de reintento: ni un lector a medio inicializar, ni un conteo de páginas obsoleto, ni un manejador de cifrado construido a partir de la clave equivocada

¿Cuántas veces reintentará PDF Library for Delphi una contraseña equivocada?

PDF Library for Delphi permite dieciséis intentos totales contra una sola llamada a LoadFromFile, LoadFromStream, o LoadFromString, contando la contraseña pasada a la propia llamada como el intento uno. OnPassword solo se dispara para los intentos del dos al dieciséis, lo que limita el callback a quince invocaciones; pida un decimoséptimo intento y PDF Library for Delphi se niega sin siquiera invocar el manejador. Deje Retry en su valor predeterminado de falso en cualquier momento, o agote los dieciséis intentos sin una contraseña correcta, y LoadFromFile devuelve 0 con LastErrorCode establecido en 404, el código de PDF Library for Delphi para una contraseña rechazada. El límite existe por razones más allá de la prolijidad: un bucle de reintento sin límite es una manera fácil de convertir una contraseña mal escrita en una denegación de servicio accidental contra cualquiera que sea el hilo que ejecuta la carga, especialmente en cuanto un manejador se conecta a algo automatizado, como una lista de contraseñas vistas anteriormente, en lugar de un humano haciendo clic a través de un diálogo. PDF Library for Delphi también respeta Abort llamado en la instancia de TPDFlib desde dentro del manejador, ya que Sender llega como ese mismo objeto, útil detrás de un botón Cancelar en un diálogo de contraseña, y detiene el bucle de reintento en la siguiente comprobación sin importar qué se hubiera establecido en Retry. Una carga que falla por una razón distinta a una contraseña equivocada, una tabla de referencia cruzada dañada por ejemplo, nunca entra en el bucle de reintento en absoluto: PDF Library for Delphi reporta LastErrorCode 401 y se detiene después del primer intento, porque ninguna cantidad de conjeturas de contraseña arregla un archivo estructuralmente roto

PDF Library for Delphi: trayectoria de intentos de uno a dieciséis que muestra a OnPassword disparándose desde el intento dos en adelante, el intento diecisiete rechazado sin invocar el manejador, y los resultados separados para éxito, LastErrorCode 404 y LastErrorCode 401
Solo la contraseña entregada a la propia llamada de carga cuenta como intento uno, y el callback nunca se ejecuta más de quince veces por carga. Un archivo estructuralmente dañado omite por completo el bucle y reporta 401 tras un solo intento

¿Funciona el bucle de reintento igual para archivos, flujos, y cadenas?

El callback OnPassword y el límite de dieciséis intentos se comportan idénticamente a través de LoadFromFile, LoadFromStream, y LoadFromString, aunque los tres puntos de entrada retienen su origen de manera distinta entre intentos. Una ruta de archivo es barata de volver a visitar, ya que cada intento simplemente reabre el archivo nombrado, y un origen de cadena ya está sentado en memoria como la propia copia de quien llama, así que ninguno de los dos necesita ayuda de quien llama entre intentos. Un flujo suministrado por quien llama es el único caso que vale la pena detenerse a considerar: LoadFromStream retrocede ese flujo de vuelta a la posición cero y lo copia internamente antes del primer intento de análisis, así que cada intento subsiguiente, y el TPDFDocument recién construido detrás de él, reproduce desde esa copia interna en lugar de desde donde sea que un análisis fallido dejara la posición del flujo. Entréguele a PDF Library for Delphi un TFileStream o TMemoryStream para un documento protegido con contraseña y no hay necesidad de rebobinarlo entre reintentos; PDF Library for Delphi ya tiene en cuenta una posición que un primer intento fallido pueda haber movido

Encajar el reintento de contraseña en una pantalla de admisión de documentos

Un flujo de trabajo de admisión de documentos es el hogar natural de este callback, porque es exactamente la forma de problema para el que se construyó OnPassword: un archivo llega desde fuera de la aplicación, su contraseña no se conoce con certeza de antemano, y la persona que suministra candidatos necesita más de un intento sin que el código circundante escriba su propio bucle de reintento alrededor de LoadFromFile

procedure TIntakeForm.SupplyPassword(Sender: TObject; AttemptNumber: Integer;
  var Password: WideString; var Retry: Boolean);
var
  Typed: string;
begin
  // AttemptNumber cuenta desde 2: la contraseña ya probada fue el intento 1.
  Typed := '';
  Retry := InputQuery('Password required',
    Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
  if Retry then
    Password := Typed;
  // Retry es False cuando el operador cancela, lo que deja
  // LastErrorCode en 404 para que quien llama lo reporte.
end;
procedure TIntakeForm.LoadInboundDocument;
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.OnPassword := SupplyPassword;
    if Lib.LoadFromFile('inbound-invoice.pdf', '') = 1 then
      RegisterIntakeDocument(Lib)        // solo un documento verificado llega aquí
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument solo recibe Lib una vez que LoadFromFile ha devuelto 1, lo que significa que alguna contraseña en ese intercambio realmente se verificó contra el manejador de cifrado del archivo; un intento rechazado nunca llega a esa línea, y tampoco lo hace un documento a medio abrir. Lo que viene después, una vez que un documento como este se confirma abierto, vale la pena echarle un segundo vistazo a su configuración de protección en lugar de asumir que la contraseña que funcionó es toda la historia de seguridad: auditar qué declara realmente el diccionario /Encrypt de un documento cubre leer el algoritmo, la revisión, y los bits de permiso que expone PDF Library for Delphi una vez que un archivo así se carga

El reintento de contraseña también es una instancia acotada de una disciplina más amplia que aplica PDF Library for Delphi en toda su capa de análisis: a un archivo que todavía no se ha probado a sí mismo no se le da ningún beneficio de la duda, ya sea que la pregunta sea qué contraseña lo desbloquea o si un campo de longitud dentro de él está mintiendo sobre el tamaño de búfer que necesita. Endurecer un analizador de PDF en Pascal contra archivos maliciosos cubre la otra mitad de esa disciplina, los decodificadores que tratan cada programa de fuente y flujo de imagen en un PDF entrante como entrada adversarial en lugar de un documento bien formado que simplemente olvidó su contraseña

OnPassword y el bucle de reintento detrás de él son parte de la biblioteca PDF PDF Library for Delphi para Delphi y C++Builder estándar, disponible en cualquier lugar donde ya estén LoadFromFile, LoadFromStream, o LoadFromString, sin necesidad de ningún módulo separado ni nivel de licencia para un documento que simplemente necesita un segundo intento con su contraseña