import os, sys
import base64
import hashlib
import simplejson as json

def my_b32encode(bytes):
    # a paddingless prettier base32 encoder
    return base64.b32encode(bytes).lower().strip("=")

def my_b32decode(s):
    # add "=" until we have a multiple of 8 characters (i.e. 40 bits / 5 bytes)
    padding = "=" * ((8-len(s)%8)%8)
    return base64.b32decode(s.upper()+padding)

def remove_prefix(s, prefix, errormsg):
    if not s.startswith(prefix):
        raise ValueError(errormsg)
    return s[len(prefix):]

def vk_to_jid(vk):
    """Return 'jid0-XYZ', where 'XYZ' is a string that securely identifies
    a specific public key. To get a suitable add-on ID, append '@jetpack'
    to this string.
    """
    # per https://developer.mozilla.org/en/Install_Manifests#id all XPI id
    # values must either be in the form of a 128-bit GUID (crazy braces
    # and all) or in the form of an email address (crazy @ and all).
    # Firefox will refuse to install an add-on with an id that doesn't
    # match one of these forms. The actual regexp is at:
    # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/XPIProvider.jsm#130
    # So the JID needs an @-suffix, and the only legal punctuation is
    # "-._". So we start with a base64 encoding, and replace the
    # punctuation (+/) with letters (AB), losing a few bits of integrity.

    # even better: windows has a maximum path length limitation of 256
    # characters:
    #  http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
    # (unless all paths are prefixed with "\\?\", I kid you not). The
    # typical install will put add-on code in a directory like:
    # C:\Documents and Settings\<username>\Application Data\Mozilla\Firefox\Profiles\232353483.default\extensions\$JID\...
    # (which is 108 chars long without the $JID).
    # Then the unpacked XPI contains packaged resources like:
    #  resources/$JID-api-utils-lib/main.js   (35 chars plus the $JID)
    #
    # We hash the pubkey into a 160 bit string, base64 encode that (with
    # AB instead of +/ to be path-safe), then bundle it into
    # "jid0-XYZ@jetpack". This gives us 40 characters. The resulting
    # main.js will have a path length of 224 characters, leaving us 32
    # characters of margin.

    # if length were no issue, we'd prefer to use this:
    # s = base64.b64encode(vk.to_string()).strip("=")
    h = hashlib.sha256("jetpack-id-v0:"+vk.to_string()).digest()[:160/8]
    s = base64.b64encode(h, "AB").strip("=")
    jid = "jid0-" + s
    return jid

def create_key(keydir, name):
    # return jid
    from ecdsa import SigningKey, NIST256p
    sk = SigningKey.generate(curve=NIST256p)
    sk_text = "private-jid0-%s" % my_b32encode(sk.to_string())
    vk = sk.get_verifying_key()
    vk_text = "public-jid0-%s" % my_b32encode(vk.to_string())
    jid = vk_to_jid(vk)
    program_id = jid + "@jetpack"
    # save privkey to ~/.jetpack-keys/$jid
    f = open(os.path.join(keydir, jid), "w")
    f.write("private-key: %s\n" % sk_text)
    f.write("public-key: %s\n" % vk_text)
    f.write("jid: %s\n" % jid)
    f.write("program-id: %s\n" % program_id)
    f.write("name: %s\n" % name)
    f.close()
    return jid

def check_for_privkey(keydir, jid, stderr):
    # the "anonymous ID" case
    if jid.startswith("anonid0-"):
        return None

    # the "old-style ID" case
    # FIXME: once it is possible for addons to change from old-style IDs
    # to new, cryptographic IDs on AMO and in Firefox, warn users that
    # continuing to use old-style IDs is less secure, and provide them with
    # instructions for changing to new IDs.
    if not jid.startswith("jid0-"):
        return None

    keypath = os.path.join(keydir, jid)
    if not os.path.isfile(keypath):
        msg = """\
Your package.json says our ID is:
  %(jid)s
But I don't have a corresponding private key in:
  %(keypath)s

If you are the original developer of this package and have recently copied
the source code from a different machine to this one, you should copy the
private key into the file named above.

Otherwise, if you are a new developer who has made a copy of an existing
package to use as a starting point, you need to remove the 'id' property
from package.json, so that we can generate a new id and keypair. This will
disassociate our new package from the old one.

If you're collaborating on the same addon with a team, make sure at least
one person on the team has the private key. In the future, you may not
be able to distribute your addon without it.
"""
        print >>stderr, msg % {"jid": jid, "keypath": keypath}
        return None
    keylines = open(keypath, "r").readlines()
    keydata = {}
    for line in keylines:
        line = line.strip()
        if line:
            k,v = line.split(":", 1)
            keydata[k.strip()] = v.strip()
    if "private-key" not in keydata:
        raise ValueError("invalid keydata: can't find 'private-key' line")
    sk_s = remove_prefix(keydata["private-key"], "private-jid0-",
                         errormsg="unable to parse private-key data")
    from ecdsa import SigningKey, VerifyingKey, NIST256p
    sk = SigningKey.from_string(my_b32decode(sk_s), curve=NIST256p)
    vk = sk.get_verifying_key()

    jid_2 = vk_to_jid(vk)
    if jid_2 != jid:
        raise ValueError("invalid keydata: private-key in %s does not match"
                         " public key for %s" % (keypath, jid))
    vk_s = remove_prefix(keydata["public-key"], "public-jid0-",
                         errormsg="unable to parse public-key data")
    vk2 = VerifyingKey.from_string(my_b32decode(vk_s), curve=NIST256p)
    if vk.to_string() != vk2.to_string():
        raise ValueError("invalid keydata: public-key mismatch")
    return sk

def preflight_config(target_cfg, filename, stderr=sys.stderr, keydir=None,
                     err_if_privkey_not_found=True):
    # check the top-level package.json for missing keys. We generate anything
    # that we can, and ask the user for the rest.
    if keydir is None:
        keydir = os.path.expanduser("~/.jetpack/keys")
    modified = False
    config = json.load(open(filename, 'r'))

    name = target_cfg["name"] # defaults to parentdir if not set

    if "id" not in config:
        print >>stderr, ("No 'id' in package.json: creating a new"
                         " keypair for you.")
        if not os.path.isdir(keydir):
            os.makedirs(keydir, 0700) # not world readable
        
        jid = create_key(keydir, name)
        config["id"] = jid
        modified = True

    # make sure we have the privkey: this catches the case where developer B
    # copies an add-on from developer A and then (accidentally) tries to
    # publish it without replacing the JID

    sk = check_for_privkey(keydir, config["id"], stderr)
    if not sk and err_if_privkey_not_found:
        return False, False

    if modified:
        i = 0
        backup = filename + ".backup"
        while os.path.exists(backup):
            if i > 1000:
                raise ValueError("I'm having problems finding a good name"
                                 " for the backup file. Please move %s out"
                                 " of the way and try again."
                                 % (filename + ".backup"))
            backup = filename + ".backup-%d" % i
            i += 1
        os.rename(filename, backup)
        new_json = json.dumps(config, indent=4)
        open(filename, 'w').write(new_json+"\n")
        return False, True

    return True, False
