# -*- coding: utf-8 -*-
#
#  Copyright 2011 Sybren A. Stüvel <sybren@stuvel.eu>
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

"""Commandline scripts.

These scripts are called by the executables defined in setup.py.
"""

from __future__ import with_statement, print_function

import abc
import sys
from optparse import OptionParser

import rsa
import rsa.bigfile
import rsa.pkcs1

HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys())


def keygen():
    """Key generator."""

    # Parse the CLI options
    parser = OptionParser(usage='usage: %prog [options] keysize',
                          description='Generates a new RSA keypair of "keysize" bits.')

    parser.add_option('--pubout', type='string',
                      help='Output filename for the public key. The public key is '
                           'not saved if this option is not present. You can use '
                           'pyrsa-priv2pub to create the public key file later.')

    parser.add_option('-o', '--out', type='string',
                      help='Output filename for the private key. The key is '
                           'written to stdout if this option is not present.')

    parser.add_option('--form',
                      help='key format of the private and public keys - default PEM',
                      choices=('PEM', 'DER'), default='PEM')

    (cli, cli_args) = parser.parse_args(sys.argv[1:])

    if len(cli_args) != 1:
        parser.print_help()
        raise SystemExit(1)

    try:
        keysize = int(cli_args[0])
    except ValueError:
        parser.print_help()
        print('Not a valid number: %s' % cli_args[0], file=sys.stderr)
        raise SystemExit(1)

    print('Generating %i-bit key' % keysize, file=sys.stderr)
    (pub_key, priv_key) = rsa.newkeys(keysize)

    # Save public key
    if cli.pubout:
        print('Writing public key to %s' % cli.pubout, file=sys.stderr)
        data = pub_key.save_pkcs1(format=cli.form)
        with open(cli.pubout, 'wb') as outfile:
            outfile.write(data)

    # Save private key
    data = priv_key.save_pkcs1(format=cli.form)

    if cli.out:
        print('Writing private key to %s' % cli.out, file=sys.stderr)
        with open(cli.out, 'wb') as outfile:
            outfile.write(data)
    else:
        print('Writing private key to stdout', file=sys.stderr)
        sys.stdout.write(data)


class CryptoOperation(object):
    """CLI callable that operates with input, output, and a key."""

    __metaclass__ = abc.ABCMeta

    keyname = 'public'  # or 'private'
    usage = 'usage: %%prog [options] %(keyname)s_key'
    description = None
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    input_help = 'Name of the file to %(operation)s. Reads from stdin if ' \
                 'not specified.'
    output_help = 'Name of the file to write the %(operation_past)s file ' \
                  'to. Written to stdout if this option is not present.'
    expected_cli_args = 1
    has_output = True

    key_class = rsa.PublicKey

    def __init__(self):
        self.usage = self.usage % self.__class__.__dict__
        self.input_help = self.input_help % self.__class__.__dict__
        self.output_help = self.output_help % self.__class__.__dict__

    @abc.abstractmethod
    def perform_operation(self, indata, key, cli_args=None):
        """Performs the program's operation.

        Implement in a subclass.

        :returns: the data to write to the output.
        """

    def __call__(self):
        """Runs the program."""

        (cli, cli_args) = self.parse_cli()

        key = self.read_key(cli_args[0], cli.keyform)

        indata = self.read_infile(cli.input)

        print(self.operation_progressive.title(), file=sys.stderr)
        outdata = self.perform_operation(indata, key, cli_args)

        if self.has_output:
            self.write_outfile(outdata, cli.output)

    def parse_cli(self):
        """Parse the CLI options

        :returns: (cli_opts, cli_args)
        """

        parser = OptionParser(usage=self.usage, description=self.description)

        parser.add_option('-i', '--input', type='string', help=self.input_help)

        if self.has_output:
            parser.add_option('-o', '--output', type='string', help=self.output_help)

        parser.add_option('--keyform',
                          help='Key format of the %s key - default PEM' % self.keyname,
                          choices=('PEM', 'DER'), default='PEM')

        (cli, cli_args) = parser.parse_args(sys.argv[1:])

        if len(cli_args) != self.expected_cli_args:
            parser.print_help()
            raise SystemExit(1)

        return cli, cli_args

    def read_key(self, filename, keyform):
        """Reads a public or private key."""

        print('Reading %s key from %s' % (self.keyname, filename), file=sys.stderr)
        with open(filename, 'rb') as keyfile:
            keydata = keyfile.read()

        return self.key_class.load_pkcs1(keydata, keyform)

    def read_infile(self, inname):
        """Read the input file"""

        if inname:
            print('Reading input from %s' % inname, file=sys.stderr)
            with open(inname, 'rb') as infile:
                return infile.read()

        print('Reading input from stdin', file=sys.stderr)
        return sys.stdin.read()

    def write_outfile(self, outdata, outname):
        """Write the output file"""

        if outname:
            print('Writing output to %s' % outname, file=sys.stderr)
            with open(outname, 'wb') as outfile:
                outfile.write(outdata)
        else:
            print('Writing output to stdout', file=sys.stderr)
            sys.stdout.write(outdata)


class EncryptOperation(CryptoOperation):
    """Encrypts a file."""

    keyname = 'public'
    description = ('Encrypts a file. The file must be shorter than the key '
                   'length in order to be encrypted. For larger files, use the '
                   'pyrsa-encrypt-bigfile command.')
    operation = 'encrypt'
    operation_past = 'encrypted'
    operation_progressive = 'encrypting'

    def perform_operation(self, indata, pub_key, cli_args=None):
        """Encrypts files."""

        return rsa.encrypt(indata, pub_key)


class DecryptOperation(CryptoOperation):
    """Decrypts a file."""

    keyname = 'private'
    description = ('Decrypts a file. The original file must be shorter than '
                   'the key length in order to have been encrypted. For larger '
                   'files, use the pyrsa-decrypt-bigfile command.')
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    key_class = rsa.PrivateKey

    def perform_operation(self, indata, priv_key, cli_args=None):
        """Decrypts files."""

        return rsa.decrypt(indata, priv_key)


class SignOperation(CryptoOperation):
    """Signs a file."""

    keyname = 'private'
    usage = 'usage: %%prog [options] private_key hash_method'
    description = ('Signs a file, outputs the signature. Choose the hash '
                   'method from %s' % ', '.join(HASH_METHODS))
    operation = 'sign'
    operation_past = 'signature'
    operation_progressive = 'Signing'
    key_class = rsa.PrivateKey
    expected_cli_args = 2

    output_help = ('Name of the file to write the signature to. Written '
                   'to stdout if this option is not present.')

    def perform_operation(self, indata, priv_key, cli_args):
        """Signs files."""

        hash_method = cli_args[1]
        if hash_method not in HASH_METHODS:
            raise SystemExit('Invalid hash method, choose one of %s' %
                             ', '.join(HASH_METHODS))

        return rsa.sign(indata, priv_key, hash_method)


class VerifyOperation(CryptoOperation):
    """Verify a signature."""

    keyname = 'public'
    usage = 'usage: %%prog [options] public_key signature_file'
    description = ('Verifies a signature, exits with status 0 upon success, '
                   'prints an error message and exits with status 1 upon error.')
    operation = 'verify'
    operation_past = 'verified'
    operation_progressive = 'Verifying'
    key_class = rsa.PublicKey
    expected_cli_args = 2
    has_output = False

    def perform_operation(self, indata, pub_key, cli_args):
        """Verifies files."""

        signature_file = cli_args[1]

        with open(signature_file, 'rb') as sigfile:
            signature = sigfile.read()

        try:
            rsa.verify(indata, signature, pub_key)
        except rsa.VerificationError:
            raise SystemExit('Verification failed.')

        print('Verification OK', file=sys.stderr)


class BigfileOperation(CryptoOperation):
    """CryptoOperation that doesn't read the entire file into memory."""

    def __init__(self):
        CryptoOperation.__init__(self)

        self.file_objects = []

    def __del__(self):
        """Closes any open file handles."""

        for fobj in self.file_objects:
            fobj.close()

    def __call__(self):
        """Runs the program."""

        (cli, cli_args) = self.parse_cli()

        key = self.read_key(cli_args[0], cli.keyform)

        # Get the file handles
        infile = self.get_infile(cli.input)
        outfile = self.get_outfile(cli.output)

        # Call the operation
        print(self.operation_progressive.title(), file=sys.stderr)
        self.perform_operation(infile, outfile, key, cli_args)

    def get_infile(self, inname):
        """Returns the input file object"""

        if inname:
            print('Reading input from %s' % inname, file=sys.stderr)
            fobj = open(inname, 'rb')
            self.file_objects.append(fobj)
        else:
            print('Reading input from stdin', file=sys.stderr)
            fobj = sys.stdin

        return fobj

    def get_outfile(self, outname):
        """Returns the output file object"""

        if outname:
            print('Will write output to %s' % outname, file=sys.stderr)
            fobj = open(outname, 'wb')
            self.file_objects.append(fobj)
        else:
            print('Will write output to stdout', file=sys.stderr)
            fobj = sys.stdout

        return fobj


class EncryptBigfileOperation(BigfileOperation):
    """Encrypts a file to VARBLOCK format."""

    keyname = 'public'
    description = ('Encrypts a file to an encrypted VARBLOCK file. The file '
                   'can be larger than the key length, but the output file is only '
                   'compatible with Python-RSA.')
    operation = 'encrypt'
    operation_past = 'encrypted'
    operation_progressive = 'encrypting'

    def perform_operation(self, infile, outfile, pub_key, cli_args=None):
        """Encrypts files to VARBLOCK."""

        return rsa.bigfile.encrypt_bigfile(infile, outfile, pub_key)


class DecryptBigfileOperation(BigfileOperation):
    """Decrypts a file in VARBLOCK format."""

    keyname = 'private'
    description = ('Decrypts an encrypted VARBLOCK file that was encrypted '
                   'with pyrsa-encrypt-bigfile')
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    key_class = rsa.PrivateKey

    def perform_operation(self, infile, outfile, priv_key, cli_args=None):
        """Decrypts a VARBLOCK file."""

        return rsa.bigfile.decrypt_bigfile(infile, outfile, priv_key)


encrypt = EncryptOperation()
decrypt = DecryptOperation()
sign = SignOperation()
verify = VerifyOperation()
encrypt_bigfile = EncryptBigfileOperation()
decrypt_bigfile = DecryptBigfileOperation()
