PDFlibPas 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 PDFlibPas, 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 PDFlibPas 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. PDFlibPas 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 PDFlibPas 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 PDFlibPas 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, PDFlibPas 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
// Simplified excerpt from inside LoadFromFile: every attempt gets a
// document that has never seen a previously rejected password. FileName,
// AttemptNumber and AttemptPassword come from the enclosing method.
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); // hand the verified document to the
Doc := nil; // caller's collection; skip the Free below
end;
Finally
Doc.Free; // a rejected attempt's reader, xref table
End; // and crypt handler are torn down right here
if Success or (LoadResult <> lrWrongPassword) then
Break; // success, or a non-password failure: stop
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á PDFlibPas una contraseña equivocada?
PDFlibPas 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 PDFlibPas 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 PDFlibPas 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. PDFlibPas 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: PDFlibPas reporta LastErrorCode 401 y se detiene después del primer intento, porque ninguna cantidad de conjeturas de contraseña arregla un archivo estructuralmente roto
¿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 PDFlibPas un TFileStream o TMemoryStream para un documento protegido con contraseña y no hay necesidad de rebobinarlo entre reintentos; PDFlibPas 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 counts from 2: the password already tried was attempt 1.
Typed := '';
Retry := InputQuery('Password required',
Format('Attempt %d of 16 - enter the document password', [AttemptNumber]), Typed);
if Retry then
Password := Typed;
// Retry is False when the operator cancels, which leaves
// LastErrorCode at 404 for the caller to report.
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) // only a verified document reaches here
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 PDFlibPas 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 PDFlibPas 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 PDFlibPas 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