Source code for zope.password.legacy

##############################################################################
#
# Copyright (c) 2009 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Legacy password managers, using now-outdated, insecure methods for hashing
"""
from codecs import getencoder

from zope.interface import implementer

from zope.password.interfaces import IMatchingPasswordManager


_encoder = getencoder("utf-8")


[docs] @implementer(IMatchingPasswordManager) class MySQLPasswordManager: """A MySQL digest manager. This Password Manager implements the digest scheme as implemented in the MySQL PASSWORD function in MySQL versions before 4.1. Note that this method results in a very weak 16-byte hash. >>> from zope.interface.verify import verifyObject >>> from zope.password.interfaces import IMatchingPasswordManager >>> from zope.password.legacy import MySQLPasswordManager >>> manager = MySQLPasswordManager() >>> verifyObject(IMatchingPasswordManager, manager) True >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}" >>> encoded = manager.encodePassword(password) >>> isinstance(encoded, bytes) True >>> print(encoded.decode()) {MYSQL}0ecd752c5097d395 >>> manager.match(encoded) True >>> manager.match(encoded.decode()) True >>> manager.checkPassword(encoded.decode(), password) True >>> manager.checkPassword(encoded, password) True >>> manager.checkPassword(encoded, password + u"wrong") False Using the password 'PHP & Information Security' should result in the hash ``379693e271cd3bd6``, according to http://phpsec.org/articles/2005/password-hashing.html Our password manager generates the same value when seeded with the same seed, so we can be sure, our output is compatible with MySQL versions before 4.1: >>> password = 'PHP & Information Security' >>> encoded = manager.encodePassword(password) >>> isinstance(encoded, bytes) True >>> print(encoded.decode()) {MYSQL}379693e271cd3bd6 >>> manager.checkPassword(encoded, password) True >>> manager.checkPassword(encoded, password + u"wrong") False The manager only claims to implement MYSQL encodings, anything not starting with the string {MYSQL} returns False: >>> manager.match('{MD5}someotherhash') False Spaces and tabs are ignored: >>> encoded = manager.encodePassword('\tign or ed') >>> print(encoded.decode()) {MYSQL}75818366052c6a78 >>> encoded = manager.encodePassword('ignored') >>> print(encoded.decode()) {MYSQL}75818366052c6a78 """ def encodePassword(self, password): nr = 1345345333 add = 7 nr2 = 0x12345671 for i in _encoder(password)[0]: if i == ord(b' ') or i == ord(b'\t'): continue # pragma: no cover (this is actually hit, but ... # coverage isn't reporting it) nr ^= (((nr & 63) + add) * i) + (nr << 8) nr2 += (nr2 << 8) ^ nr add += i r0 = nr & ((1 << 31) - 1) r1 = nr2 & ((1 << 31) - 1) return (f"{{MYSQL}}{r0:08x}{r1:08x}").encode() def checkPassword(self, encoded_password, password): if not isinstance(encoded_password, bytes): encoded_password = encoded_password.encode('ascii') return encoded_password == self.encodePassword(password) def match(self, encoded_password): if not isinstance(encoded_password, bytes): encoded_password = encoded_password.encode('ascii') return encoded_password.startswith(b'{MYSQL}')