1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
.. index:: SCRAM protocol
===================================================================
:class:`passlib.hash.scram` - SCRAM Hash
===================================================================
.. currentmodule:: passlib.hash
SCRAM is a password-based challenge response protocol defined by :rfc:`5802`.
While Passlib does not provide an implementation of SCRAM, applications
which use SCRAM on the server side frequently need a way to store
user passwords in a secure format that can be used to authenticate users over
SCRAM.
To accomplish this, Passlib provides the following
:ref:`modular-crypt-format`-compatible password hash scheme which uses the
``$scram$`` identifier. This format encodes a salt, rounds settings, and one
or more :func:`~passlib.utils.pbkdf2.pbkdf2` digests... one digest for each
of the hash algorithms the server wishes to support over SCRAM.
Since this format is PBKDF2-based, it has equivalent security to
Passlib's other :doc:`pbkdf2 hashes <passlib.hash.pbkdf2_digest>`,
and can be used to authenticate users using either the normal :ref:`password-hash-api`
or the SCRAM-specific class methods documentated below.
.. note::
If you aren't working with the SCRAM protocol, you probably
don't need to use this hash format.
Usage
=====
This class can be used like any other Passlib hash, as follows::
>>> from passlib.hash import scram
>>> #generate new salt, encrypt password against default list of algorithms
>>> h = scram.encrypt("password")
>>> h # (output split over multiple lines for readability)
'$scram$6400$.Z/znnNOKWUsBaCU$sha-1=cRseQyJpnuPGn3e6d6u6JdJWk.0,sha-256=5G\
cjEbRaUIIci1r6NAMdI9OPZbxl9S5CFR6la9CHXYc,sha-512=.DHbIm82ajXbFR196Y.9Ttbs\
gzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw'
>>> #same, but with explict number of rounds
>>> scram.encrypt("password", rounds=8000)
'$scram$8000$Y0zp/R/DeO89h/De$sha-1=eE8dq1f1P1hZm21lfzsr3CMbiEA,sha-256=Nf\
kaDFMzn/yHr/HTv7KEFZqaONo6psRu5LBBFLEbZ.o,sha-512=XnGG11X.J2VGSG1qTbkR3FVr\
9j5JwsnV5Fd094uuC.GtVDE087m8e7rGoiVEgXnduL48B2fPsUD9grBjURjkiA'
>>> #check if hash is recognized
>>> scram.identify(h)
True
>>> #check if some other hash is recognized
>>> scram.identify('$1$3azHgidD$SrJPt7B.9rekpmwJwtON31')
False
>>> #verify correct password
>>> scram.verify("password", h)
True
>>> scram.verify("secret", h) #verify incorrect password
False
Additionally, this class provides a number of useful methods
for SCRAM-specific actions::
>>> from passlib.hash import scram
>>> # generate new salt, encrypt password against default list of algorithms
>>> scram.encrypt("password")
'$scram$6400$.Z/znnNOKWUsBaCU$sha-1=cRseQyJpnuPGn3e6d6u6JdJWk.0,sha-256=5G\
cjEbRaUIIci1r6NAMdI9OPZbxl9S5CFR6la9CHXYc,sha-512=.DHbIm82ajXbFR196Y.9Ttbs\
gzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw'
>>> # generate new salt, encrypt password against specific list of algorithms
>>> # and choose explicit number of rounds
>>> h = scram.encrypt("password", rounds=1000, algs="sha-1,sha-256,md5")
>>> h
'$scram$1000$RsgZo7T2/l8rBUBI$md5=iKsH555d3ctn795Za4S7bQ,sha-1=dRcE2AUjALLF\
tX5DstdLCXZ9Afw,sha-256=WYE/LF7OntriUUdFXIrYE19OY2yL0N5qsQmdPNFn7JE'
>>> # given a scram hash, retrieve the information SCRAM needs
>>> # to authenticate using a specific mechanism -
>>> # returns salt, rounds, digest
>>> scram.extact_digest_info(h, "sha-1")
('F\xc8\x19\xa3\xb4\xf6\xfe_+\x05@H',
1000,
'u\x17\x04\xd8\x05#\x00\xb2\xc5\xb5~C\xb2\xd7K\tv}\x01\xfc')
>>> # given a scram hash, return list of digest algs present
>>> scram.extract_digest_algs(h)
["md5", "sha-1", "sha-256"]
>>> # and a standalone helper that can calculate the SaltedPassword
>>> # portion of the SCRAM protocol, taking care of SASLPrep as well.
>>> scram.derive_digest("password", b'\x01\x02\x03', 1000, "sha-1")
b'k\x086vg\xb3\xfciz\xb4\xb4\xe2JRZ\xaet\xe4`\xe7'
Interface
=========
.. note::
This hash format is new in Passlib 1.6, and it's SCRAM-specific API
may change in the next few releases, depending on user feedback.
.. autoclass:: scram()
.. rst-class:: html-toggle
Format & Algorithm
==================
An example scram hash (of the string ``password``) is::
$scram$6400$.Z/znnNOKWUsBaCU$sha-1=cRseQyJpnuPGn3e6d6u6JdJWk.0,sha-256=5G
cjEbRaUIIci1r6NAMdI9OPZbxl9S5CFR6la9CHXYc,sha-512=.DHbIm82ajXbFR196Y.9Ttb
sgzvGjbMeuWCtKve8TPjRMNoZK9EGyHQ6y0lW9OtWdHZrDZbBUhB9ou./VI2mlw
An scram hash string has the format :samp:`$scram${rounds}${salt}${alg1}={digest1},{alg2}={digest2},{...}`, where:
* ``$scram$`` is the prefix used to identify Passlib scram hashes,
following the :ref:`modular-crypt-format`
* :samp:`{rounds}` is the number of decimal rounds to use (6400 in the example),
zero-padding not allowed. this value must be in ``range(1, 2**32)``.
* :samp:`{salt}` is a base64 salt string (``.Z/znnNOKWUsBaCU`` in the example),
encoded using :func:`~passlib.utils.ab64_encode`.
* :samp:`{alg}` is a lowercase IANA hash function name [#hnames]_, which should
match the digest in the SCRAM mechanism name.
* :samp:`{digest}` is a base64 digest for the specific algorithm,
encoded using :func:`~passlib.utils.ab64_encode`.
Digests for ``sha-1``, ``sha-256``, and ``sha-512`` are present in the example.
* There will always be one or more :samp:`{alg}={digest}` pairs, separated by a
comma. Per the SCRAM specification, the algorithm ``sha-1`` should always be present.
There is also an alternate format (:samp:`$scram${rounds}${salt}${alg}{,...}`)
which is used to represent a configuration string that doesn't contain
any digests. An example would be ``$scram$6400$.Z/znnNOKWUsBaCU$sha-1,sha-256,sha-512``.
The algorithm used to calculate each digest is::
pbkdf2(salsprep(password).encode("utf-8"), salt, rounds, -1, alg)
...as laid out in the SCRAM specification [#scram]_. All digests
should verify against the same password, or the hash is considered malformed.
.. note::
This format is similar in spirit to the LDAP storage format for SCRAM hashes,
defined in :rfc:`5803`, except that it encodes everything into a single
string, and does not have any storage requirements (outside of the ability
to store 512+ character ascii strings).
Security
========
The security of this hash is only as strong as the weakest digest used
by this hash. Since the SCRAM [#scram]_ protocol requires SHA1
always be supported, this will generally be the weakest link, since
the other digests will generally be stronger ones (e.g. SHA2-256).
None-the-less, since PBKDF2 is sufficiently collision-resistant
on it's own, any pre-image weakenesses found in SHA1 should be mitigated
by the PBKDF2-HMAC-SHA1 wrapper; and should have no flaws outside of
brute-force attacks on PBKDF2-HMAC-SHA1.
.. rubric:: Footnotes
.. [#scram] The SCRAM protocol is laid out in :rfc:`5802`.
.. [#hnames] The official list of IANA-assigned hash function names -
`<http://www.iana.org/assignments/hash-function-text-names>`_
|