def test_key_storage(self): # Explicitly clear our cache. KeyStorage._cached = (None, None) # Call get method to retrieve our singleton KeyStorage. ks = KeyStorage.get() self.assertTrue(ks is not None) # We should cache this object, so another call will return the # same object. self.assertTrue(ks is KeyStorage.get()) # Now advance mock time enough to expire our cache. self.now += 365 * 24 * 60 * 60 # 1 year should be enough time self.assertTrue(ks is not KeyStorage.get())
def _create_security_token(user): """Create a CHIRP security token. Args: user: A User object. Returns: A string containing an encrypted security token that encodes the user's email address as well as a timestamp. """ timestamp = int(time.time()) plaintext = "%x %s" % (timestamp, user.email) nearest_mult_of_16 = 16 * ((len(plaintext) + 15) // 16) # Pad plaintest with whitespace to make the length a multiple of 16, # as this is a requirement of AES encryption. plaintext = plaintext.rjust(nearest_mult_of_16, ' ') if _DISABLE_CRYPTO: body = plaintext sig = "sig" else: key_storage = KeyStorage.get() body = AES.new(key_storage.aes_key, AES.MODE_CBC).encrypt(plaintext) hmac_key = key_storage.hmac_key if type(hmac_key) == unicode: # Crypto requires byte strings hmac_key = hmac_key.encode('utf8') sig = HMAC.HMAC(key=hmac_key, msg=body).hexdigest() return '%s:%s' % (sig, body)
def _parse_security_token(token): """Parse a CHIRP security token. Returns: A Credentials object, or None if the token is not valid. If a Credentials object is returned, its "user" field will not be set. """ if not token: return None if ':' not in token: logging.warn('Malformed token: no signature separator') return None sig, body = token.split(':', 1) if _DISABLE_CRYPTO: plaintext = body else: key_storage = KeyStorage.get() hmac_key = key_storage.hmac_key if type(hmac_key) == unicode: # Crypto requires byte strings hmac_key = hmac_key.encode('utf8') computed_sig = HMAC.HMAC(key=hmac_key, msg=body).hexdigest() if sig != computed_sig: logging.warn('Malformed token: invalid signature') return None try: plaintext = AES.new(key_storage.aes_key, AES.MODE_CBC).decrypt(body) except ValueError: logging.warn('Malformed token: wrong size') return None # Remove excess whitespace. plaintext = plaintext.strip() # The plaintext should contain at least one space. if ' ' not in plaintext: logging.warn('Malformed token: bad contents') return None parts = plaintext.split(' ') if len(parts) != 2: logging.warn('Malformed token: bad structure') return None timestamp, email = parts try: timestamp = int(timestamp, 16) except ValueError: logging.warn('Malformed token: bad timestamp') return None # Reject tokens that are too old or which have time-traveled. We # allow for 1s of clock skew. age_s = time.time() - timestamp if age_s < -1 or age_s > _TOKEN_TIMEOUT_S: logging.warn('Malformed token: expired (age=%ds)', age_s) return None cred = _Credentials() cred.email = email cred.security_token_is_stale = (age_s > 0.5 * _TOKEN_TIMEOUT_S) return cred