From e37fa695d3b3b0c92ccd35b384931c349e7f7163 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sun, 8 Sep 2019 00:45:30 +0200
Subject: [PATCH] fields: add PBKDF2 hashed password format (#35533)

---
 tests/test_form_pages.py | 12 ++++++------
 wcs/fields.py            |  3 ++-
 wcs/qommon/form.py       |  4 +++-
 3 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index 0b13ade6..c49efe3b 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -19,6 +19,7 @@ try:
 except ImportError:
     Image = None
 
+from django.contrib.auth.hashers import PBKDF2PasswordHasher
 from quixote.http_request import Upload as QuixoteUpload
 from wcs.qommon.emails import docutils
 from wcs.qommon.form import UploadedFile
@@ -1937,7 +1938,7 @@ def form_password_field_submit(app, password):
     formdef = create_formdef()
     formdef.enable_tracking_codes = True
     formdef.fields = [fields.PasswordField(id='0', label='password',
-        formats=['sha1', 'md5', 'cleartext'])]
+        formats=['sha1', 'md5', 'cleartext', 'pbkdf2'])]
     formdef.store()
     page = app.get('/test/')
     formdef.data_class().wipe()
@@ -1954,11 +1955,10 @@ def form_password_field_submit(app, password):
     assert formdef.data_class().count() == 1
     data_id = formdef.data_class().select()[0].id
     data = formdef.data_class().get(data_id)
-    assert data.data == {'0': {
-        'sha1': hashlib.sha1(password).hexdigest(),
-        'md5': hashlib.md5(password).hexdigest(),
-        'cleartext': unicode(password, 'utf-8'),
-        }}
+    assert data.data['0']['sha1'] == hashlib.sha1(password).hexdigest()
+    assert data.data['0']['md5'] == hashlib.md5(password).hexdigest()
+    assert data.data['0']['cleartext'] == unicode(password, 'utf-8')
+    assert PBKDF2PasswordHasher().verify(password, data.data['0']['pbkdf2'])
 
 def test_form_password_field_submit(pub):
     user = create_user(pub)
diff --git a/wcs/fields.py b/wcs/fields.py
index 717cc8c6..57b5f049 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -2285,7 +2285,7 @@ class PasswordField(WidgetField):
     confirmation = True
     confirmation_title = None
     strength_indicator = True
-    formats = ['sha1']
+    formats = ['pbkdf2']
     extra_attributes = ['formats', 'min_length', 'max_length',
             'count_uppercase', 'count_lowercase', 'count_digit',
             'count_special', 'confirmation', 'confirmation_title',
@@ -2301,6 +2301,7 @@ class PasswordField(WidgetField):
         formats = [('cleartext', _('Clear text')),
             ('md5', _('MD5')),
             ('sha1', _('SHA1')),
+            ('pbkdf2', _('PBKDF2')),
             ]
         form.add(CheckboxesWidget, 'formats', title=_('Storage formats'),
                 value=self.formats, options=formats, inline=True)
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index c882fbf9..1ae099f4 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -66,6 +66,7 @@ from django.utils.six.moves.html_parser import HTMLParser
 from django.utils.six import StringIO
 
 from django.conf import settings
+from django.contrib.auth.hashers import PBKDF2PasswordHasher
 from django.utils.safestring import mark_safe
 
 from .template import render as render_template, Template, TemplateError
@@ -2068,7 +2069,7 @@ class PasswordEntryWidget(CompositeWidget):
         confirmation_title = kwargs.get('confirmation_title') or _('Confirmation')
         self.strength_indicator = kwargs.get('strength_indicator', True)
 
-        self.formats = kwargs.get('formats', ['sha1'])
+        self.formats = kwargs.get('formats', ['pbkdf2'])
         if not self.attrs.get('readonly'):
             self.add(PasswordWidget, name='pwd1', title='',
                     value='',
@@ -2184,6 +2185,7 @@ $(function() {
             'cleartext': lambda x: x,
             'md5': lambda x: hashlib.md5(x).hexdigest(),
             'sha1': lambda x: hashlib.sha1(x).hexdigest(),
+            'pbkdf2': lambda x: PBKDF2PasswordHasher().encode(x, PBKDF2PasswordHasher().salt())
         }
 
         if pwd1:
-- 
2.23.0

