215 lines
7.1 KiB
Python
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)
|