Tome una factura en PDF que ya lleva cifrado AES-256 y pídale al componente PDFium para Delphi y C++Builder (PDFiumPas) que la marque como PDF/A para retención de archivo, o que la firme con PAdES, mediante una actualización incremental en lugar de una reescritura completa. La biblioteca no llegará ahí parcheando los bytes cifrados directamente: sus seis inyectores de marcadores a nivel de byte detectan una entrada /Encrypt existente y pasan el origen al destino byte por byte sin modificar, y su firmador PAdES lanza una excepción en lugar de emitir una firma que ningún validador aceptará
Esa es una pregunta distinta de auditar un PDF que usted no creó en busca de riesgo oculto, que es su propio ejercicio de solo lectura. Este artículo trata sobre el lado de escritura del mismo límite de confianza: qué se le permite hacer a su propio código a un archivo cuyos bytes ya están bloqueados detrás de la contraseña de otra persona, en el momento en que ese código intenta agregarle algo después del hecho
¿Qué exige ISO 32000-1 al actualizar un PDF cifrado?
ISO 32000-1 §7.5.6 exige que el trailer de una actualización incremental repita cada entrada del trailer anterior excepto /Prev, y la Tabla 15 lista /Encrypt entre las entradas que puede llevar un trailer. Omítala del nuevo trailer y un lector conforme no tiene razón para dudar de la omisión: el trailer más reciente es autoritativo, así que un lector que no encuentra ahí ningún /Encrypt decide que todo el archivo no está cifrado e intenta analizar el cuerpo más antiguo, todavía cifrado, como bytes planos. Conserve /Encrypt en el nuevo trailer pero escriba los propios objetos de la actualización como texto plano, y el fallo simplemente se mueve un paso más adelante: el lector detecta correctamente el cifrado, pasa cada objeto que toca por el cifrador del archivo, incluidos los nuevos que nunca estuvieron cifrados en primer lugar, y recibe de vuelta ruido para contenido que era perfectamente legible antes de que el descifrado lo tocara. Cualquiera de los dos errores produce un archivo que se ve como una actualización incremental normal y bien formada a nivel de byte, hasta que un lector conforme lo abre
Seis inyectores de marcadores, una puerta de cifrado de v2.14.2
PDFiumPas incluye seis inyectores de marcadores a nivel de byte, uno por cada subconjunto de PDF ISO que puede etiquetar: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), y PDF/VT-1 (ISO 16612-2). Cada uno toma los bytes que ya escribió el propio FPDF_SaveAsCopy de PDFium y les superpone una segunda actualización incremental, más pequeña: un nuevo flujo de metadatos XMP, una edición del diccionario de catálogo que apunta hacia él, y para los subconjuntos orientados a impresión un OutputIntent y perfil ICC. A partir de v2.14.2, cada uno de InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, y InjectPdfVTMarkers lee primero el trailer de origen, y si reporta una entrada /Encrypt existente, copia el origen al flujo de destino sin modificar y retorna de inmediato. Sin XMP, sin OutputIntent, sin edición de catálogo: quien llama recupera el archivo original, byte por byte
var
Src, Dst: TFileStream;
Opts: TPdfXSaveOptions;
begin
Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
try
Opts.Conformance := pxc4;
InjectPdfXMarkers(Src, Dst, Opts);
// pdfx-attempt.pdf is byte-identical to the source: still encrypted,
// no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
// nothing was corrupted either
finally
Dst.Free;
Src.Free;
end;
end;
Que se permita el cifrado no es lo mismo que sea seguro inyectar en él
PDF/E-1 y PDF/R-1 ambos permiten explícitamente que su documento anfitrión esté cifrado a nivel de especificación, lo que se lee como una exención hasta que se mira lo que realmente tiene que pasar en disco. ISO 24517-1 §6.3 permite el cifrado para PDF/E-1, e ISO 23504-1 §6.2.3 lo permite para PDF/R-1 siempre que el encabezado declare %PDF-2.0. Ninguna de las dos cláusulas dice nada sobre si un post-procesador a nivel de byte puede agregar con seguridad un objeto en texto plano a ese contenedor cifrado, y no puede, por las mismas razones de §7.5.6 que se aplican a cualquier otro subconjunto. Los propios validadores de conformidad de PDFiumPas para estos dos perfiles, ValidatePdfECompliance y ValidatePdfRCompliance, registran deliberadamente la presencia de /Encrypt sin marcarla como un defecto, lo cual es correcto para un validador de solo lectura que nunca escribe un byte. También es un patrón fácil de pasar por alto y asumir que el inyector hermano no necesita una protección separada, cuando el inyector es la única función del par que realmente tiene que negarse
¿SaveAsPdfX descifra silenciosamente su documento?
Sí, cada vez que se pasa por los métodos de conveniencia públicos en lugar de llamar a un inyector directamente. Cada uno de TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, y SaveAsPdfVT renderiza el documento actual a un flujo temporal con SaveAs(Tmp, saRemoveSecurity) antes de entregarle esos bytes a su inyector correspondiente. saRemoveSecurity se mapea a la propia bandera FPDF_REMOVE_SECURITY de PDFium, así que la copia temporal que recibe el inyector nunca estuvo cifrada en primer lugar, y la protección /Encrypt del inyector nunca tiene razón para dispararse. La salida lleva sus marcadores de PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, o PDF/VT-1, pero ya no está protegida por cualquiera que fuera la contraseña que abrió el origen
Esa compensación es invisible hasta que alguien aguas abajo abre la copia de archivo "protegida" sin contraseña y nota que simplemente funciona. La corrección no es una llamada de método distinta; PDFiumPas no tiene ninguna contraparte saAddSecurity que emparejar con saRemoveSecurity, porque el motor PDFium subyacente nunca se construyó para escribir cifrado nuevo, solo para eliminarlo. Si ambas propiedades importan para un archivo, el cifrado tiene que ser un paso separado que usted posea, aplicado después de los marcadores de conformidad, no plegado dentro de la misma llamada a SaveAsPdfA
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret'; // needed to open the source at all
Pdf.FileName := 'signed-encrypted-invoice.pdf';
Pdf.LoadDocument;
Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
// invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
// ran first inside SaveAsPdfA: the output opens without a password
finally
Pdf.Free;
end;
end;
¿Qué pasa cuando se firma un PDF cifrado con PAdES?
PDFiumPas se niega directamente, en lugar de descartar silenciosamente la solicitud de la forma en que lo hace un inyector de marcadores. Tanto TPdf.SignPades como SignPadesToStream pasan por un SignPadesBytes interno, y lo primero que hace después de analizar el trailer de origen es comprobar si hay /Encrypt. Si la entrada está presente, lanza EPadesCrypto con el mensaje "SignPadesBytes: the source document is encrypted; remove encryption before signing" en lugar de continuar más adelante. InjectPadesDssMarkers, la función que incrusta certificados, respuestas OCSP, y CRL para validación a largo plazo, aplica la comprobación idéntica por la razón idéntica, con su propio mensaje: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"
El razonamiento aquí es más estricto que el paso silencioso de los inyectores de marcadores, y deliberadamente así. Un paso silencioso es seguro para un sello PDF/A porque omitirlo deja el mismo PDF válido con el que se empezó, solo sin etiquetar. La firma no puede fallar tan silenciosamente: una firma que silenciosamente nunca se agregó se ve, para cualquier código que llama y que solo comprueba un resultado booleano, exactamente igual que una firma que se agregó con éxito. EPadesCrypto desciende de la clase Exception ordinaria, así que capturarla es manejo de excepciones normal, no una convención especial de flujo de control que haya que aprender
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.Password := 'open-secret';
Pdf.FileName := 'encrypted-contract.pdf';
Pdf.LoadDocument;
try
Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
except
on E: EPadesCrypto do
// E.Message: 'SignPadesBytes: the source document is encrypted;
// remove encryption before signing'
raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
end;
finally
Pdf.Free;
end;
end;
Secuenciar sellos de conformidad, firmas, y cifrado
La corrección práctica es de orden, no una biblioteca distinta. Aplique primero los marcadores de PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1, o PDF/VT-1, agregue cualquier firma PAdES a continuación, y solo entonces ejecute cualquiera que sea el paso de su pipeline que realmente posea el cifrado, ya sea un escritor de PDF dedicado, un dispositivo de firma, o su propia implementación de AES. La capa de actualización incremental de PDFiumPas encaja naturalmente en medio de esa secuencia, anexando objetos pequeños y específicos a un archivo que por lo demás está terminado, y el cifrado pertenece al final precisamente porque es la única operación de la cadena que el propio PDFiumPas no puede realizar ni revertir
Nada de esto cambia cómo lee PDFiumPas los datos de trailer y referencia cruzada de los que depende cada actualización incremental, que es su propia fuente de sutileza en cuanto entran en escena los flujos xref; validar los flujos de objetos y xref de un PDF cubre cómo esa misma ruta de lectura de trailer maneja las estructuras comprimidas de PDF 1.5+. Y una vez que un documento está listo para algo más fuerte que un sello de conformidad, firmar un PDF con una firma PAdES B-B en Delphi es donde SignPades toma el relevo exactamente desde donde deja este artículo
Los inyectores de marcadores y los métodos SignPades descritos aquí se incluyen como parte del componente PDFium para Delphi y C++Builder, junto con el renderizado y la inspección de solo lectura que PDFium provee de forma nativa