diff options
| author | Nick Mathewson <nickm@torproject.org> | 2008-10-14 05:10:30 +0000 | 
|---|---|---|
| committer | Nick Mathewson <nickm@torproject.org> | 2008-10-14 05:10:30 +0000 | 
| commit | d90990ee3ecd09a2725b8051759a900ebd488b8c (patch) | |
| tree | fbe68d0c4b698d45bfdcb6c91a43ea2e60c21329 /lib/thandy/keys.py | |
| parent | fb5a6115a6f3ea0216e3ca0645ba1eb31fb02876 (diff) | |
Rename glider to thandy, based on discussions on #nottor.  Please let me know ASAP if there is another program Thandy, or if it means something rude, or whatever.
git-svn-id: file:///home/or/svnrepo/updater/trunk@17085 55e972cd-5a19-0410-ae62-a4d7a52db4cd
Diffstat (limited to 'lib/thandy/keys.py')
| -rw-r--r-- | lib/thandy/keys.py | 399 | 
1 files changed, 399 insertions, 0 deletions
| diff --git a/lib/thandy/keys.py b/lib/thandy/keys.py new file mode 100644 index 0000000..5b4e072 --- /dev/null +++ b/lib/thandy/keys.py @@ -0,0 +1,399 @@ + +# These require PyCrypto. +import Crypto.PublicKey.RSA +import Crypto.Hash.SHA256 +import Crypto.Cipher.AES + +import cPickle as pickle +import binascii +import logging +import os +import struct +import sys +import simplejson +import getpass + +import thandy.formats +import thandy.util + +class PublicKey: +    def __init__(self): +        # Confusingly, these roles are the ones used for a private key to +        # remember what we're willing to do with it. +        self._roles = [] +    def format(self): +        raise NotImplemented() +    def sign(self, data): +        # returns a list of method,signature tuples. +        raise NotImplemented() +    def checkSignature(self, method, data, signature): +        # returns True, False, or raises UnknownMethod. +        raise NotImplemented() +    def getKeyID(self): +        raise NotImplemented() +    def getRoles(self): +        return self._roles +    def addRole(self, role, path): +        assert role in thandy.formats.ALL_ROLES +        self._roles.append((role, path)) +    def clearRoles(self): +        del self._roles[:] +    def hasRole(self, role, path): +        for r, p in self._roles: +            if r == role and thandy.formats.rolePathMatches(p, path): +                return True +        return False + +if hex(1L).upper() == "0X1L": +    def intToBinary(number): +        """Convert an int or long into a big-endian series of bytes. +        """ +        # This "convert-to-hex, then use binascii" approach may look silly, +        # but it's over 10x faster than the Crypto.Util.number approach. +        h = hex(long(number)) +        h = h[2:-1] +        if len(h)%2: +            h = "0"+h +        return binascii.a2b_hex(h) +elif hex(1L).upper() == "0X1": +    def intToBinary(number): +        h = hex(long(number)) +        h = h[2:] +        if len(h)%2: +            h = "0"+h +        return binascii.a2b_hex(h) +else: +    import Crypto.Util.number +    intToBinary = Crypto.Util.number.long_to_bytes +    assert None + +def binaryToInt(binary): +   """Convert a big-endian series of bytes into a long. +   """ +   return long(binascii.b2a_hex(binary), 16) + +def intToBase64(number): +    return thandy.formats.formatBase64(intToBinary(number)) + +def base64ToInt(number): +    return binaryToInt(thandy.formats.parseBase64(number)) + +def _pkcs1_padding(m, size): +    # I'd rather use OAEP+, but apparently PyCrypto barely supports +    # signature verification, and doesn't seem to support signature +    # verification with nondeterministic padding.  "argh." + +    s = [ "\x00\x01", "\xff"* (size-3-len(m)), "\x00", m ] +    r = "".join(s) +    return r + +def _xor(a,b): +    if a: +        return not b +    else: +        return b + +class RSAKey(PublicKey): +    """ +    >>> k = RSAKey.generate(bits=512) +    >>> obj = k.format() +    >>> obj['_keytype'] +    'rsa' +    >>> base64ToInt(obj['e']) +    65537L +    >>> k1 = RSAKey.fromJSon(obj) +    >>> k1.key.e == k.key.e +    True +    >>> k1.key.n == k.key.n +    True +    >>> k.getKeyID() == k1.getKeyID() +    True +    >>> s = { 'A B C' : "D", "E" : [ "F", "g", 99] } +    >>> method, sig = k.sign(obj=s) +    >>> k.checkSignature(method, sig, obj=s) +    True +    >>> s2 = [ s ] +    >>> k.checkSignature(method, sig, obj=s2) +    False +    """ +    def __init__(self, key): +        PublicKey.__init__(self) +        self.key = key +        self.keyid = None + +    @staticmethod +    def generate(bits=2048): +        key = Crypto.PublicKey.RSA.generate(bits=bits, randfunc=os.urandom) +        return RSAKey(key) + +    @staticmethod +    def fromJSon(obj): +        # obj must match RSAKEY_SCHEMA + +        thandy.formats.RSAKEY_SCHEMA.checkMatch(obj) +        n = base64ToInt(obj['n']) +        e = base64ToInt(obj['e']) +        if obj.has_key('d'): +            d = base64ToInt(obj['d']) +            p = base64ToInt(obj['p']) +            q = base64ToInt(obj['q']) +            u = base64ToInt(obj['u']) +            key = Crypto.PublicKey.RSA.construct((n, e, d, p, q, u)) +        else: +            key = Crypto.PublicKey.RSA.construct((n, e)) + +        result = RSAKey(key) +        if obj.has_key('roles'): +            for r, p in obj['roles']: +                result.addRole(r,p) + +        return result + +    def isPrivateKey(self): +        return hasattr(self.key, 'd') + +    def format(self, private=False, includeRoles=False): +        n = intToBase64(self.key.n) +        e = intToBase64(self.key.e) +        result = { '_keytype' : 'rsa', +                   'e' : e, +                   'n' : n } +        if private: +            result['d'] = intToBase64(self.key.d) +            result['p'] = intToBase64(self.key.p) +            result['q'] = intToBase64(self.key.q) +            result['u'] = intToBase64(self.key.u) +        if includeRoles: +            result['roles'] = self.getRoles() +        return result + +    def getKeyID(self): +        if self.keyid == None: +            d_obj = Crypto.Hash.SHA256.new() +            thandy.formats.getDigest(self.format(), d_obj) +            self.keyid = thandy.formats.formatHash(d_obj.digest()) +        return self.keyid + +    def _digest(self, obj, method=None): +        if method in (None, "sha256-pkcs1"): +            d_obj = Crypto.Hash.SHA256.new() +            thandy.formats.getDigest(obj, d_obj) +            digest = d_obj.digest() +            return ("sha256-pkcs1", digest) + +        raise UnknownMethod(method) + +    def sign(self, obj=None, digest=None): +        assert _xor(obj == None, digest == None) +        if digest == None: +            method, digest = self._digest(obj) +        m = _pkcs1_padding(digest, (self.key.size()+1) // 8) +        sig = intToBase64(self.key.sign(m, "")[0]) +        return (method, sig) + +    def checkSignature(self, method, sig, obj=None, digest=None): +        assert _xor(obj == None, digest == None) +        if method != "sha256-pkcs1": +            raise UnknownMethod("method") +        if digest == None: +            method, digest = self._digest(obj, method) +        sig = base64ToInt(sig) +        m = _pkcs1_padding(digest, (self.key.size()+1) // 8) +        return bool(self.key.verify(m, (sig,))) + +SALTLEN=16 + +def secretToKey(salt, secret): +    """Convert 'secret' to a 32-byte key, using a version of the algorithm +       from RFC2440.  The salt must be SALTLEN+1 bytes long, and should +       be random, except for the last byte, which encodes how time- +       consuming the computation should be. + +       (The goal is to make offline password-guessing attacks harder by +       increasing the time required to convert a password to a key, and to +       make precomputed password tables impossible to generate by ) +    """ +    assert len(salt) == SALTLEN+1 + +    # The algorithm is basically, 'call the last byte of the salt the +    # "difficulty", and all other bytes of the salt S.  Now make +    # an infinite stream of S|secret|S|secret|..., and hash the +    # first N bytes of that, where N is determined by the difficulty. +    # +    # Obviously, this wants a hash algorithm that's tricky to +    # parallelize. +    # +    # Unlike RFC2440, we use a 16-byte salt.  Because CPU times +    # have improved, we start at 16 times the previous minimum. + +    difficulty = ord(salt[-1]) +    count = (16L+(difficulty & 15)) << ((difficulty >> 4) + 10) + +    # Make 'data' nice and long, so that we don't need to call update() +    # a zillion times. +    data = salt[:-1]+secret +    if len(data)<1024: +        data *= (1024 // len(data))+1 + +    d = Crypto.Hash.SHA256.new() +    iters, leftover = divmod(count, len(data)) +    for _ in xrange(iters): +        d.update(data) +        #count -= len(data) +    if leftover: +        d.update(data[:leftover]) +        #count -= leftover +    #assert count == 0 + +    return d.digest() + +def encryptSecret(secret, password, difficulty=0x80): +    """Encrypt the secret 'secret' using the password 'password', +       and return the encrypted result.""" +    # The encrypted format is: +    #    "GKEY1"  -- 5 octets, fixed, denotes data format. +    #    SALT     -- 17 bytes, used to hash password +    #    IV       -- 16 bytes; salt for encryption +    #    ENCRYPTED IN AES256-OFB, using a key=s2k(password, salt) and IV=IV: +    #       SLEN   -- 4 bytes; length of secret, big-endian. +    #       SECRET -- len(secret) bytes +    #       D      -- 32 bytes; SHA256 hash of (salt|secret|salt). +    # +    # This format leaks the secret length, obviously. +    assert 0 <= difficulty < 256 +    salt = os.urandom(SALTLEN)+chr(difficulty) +    key = secretToKey(salt, password) + +    d_obj = Crypto.Hash.SHA256.new() +    d_obj.update(salt) +    d_obj.update(secret) +    d_obj.update(salt) +    d = d_obj.digest() + +    iv = os.urandom(16) +    e = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_OFB, iv) + +    # Stupidly, pycrypto doesn't accept that stream ciphers don't need to +    # take their input in blocks.  So pad it, then ignore the padded output. + +    padlen = 16-((len(secret)+len(d)+4) % 16) +    if padlen == 16: padlen = 0 +    pad = '\x00' * padlen + +    slen = struct.pack("!L",len(secret)) +    encrypted = e.encrypt("%s%s%s%s" % (slen, secret, d, pad)) +    if padlen: +        encrypted = encrypted[:-padlen] +    return "GKEY1%s%s%s"%(salt, iv, encrypted) + +def decryptSecret(encrypted, password): +    """Decrypt a value encrypted with encryptSecret.  Raises UnknownFormat +       or FormatError if 'encrypted' was not generated with encryptSecret. +       Raises BadPassword if the password was not correct. +    """ +    if encrypted[:5] != "GKEY1": +        raise thandy.UnknownFormat() +    encrypted = encrypted[5:] +    if len(encrypted) < SALTLEN+1+16: +        raise thandy.FormatException() + +    salt = encrypted[:SALTLEN+1] +    iv = encrypted[SALTLEN+1:SALTLEN+1+16] +    encrypted = encrypted[SALTLEN+1+16:] + +    key = secretToKey(salt, password) + +    e = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_OFB, iv) +    padlen = 16-(len(encrypted) % 16) +    if padlen == 16: padlen = 0 +    pad = '\x00' * padlen + +    decrypted = e.decrypt("%s%s"%(encrypted,pad)) +    slen = struct.unpack("!L", decrypted[:4])[0] +    secret = decrypted[4:4+slen] +    hash = decrypted[4+slen:4+slen+Crypto.Hash.SHA256.digest_size] + +    d = Crypto.Hash.SHA256.new() +    d.update(salt) +    d.update(secret) +    d.update(salt) + +    if d.digest() != hash: +        raise thandy.BadPassword() + +    return secret + +class KeyStore(thandy.formats.KeyDB): +    def __init__(self, fname, encrypted=True): +        thandy.formats.KeyDB.__init__(self) + +        self._loaded = None +        self._fname = fname +        self._passwd = None +        self._encrypted = encrypted + +    def getpass(self, reprompt=False): +        if self._passwd != None: +            return self._passwd +        while 1: +            pwd = getpass.getpass("Password: ", sys.stderr) +            if not reprompt: +                return pwd + +            pwd2 = getpass.getpass("Confirm: ", sys.stderr) +            if pwd == pwd2: +                return pwd +            else: +                print "Mismatch; try again." + +    def load(self, password=None): +        logging.info("Loading private keys from %r...", self._fname) +        if not os.path.exists(self._fname): +            logging.info("...no such file.") +            self._loaded = True +            return + +        if password is None and self._encrypted: +            password = self.getpass() + +        contents = open(self._fname, 'rb').read() +        if self._encrypted: +            contents = decryptSecret(contents, password) + +        listOfKeys = simplejson.loads(contents) +        self._passwd = password # It worked. +        if not listOfKeys.has_key('keys'): +            listOfKeys['keys'] = [] +        for obj in listOfKeys['keys']: +            key = RSAKey.fromJSon(obj) +            self.addKey(key) +            logging.info("Loaded key %s", key.getKeyID()) + +        self._loaded = True + +    def setPassword(self, passwd): +        self._passwd = passwd + +    def clearPassword(self): +        self._passwd = None + +    def save(self, password=None): +        if not self._loaded and self._encrypted: +            self.load(password) + +        if password is None: +            password = self.getpass(True) + +        logging.info("Saving private keys into %r...", self._fname) +        listOfKeys = { 'keys' : +                       [ key.format(private=True, includeRoles=True) for key in +                         self._keys.values() ] +                       } +        contents = simplejson.dumps(listOfKeys) +        if self._encrypted: +            contents = encryptSecret(contents, password) +        thandy.util.replaceFile(self._fname, contents) +        self._passwd = password # It worked. +        logging.info("Done.") + + | 
