When `base_crypt` was updated for v7, the auto-encryption at installation was dropped, with user passwords only encrypted on-demand whenever the user would connect. It is important to encrypt all passwords immediately to prevent password compromission for user who do not login often or even for deactivated users who are not allowed to login anymore. Fixes https://bugs.launchpad.net/openobject-addons/+bug/1280152 Based on LP merge proposal by Nicolas Bessi (Camptocamp): https://code.launchpad.net/~camptocamp/openobject-addons/improve_auth_crypt_3_please_launchpad_work-nbi/+merge/206476
178 lines
5.6 KiB
Python
178 lines
5.6 KiB
Python
#
|
|
# Implements encrypting functions.
|
|
#
|
|
# Copyright (c) 2008, F S 3 Consulting Inc.
|
|
#
|
|
# Maintainer:
|
|
# Alec Joseph Rivera (agi<at>fs3.ph)
|
|
# refactored by Antony Lesuisse <al<at>openerp.com>
|
|
#
|
|
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
from random import sample
|
|
from string import ascii_letters, digits
|
|
|
|
import openerp
|
|
from openerp.osv import fields, osv
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
magic_md5 = '$1$'
|
|
magic_sha256 = '$5$'
|
|
|
|
def gen_salt(length=8, symbols=None):
|
|
if symbols is None:
|
|
symbols = ascii_letters + digits
|
|
return ''.join(sample(symbols, length))
|
|
|
|
def md5crypt( raw_pw, salt, magic=magic_md5 ):
|
|
""" md5crypt FreeBSD crypt(3) based on but different from md5
|
|
|
|
The md5crypt is based on Mark Johnson's md5crypt.py, which in turn is
|
|
based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
|
|
Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
|
|
Poul and Mark. -agi
|
|
|
|
Original license:
|
|
|
|
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
*
|
|
* <phk@login.dknet.dk> wrote this file. As long as you retain this
|
|
* notice you can do whatever you want with this stuff. If we meet some
|
|
* day, and you think this stuff is worth it, you can buy me a beer in
|
|
* return.
|
|
*
|
|
* Poul-Henning Kamp
|
|
"""
|
|
raw_pw = raw_pw.encode('utf-8')
|
|
salt = salt.encode('utf-8')
|
|
hash = hashlib.md5()
|
|
hash.update( raw_pw + magic + salt )
|
|
st = hashlib.md5()
|
|
st.update( raw_pw + salt + raw_pw)
|
|
stretch = st.digest()
|
|
|
|
for i in range( 0, len( raw_pw ) ):
|
|
hash.update( stretch[i % 16] )
|
|
|
|
i = len( raw_pw )
|
|
|
|
while i:
|
|
if i & 1:
|
|
hash.update('\x00')
|
|
else:
|
|
hash.update( raw_pw[0] )
|
|
i >>= 1
|
|
|
|
saltedmd5 = hash.digest()
|
|
|
|
for i in range( 1000 ):
|
|
hash = hashlib.md5()
|
|
|
|
if i & 1:
|
|
hash.update( raw_pw )
|
|
else:
|
|
hash.update( saltedmd5 )
|
|
|
|
if i % 3:
|
|
hash.update( salt )
|
|
if i % 7:
|
|
hash.update( raw_pw )
|
|
if i & 1:
|
|
hash.update( saltedmd5 )
|
|
else:
|
|
hash.update( raw_pw )
|
|
|
|
saltedmd5 = hash.digest()
|
|
|
|
itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
|
|
rearranged = ''
|
|
for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
|
|
v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
|
|
|
|
for i in range(4):
|
|
rearranged += itoa64[v & 0x3f]
|
|
v >>= 6
|
|
|
|
v = ord( saltedmd5[11] )
|
|
|
|
for i in range( 2 ):
|
|
rearranged += itoa64[v & 0x3f]
|
|
v >>= 6
|
|
|
|
return magic + salt + '$' + rearranged
|
|
|
|
def sh256crypt(cls, password, salt, magic=magic_sha256):
|
|
iterations = 1000
|
|
# see http://en.wikipedia.org/wiki/PBKDF2
|
|
result = password.encode('utf8')
|
|
for i in xrange(cls.iterations):
|
|
result = hmac.HMAC(result, salt, hashlib.sha256).digest() # uses HMAC (RFC 2104) to apply salt
|
|
result = result.encode('base64') # doesnt seem to be crypt(3) compatible
|
|
return '%s%s$%s' % (magic_sha256, salt, result)
|
|
|
|
class res_users(osv.osv):
|
|
_inherit = "res.users"
|
|
|
|
def init(self, cr):
|
|
"""Encrypt all passwords at module installation"""
|
|
cr.execute("SELECT id, password FROM res_users WHERE password IS NOT NULL and password != ''")
|
|
for user in cr.fetchall():
|
|
self._set_encrypted_password(cr, user[0], user[1])
|
|
|
|
def _set_encrypted_password(self, cr, uid, plain_password):
|
|
"""Set an encrypted password for a given user"""
|
|
salt = gen_salt()
|
|
stored_password_crypt = md5crypt(plain_password, salt)
|
|
cr.execute("UPDATE res_users SET password = '', password_crypt = %s WHERE id = %s",
|
|
(stored_password_crypt, uid))
|
|
|
|
def set_pw(self, cr, uid, id, name, value, args, context):
|
|
if value:
|
|
self._set_encrypted_password(cr, id, value)
|
|
del value
|
|
|
|
def get_pw( self, cr, uid, ids, name, args, context ):
|
|
cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
|
|
stored_pws = cr.fetchall()
|
|
res = {}
|
|
|
|
for id, stored_pw in stored_pws:
|
|
res[id] = stored_pw
|
|
|
|
return res
|
|
|
|
_columns = {
|
|
'password': fields.function(get_pw, fnct_inv=set_pw, type='char', string='Password', invisible=True, store=True),
|
|
'password_crypt': fields.char(string='Encrypted Password', invisible=True),
|
|
}
|
|
|
|
def check_credentials(self, cr, uid, password):
|
|
# convert to base_crypt if needed
|
|
cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,))
|
|
if cr.rowcount:
|
|
stored_password, stored_password_crypt = cr.fetchone()
|
|
if stored_password and not stored_password_crypt:
|
|
self._set_encrypted_password(cr, uid, stored_password)
|
|
try:
|
|
return super(res_users, self).check_credentials(cr, uid, password)
|
|
except openerp.exceptions.AccessDenied:
|
|
# check md5crypt
|
|
if stored_password_crypt:
|
|
if stored_password_crypt[:len(magic_md5)] == magic_md5:
|
|
salt = stored_password_crypt[len(magic_md5):11]
|
|
if stored_password_crypt == md5crypt(password, salt):
|
|
return
|
|
elif stored_password_crypt[:len(magic_md5)] == magic_sha256:
|
|
salt = stored_password_crypt[len(magic_md5):11]
|
|
if stored_password_crypt == md5crypt(password, salt):
|
|
return
|
|
# Reraise password incorrect
|
|
raise
|
|
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|