зеркало из https://github.com/mozilla/FlightDeck.git
183 строки
7.2 KiB
Python
183 строки
7.2 KiB
Python
|
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-jetpack-core-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 jid_to_programid(jid):
|
||
|
return jid + "@jetpack"
|
||
|
|
||
|
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_to_programid(jid)
|
||
|
# 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 programid_to_jid(programid):
|
||
|
assert programid.endswith("@jetpack")
|
||
|
jid = programid[:-len("@jetpack")]
|
||
|
return jid
|
||
|
|
||
|
def check_for_privkey(keydir, jid, stderr):
|
||
|
if jid.startswith("anonid0-"):
|
||
|
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
|