215 lines
7.1 KiB
Python

import getpass
import click
from pyhanko.cli._root import cli_root
from pyhanko.cli.runtime import pyhanko_exception_manager
from pyhanko.cli.utils import _warn_empty_passphrase, readable_file
from pyhanko.keys import load_certs_from_pemder
from pyhanko.pdf_utils import crypt
from pyhanko.pdf_utils.crypt import StandardSecurityHandler
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko.pdf_utils.writer import copy_into_new_writer
__all__ = ['decrypt', 'encrypt_file']
@cli_root.command(help='encrypt PDF files (AES-256 only)', name='encrypt')
@click.argument('infile', type=readable_file)
@click.argument('outfile', type=click.Path(writable=True, dir_okay=False))
@click.option(
'--password',
help='password to encrypt the file with',
required=False,
type=str,
)
@click.option(
'--recipient',
required=False,
multiple=True,
help='certificate(s) corresponding to entities that '
'can decrypt the output file',
type=click.Path(readable=True, dir_okay=False),
)
def encrypt_file(infile, outfile, password, recipient):
if password and recipient:
raise click.ClickException(
"Specify either a password or a list of recipients."
)
elif not password and not recipient:
password = getpass.getpass(prompt='Output file password: ')
recipient_certs = None
if recipient:
recipient_certs = list(load_certs_from_pemder(cert_files=recipient))
with pyhanko_exception_manager():
with open(infile, 'rb') as inf:
r = PdfFileReader(inf)
w = copy_into_new_writer(r)
if recipient_certs:
w.encrypt_pubkey(recipient_certs)
else:
w.encrypt(owner_pass=password)
with open(outfile, 'wb') as outf:
w.write(outf)
@cli_root.group(
help='decrypt PDF files (any standard PDF encryption scheme)',
name='decrypt',
)
def decrypt():
pass
decrypt_force_flag = click.option(
'--force',
help='ignore access restrictions (use at your own risk)',
required=False,
type=bool,
is_flag=True,
default=False,
)
@decrypt.command(help='decrypt using password', name='password')
@click.argument('infile', type=readable_file)
@click.argument('outfile', type=click.Path(writable=True, dir_okay=False))
@click.option(
'--password',
help='password to decrypt the file with',
required=False,
type=str,
)
@decrypt_force_flag
def decrypt_with_password(infile, outfile, password, force):
with pyhanko_exception_manager():
with open(infile, 'rb') as inf:
r = PdfFileReader(inf)
if r.security_handler is None:
raise click.ClickException("File is not encrypted.")
elif not isinstance(r.security_handler, StandardSecurityHandler):
raise click.ClickException(
"File is not encrypted with the standard (password-based) security handler"
)
if not password:
password = getpass.getpass(prompt='File password: ')
auth_result = r.decrypt(password)
if auth_result.status == crypt.AuthStatus.USER and not force:
raise click.ClickException(
"Password specified was the user password, not "
"the owner password. Pass --force to decrypt the "
"file anyway."
)
elif auth_result.status == crypt.AuthStatus.FAILED:
raise click.ClickException("Password didn't match.")
w = copy_into_new_writer(r)
with open(outfile, 'wb') as outf:
w.write(outf)
@decrypt.command(help='decrypt using private key (PEM/DER)', name='pemder')
@click.argument('infile', type=readable_file)
@click.argument('outfile', type=click.Path(writable=True, dir_okay=False))
@click.option(
'--key',
type=readable_file,
required=True,
help='file containing the recipient\'s private key (PEM/DER)',
)
@click.option(
'--cert',
help='file containing the recipient\'s certificate (PEM/DER)',
type=readable_file,
required=True,
)
@click.option(
'--passfile',
required=False,
type=click.File('rb'),
help='file containing the passphrase for the private key',
show_default='stdin',
)
@click.option(
'--no-pass',
help='assume the private key file is unencrypted',
type=bool,
is_flag=True,
default=False,
show_default=True,
)
@decrypt_force_flag
def decrypt_with_pemder(infile, outfile, key, cert, passfile, force, no_pass):
if passfile is not None:
passphrase = passfile.read()
passfile.close()
elif not no_pass:
passphrase = getpass.getpass(prompt='Key passphrase: ').encode('utf-8')
if not passphrase:
_warn_empty_passphrase()
passphrase = None
else:
passphrase = None
sedk = crypt.SimpleEnvelopeKeyDecrypter.load(
key, cert, key_passphrase=passphrase
)
_decrypt_pubkey(sedk, infile, outfile, force)
def _decrypt_pubkey(
sedk: crypt.SimpleEnvelopeKeyDecrypter, infile, outfile, force
):
with pyhanko_exception_manager():
with open(infile, 'rb') as inf:
r = PdfFileReader(inf)
if r.security_handler is None:
raise click.ClickException("File is not encrypted.")
if not isinstance(r.security_handler, crypt.PubKeySecurityHandler):
raise click.ClickException(
"File was not encrypted with a public-key security handler."
)
auth_result = r.decrypt_pubkey(sedk)
if auth_result.status == crypt.AuthStatus.USER:
# TODO read 2nd bit of perms in CMS enveloped data
# is the one indicating that change of encryption is OK
if not force:
raise click.ClickException(
"Change of encryption is typically not allowed with "
"user access. Pass --force to decrypt the file anyway."
)
elif auth_result.status == crypt.AuthStatus.FAILED:
raise click.ClickException("Failed to decrypt the file.")
w = copy_into_new_writer(r)
with open(outfile, 'wb') as outf:
w.write(outf)
@decrypt.command(help='decrypt using private key (PKCS#12)', name='pkcs12')
@click.argument('infile', type=readable_file)
@click.argument('outfile', type=click.Path(writable=True, dir_okay=False))
@click.argument('pfx', type=readable_file)
@click.option(
'--passfile',
required=False,
type=click.File('r'),
help='file containing the passphrase for the PKCS#12 file',
show_default='stdin',
)
@decrypt_force_flag
def decrypt_with_pkcs12(infile, outfile, pfx, passfile, force):
if passfile is None:
passphrase = getpass.getpass(prompt='Key passphrase: ').encode('utf-8')
else:
passphrase = passfile.readline().strip().encode('utf-8')
passfile.close()
sedk = crypt.SimpleEnvelopeKeyDecrypter.load_pkcs12(
pfx, passphrase=passphrase
)
_decrypt_pubkey(sedk, infile, outfile, force)