import hashlib
import hmac
import typing as _t
from . encoding import _base64_alphabet
from . encoding import base64_decode
from . encoding import base64_encode
from . encoding import want_bytes
from . exc import BadSignature
_t_str_bytes = _t . Union [ str , bytes ]
_t_opt_str_bytes = _t . Optional [ _t_str_bytes ]
_t_secret_key = _t . Union [ _t . Iterable [ _t_str_bytes ] , _t_str_bytes ]
class SigningAlgorithm :
""" Subclasses must implement :meth:`get_signature` to provide
signature generation functionality .
"""
def get_signature ( self , key : bytes , value : bytes ) - > bytes :
""" Returns the signature for the given key and value. """
raise NotImplementedError ( )
def verify_signature ( self , key : bytes , value : bytes , sig : bytes ) - > bool :
""" Verifies the given signature matches the expected
signature .
"""
return hmac . compare_digest ( sig , self . get_signature ( key , value ) )
class NoneAlgorithm ( SigningAlgorithm ) :
""" Provides an algorithm that does not perform any signing and
returns an empty signature .
"""
def get_signature ( self , key : bytes , value : bytes ) - > bytes :
return b " "
class HMACAlgorithm ( SigningAlgorithm ) :
""" Provides signature generation using HMACs. """
#: The digest method to use with the MAC algorithm. This defaults to
#: SHA1, but can be changed to any other function in the hashlib
#: module.
default_digest_method : _t . Any = staticmethod ( hashlib . sha1 )
def __init__ ( self , digest_method : _t . Any = None ) :
if digest_method is None :
digest_method = self . default_digest_method
self . digest_method : _t . Any = digest_method
def get_signature ( self , key : bytes , value : bytes ) - > bytes :
mac = hmac . new ( key , msg = value , digestmod = self . digest_method )
return mac . digest ( )
def _make_keys_list ( secret_key : _t_secret_key ) - > _t . List [ bytes ] :
if isinstance ( secret_key , ( str , bytes ) ) :
return [ want_bytes ( secret_key ) ]
return [ want_bytes ( s ) for s in secret_key ]
class Signer :
""" A signer securely signs bytes, then unsigns them to verify that
the value hasn ' t been changed.
The secret key should be a random string of ` ` bytes ` ` and should not
be saved to code or version control . Different salts should be used
to distinguish signing in different contexts . See : doc : ` / concepts `
for information about the security of the secret key and salt .
: param secret_key : The secret key to sign and verify with . Can be a
list of keys , oldest to newest , to support key rotation .
: param salt : Extra key to combine with ` ` secret_key ` ` to distinguish
signatures in different contexts .
: param sep : Separator between the signature and value .
: param key_derivation : How to derive the signing key from the secret
key and salt . Possible values are ` ` concat ` ` , ` ` django - concat ` ` ,
or ` ` hmac ` ` . Defaults to : attr : ` default_key_derivation ` , which
defaults to ` ` django - concat ` ` .
: param digest_method : Hash function to use when generating the HMAC
signature . Defaults to : attr : ` default_digest_method ` , which
defaults to : func : ` hashlib . sha1 ` . Note that the security of the
hash alone doesn ' t apply when used intermediately in HMAC.
: param algorithm : A : class : ` SigningAlgorithm ` instance to use
instead of building a default : class : ` HMACAlgorithm ` with the
` ` digest_method ` ` .
. . versionchanged : : 2.0
Added support for key rotation by passing a list to
` ` secret_key ` ` .
. . versionchanged : : 0.18
` ` algorithm ` ` was added as an argument to the class constructor .
. . versionchanged : : 0.14
` ` key_derivation ` ` and ` ` digest_method ` ` were added as arguments
to the class constructor .
"""
#: The default digest method to use for the signer. The default is
#: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or
#: compatible object. Note that the security of the hash alone
#: doesn't apply when used intermediately in HMAC.
#:
#: .. versionadded:: 0.14
default_digest_method : _t . Any = staticmethod ( hashlib . sha1 )
#: The default scheme to use to derive the signing key from the
#: secret key and salt. The default is ``django-concat``. Possible
#: values are ``concat``, ``django-concat``, and ``hmac``.
#:
#: .. versionadded:: 0.14
default_key_derivation : str = " django-concat "
def __init__ (
self ,
secret_key : _t_secret_key ,
salt : _t_opt_str_bytes = b " itsdangerous.Signer " ,
sep : _t_str_bytes = b " . " ,
key_derivation : _t . Optional [ str ] = None ,
digest_method : _t . Optional [ _t . Any ] = None ,
algorithm : _t . Optional [ SigningAlgorithm ] = None ,
) :
#: The list of secret keys to try for verifying signatures, from
#: oldest to newest. The newest (last) key is used for signing.
#:
#: This allows a key rotation system to keep a list of allowed
#: keys and remove expired ones.
self . secret_keys : _t . List [ bytes ] = _make_keys_list ( secret_key )
self . sep : bytes = want_bytes ( sep )
if self . sep in _base64_alphabet :
raise ValueError (
" The given separator cannot be used because it may be "
" contained in the signature itself. ASCII letters, "
" digits, and ' -_= ' must not be used. "
)
if salt is not None :
salt = want_bytes ( salt )
else :
salt = b " itsdangerous.Signer "
self . salt = salt
if key_derivation is None :
key_derivation = self . default_key_derivation
self . key_derivation : str = key_derivation
if digest_method is None :
digest_method = self . default_digest_method
self . digest_method : _t . Any = digest_method
if algorithm is None :
algorithm = HMACAlgorithm ( self . digest_method )
self . algorithm : SigningAlgorithm = algorithm
@property
def secret_key ( self ) - > bytes :
""" The newest (last) entry in the :attr:`secret_keys` list. This
is for compatibility from before key rotation support was added .
"""
return self . secret_keys [ - 1 ]
def derive_key ( self , secret_key : _t_opt_str_bytes = None ) - > bytes :
""" This method is called to derive the key. The default key
derivation choices can be overridden here . Key derivation is not
intended to be used as a security method to make a complex key
out of a short password . Instead you should use large random
secret keys .
: param secret_key : A specific secret key to derive from .
Defaults to the last item in : attr : ` secret_keys ` .
. . versionchanged : : 2.0
Added the ` ` secret_key ` ` parameter .
"""
if secret_key is None :
secret_key = self . secret_keys [ - 1 ]
else :
secret_key = want_bytes ( secret_key )
if self . key_derivation == " concat " :
return _t . cast ( bytes , self . digest_method ( self . salt + secret_key ) . digest ( ) )
elif self . key_derivation == " django-concat " :
return _t . cast (
bytes , self . digest_method ( self . salt + b " signer " + secret_key ) . digest ( )
)
elif self . key_derivation == " hmac " :
mac = hmac . new ( secret_key , digestmod = self . digest_method )
mac . update ( self . salt )
return mac . digest ( )
elif self . key_derivation == " none " :
return secret_key
else :
raise TypeError ( " Unknown key derivation method " )
def get_signature ( self , value : _t_str_bytes ) - > bytes :
""" Returns the signature for the given value. """
value = want_bytes ( value )
key = self . derive_key ( )
sig = self . algorithm . get_signature ( key , value )
return base64_encode ( sig )
def sign ( self , value : _t_str_bytes ) - > bytes :
""" Signs the given string. """
value = want_bytes ( value )
return value + self . sep + self . get_signature ( value )
def verify_signature ( self , value : _t_str_bytes , sig : _t_str_bytes ) - > bool :
""" Verifies the signature for the given value. """
try :
sig = base64_decode ( sig )
except Exception :
return False
value = want_bytes ( value )
for secret_key in reversed ( self . secret_keys ) :
key = self . derive_key ( secret_key )
if self . algorithm . verify_signature ( key , value , sig ) :
return True
return False
def unsign ( self , signed_value : _t_str_bytes ) - > bytes :
""" Unsigns the given string. """
signed_value = want_bytes ( signed_value )
if self . sep not in signed_value :
raise BadSignature ( f " No { self . sep !r} found in value " )
value , sig = signed_value . rsplit ( self . sep , 1 )
if self . verify_signature ( value , sig ) :
return value
raise BadSignature ( f " Signature { sig !r} does not match " , payload = value )
def validate ( self , signed_value : _t_str_bytes ) - > bool :
""" Only validates the given signed value. Returns ``True`` if
the signature exists and is valid .
"""
try :
self . unsign ( signed_value )
return True
except BadSignature :
return False