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 def gen_salt( length=8, symbols=ascii_letters + digits ):
50 return ''.join( sample( symbols, length ) )
52 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
53 # based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
54 # Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
59 # * "THE BEER-WARE LICENSE" (Revision 42):
61 # * <phk@login.dknet.dk> wrote this file. As long as you retain this
62 # * notice you can do whatever you want with this stuff. If we meet some
63 # * day, and you think this stuff is worth it, you can buy me a beer in
69 #TODO: py>=2.6: from hashlib import md5
72 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
73 raw_pw = raw_pw.encode('utf-8')
74 salt = salt.encode('utf-8')
76 hash.update( raw_pw + magic + salt )
78 st.update( raw_pw + salt + raw_pw)
81 for i in range( 0, len( raw_pw ) ):
82 hash.update( stretch[i % 16] )
90 hash.update( raw_pw[0] )
93 saltedmd5 = hash.digest()
95 for i in range( 1000 ):
101 hash.update( saltedmd5 )
106 hash.update( raw_pw )
108 hash.update( saltedmd5 )
110 hash.update( raw_pw )
112 saltedmd5 = hash.digest()
114 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
117 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
118 v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
121 rearranged += itoa64[v & 0x3f]
124 v = ord( saltedmd5[11] )
127 rearranged += itoa64[v & 0x3f]
130 return magic + salt + '$' + rearranged
132 class users(osv.osv):
136 # Add handlers for 'input_pw' field.
138 def set_pw(self, cr, uid, id, name, value, args, context):
140 raise osv.except_osv(_('Error'), _("Please specify the password !"))
142 obj = pooler.get_pool(cr.dbname).get('res.users')
143 if not hasattr(obj, "_salt_cache"):
146 salt = obj._salt_cache[id] = gen_salt()
147 encrypted = encrypt_md5(value, salt)
148 cr.execute('update res_users set password=%s where id=%s',
149 (encrypted.encode('utf-8'), int(id)))
153 def get_pw( self, cr, uid, ids, name, args, context ):
154 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
155 stored_pws = cr.fetchall()
158 for id, stored_pw in stored_pws:
164 # The column size could be smaller as it is meant to store a hash, but
165 # an existing column cannot be downsized; thus we use the original
167 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
168 size=64, string='Password', invisible=True,
172 def login(self, db, login, password):
176 raise RuntimeError("Cannot authenticate to False db!")
179 cr = pooler.get_db(db).cursor()
180 return self._login(cr, db, login, password)
183 logging.getLogger('netsvc').exception('Could not authenticate')
184 return Exception('Access Denied')
189 def _login(self, cr, db, login, password):
190 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
191 (login.encode('utf-8'),))
194 stored_pw, id = cr.fetchone()
196 # Return early if no one has a login name like that.
199 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
202 # means couldn't encrypt or user is not active!
205 # Calculate an encrypted password from the user-provided
207 obj = pooler.get_pool(db).get('res.users')
208 if not hasattr(obj, "_salt_cache"):
210 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
211 encrypted_pw = encrypt_md5(password, salt)
213 # Check if the encrypted password matches against the one in the db.
214 cr.execute("""UPDATE res_users
215 SET date=now() AT TIME ZONE 'UTC'
216 WHERE id=%s AND password=%s AND active
218 (int(id), encrypted_pw.encode('utf-8')))
227 def check(self, db, uid, passwd):
229 # empty passwords disallowed for obvious security reasons
230 raise security.ExceptionNoTb('AccessDenied')
232 # Get a chance to hash all passwords in db before using the uid_cache.
233 obj = pooler.get_pool(db).get('res.users')
234 if not hasattr(obj, "_salt_cache"):
236 self._uid_cache.get(db, {}).clear()
238 cached_pass = self._uid_cache.get(db, {}).get(uid)
239 if (cached_pass is not None) and cached_pass == passwd:
242 cr = pooler.get_db(db).cursor()
244 if uid not in self._salt_cache.get(db, {}):
245 # If we don't have cache, we have to repeat the procedure
246 # through the login function.
247 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
248 stored_login = cr.fetchone()
250 stored_login = stored_login[0]
252 res = self._login(cr, db, stored_login, passwd)
254 raise security.ExceptionNoTb('AccessDenied')
256 salt = self._salt_cache[db][uid]
257 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
258 (int(uid), encrypt_md5(passwd, salt)))
259 res = cr.fetchone()[0]
264 raise security.ExceptionNoTb('AccessDenied')
267 if self._uid_cache.has_key(db):
268 ulist = self._uid_cache[db]
271 self._uid_cache[db] = {uid: passwd}
274 def maybe_encrypt(self, cr, pw, id):
275 """ Return the password 'pw', making sure it is encrypted.
277 If the password 'pw' is not encrypted, then encrypt all active passwords
278 in the db. Returns the (possibly newly) encrypted password for 'id'.
281 if not pw.startswith(magic_md5):
282 cr.execute("SELECT id, password FROM res_users " \
283 "WHERE active=true AND password NOT LIKE '$%'")
284 # Note that we skip all passwords like $.., in anticipation for
285 # more than md5 magic prefixes.
288 encrypted = encrypt_md5(p, gen_salt())
289 cr.execute('UPDATE res_users SET password=%s where id=%s',
292 encrypted_res = encrypted
298 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: