Пример #1
0
def valid_password(a, password):
    # bail out early if the account or password's invalid
    if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
        return False

    # standardize on utf-8 encoding
    password = filters._force_utf8(password)

    # this is really easy if it's a sexy bcrypt password
    if a.password.startswith('$2a$'):
        expected_hash = bcrypt.hashpw(password, a.password)
        if constant_time_compare(a.password, expected_hash):
            return a
        return False

    # alright, so it's not bcrypt. how old is it?
    # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
    # otherwise it's sha-1 with no salt.
    salt = ''
    if len(a.password) == 43:
        salt = a.password[:3]
    expected_hash = passhash(a.name, password, salt)

    if not constant_time_compare(a.password, expected_hash):
        return False

    # since we got this far, it's a valid password but in an old format
    # let's upgrade it
    a.password = bcrypt_password(password)
    a._commit()
    return a
Пример #2
0
def valid_password(a, password, compare_password=None):
    # bail out early if the account or password's invalid
    if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
        return False

    convert_password = False
    if compare_password is None:
        convert_password = True
        compare_password = a.password

    # standardize on utf-8 encoding
    password = filters._force_utf8(password)

    if compare_password.startswith('$2a$'):
        # it's bcrypt.

        try:
            expected_hash = bcrypt.hashpw(password, compare_password)
        except ValueError:
            # password is invalid because it contains null characters
            return False

        if not constant_time_compare(compare_password, expected_hash):
            return False

        # if it's using the current work factor, we're done, but if it's not
        # we'll have to rehash.
        # the format is $2a$workfactor$salt+hash
        work_factor = int(compare_password.split("$")[2])
        if work_factor == g.bcrypt_work_factor:
            return a
    else:
        # alright, so it's not bcrypt. how old is it?
        # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
        # otherwise it's sha-1 with no salt.
        salt = ''
        if len(compare_password) == 43:
            salt = compare_password[:3]
        expected_hash = passhash(a.name, password, salt)

        if not constant_time_compare(compare_password, expected_hash):
            return False

    # since we got this far, it's a valid password but in an old format
    # let's upgrade it
    if convert_password:
        a.password = bcrypt_password(password)
        a._commit()
    return a
Пример #3
0
def valid_password(a, password):
    try:
        # A constant_time_compare isn't strictly required here
        # but it is doesn't hurt
        if constant_time_compare(a.password, passhash(a.name, password, '')):
            #add a salt
            a.password = passhash(a.name, password, True)
            a._commit()
            return a
        else:
            salt = a.password[:3]
            if constant_time_compare(a.password, passhash(a.name, password, salt)):
                return a
    except AttributeError, UnicodeEncodeError:
        return False
Пример #4
0
    def get_authenticated_account(self):
        from r2.models import Account, NotFound, register

        try:
            authorization = request.environ.get("HTTP_AUTHORIZATION")
            username, password = parse_http_basic(authorization)
        except RequirementException:
            return None

        try:
            account = Account._by_name(username)
        except NotFound:
            if g.auth_trust_http_authorization:
                # note: we're explicitly allowing automatic re-registration of
                # _deleted accounts and login of _banned accounts here because
                # we're trusting you know what you're doing in an SSO situation
                account = register(username, password, request.ip)
            else:
                return None

        # if we're to trust the authorization headers, don't check passwords
        if g.auth_trust_http_authorization:
            return account

        # not all systems support bcrypt in the standard crypt
        if account.password.startswith("$2a$"):
            expected_hash = bcrypt.hashpw(password, account.password)
        else:
            expected_hash = crypt.crypt(password, account.password)

        if not constant_time_compare(expected_hash, account.password):
            return None
        return account
Пример #5
0
def set_up_embed(embed_key, sr, thing, showedits):
    expected_mac = hmac.new(g.secrets["comment_embed"], thing._id36, hashlib.sha1).hexdigest()
    if not constant_time_compare(embed_key or "", expected_mac):
        abort(401)

    try:
        author = Account._byID(thing.author_id) if thing.author_id else None
    except NotFound:
        author = None

    iso_timestamp = request.GET.get("created", "")

    c.embed_config = {
        "eventtracker_url": g.eventtracker_url or "",
        "anon_eventtracker_url": g.anon_eventtracker_url or "",
        "created": iso_timestamp,
        "showedits": showedits,
        "thing": {
            "id": thing._id,
            "sr_id": sr._id,
            "sr_name": sr.name,
            "edited": edited_after(thing, iso_timestamp, showedits),
            "deleted": thing.deleted or author._deleted,
        },
    }

    c.render_style = "iframe"
    c.user = UnloggedUser([c.lang])
    c.user_is_loggedin = False
    c.forced_loggedout = True
Пример #6
0
    def POST_timings(self, action_name, verification, **kwargs):
        lookup = {
            "dns_timing": "dns",
            "tcp_timing": "tcp",
            "request_timing": "request",
            "response_timing": "response",
            "dom_loading_timing": "dom_loading",
            "dom_interactive_timing": "dom_interactive",
            "dom_content_loaded_timing": "dom_content_loaded",
        }

        if not (action_name and verification):
            abort(422)

        expected_mac = hmac.new(g.secrets["action_name"], action_name, hashlib.sha1).hexdigest()

        if not constant_time_compare(verification, expected_mac):
            abort(422)

        # action_name comes in the format 'controller.METHOD_action'
        stat_tpl = "service_time.web.{}.frontend".format(action_name)
        stat_aggregate = "service_time.web.frontend"

        for key, name in lookup.iteritems():
            val = kwargs[key]
            if val >= 0:
                g.stats.simple_timing(stat_tpl + "." + name, val)
                g.stats.simple_timing(stat_aggregate + "." + name, val)

        abort(204)
Пример #7
0
    def GET_mediaembed(self, link, credentials):
        if request.host != g.media_domain:
            # don't serve up untrusted content except on our
            # specifically untrusted domain
            abort(404)

        if link.subreddit_slow.type in Subreddit.private_types:
            expected_mac = hmac.new(g.secrets["media_embed"], link._id36,
                                    hashlib.sha1).hexdigest()
            if not constant_time_compare(credentials or "", expected_mac):
                abort(404)

        if not c.secure:
            media_object = link.media_object
        else:
            media_object = link.secure_media_object

        if not media_object:
            abort(404)
        elif isinstance(media_object, dict):
            # otherwise it's the new style, which is a dict(type=type, **args)
            media_embed = get_media_embed(media_object)
            content = media_embed.content

        c.allow_framing = True

        return MediaEmbedBody(body = content).render()
Пример #8
0
def parse_and_validate_reply_to_address(address):
    """Validate the address and parse out and return the message id.

    This is the reverse operation of `get_reply_to_address`.

    """

    recipient, sep, domain = address.partition("@")
    if not sep or not recipient or domain != g.modmail_email_domain:
        return

    main, sep, remainder = recipient.partition("+")
    if not sep or not main or main != "zendeskreply":
        return

    try:
        email_id, email_mac = remainder.split("-")
    except ValueError:
        return

    expected_mac = hmac.new(
        g.secrets['modmail_email_secret'], email_id, hashlib.sha256).hexdigest()

    if not constant_time_compare(expected_mac, email_mac):
        return

    message_id36 = email_id
    return message_id36
Пример #9
0
def valid_admin_cookie(cookie):
    if g.read_only_mode:
        return (False, None)

    # parse the cookie
    try:
        first_login, last_request, hash = cookie.split(',')
    except ValueError:
        return (False, None)

    # make sure it's a recent cookie
    try:
        first_login_time = datetime.strptime(first_login, COOKIE_TIMESTAMP_FORMAT)
        last_request_time = datetime.strptime(last_request, COOKIE_TIMESTAMP_FORMAT)
    except ValueError:
        return (False, None)

    cookie_age = datetime.utcnow() - first_login_time
    if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
        return (False, None)

    idle_time = datetime.utcnow() - last_request_time
    if idle_time.total_seconds() > g.ADMIN_COOKIE_MAX_IDLE:
        return (False, None)

    # validate
    expected_cookie = c.user.make_admin_cookie(first_login, last_request)
    return (constant_time_compare(cookie, expected_cookie),
            first_login)
Пример #10
0
    def get_authenticated_account(self):
        from r2.models import Account, NotFound

        quoted_session_cookie = request.cookies.get(g.login_cookie)
        if not quoted_session_cookie:
            return None
        session_cookie = urllib.unquote(quoted_session_cookie)

        try:
            uid, timestr, hash = session_cookie.split(",")
            uid = int(uid)
        except:
            return None

        try:
            account = Account._byID(uid, data=True)
        except NotFound:
            return None

        expected_cookie = account.make_cookie(timestr)
        if not constant_time_compare(session_cookie, expected_cookie):
            return None
        
        if not hooks.get_hook("enhanced.privacy.check").call_until_return(uid=uid, hash=hash):
            return None
        
        return account
Пример #11
0
 def _get_client_auth(self):
     auth = request.headers.get("Authorization")
     try:
         client_id, client_secret = parse_http_basic(auth)
         client = OAuth2Client.get_token(client_id)
         require(client)
         require(constant_time_compare(client.secret, client_secret))
         return client
     except RequirementException:
         abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])
Пример #12
0
def valid_password(a, password, compare_password=None):
    # bail out early if the account or password's invalid
    if not hasattr(a, 'name') or not hasattr(a, 'password') or not password:
        return False

    convert_password = False
    if compare_password is None:
        convert_password = True
        compare_password = a.password

    # standardize on utf-8 encoding
    password = filters._force_utf8(password)

    if compare_password.startswith('$2a$'):
        # it's bcrypt.
        expected_hash = bcrypt.hashpw(password, compare_password)
        if not constant_time_compare(compare_password, expected_hash):
            return False

        # if it's using the current work factor, we're done, but if it's not
        # we'll have to rehash.
        # the format is $2a$workfactor$salt+hash
        work_factor = int(compare_password.split("$")[2])
        if work_factor == g.bcrypt_work_factor:
            return a
    else:
        # alright, so it's not bcrypt. how old is it?
        # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
        # otherwise it's sha-1 with no salt.
        salt = ''
        if len(compare_password) == 43:
            salt = compare_password[:3]
        expected_hash = passhash(a.name, password, salt)

        if not constant_time_compare(compare_password, expected_hash):
            return False

    # since we got this far, it's a valid password but in an old format
    # let's upgrade it
    if convert_password:
        a.password = bcrypt_password(password)
        a._commit()
    return a
Пример #13
0
def valid_feed(name, feedhash, path):
    if name and feedhash and path:
        from r2.lib.template_helpers import add_sr
        path = add_sr(path)
        try:
            user = Account._by_name(name)
            if (user.pref_private_feeds and
                constant_time_compare(feedhash, make_feedhash(user, path))):
                return user
        except NotFound:
            pass
Пример #14
0
def valid_feed(name, feedhash, path):
    if name and feedhash and path:
        from r2.lib.template_helpers import add_sr
        path = add_sr(path)
        try:
            user = Account._by_name(name)
            if (user.pref_private_feeds and constant_time_compare(
                    feedhash, make_feedhash(user, path))):
                return user
        except NotFound:
            pass
Пример #15
0
 def _get_client_auth(self):
     auth = request.headers.get("Authorization")
     try:
         client_id, client_secret = parse_http_basic(auth)
         require(client_id)
         require(client_secret)
         client = OAuth2Client.get_token(client_id)
         require(client)
         require(constant_time_compare(client.secret, client_secret))
         return client
     except RequirementException:
         abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])
Пример #16
0
def valid_cookie(cookie):
    try:
        uid, timestr, hash = cookie.split(',')
        uid = int(uid)
    except:
        return (False, False)

    if g.read_only_mode:
        return (False, False)

    try:
        account = Account._byID(uid, True)
        if account._deleted:
            return (False, False)
    except NotFound:
        return (False, False)

    if constant_time_compare(cookie, account.make_cookie(timestr, admin = False)):
        return (account, False)
    elif constant_time_compare(cookie, account.make_cookie(timestr, admin = True)):
        return (account, True)
    return (False, False)
Пример #17
0
def valid_cookie(cookie):
    try:
        uid, timestr, hash = cookie.split(',')
        uid = int(uid)
    except:
        return (False, False)

    if g.read_only_mode:
        return (False, False)

    try:
        account = Account._byID(uid, True)
        if account._deleted:
            return (False, False)
    except NotFound:
        return (False, False)

    if constant_time_compare(cookie, account.make_cookie(timestr, admin = False)):
        return (account, False)
    elif constant_time_compare(cookie, account.make_cookie(timestr, admin = True)):
        return (account, True)
    return (False, False)
Пример #18
0
    def POST_revoke_token(self, token_id, token_hint):
        '''Revoke an OAuth2 access or refresh token.

        token_type_hint is optional, and hints to the server
        whether the passed token is a refresh or access token.

        A call to this endpoint is considered a success if
        the passed `token_id` is no longer valid. Thus, if an invalid
        `token_id` was passed in, a successful 204 response will be returned.

        See [RFC7009](http://tools.ietf.org/html/rfc7009)

        '''
        self.OPTIONS_revoke_token()
        # In success cases, this endpoint returns no data.
        response.status = 204

        if not token_id:
            return

        types = (OAuth2AccessToken, OAuth2RefreshToken)
        if token_hint == "refresh_token":
            types = reversed(types)

        for token_type in types:
            try:
                token = token_type._byID(token_id)
            except tdb_cassandra.NotFound:
                g.stats.simple_event(
                    'oauth2.POST_revoke_token.cass_not_found.%s'
                    % token_type.__name__)
                continue
            else:
                break
        else:
            # No Token found. The given token ID is already gone
            # or never existed. Either way, from the client's perspective,
            # the passed in token is no longer valid.
            return

        if constant_time_compare(token.client_id, c.oauth2_client._id):
            token.revoke()
        else:
            # RFC 7009 is not clear on how to handle this case.
            # Given that a malicious client could do much worse things
            # with a valid token then revoke it, returning an error
            # here is best as it may help certain clients debug issues
            response.status = 400
            g.stats.simple_event(
                'oauth2.errors.REVOKE_TOKEN_UNAUTHORIZED_CLIENT')
            return self.api_wrapper({"error": "unauthorized_client"})
Пример #19
0
    def get_client_ip(self, environ):
        try:
            client_ip = environ["HTTP_CF_CONNECTING_IP"]
            provided_hash = environ["HTTP_CF_CIP_TAG"].lower()
        except KeyError:
            return None

        secret = g.secrets["cdn_ip_verification"]
        expected_hash = hashlib.sha1(client_ip + secret).hexdigest()

        if not constant_time_compare(expected_hash, provided_hash):
            return None

        return client_ip
Пример #20
0
    def get_client_ip(self, environ):
        try:
            client_ip = environ["HTTP_CF_CONNECTING_IP"]
            provided_hash = environ["HTTP_CF_CIP_TAG"].lower()
        except KeyError:
            return None

        secret = g.secrets["cdn_ip_verification"]
        expected_hash = hashlib.sha1(client_ip + secret).hexdigest()

        if not constant_time_compare(expected_hash, provided_hash):
            return None

        return client_ip
Пример #21
0
def valid_password(a, password):
    # bail out early if the account or password's invalid
    if not hasattr(a, "name") or not hasattr(a, "password") or not password:
        return False

    # standardize on utf-8 encoding
    password = filters._force_utf8(password)

    if a.password.startswith("$2a$"):
        # it's bcrypt.
        expected_hash = bcrypt.hashpw(password, a.password)
        if not constant_time_compare(a.password, expected_hash):
            return False

        # if it's using the current work factor, we're done, but if it's not
        # we'll have to rehash.
        # the format is $2a$workfactor$salt+hash
        work_factor = int(a.password.split("$")[2])
        if work_factor == g.bcrypt_work_factor:
            return a
    else:
        # alright, so it's not bcrypt. how old is it?
        # if the length of the stored hash is 43 bytes, the sha-1 hash has a salt
        # otherwise it's sha-1 with no salt.
        salt = ""
        if len(a.password) == 43:
            salt = a.password[:3]
        expected_hash = passhash(a.name, password, salt)

        if not constant_time_compare(a.password, expected_hash):
            return False

    # since we got this far, it's a valid password but in an old format
    # let's upgrade it
    a.password = bcrypt_password(password)
    a._commit()
    return a
Пример #22
0
def valid_cookie(cookie):
    try:
        uid, timestr, hash = cookie.split(',')
        uid = int(uid)
    except:
        return False

    if g.read_only_mode:
        return False

    try:
        account = Account._byID(uid, True)
        if account._deleted:
            return False
    except NotFound:
        return False

    if constant_time_compare(cookie, account.make_cookie(timestr)):
        return account
    return False
Пример #23
0
def valid_cookie(cookie):
    try:
        uid, timestr, hash = cookie.split(',')
        uid = int(uid)
    except:
        return False

    if g.read_only_mode:
        return False

    try:
        account = Account._byID(uid, True)
        if account._deleted:
            return False
    except NotFound:
        return False

    if constant_time_compare(cookie, account.make_cookie(timestr)):
        return account
    return False
Пример #24
0
def cookie():
    """Authenticate the user given a session cookie."""
    session_cookie = c.cookies.get(g.login_cookie)
    if not session_cookie:
        return None

    cookie = session_cookie.value
    try:
        uid, timestr, hash = cookie.split(",")
        uid = int(uid)
    except:
        return None

    try:
        account = Account._byID(uid, data=True)
    except NotFound:
        return None

    if not constant_time_compare(cookie, account.make_cookie(timestr)):
        return None
    return account
Пример #25
0
def cookie():
    """Authenticate the user given a session cookie."""
    session_cookie = c.cookies.get(g.login_cookie)
    if not session_cookie:
        return None

    cookie = session_cookie.value
    try:
        uid, timestr, hash = cookie.split(",")
        uid = int(uid)
    except:
        return None

    try:
        account = Account._byID(uid, data=True)
    except NotFound:
        return None

    if not constant_time_compare(cookie, account.make_cookie(timestr)):
        return None
    return account
Пример #26
0
def valid_admin_cookie(cookie):
    if g.read_only_mode:
        return False

    # parse the cookie
    try:
        timestr, hash = cookie.split(',')
    except ValueError:
        return False

    # make sure it's a recent cookie
    try:
        cookie_time = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S')
    except ValueError:
        return False

    cookie_age = datetime.utcnow() - cookie_time
    if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL:
        return False

    # validate
    return constant_time_compare(cookie, c.user.make_admin_cookie(timestr))
Пример #27
0
def valid_otp_cookie(cookie):
    if g.read_only_mode:
        return False

    # parse the cookie
    try:
        remembered_at, signature = cookie.split(",")
    except ValueError:
        return False

    # make sure it hasn't expired
    try:
        remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)
    except ValueError:
        return False

    age = datetime.utcnow() - remembered_at_time
    if age.total_seconds() > g.OTP_COOKIE_TTL:
        return False

    # validate
    expected_cookie = c.user.make_otp_cookie(remembered_at)
    return constant_time_compare(cookie, expected_cookie)
Пример #28
0
def valid_otp_cookie(cookie):
    if g.read_only_mode:
        return False

    # parse the cookie
    try:
        remembered_at, signature = cookie.split(",")
    except ValueError:
        return False

    # make sure it hasn't expired
    try:
        remembered_at_time = datetime.strptime(remembered_at, COOKIE_TIMESTAMP_FORMAT)
    except ValueError:
        return False

    age = datetime.utcnow() - remembered_at_time
    if age.total_seconds() > g.OTP_COOKIE_TTL:
        return False

    # validate
    expected_cookie = c.user.make_otp_cookie(remembered_at)
    return constant_time_compare(cookie, expected_cookie)
Пример #29
0
    def get_authenticated_account(self):
        from r2.models import Account, NotFound

        quoted_session_cookie = request.cookies.get(g.login_cookie)
        if not quoted_session_cookie:
            return None
        session_cookie = urllib.unquote(quoted_session_cookie)

        try:
            uid, timestr, hash = session_cookie.split(",")
            uid = int(uid)
        except:
            return None

        try:
            account = Account._byID(uid, data=True)
        except NotFound:
            return None

        expected_cookie = account.make_cookie(timestr)
        if not constant_time_compare(session_cookie, expected_cookie):
            return None
        return account
Пример #30
0
def http_basic():
    """Authenticate the user based on their HTTP "Authorization" header."""
    import crypt

    try:
        authorization = request.environ.get("HTTP_AUTHORIZATION")
        username, password = parse_http_basic(authorization)
    except RequirementException:
        return None

    try:
        account = Account._by_name(username)
    except NotFound:
        return None

    # not all systems support bcrypt in the standard crypt
    if account.password.startswith("$2a$"):
        expected_hash = bcrypt.hashpw(password, account.password)
    else:
        expected_hash = crypt.crypt(password, account.password)

    if not constant_time_compare(expected_hash, account.password):
        return None
    return account
Пример #31
0
def validate_mailgun_webhook(timestamp, token, signature):
    """Check whether this is a valid webhook sent by Mailgun.

    See https://documentation.mailgun.com/user_manual.html#securing-webhooks

    NOTE:
    A single Mailgun account is used for both outbound email (Mailgun HTTP API)
    and inbound email (Mailgun Routes + MailgunWebhookController). As a result
    the `mailgun_api_key` is used by both.

    """

    message = ''.join((timestamp, token))
    expected_mac = hmac.new(g.secrets['mailgun_api_key'], message,
                            hashlib.sha256).hexdigest()
    if not constant_time_compare(expected_mac, signature):
        g.stats.simple_event("mailgun.incoming.bad_signature")
        return False

    if abs(int(timestamp) - time.time()) > MAX_TIMESTAMP_DEVIATION:
        g.stats.simple_event("mailgun.incoming.bad_timestamp")
        return False

    return True
Пример #32
0
def validate_mailgun_webhook(timestamp, token, signature):
    """Check whether this is a valid webhook sent by Mailgun.

    See https://documentation.mailgun.com/user_manual.html#securing-webhooks

    NOTE:
    A single Mailgun account is used for both outbound email (Mailgun HTTP API)
    and inbound email (Mailgun Routes + MailgunWebhookController). As a result
    the `mailgun_api_key` is used by both.

    """

    message = ''.join((timestamp, token))
    expected_mac = hmac.new(
        g.secrets['mailgun_api_key'], message, hashlib.sha256).hexdigest()
    if not constant_time_compare(expected_mac, signature):
        g.stats.simple_event("mailgun.incoming.bad_signature")
        return False

    if abs(int(timestamp) - time.time()) > MAX_TIMESTAMP_DEVIATION:
        g.stats.simple_event("mailgun.incoming.bad_timestamp")
        return False

    return True
Пример #33
0
    def GET_mediaembed(self, link, credentials):
        if request.host != g.media_domain:
            # don't serve up untrusted content except on our
            # specifically untrusted domain
            abort(404)

        if link.subreddit_slow.type in Subreddit.private_types:
            expected_mac = hmac.new(g.secrets["media_embed"], link._id36,
                                    hashlib.sha1).hexdigest()
            if not constant_time_compare(credentials or "", expected_mac):
                abort(404)

        if not c.secure:
            media_object = link.media_object
        else:
            media_object = link.secure_media_object

        if not media_object:
            if should_use_embedly_card(link):
                media_embed = get_embedly_card(link.url)
                content = media_embed.content
                body = EmbedlyCardMediaEmbedBody(
                    body=content,
                    id36=link._id36,
                )
            else:
                abort(404)
        elif isinstance(media_object, dict):
            # otherwise it's the new style, which is a dict(type=type, **args)
            media_embed = get_media_embed(media_object)
            content = media_embed.content
            body = MediaEmbedBody(body=content)

        c.allow_framing = True

        return body.render()
Пример #34
0
def http_basic():
    """Authenticate the user based on their HTTP "Authorization" header."""
    import crypt

    try:
        authorization = request.environ.get("HTTP_AUTHORIZATION")
        username, password = parse_http_basic(authorization)
    except RequirementException:
        return None

    try:
        account = Account._by_name(username)
    except NotFound:
        return None

    # not all systems support bcrypt in the standard crypt
    if account.password.startswith("$2a$"):
        expected_hash = bcrypt.hashpw(password, account.password)
    else:
        expected_hash = crypt.crypt(password, account.password)

    if not constant_time_compare(expected_hash, account.password):
        return None
    return account
Пример #35
0
 def validate_secret(self, secret):
     if not constant_time_compare(secret, self.webhook_secret):
         g.log.error('%s: invalid webhook secret from %s' % (self.name,
                                                             request.ip))
         self.abort403() 
Пример #36
0
def valid_signature(payload, signature):
    """Checks if `signature` matches `payload`.

    `Signature` (at least as of version 1) be of the form:

       {global_version}:{platform}:{version}:{signature}

    where:

      * global_version (currently hard-coded to be "1") can be used to change
            this header's underlying schema later if needs be.  As such, can
            be treated as a protocol version.
      * platform is the client platform type (generally "ios" or "android")
      * version is the client's token version (can be updated and incremented
            per app build as needs be.
      * signature is the hmac of the request's POST body with the token derived
            from the above three parameters via `get_secret_token`
    """
    result = Storage(
        global_version=-1,
        platform=None,
        version=-1,
        mac=None,
        valid=False,
        epoch=None,
        error=ERRORS.UNKNOWN,
    )

    sig_match = SIG_HEADER_RE.match(signature or "")
    if not sig_match:
        result.error = ERRORS.INVALID_FORMAT
        return result

    sig_header_dict = sig_match.groupdict()
    # we're matching \d so this shouldn't throw a TypeError
    result.global_version = int(sig_header_dict['global_version'])
    # incrementing this value is drastic.  We can't validate a token protocol
    # we don't understand.
    if result.global_version > GLOBAL_TOKEN_VERSION:
        result.error = ERRORS.UNKOWN_GLOBAL_VERSION
        return result

    # currently there's only one version, but here's where we'll eventually
    # patch in more.
    sig_match = SIG_CONTENT_V1_RE.match(sig_header_dict['payload'])
    if not sig_match:
        result.error = ERRORS.UNPARSEABLE
        return result

    result.update(sig_match.groupdict())
    result.version = int(result.version)
    result.epoch = int(result.epoch)

    # verify that the token provided hasn't been invalidated
    if is_invalid_token(result.platform, result.version):
        result.error = ERRORS.INVALIDATED_TOKEN
        return result

    if not valid_epoch(result.platform, result.epoch):
        result.error = ERRORS.EXPIRED_TOKEN
        return result

    # get the expected secret used to verify this request.
    secret_token = get_secret_token(
        result.platform,
        result.version,
        global_version=result.global_version,
    )
    result.valid = constant_time_compare(
        result.mac,
        versioned_hmac(secret_token, epoch_wrap(result.epoch, payload),
                       result.global_version),
    )
    if result.valid:
        result.error = None
    else:
        result.error = ERRORS.SIGNATURE_MISMATCH

    return result
Пример #37
0
    def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id,
                 payer_email, mc_currency, mc_gross, custom):

        parameters = request.POST.copy()

        # Make sure it's really PayPal
        if not constant_time_compare(paypal_secret,
                                     g.secrets['paypal_webhook']):
            log_text("invalid IPN secret",
                     "%s guessed the wrong IPN secret" % request.ip,
                     "warning")
            raise ValueError

        # Return early if it's an IPN class we don't care about
        response, psl = check_payment_status(payment_status)
        if response:
            return response

        # Return early if it's a txn_type we don't care about
        response, subscription = check_txn_type(parameters['txn_type'], psl)
        if subscription is None:
            subscr_id = None
        elif subscription == "new":
            subscr_id = parameters['subscr_id']
        elif subscription == "cancel":
            cancel_subscription(parameters['subscr_id'])
        else:
            raise ValueError("Weird subscription: %r" % subscription)

        if response:
            return response

        # Check for the debug flag, and if so, dump the IPN dict
        if g.cache.get("ipn-debug"):
            g.cache.delete("ipn-debug")
            dump_parameters(parameters)

        if mc_currency != 'USD':
            raise ValueError("Somehow got non-USD IPN %r" % mc_currency)

        if not (txn_id and paying_id and payer_email and mc_gross):
            dump_parameters(parameters)
            raise ValueError("Got incomplete IPN")

        pennies = int(mc_gross * 100)
        months, days = months_and_days_from_pennies(pennies)

        # Special case: autorenewal payment
        existing = existing_subscription(subscr_id, paying_id, custom)
        if existing:
            if existing != "deleted account":
                try:
                    create_claimed_gold ("P" + txn_id, payer_email, paying_id,
                                         pennies, days, None, existing._id,
                                         c.start_time, subscr_id)
                except IntegrityError:
                    return "Ok"
                admintools.engolden(existing, days)

                g.log.info("Just applied IPN renewal for %s, %d days" %
                           (existing.name, days))
            return "Ok"

        # More sanity checks that all non-autorenewals should pass:

        if not custom:
            dump_parameters(parameters)
            raise ValueError("Got IPN with txn_id=%s and no custom"
                             % txn_id)

        self.finish(parameters, "P" + txn_id,
                    payer_email, paying_id, subscr_id,
                    custom, pennies, months, days)
Пример #38
0
    def POST_report_cache_poisoning(
        self,
        report_mac,
        poisoner_name,
        poisoner_id,
        poisoner_canary,
        victim_canary,
        render_time,
        route_name,
        url,
        source,
        cache_policy,
        resp_headers,
    ):
        """Report an instance of cache poisoning and its details"""

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        # Eh? Why are you reporting this if the canaries are the same?
        if poisoner_canary == victim_canary:
            abort(400)

        expected_mac = make_poisoning_report_mac(
            poisoner_canary=poisoner_canary,
            poisoner_name=poisoner_name,
            poisoner_id=poisoner_id,
            cache_policy=cache_policy,
            source=source,
            route_name=route_name,
        )
        if not constant_time_compare(report_mac, expected_mac):
            abort(403)

        if resp_headers:
            try:
                resp_headers = json.loads(resp_headers)
                # Verify this is a JSON map of `header_name => [value, ...]`
                if not isinstance(resp_headers, dict):
                    abort(400)
                for hdr_name, hdr_vals in resp_headers.iteritems():
                    if not isinstance(hdr_name, basestring):
                        abort(400)
                    if not all(isinstance(h, basestring) for h in hdr_vals):
                        abort(400)
            except ValueError:
                abort(400)

        if not resp_headers:
            resp_headers = {}

        poison_info = dict(
            poisoner_name=poisoner_name,
            poisoner_id=str(poisoner_id),
            # Convert the JS timestamp to a standard one
            render_time=render_time * 1000,
            route_name=route_name,
            url=url,
            source=source,
            cache_policy=cache_policy,
            resp_headers=resp_headers,
        )

        # For immediate feedback when tracking the effects of caching changes
        g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy))
        # For longer-term diagnosing of caching issues
        g.events.cache_poisoning_event(poison_info, request=request, context=c)

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10)

        return self.api_wrapper({})
Пример #39
0
def valid_signature(payload, signature, field=None):
    """Checks if `signature` matches `payload`.

    `Signature` (at least as of version 1) be of the form:

       {global_version}:{platform}:{version}:{signature}

    where:

      * global_version (currently hard-coded to be "1") can be used to change
            this header's underlying schema later if needs be.  As such, can
            be treated as a protocol version.
      * platform is the client platform type (generally "ios" or "android")
      * version is the client's token version (can be updated and incremented
            per app build as needs be.
      * signature is the hmac of the request's POST body with the token derived
            from the above three parameters via `get_secret_token`

    :param str payload: the signed data
    :param str signature: the signature of the payload
    :param str field: error field to set (one of "ua", "body")
    :returns: object with signature validity and any errors
    :rtype: :py:class:`SigningResult`
    """
    result = SigningResult()

    # if the signature is unparseable, there's not much to do
    sig_match = SIG_HEADER_RE.match(signature or "")
    if not sig_match:
        result.add_error(ERRORS.INVALID_FORMAT, field=field)
        return result

    sig_header_dict = sig_match.groupdict()
    # we're matching \d so this shouldn't throw a TypeError
    result.global_version = int(sig_header_dict["global_version"])

    # incrementing this value is drastic.  We can't validate a token protocol
    # we don't understand.
    if result.global_version > GLOBAL_TOKEN_VERSION:
        result.add_error(ERRORS.UNKOWN_GLOBAL_VERSION, field=field)
        return result

    # currently there's only one version, but here's where we'll eventually
    # patch in more.
    sig_match = SIG_CONTENT_V1_RE.match(sig_header_dict["payload"])
    if not sig_match:
        result.add_error(ERRORS.UNPARSEABLE, field=field)
        return result

    # slop the matched data over to the SigningResult
    sig_match_dict = sig_match.groupdict()
    result.platform = sig_match_dict["platform"]
    result.version = int(sig_match_dict["version"])
    result.epoch = int(sig_match_dict["epoch"])
    result.mac = sig_match_dict["mac"]

    # verify that the token provided hasn't been invalidated
    if is_invalid_token(result.platform, result.version):
        result.add_error(ERRORS.INVALIDATED_TOKEN, field=field)
        return result

    # check the epoch validity, but don't fail -- leave that up to the
    # validator!
    if not valid_epoch(result.platform, result.epoch):
        result.add_error(ERRORS.EXPIRED_TOKEN, field=field, details=result.epoch)

    # get the expected secret used to verify this request.
    secret_token = get_secret_token(result.platform, result.version, global_version=result.global_version)

    result.valid_hmac = constant_time_compare(
        result.mac, versioned_hmac(secret_token, epoch_wrap(result.epoch, payload), result.global_version)
    )

    if not result.valid_hmac:
        result.add_error(ERRORS.SIGNATURE_MISMATCH, field=field)

    return result
Пример #40
0
def check_tick_mac(seconds_left, tick_time, observed_mac):
    expected_message = "%s/%s" % (seconds_left, tick_time)
    expected_mac = hmac.new(
        THEBUTTON_SECRET, expected_message, hashlib.sha1).hexdigest()
    return constant_time_compare(expected_mac, observed_mac)
Пример #41
0
def valid_signature(payload, signature, field=None):
    """Checks if `signature` matches `payload`.

    `Signature` (at least as of version 1) be of the form:

       {global_version}:{platform}:{version}:{signature}

    where:

      * global_version (currently hard-coded to be "1") can be used to change
            this header's underlying schema later if needs be.  As such, can
            be treated as a protocol version.
      * platform is the client platform type (generally "ios" or "android")
      * version is the client's token version (can be updated and incremented
            per app build as needs be.
      * signature is the hmac of the request's POST body with the token derived
            from the above three parameters via `get_secret_token`

    :param str payload: the signed data
    :param str signature: the signature of the payload
    :param str field: error field to set (one of "ua", "body")
    :returns: object with signature validity and any errors
    :rtype: :py:class:`SigningResult`
    """
    result = SigningResult()

    # if the signature is unparseable, there's not much to do
    sig_match = SIG_HEADER_RE.match(signature or "")
    if not sig_match:
        result.add_error(ERRORS.INVALID_FORMAT, field=field)
        return result

    sig_header_dict = sig_match.groupdict()
    # we're matching \d so this shouldn't throw a TypeError
    result.global_version = int(sig_header_dict['global_version'])

    # incrementing this value is drastic.  We can't validate a token protocol
    # we don't understand.
    if result.global_version > GLOBAL_TOKEN_VERSION:
        result.add_error(ERRORS.UNKOWN_GLOBAL_VERSION, field=field)
        return result

    # currently there's only one version, but here's where we'll eventually
    # patch in more.
    sig_match = SIG_CONTENT_V1_RE.match(sig_header_dict['payload'])
    if not sig_match:
        result.add_error(ERRORS.UNPARSEABLE, field=field)
        return result

    # slop the matched data over to the SigningResult
    sig_match_dict = sig_match.groupdict()
    result.platform = sig_match_dict['platform']
    result.version = int(sig_match_dict['version'])
    result.epoch = int(sig_match_dict['epoch'])
    result.mac = sig_match_dict['mac']

    # verify that the token provided hasn't been invalidated
    if is_invalid_token(result.platform, result.version):
        result.add_error(ERRORS.INVALIDATED_TOKEN, field=field)
        return result

    # check the epoch validity, but don't fail -- leave that up to the
    # validator!
    if not valid_epoch(result.platform, result.epoch):
        result.add_error(
            ERRORS.EXPIRED_TOKEN,
            field=field,
            details=result.epoch,
        )

    # get the expected secret used to verify this request.
    secret_token = get_secret_token(
        result.platform,
        result.version,
        global_version=result.global_version,
    )

    result.valid_hmac = constant_time_compare(
        result.mac,
        versioned_hmac(secret_token, epoch_wrap(result.epoch, payload),
                       result.global_version),
    )

    if not result.valid_hmac:
        result.add_error(ERRORS.SIGNATURE_MISMATCH, field=field)

    return result
Пример #42
0
 def run(self, secret):
     if secret and constant_time_compare(secret, g.ADMINSECRET):
         return
     super(VAdminOrAdminSecret, self).run()
Пример #43
0
def is_valid_click_url(link, click_url, click_hash):
    expected_mac = get_click_url_hmac(link, click_url)

    return constant_time_compare(click_hash, expected_mac)
Пример #44
0
    def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id,
                 payer_email, mc_currency, mc_gross, custom):

        parameters = request.POST.copy()

        # Make sure it's really PayPal
        if not constant_time_compare(paypal_secret,
                                     g.secrets['paypal_webhook']):
            log_text("invalid IPN secret",
                     "%s guessed the wrong IPN secret" % request.ip,
                     "warning")
            raise ValueError

        # Return early if it's an IPN class we don't care about
        response, psl = check_payment_status(payment_status)
        if response:
            return response

        # Return early if it's a txn_type we don't care about
        response, subscription = check_txn_type(parameters['txn_type'], psl)
        if subscription is None:
            subscr_id = None
        elif subscription == "new":
            subscr_id = parameters['subscr_id']
        elif subscription == "cancel":
            cancel_subscription(parameters['subscr_id'])
        else:
            raise ValueError("Weird subscription: %r" % subscription)

        if response:
            return response

        # Check for the debug flag, and if so, dump the IPN dict
        if g.cache.get("ipn-debug"):
            g.cache.delete("ipn-debug")
            dump_parameters(parameters)

        if mc_currency != 'USD':
            raise ValueError("Somehow got non-USD IPN %r" % mc_currency)

        if not (txn_id and paying_id and payer_email and mc_gross):
            dump_parameters(parameters)
            raise ValueError("Got incomplete IPN")

        pennies = int(mc_gross * 100)
        months, days = months_and_days_from_pennies(pennies)

        # Special case: autorenewal payment
        existing = existing_subscription(subscr_id, paying_id, custom)
        if existing:
            if existing != "deleted account":
                try:
                    create_claimed_gold ("P" + txn_id, payer_email, paying_id,
                                         pennies, days, None, existing._id,
                                         c.start_time, subscr_id)
                except IntegrityError:
                    return "Ok"
                admintools.engolden(existing, days)

                g.log.info("Just applied IPN renewal for %s, %d days" %
                           (existing.name, days))
            return "Ok"

        # More sanity checks that all non-autorenewals should pass:

        if not custom:
            dump_parameters(parameters)
            raise ValueError("Got IPN with txn_id=%s and no custom"
                             % txn_id)

        self.finish(parameters, "P" + txn_id,
                    payer_email, paying_id, subscr_id,
                    custom, pennies, months, days)
Пример #45
0
    def POST_report_cache_poisoning(
            self,
            report_mac,
            poisoner_name,
            poisoner_id,
            poisoner_canary,
            victim_canary,
            render_time,
            route_name,
            url,
            source,
            cache_policy,
            resp_headers,
    ):
        """Report an instance of cache poisoning and its details"""

        self.OPTIONS_report_cache_poisoning()

        if c.errors:
            abort(400)

        # prevent simple CSRF by requiring a custom header
        if not request.headers.get('X-Loggit'):
            abort(403)

        # Eh? Why are you reporting this if the canaries are the same?
        if poisoner_canary == victim_canary:
            abort(400)

        expected_mac = make_poisoning_report_mac(
            poisoner_canary=poisoner_canary,
            poisoner_name=poisoner_name,
            poisoner_id=poisoner_id,
            cache_policy=cache_policy,
            source=source,
            route_name=route_name,
        )
        if not constant_time_compare(report_mac, expected_mac):
            abort(403)

        if resp_headers:
            try:
                resp_headers = json.loads(resp_headers)
                # Verify this is a JSON map of `header_name => [value, ...]`
                if not isinstance(resp_headers, dict):
                    abort(400)
                for hdr_name, hdr_vals in resp_headers.iteritems():
                    if not isinstance(hdr_name, basestring):
                        abort(400)
                    if not all(isinstance(h, basestring) for h in hdr_vals):
                        abort(400)
            except ValueError:
                abort(400)

        if not resp_headers:
            resp_headers = {}

        poison_info = dict(
            poisoner_name=poisoner_name,
            poisoner_id=str(poisoner_id),
            # Convert the JS timestamp to a standard one
            render_time=render_time * 1000,
            route_name=route_name,
            url=url,
            source=source,
            cache_policy=cache_policy,
            resp_headers=resp_headers,
        )

        # For immediate feedback when tracking the effects of caching changes
        g.stats.simple_event("cache.poisoning.%s.%s" % (source, cache_policy))
        # For longer-term diagnosing of caching issues
        g.events.cache_poisoning_event(poison_info, request=request, context=c)

        VRatelimit.ratelimit(rate_ip=True, prefix="rate_poison_", seconds=10)

        return self.api_wrapper({})
Пример #46
0
def valid_signature(payload, signature):
    """Checks if `signature` matches `payload`.

    `Signature` (at least as of version 1) be of the form:

       {global_version}:{platform}:{version}:{signature}

    where:

      * global_version (currently hard-coded to be "1") can be used to change
            this header's underlying schema later if needs be.  As such, can
            be treated as a protocol version.
      * platform is the client platform type (generally "ios" or "android")
      * version is the client's token version (can be updated and incremented
            per app build as needs be.
      * signature is the hmac of the request's POST body with the token derived
            from the above three parameters via `get_secret_token`
    """
    result = Storage(
        global_version=-1,
        platform=None,
        version=-1,
        mac=None,
        valid=False,
        epoch=None,
        error=ERRORS.UNKNOWN,
    )

    sig_match = SIG_HEADER_RE.match(signature or "")
    if not sig_match:
        result.error = ERRORS.INVALID_FORMAT
        return result

    sig_header_dict = sig_match.groupdict()
    # we're matching \d so this shouldn't throw a TypeError
    result.global_version = int(sig_header_dict['global_version'])
    # incrementing this value is drastic.  We can't validate a token protocol
    # we don't understand.
    if result.global_version > GLOBAL_TOKEN_VERSION:
        result.error = ERRORS.UNKOWN_GLOBAL_VERSION
        return result

    # currently there's only one version, but here's where we'll eventually
    # patch in more.
    sig_match = SIG_CONTENT_V1_RE.match(sig_header_dict['payload'])
    if not sig_match:
        result.error = ERRORS.UNPARSEABLE
        return result

    result.update(sig_match.groupdict())
    result.version = int(result.version)
    result.epoch = int(result.epoch)

    # verify that the token provided hasn't been invalidated
    if is_invalid_token(result.platform, result.version):
        result.error = ERRORS.INVALIDATED_TOKEN
        return result

    if not valid_epoch(result.platform, result.epoch):
        result.error = ERRORS.EXPIRED_TOKEN
        return result

    # get the expected secret used to verify this request.
    secret_token = get_secret_token(
        result.platform,
        result.version,
        global_version=result.global_version,
    )
    result.valid = constant_time_compare(
        result.mac,
        versioned_hmac(
            secret_token,
            epoch_wrap(result.epoch, payload),
            result.global_version
        ),
    )
    if result.valid:
        result.error = None
    else:
        result.error = ERRORS.SIGNATURE_MISMATCH

    return result
Пример #47
0
def is_valid_click_url(link, click_url, click_hash):
    expected_mac = get_click_url_hmac(link, click_url)

    return constant_time_compare(click_hash, expected_mac)
Пример #48
0
 def validate_secret(self, secret):
     if not constant_time_compare(secret, self.webhook_secret):
         g.log.error('%s: invalid webhook secret from %s' % (self.name,
                                                             request.ip))
         self.abort403() 
Пример #49
0
def check_tick_mac(seconds_left, tick_time, observed_mac):
    expected_message = "%s/%s" % (seconds_left, tick_time)
    expected_mac = hmac.new(THEBUTTON_SECRET, expected_message,
                            hashlib.sha1).hexdigest()
    return constant_time_compare(expected_mac, observed_mac)