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=ascii_letters + digits ):
52 return ''.join( sample( symbols, length ) )
54 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
55 # based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
56 # Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
61 # * "THE BEER-WARE LICENSE" (Revision 42):
63 # * <phk@login.dknet.dk> wrote this file. As long as you retain this
64 # * notice you can do whatever you want with this stuff. If we meet some
65 # * day, and you think this stuff is worth it, you can buy me a beer in
71 #TODO: py>=2.6: from hashlib import md5
74 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
75 raw_pw = raw_pw.encode('utf-8')
76 salt = salt.encode('utf-8')
78 hash.update( raw_pw + magic + salt )
80 st.update( raw_pw + salt + raw_pw)
83 for i in range( 0, len( raw_pw ) ):
84 hash.update( stretch[i % 16] )
92 hash.update( raw_pw[0] )
95 saltedmd5 = hash.digest()
97 for i in range( 1000 ):
101 hash.update( raw_pw )
103 hash.update( saltedmd5 )
108 hash.update( raw_pw )
110 hash.update( saltedmd5 )
112 hash.update( raw_pw )
114 saltedmd5 = hash.digest()
116 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
119 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
120 v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
123 rearranged += itoa64[v & 0x3f]
126 v = ord( saltedmd5[11] )
129 rearranged += itoa64[v & 0x3f]
132 return magic + salt + '$' + rearranged
134 class users(osv.osv):
138 # Add handlers for 'input_pw' field.
140 def set_pw(self, cr, uid, id, name, value, args, context):
142 raise osv.except_osv(_('Error'), _("Please specify the password !"))
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)
150 cr.execute('update res_users set password=%s where id=%s',
151 (encrypted.encode('utf-8'), int(id)))
155 def get_pw( self, cr, uid, ids, name, args, context ):
156 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
157 stored_pws = cr.fetchall()
160 for id, stored_pw in stored_pws:
166 # The column size could be smaller as it is meant to store a hash, but
167 # an existing column cannot be downsized; thus we use the original
169 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
170 size=64, string='Password', invisible=True,
174 def login(self, db, login, password):
178 raise RuntimeError("Cannot authenticate to False db!")
181 cr = pooler.get_db(db).cursor()
182 return self._login(cr, db, login, password)
184 _logger.exception('Could not authenticate')
185 return Exception('Access Denied')
190 def _login(self, cr, db, login, password):
191 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
192 (login.encode('utf-8'),))
195 stored_pw, id = cr.fetchone()
197 # Return early if no one has a login name like that.
200 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
203 # means couldn't encrypt or user is not active!
206 # Calculate an encrypted password from the user-provided
208 obj = pooler.get_pool(db).get('res.users')
209 if not hasattr(obj, "_salt_cache"):
211 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
212 encrypted_pw = encrypt_md5(password, salt)
214 # Check if the encrypted password matches against the one in the db.
215 cr.execute("""UPDATE res_users
216 SET date=now() AT TIME ZONE 'UTC'
217 WHERE id=%s AND password=%s AND active
219 (int(id), encrypted_pw.encode('utf-8')))
228 def check(self, db, uid, passwd):
230 # empty passwords disallowed for obvious security reasons
231 raise security.ExceptionNoTb('AccessDenied')
233 # Get a chance to hash all passwords in db before using the uid_cache.
234 obj = pooler.get_pool(db).get('res.users')
235 if not hasattr(obj, "_salt_cache"):
237 self._uid_cache.get(db, {}).clear()
239 cached_pass = self._uid_cache.get(db, {}).get(uid)
240 if (cached_pass is not None) and cached_pass == passwd:
243 cr = pooler.get_db(db).cursor()
245 if uid not in self._salt_cache.get(db, {}):
246 # If we don't have cache, we have to repeat the procedure
247 # through the login function.
248 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
249 stored_login = cr.fetchone()
251 stored_login = stored_login[0]
253 res = self._login(cr, db, stored_login, passwd)
255 raise security.ExceptionNoTb('AccessDenied')
257 salt = self._salt_cache[db][uid]
258 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
259 (int(uid), encrypt_md5(passwd, salt)))
260 res = cr.fetchone()[0]
265 raise security.ExceptionNoTb('AccessDenied')
268 if self._uid_cache.has_key(db):
269 ulist = self._uid_cache[db]
272 self._uid_cache[db] = {uid: passwd}
275 def maybe_encrypt(self, cr, pw, id):
276 """ Return the password 'pw', making sure it is encrypted.
278 If the password 'pw' is not encrypted, then encrypt all active passwords
279 in the db. Returns the (possibly newly) encrypted password for 'id'.
282 if not pw.startswith(magic_md5):
283 cr.execute("SELECT id, password FROM res_users " \
284 "WHERE active=true AND password NOT LIKE '$%'")
285 # Note that we skip all passwords like $.., in anticipation for
286 # more than md5 magic prefixes.
289 encrypted = encrypt_md5(p, gen_salt())
290 cr.execute('UPDATE res_users SET password=%s where id=%s',
293 encrypted_res = encrypted
299 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: