#!/usr/bin/env python3
#
#######################################################################
#
# A script to paste to https://cpaste.org/
#
# Copyright (c) 2013-2019 Andreas Schneider <asn@samba.org>
# Copyright (c) 2013      Alexander Bokovoy <ab@samba.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#######################################################################
#
# Requires: python3-requests
# Requires: python3-cryptography
#
# Optionally requires: python-Pygments
#

import os
import sys
import json
import base64
import zlib
import requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
from optparse import OptionParser
from mimetypes import guess_type
try:
    from pygments.lexers import guess_lexer, guess_lexer_for_filename
    from pygments.util import ClassNotFound
    guess_lang = True
except ImportError:
    guess_lang = False


def base58_encode(v: bytes):
    # 58 char alphabet
    alphabet = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    alphabet_len = len(alphabet)

    nPad = len(v)
    v = v.lstrip(b'\0')
    nPad -= len(v)

    x = 0
    for (i, c) in enumerate(v[::-1]):
        if isinstance(c, str):
            c = ord(c)
        x += c << (8 * i)

    string = b''
    while x:
        x, idx = divmod(x, alphabet_len)
        string = alphabet[idx:idx+1] + string

    return (alphabet[0:1] * nPad + string)


def json_encode(d):
    return json.dumps(d, separators=(',', ':')).encode('utf-8')


#
# The encryption format is described here:
# https://github.com/PrivateBin/PrivateBin/wiki/Encryption-format
#
def privatebin_encrypt(paste_passphrase,
                       paste_password,
                       paste_plaintext,
                       paste_formatter,
                       paste_attachment_name,
                       paste_attachment,
                       paste_compress,
                       paste_burn,
                       paste_opendicussion):
    if paste_password:
        paste_passphrase += bytes(paste_password, 'utf-8')

    # PBKDF
    kdf_salt = bytes(os.urandom(8))
    kdf_iterations = 100000
    kdf_keysize = 256  # size of resulting kdf_key

    backend = default_backend()
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),
                     length=int(kdf_keysize / 8),  # 256bit
                     salt=kdf_salt,
                     iterations=kdf_iterations,
                     backend=backend)
    kdf_key = kdf.derive(paste_passphrase)

    # AES-GCM
    adata_size = 128

    cipher_iv = bytes(os.urandom(int(adata_size / 8)))
    cipher_algo = "aes"
    cipher_mode = "gcm"

    compression_type = "none"
    if paste_compress:
        compression_type = "zlib"

    # compress plaintext
    paste_data = {'paste': paste_plaintext}
    if paste_attachment_name and paste_attachment:
        paste_data['attachment'] = paste_attachment
        paste_data['attachment_name'] = paste_attachment_name
        print(paste_attachment_name)
        print(paste_attachment)

    if paste_compress:
        zobj = zlib.compressobj(wbits=-zlib.MAX_WBITS)
        paste_blob = zobj.compress(json_encode(paste_data)) + zobj.flush()
    else:
        paste_blob = json_encode(paste_data)

    # Associated data to authenticate
    paste_adata = [
            [
                base64.b64encode(cipher_iv).decode("utf-8"),
                base64.b64encode(kdf_salt).decode("utf-8"),
                kdf_iterations,
                kdf_keysize,
                adata_size,
                cipher_algo,
                cipher_mode,
                compression_type,
            ],
            paste_formatter,
            int(paste_opendicussion),
            int(paste_burn),
    ]

    paste_adata_json = json_encode(paste_adata)

    aesgcm = AESGCM(kdf_key)
    ciphertext = aesgcm.encrypt(cipher_iv, paste_blob, paste_adata_json)

    # Validate
    # aesgcm.decrypt(cipher_iv, ciphertext, paste_adata_json)

    paste_ciphertext = base64.b64encode(ciphertext).decode("utf-8")

    return paste_adata, paste_ciphertext


def privatebin_send(paste_url,
                    paste_password,
                    paste_plaintext,
                    paste_formatter,
                    paste_attachment_name,
                    paste_attachment,
                    paste_compress,
                    paste_burn,
                    paste_opendicussion,
                    paste_expire):
    paste_passphrase = bytes(os.urandom(32))

    paste_adata, paste_ciphertext = privatebin_encrypt(paste_passphrase,
                                                       paste_password,
                                                       paste_plaintext,
                                                       paste_formatter,
                                                       paste_attachment_name,
                                                       paste_attachment,
                                                       paste_compress,
                                                       paste_burn,
                                                       paste_opendicussion)

    # json payload for the post API
    # https://github.com/PrivateBin/PrivateBin/wiki/API
    payload = {
            "v": 2,
            "adata": paste_adata,
            "ct": paste_ciphertext,
            "meta": {
                "expire": paste_expire,
            }
    }

    # http content type
    headers = {'X-Requested-With': 'JSONHttpRequest'}

    r = requests.post(paste_url,
                      data=json_encode(payload),
                      headers=headers)
    r.raise_for_status()

    try:
        result = r.json()
    except:
        print('Oops, error: %s' % (r.text))
        sys.exit(1)

    paste_status = result['status']
    if paste_status:
        paste_message = result['message']
        print("Oops, error: %s" % paste_message)
        sys.exit(1)

    paste_id = result['id']
    paste_url_id = result['url']
    paste_deletetoken = result['deletetoken']

    print('Delete paste: %s/?pasteid=%s&deletetoken=%s' %
          (paste_url, paste_id, paste_deletetoken))
    print('')
    print('### Paste (%s): %s%s#%s' %
          (paste_formatter,
           paste_url,
           paste_url_id,
           base58_encode(paste_passphrase).decode('utf-8')))


def guess_lang_formatter(paste_plaintext, paste_filename=None):
    paste_formatter = 'plaintext'

    # Map numpy to python because the numpy lexer gives false positives
    # when guessing.
    lexer_lang_map = {'numpy': 'python'}

    # If we have a filename, try guessing using the more reliable
    # guess_lexer_for_filename function.
    # If that fails, try the guess_lexer function on the code.
    lang = None
    if paste_filename:
        try:
            lang = guess_lexer_for_filename(paste_filename,
                                            paste_plaintext).name.lower()
        except ClassNotFound:
            print("No guess by filename")
            pass
    else:
        try:
            lang = guess_lexer(paste_plaintext).name.lower()
        except ClassNotFound:
            pass

    if lang:
        if lang == 'markdown':
            paste_formatter = 'markdown'
        if lang != 'text only':
            paste_formatter = 'syntaxhighlighting'

    return paste_formatter


def main():
    parser = OptionParser()

    parser.add_option("-f", "--file", dest="filename",
                      help="Read from a file instead of stdin",
                      metavar="FILE")
    parser.add_option("-p", "--password", dest="password",
                      help="Create a password protected paste",
                      metavar="PASSWORD")
    parser.add_option("-e", "--expire",
                      action="store", dest="expire", default="1day",
                      choices=["5min",
                               "10min",
                               "1hour",
                               "1day",
                               "1week",
                               "1month",
                               "1year",
                               "never"],
                      help="Expiration time of the paste (default: 1day)")
    parser.add_option("-s", "--sourcecode",
                      action="store_true", dest="source", default=False,
                      help="Use source code highlighting")
    parser.add_option("-m", "--markdown",
                      action="store_true", dest="markdown", default=False,
                      help="Parse paste as markdown")
    parser.add_option("-b", "--burn",
                      action="store_true", dest="burn", default=False,
                      help="Burn paste after reading")
    parser.add_option("-o", "--opendiscussion",
                      action="store_true", dest="opendiscussion",
                      default=False,
                      help="Allow discussion for the paste")
    parser.add_option("-a", "--attachment", dest="attachment",
                      help="Specify path to a file to attachment to the paste",
                      metavar="FILE")

    (options, args) = parser.parse_args()

    paste_url = 'https://cpaste.org'
    paste_formatter = 'plaintext'
    paste_compress = True
    paste_expire = '1day'
    paste_opendiscussion = 0
    paste_burn = 0
    paste_password = None
    paste_attachment_name = None
    paste_attachment = None

    if options.filename:
        f = open(options.filename)
        if not f:
            print("Oops, could not open file!")

        paste_plaintext = f.read()
        f.close()
    else:
        paste_plaintext = sys.stdin.read()

    if not paste_plaintext:
        print("Oops, we have no data")
        sys.exit(1)

    if options.burn:
        paste_burn = 1

    if options.opendiscussion:
        paste_opendiscussion = 1

    if options.source:
        paste_formatter = 'syntaxhighlighting'
    elif options.markdown:
        paste_formatter = 'markdown'
    elif guess_lang:
        paste_formatter = guess_lang_formatter(paste_plaintext,
                                               options.filename)

    if options.expire:
        paste_expire = options.expire

    if options.password:
        paste_password = options.password

    if options.attachment:
        paste_attachment_name = os.path.basename(options.attachment)
        mime = guess_type(options.attachment, strict=False)[0]
        if not mime:
            mime = 'application/octet-stream'

        f = open(options.attachment, mode='rb')
        if not f:
            print("Oops, could not open file for attachment!")

        data = f.read()
        f.close()

        paste_attachment = 'data:%s;base64,' % (mime)
        paste_attachment += base64.b64encode(data).decode('utf-8')

    privatebin_send(paste_url,
                    paste_password,
                    paste_plaintext,
                    paste_formatter,
                    paste_attachment_name,
                    paste_attachment,
                    paste_compress,
                    paste_burn,
                    paste_opendiscussion,
                    paste_expire)

    sys.exit(0)


if __name__ == "__main__":
    main()

