💡 Full working example available on GitHub:
qr-sign-password-protected-pdf-python

Introducción

Existe un patrón de tres pasos al que la mayoría de los equipos recurren cuando un documento que necesita firmarse resulta estar cifrado: descifrarlo, firmar el texto plano y volver a cifrar el resultado. Funciona. También significa que durante unos cientos de milisegundos una copia legible de un documento deliberadamente protegido existe en un directorio temporal, y en una canalización auditada esa ventana es el hallazgo más que la firma.

Firmar un PDF protegido es una capacidad de GroupDocs.Signature para Python vía .NET que omite esos tres pasos por completo: la contraseña abre la fuente en el mismo lugar, se aplica la firma y la salida se escribe de nuevo protegida. Este artículo compara las cuatro rutas de contraseña —dos que funcionan y dos que fallan a propósito— y cubre el contrato de error que es específico de este enlace.

Por Qué Importa

El manejo de contraseñas es donde las canalizaciones de documentos se filtran. No a través de la biblioteca de firma, normalmente, sino a través del andamiaje que la rodea: el archivo temporal que se suponía debía eliminarse, el manejador de excepciones que engulló un error de contraseña incorrecta y volvió a intentar indefinidamente, la copia firmada entregada con una contraseña que el destinatario nunca conoció.

Los tres casos tienen la misma causa raíz, que es que la contraseña se trata como algo que hay que eliminar del camino en lugar de como parte de la operación. LoadOptions y SaveOptions la devuelven a la operación.

Requisitos Previos

Python 3 y groupdocs-signature-net==26.1, más un PDF con una contraseña de usuario. Sin una licencia la biblioteca se ejecuta en modo de evaluación, lo que aún firma pero añade su propio texto a la página.

Instalación

pip install groupdocs-signature-net==26.1

Método 1 – Mantener la contraseña original

El predeterminado, y el que necesita menos código. La contraseña se pasa a través de LoadOptions, y no se pasa ningún SaveOptions:

load_options = LoadOptions()
load_options.password = password
options = _build_qr_options(qr_text)
with signature.Signature(source_path, load_options) as sign:
    result = sign.sign(output_path, options)
    return len(result.succeeded)

La ausencia de SaveOptions es lo que hace el trabajo real aquí. use_original_password por defecto es True, por lo que GroupDocs vuelve a aplicar la contraseña de origen a la salida firmada. No hay ningún momento en que exista una versión sin protección, en disco o de otro modo, y len(result.succeeded) informa cuántas firmas se escribieron.

Método 2 – Cambiar la clave de la copia firmada

Cuando el documento firmado se entrega a otra parte, la medida sensata es dar a la copia sus propias credenciales y dejar la fuente intacta:

save_options = SaveOptions()
save_options.password = new_password
save_options.use_original_password = False
with signature.Signature(source_path, load_options) as sign:
    result = sign.sign(output_path, options, save_options)
    return len(result.succeeded)

Ambas líneas de SaveOptions son obligatorias, y este es el detalle que vale la pena recordar: establecer password mientras se deja use_original_password en su valor predeterminado no produce ningún efecto observable. La bandera gana, la salida conserva la contraseña antigua, y lo descubres cuando el destinatario informa que la contraseña que enviaste no funciona.

Métodos 3 y 4 – Los dos fallos

Un documento cifrado responde de manera diferente a una contraseña ausente y a una incorrecta, y la diferencia merece ser manejada.

Sin LoadOptions en absoluto, la apertura falla y no se escribe nada:

try:
    with signature.Signature(source_path) as sign:
        sign.sign(output_path, options)
    return ""
except RuntimeError as error:
    return proxy_error_name(error)

Eso devuelve PasswordRequiredException. Proporciona una contraseña incorrecta y el mismo código devuelve IncorrectPasswordException. Una indica que se debe preguntar al usuario por una credencial; la otra indica que la credencial que tienes está obsoleta. Un manejador que no pueda distinguirlas termina reintentando una contraseña que nunca funcionará.

El contrato de error, y por qué el código obvio se rompe

Esta es la parte que cuesta una tarde si nadie te advierte. El enlace expone PasswordRequiredException, IncorrectPasswordException y GroupDocsSignatureException como nombres simples que no heredan de BaseException. Escribe el manejador intuitivo:

except IncorrectPasswordException:
    ...

y Python lanza TypeError: catching classes that do not inherit from BaseException is not allowed. El error original desaparece, reemplazado por uno que apunta a tu línea except en lugar de a la contraseña. Escribí exactamente ese manejador la primera vez, y los veinte minutos que pasé leyendo el TypeError son la razón por la que existe esta sección.

Lo que realmente llega es un RuntimeError cuyo mensaje comienza con Proxy error(<Name>): . Analizar ese prefijo recupera la causa:

message = str(error)
marker = "Proxy error("
if not message.startswith(marker):
    return ""
start = len(marker)
end = message.find(")", start)
if end < 0:
    return ""
return message[start:end]

Rama según el nombre devuelto en lugar de según el texto del mensaje, que lleva rutas de archivo y varía entre ejecuciones.

Inspeccionar antes de firmar

Hay una quinta ruta que vale la pena conocer, y no escribe nada en absoluto. Abrir el documento con LoadOptions y llamar a get_document_info devuelve el formato, el número de páginas y el tamaño mientras el archivo permanece cifrado en disco:

with signature.Signature(source_path, load_options) as sign:
    info = sign.get_document_info()
    return info.file_type.file_format, info.page_count, info.size

Dos usos para ello. Cuando la contraseña proviene de un formulario de usuario, esto valida la credencial con una llamada barata en lugar de a mitad de un lote de doscientos documentos. Y cuando una canalización no tiene permitido almacenar texto plano en absoluto, aún permite que esa canalización informe sobre lo que está reteniendo —recuentos de páginas para un registro de auditoría, tamaños para una cuota— sin descifrar nada.

Comparación de los Métodos: Cuándo Usar Cada Uno

Método Mejor para Ventajas clave Limitaciones
Mantener contraseña original canalizaciones que firman en el mismo lugar sin SaveOptions, nada escrito en claro el destinatario necesita la contraseña de origen
Cambiar clave al guardar entrega a otra parte la fuente conserva su credencial, la copia obtiene una nueva dos líneas de SaveOptions, fácil de configurar solo una
Sin contraseña (falla) demostrar el contrato en pruebas falla al abrir, no escribe nada no es una ruta de firma
Contraseña incorrecta (falla) distinguir una credencial obsoleta nombre de excepción distinto no es una ruta de firma

¿Vale la pena la verificación de lectura adicional?

Sí, por dos razones. Reabrir el archivo firmado con QrCodeVerifyOptions demuestra que la firma sobrevivió al guardado, y como la reapertura debe suministrar la contraseña, también prueba que la salida sigue realmente cifrada. Un recuento cero casi siempre indica un problema de licencia más que un fallo de firma: la llamada sign lanza cuando falla realmente, por lo que silencio más cero apunta a una compilación sin licencia.

Qué cuesta cambiar

Nada estructural. Si tu código ya descifra a un archivo temporal, el cambio consiste en eliminar ese paso, mover la contraseña a LoadOptions y eliminar la llamada de re‑cifrado al final —normalmente una pérdida neta de líneas. La llamada de firma en sí no cambia de forma, y la salida es byte‑por‑byte un PDF firmado con la misma protección que tenía al entrar.

El único lugar que hay que revisar con cuidado es el código de limpieza. Una canalización construida alrededor de descifrar‑firmar‑re‑cifrar suele tener un bloque finally que elimina el archivo temporal, y una vez que el archivo temporal desaparece ese bloque está intentando eliminar una ruta que ya no existe.

Buenas Prácticas

  • Deja use_original_password tal cual a menos que estés rotando deliberadamente; el valor predeterminado es el seguro.
  • Analiza el nombre del proxy una sola vez, en un helper, y rama según él en el resto del código.
  • Valida una contraseña suministrada por el usuario con get_document_info antes de iniciar un lote, de modo que una credencial incorrecta cueste una llamada barata en lugar de una ejecución a medio terminar.
  • Nunca sobrescribas la salida firmada sobre la ruta de origen, de modo que un error deje recuperable el original.

Conclusión

La contraseña no es un obstáculo que haya que eludir antes de firmar, es un argumento de la operación. Ábrela con LoadOptions, decide la protección de salida con SaveOptions, analiza el nombre del proxy cuando algo falla y verifica mediante la contraseña después. El ejemplo ejecuta los cuatro caminos en una sola ejecución, por lo que la diferencia entre ellos se ve con un solo comando en lugar de un párrafo de confianza.

Recursos Adicionales