#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Generate valid and signed descriptors for mocked Tor relays or bridges.

.. todo:: Finish enough CFFI_ bindings for the newer PyNaCl_ (or enough of the
    SWIG_ bindings for the older pynacl_) to be able to emulate the following
    curvecp_ command (the ``curvecp*`` commands require libchloride_):

        $ curvecpmakekey ntor-key
        $ curvecpprintkey ntor-key > ntor-key.hex
        $ python -c 'import binascii, sys; \
            key_hex=open('./ntor-key.hex','rb').read();\
            key_b64=binascii.b2a_base64(binascii.unhexlify(key_hex));\
            sys.stdout.write(key_b64);'

    .. _CFFI: https://cffi.readthedocs.org
    .. _PyNaCl: https://github.com/seanlynch/pynacl
    .. _SWIG: https://github.com/swig/swig
    .. _pynacl: https://github.com/seanlynch/pynacl
    .. _curvecp: http://curvecp.org/
    .. _libchloride: https://github.com/jedisct1/libchloride

.. authors:: Isis Lovecruft <isis@torproject.org> 0xA3ADB67A2CDB8B35
             Matthew Finkel <sysrqb@torproject.org>
.. licence:: see LICENSE file for licensing details
.. copyright:: (c) 2013-2014 The Tor Project, Inc.
               (c) 2013-2014 Isis Lovecruft
               (c) 2013-2014 Matthew Finkel
"""

from __future__ import print_function
from __future__ import absolute_import

import binascii
import hashlib
import ipaddr
import logging
import math
import os
import sys
import random
import re
import traceback

from datetime import datetime
from codecs   import open as open

try:
    import OpenSSL.crypto
except (ImportError, NameError) as error:
    print("This script requires pyOpenSSL>=0.13.0")
    raise SystemExit(error.message)

from leekspin import const
from leekspin import crypto
from leekspin import nicknames
from leekspin import ntor
from leekspin import rsa
from leekspin import tls
from leekspin import torversions
from leekspin import util


logging.getLogger('leekspin')

#: If the pynacl was found by :attr:`leekspin.ntor.nacl`.
nacl = ntor.nacl


def makeProtocolsLine(version=None):
    """Generate an appropriate [bridge-]server-descriptor 'protocols' line.

    :param string version: One of ``SERVER_VERSIONS``.
    :rtype: string
    :returns: An '@type [bridge-]server-descriptor' 'protocols' line.
    """
    line = ''
    if (version is not None) and torversions.shouldHaveOptPrefix(version):
        line += 'opt '
    line += 'protocols Link 1 2 Circuit 1'
    return line


def makeFingerprintLine(fingerprint, version=None):
    """Generate an appropriate [bridge-]server-descriptor 'fingerprint' line.

    For example, for tor-0.2.3.25 and prior versions, this would look like:
      |
      | opt fingerprint D4BB C339 2560 1B7F 226E 133B A85F 72AF E734 0B29
      |

    :param string fingerprint: A public key fingerprint in groups of four,
         separated by spaces.
    :param string version: One of ``SERVER_VERSIONS``.
    :rtype: string
    :returns: An '@type [bridge-]server-descriptor' 'published' line.
    """
    line = ''
    if (version is not None) and torversions.shouldHaveOptPrefix(version):
        line += 'opt '
    line += 'fingerprint %s' % crypto.convertToSpaceyFingerprint(fingerprint)
    return line

def makeBandwidthLine(variance=30):
    """Create a random 'bandwidth' line with some plausible burst variance.

    From torspec.git/dir-spec.txt, §2.1 "Router descriptors":
      | "bandwidth" bandwidth-avg bandwidth-burst bandwidth-observed NL
      |
      | [Exactly once]
      |
      |   Estimated bandwidth for this router, in bytes per second.  The
      |   "average" bandwidth is the volume per second that the OR is willing
      |   to sustain over long periods; the "burst" bandwidth is the volume
      |   that the OR is willing to sustain in very short intervals.  The
      |   "observed" value is an estimate of the capacity this relay can
      |   handle.  The relay remembers the max bandwidth sustained output over
      |   any ten second period in the past day, and another sustained input.
      |   The "observed" value is the lesser of these two numbers.

    The "observed" bandwidth, in this function, is taken as some random value,
    bounded between 20KB/s and 2MB/s. For example, say:

    >>> import math
    >>> variance = 25
    >>> observed = 180376
    >>> percentage = float(variance) / 100.
    >>> percentage
    0.25

    The ``variance`` in this context is the percentage of the "observed"
    bandwidth, which will be added to the "observed" bandwidth, and becomes
    the value for the "burst" bandwidth:

    >>> burst = observed + math.ceil(observed * percentage)
    >>> assert burst > observed

    This doesn't do much, since the "burst" bandwidth in a real
    [bridge-]server-descriptor is reported by the OR; this function mostly
    serves to avoid generating completely-crazy, totally-implausible bandwidth
    values. The "average" bandwidth value is then just the mean value of the
    other two.

    :param integer variance: The percent of the fake "observed" bandwidth to
        increase the "burst" bandwidth by.
    :rtype: string
    :returns: A "bandwidth" line for a [bridge-]server-descriptor.
    """
    observed = random.randint(20 * 2**10, 2 * 2**30)
    percentage = float(variance) / 100.
    burst = int(observed + math.ceil(observed * percentage))
    bandwidths = [burst, observed]
    nitems = len(bandwidths) if (len(bandwidths) > 0) else float('nan')
    avg = int(math.ceil(float(sum(bandwidths)) / nitems))
    line = "bandwidth %s %s %s" % (avg, burst, observed)
    return line

def makeExtraInfoDigestLine(hexdigest, version):
    """Create a line to embed the hex SHA-1 digest of the extrainfo.

    :param string hexdigest: Should be the hex-encoded (uppercase) output of
        the SHA-1 digest of the generated extrainfo document (this is the
        extra-info descriptor, just without the signature at the end). This is
        the same exact digest which gets signed by the OR server identity key,
        and that signature is appended to the extrainfo document to create the
        extra-info descriptor.
    :param string version: One of ``SERVER_VERSIONS``.
    :rtype: string
    :returns: An ``@type [bridge-]server-descriptor`` 'extra-info-digest'
        line.
    """
    line = ''
    if (version is not None) and torversions.shouldHaveOptPrefix(version):
        line += 'opt '
    line += 'extra-info-digest %s' % hexdigest
    return line

def makeHSDirLine(version):
    """This line doesn't do much… all the cool kids are HSDirs these days.

    :param string version: One of ``SERVER_VERSIONS``.
    :rtype: string
    :returns: An ``@type [bridge-]server-descriptor`` 'hidden-service-dir'
        line.
    """
    line = ''
    if (version is not None) and torversions.shouldHaveOptPrefix(version):
        line += 'opt '
    line += 'hidden-service-dir'
    return line

def makeOnionKeys(bridge=True, digest='sha1'):
    """Make all the keys and certificates necessary to fake an OR.

    The encodings for the various key and descriptor digests needed are
    described in dir-spec.txt and tor-spec.txt, the latter mostly for the
    padding and encoding used in the creation of an OR's keys.

    For the "router" line in a networkstatus document, the following encodings
    are specified:

    From dir-spec.txt, commit 36761c7d5, L1504-1512:
      |
      |                                 […] "Identity" is a hash of its
      | identity key, encoded in base64, with trailing equals sign(s)
      | removed.  "Digest" is a hash of its most recent descriptor as
      | signed (that is, not including the signature), encoded in base64.
      |

    Before the hash digest of an OR's identity key is base64-encoded for
    inclusion in a networkstatus document, the hash digest is created in the
    following manner:

    From tor-spec.txt, commit 36761c7d5, L109-110:
      |
      | When we refer to "the hash of a public key", we mean the SHA-1 hash of the
      | DER encoding of an ASN.1 RSA public key (as specified in PKCS.1).
      |

    From tor-spec.txt, commit 36761c7d5, L785-787:
      |
      | The "legacy identity" and "identity fingerprint" fields are the SHA1
      | hash of the PKCS#1 ASN1 encoding of the next onion router's identity
      | (signing) key.  (See 0.3 above.)
      |

    :param boolean bridge: If False, generate a server OR ID key, a signing
        key, and a TLS certificate/key pair. If True, generate a client ID key
        as well.
    :param string digest: The digest to use. (default: 'sha1')
    :returns: The server ID key, and a tuple of strings (fingerprint,
       onion-key, signing-key), where onion-key and secret key are the strings
       which should directly go into a server-descriptor. There are a *ton* of
       keys and certs in the this function. If you need more for some reason,
       this is definitely the thing you want to modify.
    """
    serverID = rsa.createKey(True)
    SIDSKey, SIDSCert, SIDPKey, SIDPCert = serverID
    serverLinkCert = tls.createTLSLinkCert()
    serverLinkCert.sign(SIDSKey, digest)

    if bridge:
        # For a bridge, a "client" ID key is used to generate the fingerprint
        clientID = rsa.createKey(True)
        CIDSKey, CIDSCert, CIDPKey, CIDPCert = clientID

        # XXX I think we're missing some of the signatures
        #     see torspec.git/tor-spec.txt §4.2 on CERTS cells
        clientLinkCert = tls.createTLSLinkCert()
        clientLinkCert.sign(CIDSKey, digest)
    else:
        CIDSKey, CIDSCert, CIDPKey, CIDPCert = serverID

    signing = rsa.createKey()
    signSKey, signSCert, signPKey, signPCert = signing
    onion = rsa.createKey()
    onionSKey, onionSCert, onionPKey, onionPCert = onion

    onionKeyString   = 'onion-key\n%s' % tls.getPublicKey(onionPCert)
    signingKeyString = 'signing-key\n%s' % tls.getPublicKey(signPCert)

    return SIDSKey, SIDPCert, (onionKeyString, signingKeyString)

def generateExtraInfo(nickname, fingerprint, ts, ipv4, port):
    """Create an OR extra-info document.

    See §2.2 "Extra-info documents" in torspec.git/dir-spec.txt.

    :param str nickname: The router's nickname.
    :param str fingerprint: A space-separated, hex-encoded, SHA-1 digest of
        the OR's private identity key. See :func:`convertToSpaceyFingerprint`.
    :param str ts: An ISO-8601 timestamp. See :func:`makeTimeStamp`.
    :param str ipv4: An IPv4 address.
    :param str port: The OR's ORPort.
    :rtype: str
    :returns: An extra-info document (unsigned).
    """
    extra = []
    extra.append("extra-info %s %s" % (nickname, fingerprint))
    extra.append("published %s" % ts)
    extra.append("write-history %s (900 s)\n3188736,2226176,2866176" % ts)
    extra.append("read-history %s (900 s)\n3891200,2483200,2698240" % ts)
    extra.append("dirreq-write-history %s (900 s)\n1024,0,2048" % ts)
    extra.append("dirreq-read-history %s (900 s)\n0,0,0" % ts)
    extra.append("geoip-db-digest %s\ngeoip6-db-digest %s"
                 % (util.getHexString(40), util.getHexString(40)))
    extra.append("dirreq-stats-end %s (86400 s)\ndirreq-v3-ips" % ts)
    extra.append("dirreq-v3-reqs\ndirreq-v3-resp")
    extra.append(
        "ok=16,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0")
    extra.append("dirreq-v3-direct-dl complete=0,timeout=0,running=0")
    extra.append("dirreq-v3-tunneled-dl complete=12,timeout=0,running=0")
    extra.append("transport obfs3 %s:%d" % (ipv4, port + 1))
    extra.append("transport obfs2 %s:%d" % (ipv4, port + 2))
    extra.append("bridge-stats-end %s (86400 s)\nbridge-ips ca=8" % ts)
    extra.append("bridge-ip-versions v4=8,v6=0\nbridge-ip-transports <OR>=8")
    extra.append("router-signature\n")

    return '\n'.join(extra)

def generateNetstatus(nickname, idkey_digest, server_desc_digest, timestamp,
                      ipv4, orport, ipv6=None, dirport=None,
                      flags='Fast Guard Running Stable Valid',
                      bandwidth_line=None):
    """Generate an ``@type networkwork-status`` document (unsigned).

    DOCDOC

    :param str nickname: The router's nickname.
    :param string idkey_digest: The SHA-1 digest of the router's public identity
        key.
    :param XXX server_desc_digest: The SHA-1 digest of the router's
        ``@type [bridge-]server-descriptor``, before the descriptor is signed.
    :param XXX timestamp:
    """

    idkey_b64  = binascii.b2a_base64(idkey_digest)
    idb64      = str(idkey_b64).strip().rstrip('==')
    server_b64 = binascii.b2a_base64(server_desc_digest)
    srvb64     = str(server_b64).strip().rstrip('==')

    if bandwidth_line is not None:
        bw = int(bandwidth_line.split()[-1]) / 1024  # The 'observed' value
    dirport = dirport if dirport else 0

    status = []
    status.append("r %s %s %s %s %s %s %d" % (nickname, idb64, srvb64, timestamp,
                                              ipv4, orport, dirport))
    if ipv6 is not None:
        status.append("a [%s]:%s" % (ipv6, orport))
    status.append("s %s\nw Bandwidth=%s\np reject 1-65535\n" % (flags, bw))

    return '\n'.join(status)

def signDescriptorDigest(key, descriptorDigest, digest='sha1'):
    """Ugh...I hate OpenSSL.

    The extra-info-digest is a SHA-1 hash digest of the extrainfo document,
    that is, the entire extrainfo descriptor up until the end of the
    'router-signature' line and including the newline, but not the actual
    signature.

    The signature at the end of the extra-info descriptor is a signature of
    the above extra-info-digest. This signature is appended to the end of the
    extrainfo document, and the extra-info-digest is added to the
    'extra-info-digest' line of the [bridge-]server-descriptor.

    The first one of these was created with a raw digest, the second with a
    hexdigest. They both encode the the 'sha1' digest type if you check the
    `-asnparse` output (instead of `-raw -hexdump`).

    .. command:: openssl rsautl -inkey eiprivkey -verify -in eisig1 -raw -hexdump
      |
      | 0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0050 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 21 30   .............0!0
      | 0060 - 09 06 05 2b 0e 03 02 1a-05 00 04 14 42 25 41 fb   ...+........B%A.
      | 0070 - 82 ef 11 f4 5f 2c 95 53-67 2d bb fe 7f c2 34 7f   ...._,.Sg-....4.

    .. command:: openssl rsautl -inkey eiprivkey -verify -in eisig2 -raw -hexdump
      |
      | 0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff   ................
      | 0050 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 21 30   .............0!0
      | 0060 - 09 06 05 2b 0e 03 02 1a-05 00 04 14 44 30 ab 90   ...+........D0..
      | 0070 - 93 d1 08 21 df 87 c2 39-2a 04 1c a5 bb 34 44 cd   ...!...9*....4D.

    .. todo:: See the RSA PKCS_ Standard v2.2 for why this function is totally
       wrong.

    .. _PKCS: http://www.emc.com/collateral/white-papers/h11300-pkcs-1v2-2-rsa-cryptography-standard-wp.pdf

    :type key: :class:`OpenSSL.crypto.PKey`
    :param key: An RSA private key.
    :param string descriptorDigest: The raw SHA-1 digest of any descriptor
        document.
    :param string digest: The digest to use. (default: 'sha1')
    """
    sig = binascii.b2a_base64(OpenSSL.crypto.sign(key, descriptorDigest,
                                                  digest))
    sigCopy = sig
    originalLength = len(sigCopy.replace('\n', ''))

    # Only put 64 bytes of the base64 signature per line:
    sigSplit = []
    while len(sig) > 0:
        sigSplit.append(sig[:64])
        sig = sig[64:]
    sigFormatted = '\n'.join(sigSplit)

    sigFormattedCopy = sigFormatted
    formattedLength = len(sigFormattedCopy.replace('\n', ''))

    if originalLength != formattedLength:
        print("WARNING: signDescriptorDocument(): %s"
              % "possible bad reformatting for signature.")
        print("DEBUG: signDescriptorDocument(): original=%d formatted=%d"
              % (originalLength, formattedLength))
        print("DEBUG: original:\n%s\nformatted:\n%s"
              % (sigCopy, sigFormatted))

    sigWithHeaders = const.TOR_BEGIN_SIG + '\n' \
                     + sigFormatted \
                     + const.TOR_END_SIG + '\n'
    return sigWithHeaders

def generateDescriptors():
    """Create keys, certs, signatures, documents and descriptors for an OR.

    :returns:
        A 3-tuple of strings:
          - a ``@type [bridge-]extra-info`` descriptor,
          - a ``@type [bridge-]server-descriptor``, and
          - a ``@type network-status`` document
       for a mock Tor relay/bridge.
    """
    ipv4 = util.randomIPv4()
    ipv6 = util.randomIPv6()
    port = util.randomPort()

    nick = nicknames.generateNickname()
    vers = torversions.getRandomVersion()
    uptime = int(random.randint(1800, 63072000))
    bandwidth = makeBandwidthLine()
    timestamp = util.makeTimeStamp(variation=True, period=36)
    protocols = makeProtocolsLine(vers)

    SIDSKey, SIDPCert, (onionkey, signingkey) = makeOnionKeys()
    idkey_private = tls.getPrivateKey(SIDSKey)
    idkey_digest = hashlib.sha1(idkey_private).digest()

    idkey_public = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_ASN1,
                                                  SIDPCert.get_pubkey())
    idkey_public = re.sub(const.OPENSSL_BEGIN_KEY, '', idkey_public)
    idkey_public = re.sub(const.OPENSSL_END_KEY, '', idkey_public)
    idkey_public = idkey_public.strip()

    ident_digest = hashlib.sha1(idkey_public).digest()
    fingerprint = hashlib.sha1(idkey_public).hexdigest().upper()
    fpr = crypto.convertToSpaceyFingerprint(fingerprint)

    extrainfo_document = generateExtraInfo(nick, fingerprint,
                                           timestamp, ipv4, port)
    extrainfo_digest = hashlib.sha1(extrainfo_document).digest()
    extrainfo_hexdigest = hashlib.sha1(extrainfo_document).hexdigest().upper()
    extrainfo_sig = signDescriptorDigest(SIDSKey, extrainfo_digest)
    extrainfo_desc = extrainfo_document + extrainfo_sig

    server = []
    server.append("@purpose bridge")
    server.append("router %s %s %s 0 0" % (nick, ipv4, port))
    server.append("or-address [%s]:%s" % (ipv6, port))
    server.append("platform Tor %s on Linux" % vers)
    server.append("%s\npublished %s" % (protocols, timestamp))
    server.append("%s" % makeFingerprintLine(fingerprint, vers))
    server.append("uptime %s\n%s" % (uptime, bandwidth))
    server.append("%s" % makeExtraInfoDigestLine(extrainfo_hexdigest, vers))
    server.append("%s%s%s" % (onionkey, signingkey, makeHSDirLine(vers)))
    server.append("contact Somebody <somebody@example.com>")
    if nacl is not None:
        ntorkey = ntor.getNTORPublicKey()
        if ntorkey is not None:
            server.append("ntor-onion-key %s" % ntorkey)
    server.append("reject *:*\nrouter-signature\n")
    server_desc = '\n'.join(server)

    server_desc_digest = hashlib.sha1(server_desc).digest()
    netstatus_desc = generateNetstatus(nick, ident_digest, server_desc_digest,
                                       timestamp, ipv4, port, ipv6=ipv6,
                                       bandwidth_line=bandwidth)
    server_desc += signDescriptorDigest(SIDSKey, server_desc_digest)
    return extrainfo_desc, server_desc, netstatus_desc

def writeDescToFile(filename, descriptors):
    """Open ``filename`` and write a string containing descriptors into it.

    :param string filename: The name of the file to write to.
    :param string descriptors: A giant string containing descriptors,
        newlines, formatting, whatever is necessary to make it look like a
        file tor would generate.
    """
    encoding = sys.getfilesystemencoding()
    descript = descriptors.encode(encoding, 'replace')
    try:
        with open(filename, 'wb', encoding=encoding, errors='replace') as fh:
            fh.write(descript)
            fh.flush()
    except (IOError, OSError) as err:
        print("Failure while attempting to write descriptors to file '%s': %s"
              % (filename, err.message))

def create(count):
    """Generate all types of descriptors and write them to files.

    :param integer count: How many sets of descriptors to generate, i.e. how
        many mock bridges/relays to create.
    """
    if nacl is None:
        logging.warn("WARNING: Can't import PyNaCl. NTOR key generation is disabled.")
    print("Generating %d bridge descriptors..." % int(count))
    logging.info("Generated router nicknames:")

    server_descriptors    = list()
    netstatus_consensus   = list()
    extrainfo_descriptors = list()
    try:
        for i in xrange(int(count)):
            try:
                extrainfo, server, netstatus = generateDescriptors()
            except Exception as error:
                err, msg, tb = sys.exc_info()
                print(traceback.print_tb(tb))
                print(error)
            else:
                server_descriptors.append(server)
                netstatus_consensus.append(netstatus)
                extrainfo_descriptors.append(extrainfo)
    except KeyboardInterrupt as keyint:
        print("Received keyboard interrupt.")
        print("Stopping descriptor creation and exiting.")
        code = 1515
    finally:
        print("Writing descriptors to files...", end="")

        cached = "cached-extrainfo.new"
        descriptor_files = {
            "networkstatus-bridges": ''.join(netstatus_consensus),
            "bridge-descriptors": ''.join(server_descriptors),
            "cached-extrainfo.new": ''.join(extrainfo_descriptors)}

        if not os.path.isfile(cached):
            with open(cached, 'wb') as fh:
                fh.flush()
        if os.path.isfile(cached):
            os.rename(cached, "cached-extrainfo")

        for fn, giantstring in descriptor_files.items():
            writeDescToFile(fn, giantstring)
        print("Done.")
        code = 0
        sys.exit(code)

if __name__ == "__main__":
    try:
        parser = util.getArgParser()
        options = parser.parse_args()

        if options.quiet:
            print = lambda x: True
        if options.version:
            print("gen_bridge_descriptors-%s" % parser.version)
            sys.exit(0)
        if options.descriptors and (options.descriptors > 0):
            create(options.descriptors)
        else:
            raise SystemExit(parser.format_help())

    except Exception as error:
        raise SystemExit(error)
