Skip to content

Commit 57b0d9d

Browse files
author
Ralf Schlatterbeck
committed
Add new config-option 'migrate_passwords' in section 'web'...
...to auto-migrate passwords at web-login time. Default for the new option is "yes" so if you don't want that passwords are auto-migrated to a more secure password scheme on user login, set this to "no" before running your tracker(s) after the upgrade.
1 parent 322fa04 commit 57b0d9d

File tree

6 files changed

+82
-19
lines changed

6 files changed

+82
-19
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ Fixed:
7979
- Fix Password handling security issue2550688 (thanks Joseph Myers for
8080
reporting and Eli Collins for fixing) -- this fixes all observations
8181
by Joseph Myers except for auto-migration of existing passwords.
82+
- Add new config-option 'migrate_passwords' in section 'web' to
83+
auto-migrate passwords at web-login time. Default for the new option
84+
is "yes" so if you don't want that passwords are auto-migrated to a
85+
more secure password scheme on user login, set this to "no" before
86+
running your tracker(s) after the upgrade.
8287

8388
2010-10-08 1.4.16 (r4541)
8489

doc/upgrading.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ steps.
1616
Migrating from 1.4.x to 1.4.17
1717
==============================
1818

19+
There is a new config-option 'migrate_passwords' in section 'web' to
20+
auto-migrate passwords at web-login time to a more secure storage
21+
scheme. Default for the new option is "yes" so if you don't want that
22+
passwords are auto-migrated to a more secure password scheme on user
23+
login, set this to "no" before running your tracker(s) after the
24+
upgrade.
25+
1926
Searching now requires either read-permission without a check method, or
2027
you will have to add a "Search" permission for a class or a list of
2128
properties for a class (if you want to allow searching). For the classic

roundup/cgi/actions.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,12 +1005,18 @@ def verifyLogin(self, username, password):
10051005
raise exceptions.LoginError(self._(
10061006
"You do not have permission to login"))
10071007

1008-
def verifyPassword(self, userid, password):
1009-
'''Verify the password that the user has supplied'''
1010-
stored = self.db.user.get(userid, 'password')
1011-
if password == stored:
1008+
def verifyPassword(self, userid, givenpw):
1009+
'''Verify the password that the user has supplied.
1010+
Optionally migrate to new password scheme if configured
1011+
'''
1012+
db = self.db
1013+
stored = db.user.get(userid, 'password')
1014+
if givenpw == stored:
1015+
if db.config.WEB_MIGRATE_PASSWORDS and stored.needs_migration():
1016+
db.user.set(userid, password=password.Password(givenpw))
1017+
db.commit()
10121018
return 1
1013-
if not password and not stored:
1019+
if not givenpw and not stored:
10141020
return 1
10151021
return 0
10161022

roundup/configuration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ def str2value(self, value):
579579
"Setting this option makes Roundup display error tracebacks\n"
580580
"in the user's browser rather than emailing them to the\n"
581581
"tracker admin."),
582+
(BooleanOption, "migrate_passwords", "yes",
583+
"Setting this option makes Roundup migrate passwords with\n"
584+
"an insecure password-scheme to a more secure scheme\n"
585+
"when the user logs in via the web-interface."),
582586
)),
583587
("rdbms", (
584588
(Option, 'name', 'roundup',

roundup/password.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,27 +116,33 @@ class PasswordValueError(ValueError):
116116
""" The password value is not valid """
117117
pass
118118

119+
def pbkdf2_unpack(pbkdf2):
120+
""" unpack pbkdf2 encrypted password into parts,
121+
assume it has format "{rounds}${salt}${digest}
122+
"""
123+
if isinstance(pbkdf2, unicode):
124+
pbkdf2 = pbkdf2.encode("ascii")
125+
try:
126+
rounds, salt, digest = pbkdf2.split("$")
127+
except ValueError:
128+
raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
129+
if rounds.startswith("0"):
130+
raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
131+
try:
132+
rounds = int(rounds)
133+
except ValueError:
134+
raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
135+
raw_salt = h64decode(salt)
136+
return rounds, salt, raw_salt, digest
137+
119138
def encodePassword(plaintext, scheme, other=None):
120139
"""Encrypt the plaintext password.
121140
"""
122141
if plaintext is None:
123142
plaintext = ""
124143
if scheme == "PBKDF2":
125144
if other:
126-
#assume it has format "{rounds}${salt}${digest}"
127-
if isinstance(other, unicode):
128-
other = other.encode("ascii")
129-
try:
130-
rounds, salt, digest = other.split("$")
131-
except ValueError:
132-
raise PasswordValueError, "invalid PBKDF2 hash (wrong number of separators)"
133-
if rounds.startswith("0"):
134-
raise PasswordValueError, "invalid PBKDF2 hash (zero-padded rounds)"
135-
try:
136-
rounds = int(rounds)
137-
except ValueError:
138-
raise PasswordValueError, "invalid PBKDF2 hash (invalid rounds)"
139-
raw_salt = h64decode(salt)
145+
rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
140146
else:
141147
raw_salt = getrandbytes(20)
142148
salt = h64encode(raw_salt)
@@ -249,6 +255,17 @@ def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False):
249255
self.password = None
250256
self.plaintext = None
251257

258+
def needs_migration(self):
259+
""" Password has insecure scheme or other insecure parameters
260+
and needs migration to new password scheme
261+
"""
262+
if self.scheme != 'PBKDF2':
263+
return True
264+
rounds, salt, raw_salt, digest = pbkdf2_unpack(self.password)
265+
if rounds < 1000:
266+
return True
267+
return False
268+
252269
def unpack(self, encrypted, scheme=None, strict=False):
253270
"""Set the password info from the scheme:<encryted info> string
254271
(the inverse of __str__)

test/test_cgi.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,30 @@ def testEmptyPasswordNotSet(self):
425425
':confirm:password': ''}, 'user', nodeid),
426426
({('user', nodeid): {}}, []))
427427

428+
def testPasswordMigration(self):
429+
chef = self.db.user.lookup('Chef')
430+
form = dict(__login_name='Chef', __login_password='foo')
431+
cl = self._make_client(form)
432+
# assume that the "best" algorithm is the first one and doesn't
433+
# need migration, all others should be migrated.
434+
for scheme in password.Password.known_schemes[1:]:
435+
pw1 = password.Password('foo', scheme=scheme)
436+
self.assertEqual(pw1.needs_migration(), True)
437+
self.db.user.set(chef, password=pw1)
438+
self.db.commit()
439+
actions.LoginAction(cl).handle()
440+
pw = self.db.user.get(chef, 'password')
441+
self.assertEqual(pw, 'foo')
442+
self.assertEqual(pw.needs_migration(), False)
443+
pw1 = pw
444+
self.assertEqual(pw1.needs_migration(), False)
445+
scheme = password.Password.known_schemes[0]
446+
self.assertEqual(scheme, pw1.scheme)
447+
actions.LoginAction(cl).handle()
448+
pw = self.db.user.get(chef, 'password')
449+
self.assertEqual(pw, 'foo')
450+
self.assertEqual(pw, pw1)
451+
428452
#
429453
# Boolean
430454
#

0 commit comments

Comments
 (0)