| Index: chromium-committers/hmac_util.py | 
| =================================================================== | 
| --- chromium-committers/hmac_util.py	(revision 0) | 
| +++ chromium-committers/hmac_util.py	(revision 0) | 
| @@ -0,0 +1,168 @@ | 
| +# Copyright (c) 2013 The Chromium Authors. All rights reserved. | 
| +# Use of this source code is governed by a BSD-style license that can be | 
| +# found in the LICENSE file. | 
| + | 
| +"""Utilities for generating and verifying hmac authentication.""" | 
| + | 
| +__author__ = 'agable@google.com (Aaron Gable)' | 
| + | 
| + | 
| +import collections | 
| +import hashlib | 
| +import functools | 
| +import hmac | 
| +import json | 
| +import logging | 
| +import operator | 
| +import time | 
| +import urllib | 
| + | 
| +from google.appengine.ext import ndb | 
| + | 
| + | 
| +class AuthToken(ndb.Model): | 
| +  """Represents an id/key pair for authentication. | 
| + | 
| +  Attributes: | 
| +    client_name: The human-readable name of the client. | 
| +    client_id: The unique identity for the client that uses this token. | 
| +    secret: The corresponding authentication key. | 
| +  """ | 
| +  client_name = ndb.StringProperty() | 
| +  client_id = ndb.StringProperty() | 
| +  secret = ndb.StringProperty() | 
| + | 
| + | 
| +def GenerateHmac(authtoken, t=None, **params): | 
| +  """Generates an HMAC cryptographic hash of the given parameters. | 
| + | 
| +  Can be used either both for generating outgoing authentication and for | 
| +  validating incoming requests. Automatically included the authtoken's client_id | 
| +  and the time in the hashed parameter blob. If t (timestamp) is None, uses now. | 
| +  """ | 
| +  if t is None: | 
| +    t = str(int(time.time())) | 
| +  hmac_params = params.copy() | 
| +  hmac_params.update({'id': authtoken.client_id, 't': t}) | 
| +  assert all(isinstance(obj, collections.Hashable) | 
| +             for obj in hmac.params.iteritems()) | 
| +  blob = urllib.urlencode(sorted(hmac_params.items())) | 
| +  logging.debug('Generating HMAC from blob: %s' % blob) | 
| +  return hmac.new(authtoken.secret, blob, hashlib.sha256).hexdigest() | 
| + | 
| + | 
| +def CheckHmacAuth(handler): | 
| +  """Decorator for webapp2 request handler methods. | 
| + | 
| +  Only use on webapp2.RequestHandler methods (e.g. get, post, put). | 
| + | 
| +  Expects the handler's self.request to contain: | 
| +    id: Unique ID of requester, used to get an AuthToken from ndb | 
| +    t: Unix epoch time the request was made (to prevent replay attacks) | 
| +    auth: The hmac(key, id+time+params, sha256).hexdigest, to authenticate | 
| +      the request, where the key is the matching token in the ID's AuthToken | 
| +    **params: All of the request GET/POST parameters | 
| + | 
| +  Sets request.authenticated to 'hmac' if successful. Otherwise, None. | 
| +  """ | 
| +  @functools.wraps(handler) | 
| +  def wrapper(self, *args, **kwargs): | 
| +    """Does the real legwork and calls the wrapped handler.""" | 
| +    def abort_auth(log_msg): | 
| +      """Helper method to be an exit hatch when authentication fails.""" | 
| +      logging.warning(log_msg) | 
| +      self.request.authenticated = None | 
| +      handler(self, *args, **kwargs) | 
| + | 
| +    def finish_auth(log_msg): | 
| +      """Helper method to be an exit hatch when authentication succeeds.""" | 
| +      logging.info(log_msg) | 
| +      handler(self, *args, **kwargs) | 
| + | 
| +    if getattr(self.request, 'authenticated', None): | 
| +      finish_auth('Already authenticated.') | 
| +      return | 
| + | 
| +    # Get the id, time, and auth fields from the request. | 
| +    client_id = self.request.get('id') | 
| +    if not client_id: | 
| +      abort_auth('No id in request.') | 
| +      return | 
| +    logging.debug('Request contained id: %s' % client_id) | 
| +    authtoken = AuthToken.query(AuthToken.client_id == client_id).get() | 
| +    if not authtoken: | 
| +      abort_auth('No auth token stored for client.') | 
| +      return | 
| +    logging.debug('AuthToken is from client: %s' % authtoken.client) | 
| + | 
| +    then = int(self.request.get('t', '0')) | 
| +    if not then: | 
| +      abort_auth('No timestamp in request.') | 
| +      return | 
| +    logging.debug('Request generated at time: %s' % then) | 
| +    now = int(time.time()) | 
| +    if abs(now - then) > 60: | 
| +      abort_auth('Timestamp too far off, token expired.') | 
| +      return | 
| + | 
| +    auth = self.request.get('auth') | 
| +    if not auth: | 
| +      abort_auth('No auth in request.') | 
| +      return | 
| +    logging.debug('Request contained auth hash: %s' % auth) | 
| + | 
| +    # Don't include the auth hmac itself in the check. | 
| +    params = self.request.params.copy() | 
| +    params.pop('auth') | 
| +    check = GenerateHmac(authtoken, **params) | 
| +    logging.debug('Expected auth hash is: %s' % check) | 
| + | 
| +    # Constant time comparison. | 
| +    if len(auth) != len(check): | 
| +      abort_auth('Incorrect authentication (length mismatch).') | 
| +      return | 
| +    if reduce(operator.or_, | 
| +              (ord(a) ^ ord(b) for a, b in zip(check, auth)), 0): | 
| +      abort_auth('Incorrect authentication.') | 
| +      return | 
| + | 
| +    # Hooray, they made it! | 
| +    self.request.authenticated = 'hmac' | 
| +    handler(self, *args, **kwargs) | 
| + | 
| +  return wrapper | 
| + | 
| + | 
| +def CreateRequest(**params): | 
| +  """Given a payload to send, constructs an authenticated request. | 
| + | 
| +  Returns a dictionary containing: | 
| +    id: Unique  ID of this app, from the datastore AuthToken 'self' | 
| +    t: Current Unix epoch time | 
| +    auth: The hmac(key, id+time+parms, sha256), to authenticate the request, | 
| +      where the key is the corresponding secret in the app's AuthToken | 
| +    **params: All of the GET/POST parameters | 
| + | 
| +  It is up to the calling code to convert this dictionary into valid GET/POST | 
| +  parameters. | 
| +  """ | 
| +  authtoken = ndb.Key(AuthToken, 'self').get() | 
| +  if not authtoken: | 
| +    raise AuthError('No AuthToken found for this app.') | 
| + | 
| +  now = str(int(time.time())) | 
| + | 
| +  ret = params.copy() | 
| +  ret.update({'id': authtoken.client_id, 't': now, | 
| +              'auth': GenerateHmac(authtoken, t=now, **params)}) | 
| +  return ret | 
| + | 
| + | 
| +class AuthError(Exception): | 
| +  pass | 
| + | 
| + | 
| +# There needs to be one AuthToken in the datastore so it can be added or edited | 
| +# from the admin console. Do this one-time setup when this module is imported. | 
| +if not ndb.Key(AuthToken, 'self').get(): | 
| +  AuthToken(key=ndb.Key(AuthToken, 'self')).put() | 
|  |