Artículo técnico

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

PDF Library for Delphi reintenta una contraseña errónea en un PDF cifrado descartando el TPDFDocument que acaba de fallar y creando uno completamente nuevo para el siguiente intento, gobernado por un callback OnPassword (TPDFlibPasswordEvent) que se ejecuta hasta dieciséis veces antes de rendirse. Eso es un apartamiento deliberado del instinto al que recurre primero la mayoría de los desarrolladores Delphi: conservar el objeto de documento ya alojado en memoria, alimentarlo con una contraseña corregida, y volver a cargarlo en el sitio en lugar de empezar de nuevo desde nada. El bucle de reintento de PDF Library for Delphi, añadido en la v3.245.0, adopta la postura opuesta, por razones específicas de 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 acaben topándose con él: una pantalla de admisión acepta un PDF, un trailer cifrado fuerza un diálogo de contraseña, el operador se equivoca al teclear la cadena, y el diálogo reaparece para un segundo intento. Nada de 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 al que le sigue

¿Por qué no podéis simplemente reintentar sobre 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 internamente ese objeto 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 la fuente subyacente, y construir un gestor de cifrado a partir de cualquiera que sea la contraseña suministrada, todo ello antes de que PDF Library for Delphi pueda siquiera comprobar si esa contraseña es correcta. Cuando resulta que la contraseña está mal, la rutina de carga interna del documento limpia el lector, la tabla de referencia cruzada, y el gestor de cifrado como parte de fallar hacia fuera, exactamente como debería, lo que significa que no hay ningún analizador a medio construir esperando ahí a una contraseña corregida en una segunda llamada. Conducid de todos modos ese mismo objeto a través de otro intento de carga y el modo de fallo es exactamente del tipo que es un suplicio depurar: aflora un error desde estado interno construido para un análisis distinto, ya fallido, sin nada en él que apunte obviamente de vuelta a la contraseña tres llamadas más arriba. PDF Library for Delphi evita toda esa clase de problema no intentando jamás recuperar un objeto de documento en cuanto ha fallado al abrir; cada intento recibe un documento que nunca ha visto una contraseña errónea, 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 incorrecta, y le entrega al gestor tres cosas: qué intento está a punto de ejecutarse, un parámetro Password que sobrescribir con el siguiente candidato, y un indicador Retry que por defecto es false

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 a LoadFromFile cuenta como intento uno, así que la primera vez que se dispara OnPassword en absoluto, AttemptNumber llega como 2. Dejad Retry sin establecer y la carga falla limpiamente con LastErrorCode 404; ponedlo a true y PDF Library for Delphi vuelve a intentarlo con cualquiera que sea lo que el gestor 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 completa de apertura 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 con él su lector, su tabla de referencia cruzada y su gestor de cifrado, y el siguiente intento empieza de nuevo con un objeto que no tiene historial alguno

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 gestor criptográfico, y la 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 errónea
// Extracto simplificado del interior de LoadFromFile: cada intento obtiene un
// documento que nunca ha visto una contraseña rechazada anteriormente. FileName,
// AttemptNumber y AttemptPassword provienen del método que lo contiene.
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 xref y el manejador
    End;                            // de cifrado de un intento rechazado se destruyen aquí mismo
    if Success or (LoadResult <> lrWrongPassword) then
      Break;                        // éxito, o un fallo que no es de contraseña: detener
    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 instrucción. Un documento que falla se 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 se añade jamás a FDocs, la colección que mantiene TPDFlib 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 recuento de páginas obsoleto, ni un gestor 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 única llamada a LoadFromFile, LoadFromStream o LoadFromString, contando la contraseña pasada a la propia llamada como el intento uno. OnPassword solo se dispara jamás para los intentos del dos al dieciséis, lo que limita el callback a quince invocaciones; pedid un decimoséptimo intento y PDF Library for Delphi se niega sin siquiera invocar al gestor. Dejad Retry en su valor por defecto de false en cualquier momento, o agotad los dieciséis intentos sin una contraseña correcta, y LoadFromFile devuelve 0 con LastErrorCode puesto a 404, el código de PDF Library for Delphi para una contraseña rechazada. El límite existe por razones más allá del orden: un bucle de reintento sin límite es una forma fácil de convertir una contraseña mal tecleada en una denegación de servicio accidental contra cualquiera que sea el hilo que ejecute la carga, sobre todo en cuanto un gestor se conecta a algo automatizado, como una lista de contraseñas vistas anteriormente, en lugar de a un humano haciendo clic a través de un diálogo. PDF Library for Delphi también respeta Abort llamado sobre la instancia TPDFlib desde dentro del gestor, 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 a qué se hubiera puesto Retry. Una carga que falla por una razón distinta de una contraseña equivocada, una tabla de referencia cruzada dañada, por ejemplo, nunca entra en absoluto en el bucle de reintento: PDF Library for Delphi reporta LastErrorCode 401 y se detiene tras el primer intento, porque ningún número de intentos de contraseña arregla un archivo estructuralmente roto

PDF Library for Delphi: Trayectoria de intentos de uno a dieciséis que muestra OnPassword disparándose desde el intento dos, el intento diecisiete rechazado sin invocar el manejador, y los desenlaces separados de é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 roto omite por completo el bucle e informa de 401 tras un único 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 de forma idéntica en LoadFromFile, LoadFromStream y LoadFromString, aunque los tres puntos de entrada retienen su fuente de forma distinta entre intentos. Una ruta de archivo es barata de revisitar, ya que cada intento simplemente vuelve a abrir el archivo nombrado, y una fuente de cadena ya reside en memoria como la propia copia de quien llama, así que ninguna de las dos necesita ninguna ayuda de quien llama entre intentos. Un flujo suministrado por quien llama es el único caso que merece la pena detenerse a considerar: LoadFromStream vuelve a situar ese flujo en la posición cero y lo copia internamente antes del primer intento de análisis, así que cada intento posterior, y el TPDFDocument recién construido detrás de él, reproduce desde esa copia interna en lugar de desde dondequiera que un análisis fallido dejara la posición del flujo. Entregadle a PDF Library for Delphi un TFileStream o un TMemoryStream para un documento protegido por contraseña y no hace falta 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 informe.
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 hasta aquí
    else
      LogRejectedIntake('inbound-invoice.pdf', Lib.LastErrorCode);
  finally
    Lib.Free;
  end;
end;

RegisterIntakeDocument solo recibe jamás Lib en cuanto LoadFromFile ha devuelto 1, lo que significa que alguna contraseña en ese intercambio realmente se verificó contra el gestor 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, en cuanto se confirma que un documento como este está abierto, merece una segunda mirada a sus ajustes de protección en lugar de una suposición de que la contraseña que funcionó es toda la historia de seguridad: auditar qué declara realmente el diccionario /Encrypt de un documento cubre la lectura del algoritmo, la revisión y los bits de permiso que expone PDF Library for Delphi en cuanto se carga un archivo como este

El reintento de contraseña también es una instancia estrecha de una disciplina más amplia que aplica PDF Library for Delphi a lo largo de toda su capa de análisis: un archivo que todavía no se ha demostrado a sí mismo no recibe 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 cada flujo de imagen de un PDF entrante como entrada adversaria en lugar de como un documento bien formado que simplemente olvidó su contraseña

OnPassword y el bucle de reintento que hay detrás forman parte de la biblioteca PDF PDF Library for Delphi para Delphi y C++Builder estándar, disponible en cualquier sitio donde ya estén LoadFromFile, LoadFromStream o LoadFromString, sin necesidad de ningún módulo aparte ni de ningún nivel de licencia adicional para un documento al que simplemente le hace falta un segundo intento con su contraseña