4 # Implements encrypting functions.
6 # Copyright (c) 2008, F S 3 Consulting Inc.
9 # Alec Joseph Rivera (agi<at>fs3.ph)
15 # This program as such is intended to be used by professional programmers
16 # who take the whole responsibility of assessing all potential consequences
17 # resulting from its eventual inadequacies and bugs. End users who are
18 # looking for a ready-to-use solution with commercial guarantees and
19 # support are strongly adviced to contract a Free Software Service Company.
21 # This program is Free Software; you can redistribute it and/or modify it
22 # under the terms of the GNU General Public License as published by the
23 # Free Software Foundation; either version 2 of the License, or (at your
24 # option) any later version.
26 # This program is distributed in the hope that it will be useful, but
27 # WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
29 # Public License for more details.
31 # You should have received a copy of the GNU General Public License along
32 # with this program; if not, write to the:
34 # Free Software Foundation, Inc.
35 # 59 Temple Place - Suite 330
36 # Boston, MA 02111-1307
39 from random import seed, sample
40 from string import ascii_letters, digits
41 from osv import fields,osv
43 from tools.translate import _
44 from service import security
48 _logger = logging.getLogger(__name__)
50 def gen_salt( length=8, symbols=None):
52 symbols = ascii_letters + digits
54 return ''.join( sample( symbols, length ) )
56 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
57 # based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
58 # Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
63 # * "THE BEER-WARE LICENSE" (Revision 42):
65 # * <phk@login.dknet.dk> wrote this file. As long as you retain this
66 # * notice you can do whatever you want with this stuff. If we meet some
67 # * day, and you think this stuff is worth it, you can buy me a beer in
73 #TODO: py>=2.6: from hashlib import md5
76 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
77 raw_pw = raw_pw.encode('utf-8')
78 salt = salt.encode('utf-8')
80 hash.update( raw_pw + magic + salt )
82 st.update( raw_pw + salt + raw_pw)
85 for i in range( 0, len( raw_pw ) ):
86 hash.update( stretch[i % 16] )
94 hash.update( raw_pw[0] )
97 saltedmd5 = hash.digest()
99 for i in range( 1000 ):
103 hash.update( raw_pw )
105 hash.update( saltedmd5 )
110 hash.update( raw_pw )
112 hash.update( saltedmd5 )
114 hash.update( raw_pw )
116 saltedmd5 = hash.digest()
118 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
121 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
122 v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
125 rearranged += itoa64[v & 0x3f]
128 v = ord( saltedmd5[11] )
131 rearranged += itoa64[v & 0x3f]
134 return magic + salt + '$' + rearranged
136 class users(osv.osv):
140 # Add handlers for 'input_pw' field.
142 def set_pw(self, cr, uid, id, name, value, args, context):
144 obj = pooler.get_pool(cr.dbname).get('res.users')
145 if not hasattr(obj, "_salt_cache"):
148 salt = obj._salt_cache[id] = gen_salt()
149 encrypted = encrypt_md5(value, salt)
152 #setting a password to '' is allowed. It can be used to inactivate the classic log-in of the user
153 #while the access can still be granted by another login method (openid...)
155 cr.execute('update res_users set password=%s where id=%s',
156 (encrypted.encode('utf-8'), int(id)))
159 def get_pw( self, cr, uid, ids, name, args, context ):
160 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
161 stored_pws = cr.fetchall()
164 for id, stored_pw in stored_pws:
170 # The column size could be smaller as it is meant to store a hash, but
171 # an existing column cannot be downsized; thus we use the original
173 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
174 size=64, string='Password', invisible=True,
178 def login(self, db, login, password):
182 raise RuntimeError("Cannot authenticate to False db!")
185 cr = pooler.get_db(db).cursor()
186 return self._login(cr, db, login, password)
188 _logger.exception('Cannot authenticate.')
189 return Exception('Access denied.')
194 def _login(self, cr, db, login, password):
195 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
196 (login.encode('utf-8'),))
199 stored_pw, id = cr.fetchone()
201 # Return early if no one has a login name like that.
204 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
207 # means couldn't encrypt or user is not active!
210 # Calculate an encrypted password from the user-provided
212 obj = pooler.get_pool(db).get('res.users')
213 if not hasattr(obj, "_salt_cache"):
215 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
216 encrypted_pw = encrypt_md5(password, salt)
218 # Check if the encrypted password matches against the one in the db.
219 cr.execute("""UPDATE res_users
220 SET login_date=now() AT TIME ZONE 'UTC'
221 WHERE id=%s AND password=%s AND active
223 (int(id), encrypted_pw.encode('utf-8')))
232 def check(self, db, uid, passwd):
234 # empty passwords disallowed for obvious security reasons
235 raise security.ExceptionNoTb('AccessDenied')
237 # Get a chance to hash all passwords in db before using the uid_cache.
238 obj = pooler.get_pool(db).get('res.users')
239 if not hasattr(obj, "_salt_cache"):
241 self._uid_cache.get(db, {}).clear()
243 cached_pass = self._uid_cache.get(db, {}).get(uid)
244 if (cached_pass is not None) and cached_pass == passwd:
247 cr = pooler.get_db(db).cursor()
249 if uid not in self._salt_cache.get(db, {}):
250 # If we don't have cache, we have to repeat the procedure
251 # through the login function.
252 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
253 stored_login = cr.fetchone()
255 stored_login = stored_login[0]
257 res = self._login(cr, db, stored_login, passwd)
259 raise security.ExceptionNoTb('AccessDenied')
261 salt = self._salt_cache[db][uid]
262 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
263 (int(uid), encrypt_md5(passwd, salt)))
264 res = cr.fetchone()[0]
269 raise security.ExceptionNoTb('AccessDenied')
272 if self._uid_cache.has_key(db):
273 ulist = self._uid_cache[db]
276 self._uid_cache[db] = {uid: passwd}
279 def maybe_encrypt(self, cr, pw, id):
280 """ Return the password 'pw', making sure it is encrypted.
282 If the password 'pw' is not encrypted, then encrypt all active passwords
283 in the db. Returns the (possibly newly) encrypted password for 'id'.
286 if not pw.startswith(magic_md5):
287 cr.execute("SELECT id, password FROM res_users " \
288 "WHERE active=true AND password NOT LIKE '$%'")
289 # Note that we skip all passwords like $.., in anticipation for
290 # more than md5 magic prefixes.
293 encrypted = encrypt_md5(p, gen_salt())
294 cr.execute('UPDATE res_users SET password=%s where id=%s',
297 encrypted_res = encrypted
304 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: