Changeset - dd676aab3b4d
[Not reviewed]
default
0 1 0
Søren Løvborg - 10 years ago 2015-09-02 17:47:03
sorenl@unity3d.com
auth: use HMAC-SHA1 to calculate password reset token

The use of standard cryptographic primitives is always preferable, and
in this case allows us not to worry about length extension attacks
and possibly any number of issues that I'm not presently aware of.

Also fix a potential Unicode encoding problem.
1 file changed with 33 insertions and 23 deletions:
0 comments (0 inline, 0 general)
kallithea/model/user.py
Show inline comments
 
@@ -24,12 +24,13 @@ Original author and date, and relevant c
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 

	
 
import hashlib
 
import hmac
 
import logging
 
import time
 
import traceback
 

	
 
from pylons import config
 
from pylons.i18n.translation import _
 
@@ -276,38 +277,47 @@ class UserModel(BaseModel):
 

	
 
        from kallithea.lib.hooks import log_delete_user
 
        log_delete_user(user.get_dict(), cur_user)
 

	
 
    def get_reset_password_token(self, user, timestamp, session_id):
 
        """
 
        The token is calculated as SHA1 hash of the following:
 
        The token is a 40-digit hexstring, calculated as a HMAC-SHA1.
 

	
 
         * user's identifier (number, not a name)
 
         * timestamp
 
         * hashed user's password
 
         * session identifier
 
         * per-application secret
 
        In a traditional HMAC scenario, an attacker is unable to know or
 
        influence the secret key, but can know or influence the message
 
        and token. This scenario is slightly different (in particular
 
        since the message sender is also the message recipient), but
 
        sufficiently similar to use an HMAC. Benefits compared to a plain
 
        SHA1 hash includes resistance against a length extension attack.
 

	
 
        The HMAC key consists of the following values (known only to the
 
        server and authorized users):
 

	
 
        * per-application secret (the `app_instance_uuid` setting), without
 
          which an attacker cannot counterfeit tokens
 
        * hashed user password, invalidating the token upon password change
 

	
 
        We use numeric user's identifier, as it's fixed and doesn't change,
 
        so renaming users doesn't affect the mechanism. Timestamp is added
 
        to make it possible to limit the token's validness (currently hard
 
        coded to 24h), and we don't want users to be able to fake that field
 
        easily. Hashed user's password is needed to prevent using the token
 
        again once the password has been changed. Session identifier is
 
        an additional security measure to ensure someone else stealing the
 
        token can't use it. Finally, per-application secret is just another
 
        way to make it harder for an attacker to guess all values in an
 
        attempt to generate a valid token.
 
        The HMAC message consists of the following values (potentially known
 
        to an attacker):
 

	
 
        * session ID (the anti-CSRF token), requiring an attacker to have
 
          access to the browser session in which the token was created
 
        * numeric user ID, limiting the token to a specific user (yet allowing
 
          users to be renamed)
 
        * user email address
 
        * time of token issue (a Unix timestamp, to enable token expiration)
 

	
 
        The key and message values are separated by NUL characters, which are
 
        guaranteed not to occur in any of the values.
 
        """
 
        return hashlib.sha1('\0'.join([
 
            str(user.user_id),
 
            str(timestamp),
 
            user.password,
 
            session_id,
 
            config.get('app_instance_uuid'),
 
        ])).hexdigest()
 
        app_secret = config.get('app_instance_uuid')
 
        return hmac.HMAC(
 
            key=u'\0'.join([app_secret, user.password]).encode('utf-8'),
 
            msg=u'\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
 
            digestmod=hashlib.sha1,
 
        ).hexdigest()
 

	
 
    def send_reset_password_email(self, data):
 
        """
 
        Sends email with a password reset token and link to the password
 
        reset confirmation page with all information (including the token)
 
        pre-filled. Also returns URL of that page, only without the token,
0 comments (0 inline, 0 general)