Skip to content

Commit 3b528c4

Browse files
committed
Added codec: scrypt
1 parent 34b6ca2 commit 3b528c4

5 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
436436
- [X] `oracle10`: legacy Oracle 10g password hash (parametrized with the user name) ; `oracle11` for the 11g version
437437
- [X] `phpass`: portable PHP password hash (parametrized with the iteration count ; used e.g. by WordPress)
438438
- [X] `ripemd160`: aka RIPEMD-160 (relies on `hashlib` when available, pure Python otherwise)
439+
- [X] `scrypt`: aka scrypt (RFC 7914), parameterized with its cost ln, block size r and parallelism p (relies on `hashlib`)
439440
- [X] `sha`: aka Secure Hash Algorithms ; includes SHA0, SHA1, 224, 256, 384, 512 (Python2/3) but also SHA3-224, -256, -384 and -512 (Python 3 only ; relies on `hashlib`)
440441
- [X] `shake`: aka SHAKE hashing (Python 3 only ; relies on `hashlib`)
441442
- [X] `snefru`: aka Snefru (Merkle's hash function) in 32 to 480 bits, with 2 to 8 passes

‎docs/pages/enc/hashing.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,19 @@ This one-way transformation function, used e.g. in Bitcoin addresses, relies on
362362
'f0c90bd665512fc158f3c47d6de76b58783c404c'
363363
```
364364

365+
### Scrypt
366+
367+
These one-way transformation functions compute the scrypt password hash ([RFC 7914](https://datatracker.ietf.org/doc/html/rfc7914)), a memory-hard key derivation function. The result is written in the modular crypt format `$scrypt$ln=...,r=...,p=...$salt$digest` (as [passlib](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.scrypt.html) does). The cost parameters are given with the codec name, in the order ln (the base-2 logarithm of the CPU/memory cost N), r (the block size) and p (the parallelism) ; they default to ln=16, r=8 and p=1. A random salt is generated each time. It relies on [`hashlib`](https://docs.python.org/3/library/hashlib.html).
368+
369+
**Codec** | **Conversions** | **Aliases** | **Comment**
370+
:---: | :---: | --- | ---
371+
`scrypt` | data --> scrypt(data) | `scrypt-14`, `scrypt-14-8-2`, ... | parametrized with ln, r and p ; random salt
372+
373+
```python
374+
>>> codext.encode("password", "scrypt")[:21]
375+
'$scrypt$ln=16,r=8,p=1'
376+
```
377+
365378
-----
366379

367380
### Secure Hash Algorithm

‎src/codext/hashing/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .phpass import *
2121
from .postgres import *
2222
from .ripemd import *
23+
from .scrypt import *
2324
from .sha import *
2425
from .shake import *
2526
from .snefru import *

‎src/codext/hashing/scrypt.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Scrypt Hashing Codec - string hashing with scrypt.
3+
4+
scrypt, designed by Colin Percival in 2009 (RFC 7914), is a memory-hard password-based key derivation function whose
5+
cost is set by three parameters: the CPU/memory cost N (a power of two, given as its base-2 logarithm), the block size
6+
r and the parallelism p. The result is written in the modular crypt format
7+
"$scrypt$ln=<log2n>,r=<r>,p=<p>$<salt>$<digest>", with the 16-byte salt and the 32-byte digest encoded in base64
8+
(without padding). The parameters are given in the codec name (e.g. "scrypt-14" or "scrypt-14-8-2" ; they default to
9+
ln=16, r=8, p=1) and a random salt is generated each time, hence the output differs between calls. This codec returns
10+
the hash string (one-way), for use with other codecs in encoding chains.
11+
12+
This codec:
13+
- transforms strings from str to str
14+
- transforms strings from bytes to bytes
15+
- transforms file content from str to bytes (write)
16+
17+
Reference: https://datatracker.ietf.org/doc/html/rfc7914
18+
"""
19+
import os
20+
from base64 import b64encode
21+
22+
from ..__common__ import *
23+
24+
25+
def _b64(data):
26+
return b64encode(data).decode().rstrip("=")
27+
28+
29+
def scrypt(ln=None, r=None, p=None):
30+
ln = int(ln) if ln not in (None, "") else 16
31+
r = int(r) if r not in (None, "") else 8
32+
p = int(p) if p not in (None, "") else 1
33+
if not 1 <= ln <= 31:
34+
raise LookupError("Bad parameter for encoding 'scrypt': ln must be between 1 and 31")
35+
if r < 1 or p < 1:
36+
raise LookupError("Bad parameter for encoding 'scrypt': r and p must be strictly positive")
37+
def _hash(text, errors="strict"):
38+
salt, n = os.urandom(16), 1 << ln
39+
digest = hashlib.scrypt(b(ensure_str(text)), salt=salt, n=n, r=r, p=p, dklen=32, maxmem=128 * r * (n + p + 2))
40+
return "$scrypt$ln=%d,r=%d,p=%d$%s$%s" % (ln, r, p, _b64(salt), _b64(digest)), len(text)
41+
return _hash
42+
43+
44+
add("scrypt", scrypt, pattern=r"^scrypt(?:[-_](\d{1,2}))?(?:[-_](\d+))?(?:[-_](\d+))?$", guess=None)

‎tests/test_manual.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,11 @@ def fixed(data):
968968
("sun-md5-crypt-0", bytes([38, 39, 40, 41, 42, 43, 44, 45]), "$md5$abcdefgh$$ZxccX5lio58EZKsCI/Peg0"),
969969
("sun-md5-crypt-100", bytes([38, 39, 40, 41, 42, 43, 44, 45]),
970970
"$md5,rounds=100$abcdefgh$$M/k9.q5540c7/LWDsTI8I."),
971+
# scrypt uses a 16-byte random salt and hashlib.scrypt (reference vectors of passlib)
972+
("scrypt", bytes(range(16)),
973+
"$scrypt$ln=16,r=8,p=1$AAECAwQFBgcICQoLDA0ODw$jWPkcxERY25E9gwism7ggXZkARLbUPyOZiOM5ZQx95s"),
974+
("scrypt-10-4-2", bytes(range(16)),
975+
"$scrypt$ln=10,r=4,p=2$AAECAwQFBgcICQoLDA0ODw$i6PwKXRFGHVv2momDm8VxbF0U/YH0gT8dlATuDEwOQQ"),
971976
]
972977
for e, data, r in salted:
973978
with patch("os.urandom", fixed(data)):
@@ -976,7 +981,7 @@ def fixed(data):
976981
self.assertNotEqual(codecs.encode(PW, "mssql2000"), codecs.encode(PW, "mssql2000"))
977982
for e in ["ldap-md5", "cisco-pix", "postgres-md5-x", "oracle10-x", "mscash-x", "mscash2-x", "mssql2000",
978983
"mssql2005", "oracle11", "phpass", "bigcrypt", "crypt16", "bsdi-crypt", "bsd-nthash",
979-
"sun-md5-crypt"]:
984+
"sun-md5-crypt", "scrypt"]:
980985
self.assertRaises(NotImplementedError, codecs.decode, PW, e)
981986
# long passwords exercise the multi-block paths of bigcrypt and bsdi-crypt
982987
with patch("os.urandom", fixed(bytes([38, 39]))):
@@ -987,6 +992,8 @@ def fixed(data):
987992
self.assertRaises(LookupError, codecs.encode, PW, "phpass-6")
988993
self.assertRaises(LookupError, codecs.encode, PW, "bsdi-crypt-0")
989994
self.assertRaises(LookupError, codecs.encode, "a" * 17, "cisco-pix")
995+
self.assertRaises(LookupError, codecs.encode, PW, "scrypt-0")
996+
self.assertRaises(LookupError, codecs.encode, PW, "scrypt-10-0")
990997

991998
def test_codec_argon2(self):
992999
# Argon2, through the argon2-cffi package (skipped when it is not installed)

0 commit comments

Comments
 (0)