Artículo técnico

Abrir PDF cifrados con una clave raw en Delphi

PDF Library for Delphi puede abrir un PDF cifrado a partir de su clave raw de cifrado de fichero en lugar de una contraseña. DAOpenFileWithEncryptionKey acepta la clave como texto hexadecimal, la comprueba contra el verificador que ya está almacenado en el diccionario de cifrado y devuelve un handle de Direct Access de solo lectura; DAOpenFromStreamWithEncryptionKey hace lo mismo para un TStream propiedad del caller. Ambas funciones llegaron en v3.496.0

El escenario es concreto y real. En un trabajo forense aparece una clave recuperada de una imagen de memoria, pero no hay contraseña. Un proceso de archivo masivo tiene diez mil documentos cuyas claves de fichero están en una base de datos de escrow porque el sistema DRM de origen dejó de emitir contraseñas hace años. Una migración desde un producto de gestión de derechos retirado conserva el material de clave y nada más. En todos esos casos, la credencial disponible es el resultado de la derivación de claves, no la entrada, y ningún parámetro de contraseña de la API puede aceptarla

¿Por qué una clave de cifrado de fichero no es una contraseña?

Una contraseña y una clave de cifrado de fichero se encuentran a lados opuestos de la derivación de claves en el security handler estándar de PDF (ISO 32000-1 §7.6.3). El handler toma una contraseña, la mezcla con /O, /P, el ID de fichero y un hash específico de la revisión, y produce la clave de fichero. Introduzca una clave de fichero en el espacio de la contraseña y obtendrá un sinsentido hasheado en otro sinsentido, por eso hace falta un punto de entrada propio en vez de un flag añadido a DAOpenFile

El punto donde se puede inyectar la clave raw viene fijado por la revisión. Las revisiones 2 a 4 todavía derivan una clave distinta por objeto a partir de la clave de fichero, el número de objeto, el número de generación y, para AESV2, la sal de AES, por lo que tener la clave de fichero no permite saltarse la derivación a nivel de objeto. Las revisiones 5 a 7 utilizan directamente la clave de fichero de 32 bytes para AES-256, sin un paso por objeto. La única capa común a ambas es la propia clave de fichero, y ese es el único lugar donde PDF Library for Delphi acepta una clave suministrada externamente. Si todavía dispone de la contraseña, manténgase en la ruta normal y deje que el ciclo de reintento de contraseña gestione un primer intento incorrecto, porque la entrada de clave raw renuncia deliberadamente a varias comodidades que conserva la ruta de contraseña

¿Qué entrada acepta DAOpenFileWithEncryptionKey?

Solo hexadecimal ASCII sin prefijo, sin espacios, de longitud par, y el número de bytes decodificados debe coincidir exactamente con la revisión de cifrado. Para las revisiones 2 a 4, la longitud esperada procede de /Length en el diccionario de cifrado: un múltiplo de 8 bits entre 5 y 16 bytes, con 40 bits por defecto cuando falta /Length. Para las revisiones 5 a 7 son exactamente 32 bytes, sin negociación. Un prefijo 0x, un número impar de dígitos, más de 64 caracteres hexadecimales, un bit desconocido en Options o un documento que no esté cifrado producen todos el mismo resultado: un handle 0 y LastErrorCode fijado a PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, que es 425. La rigidez es intencionada. Un parser tolerante que recorte espacios y rellene con ceros una entrada corta convertirá alegremente un pegado truncado del portapapeles en una credencial y fallará después en un punto mucho menos legible

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex tiene 32 caracteres hexadecimales para AES-128 R4 y 64 para AES-256 R6
    FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
    If FileHandle= 0 Then
      Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
    Try
      PageRef:= Lib.DAFindPage(FileHandle, 1);
      WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
    Finally
      Lib.DACloseFile(FileHandle);
    End;
  Finally
    Lib.Free;
  End;
End;

Los demás códigos de fallo siguen siendo distintos para que un trabajo por lotes pueda separar el error del operador de los problemas de la evidencia: 411 cuando no existe el fichero, 401 cuando no se puede abrir para lectura y 409 cuando la estructura de referencias cruzadas está dañada. Todo lo relacionado con la clave se colapsa en 425, deliberadamente, porque un punto de entrada de clave raw que informe de qué parte de la clave era incorrecta se convertiría en un oráculo

¿Qué demuestra realmente la verificación?

PDF Library for Delphi demuestra que la clave suministrada pertenece a este documento, utilizando el verificador que ya contiene el diccionario de cifrado, y la comprobación cambia según la revisión. La revisión 2 vuelve a calcular el cifrado RC4 de la cadena de padding estándar de 32 bytes y compara los 32 bytes con /U. Las revisiones 3 y 4 hashean el padding junto con el ID de fichero, ejecutan la pasada RC4 más las 19 rondas derivadas mediante XOR y comparan los primeros 16 bytes de /U. Las revisiones 5 a 7 descifran la cadena /Perms de 16 bytes con un IV a cero y comprueban a la vez cuatro cosas independientes: la palabra de permisos little-endian frente a /P, los cuatro bytes FF de las posiciones 5 a 8, el flag de cifrado de metadatos como T o F y la marca adb de las posiciones 10 a 12

Cuando existe un verificador pero no coincide, la apertura se rechaza siempre. Conviene decirlo sin rodeos, porque esa es la garantía sobre la que se sostiene toda la función. Tenga también presente lo que no es la verificación: demuestra que la clave descifra este fichero, no que nadie le haya autorizado a utilizarlo. La palabra de permisos recuperada de /Perms es una evidencia sobre la clave, no una concesión, y si quiere saber qué afirma permitir realmente el documento, ese es un trabajo separado para una auditoría de cifrado y permisos. Los problemas de normalización del lado de la contraseña, como el uso de SASLprep con contraseñas AES-256 no ASCII, sencillamente no aparecen aquí, porque ninguna cadena llega a un hash

¿Cuándo se aplica PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED cubre exactamente una situación: el documento no lleva un verificador utilizable, porque falta /Perms, no tiene 16 bytes o /U es demasiado corto para compararlo. No puede ignorar evidencias que fallan. Corrompa un dígito hexadecimal dentro de /Perms, pase la clave correcta con la opción activada, y PDF Library for Delphi seguirá devolviendo 0 y 425. Pase una clave de 32 bytes a cero contra un verificador intacto con la opción activada, y la respuesta será la misma. La opción relaja la ausencia de pruebas, nunca una contradicción de las mismas. Como una apertura de recuperación y una apertura verificada son estados epistémicos distintos, también se informan por separado en lugar de fundirse en el valor de retorno, y DAGetEncryptionKeyValidation recibe el handle abierto y responde con una de tres constantes:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) significa que había un verificador y coincidió
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) significa que la clave se aceptó solo porque no se pudo evaluar ningún verificador y el caller solicitó explícitamente esa política
  • PDF_RAW_KEY_VALIDATION_NONE (0) es lo que informa un handle abierto con una contraseña normal
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // ¿No hay verificador en este fichero? Reintentar con una política de recuperación explícita
  FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
    PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
  Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
    PDF_RAW_KEY_VALIDATION_VERIFIED:
      Chain.Note('key verified against the encryption dictionary');
    PDF_RAW_KEY_VALIDATION_UNVERIFIED:
      Chain.Note('no verifier available: extraction is unattested');
  End;
End;

Solo lectura por construcción y quién es propietario del stream

La entrada de fichero con clave raw siempre abre la fuente con fmOpenRead or fmShareDenyWrite y marca como de solo lectura toda la cadena de Direct Access, por lo que DAAppendFile rechaza la escritura in situ y devuelve 2 en lugar de intentar una actualización incremental. No es una política de la que se pueda convencer al handle para que salga; se fija en el constructor antes incluso de analizar el fichero. En un trabajo de evidencia, la propiedad que interesa es que los bytes de la fuente sean idénticos byte a byte después de cerrar el handle, y la suite de regresión lo afirma exactamente tanto para un fixture AES-128 de revisión 4 como para uno AES-256 de revisión 6. La entrada de stream se comporta igual en las escrituras y añade una regla: DAOpenFromStreamWithEncryptionKey nunca toma la propiedad, así que DACloseFile deja vivo su TStream y usted debe liberarlo

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = handle de solo lectura: exportar a otro lugar, nunca añadir a la evidencia
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // el handle nunca fue propietario de este stream
End;

Higiene de claves y qué exporta la DLL

Al cerrar la cadena se sobrescriben la clave de fichero, la caché de contraseñas y las claves de objeto derivadas, y la clave decodificada se borra en el bloque Finally del propio punto de entrada tanto si la apertura ha tenido éxito como si no. Hay un detalle sutil detrás de esto que costó tiempo real de depuración: cualquier copia que tenga que sobrevivir se clona explícitamente con SetLength y Move en lugar de asignarse. Asigne un AnsiString a otro en Delphi y ambos nombres compartirán un buffer mediante copy-on-write, así que borrar el lado del caller pondría a cero la clave que todavía utiliza el handler criptográfico y el documento se descifraría produciendo basura por razones que ningún stack trace explicaría. Solo los puntos de entrada basados en fichero cruzan la frontera de la DLL, en sus formas wide y ANSI, junto con el accessor del estado de validación; la variante TStream se queda solo en Delphi porque depende de la vida útil de los objetos Delphi y de unas semánticas de referencia que no tienen una representación honesta en una ABI C plana. Si su herramienta de recuperación es un cliente DLL, planifique pasar por un fichero temporal y borrarlo bajo los mismos controles que aplica a la clave

Trate la clave hexadecimal como material de credencial y aplíquele las reglas de manejo que daría a una contraseña de documento, y conserve el estado de validación en el registro que produzca su cadena de custodia para que un lector posterior pueda distinguir una extracción verificada de una no atestiguada. Si está evaluando un componente PDF para Delphi destinado a forense, archivo masivo o migración DRM, los puntos de entrada de clave raw, la garantía de solo lectura y la superficie de auditoría de cifrado forman parte de la misma biblioteca, y puede consultar la lista completa de funciones en la página del producto PDF Library for Delphi