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 ):
74 hash.update( raw_pw + magic + salt )
76 st.update( raw_pw + salt + raw_pw)
79 for i in range( 0, len( raw_pw ) ):
80 hash.update( stretch[i % 16] )
88 hash.update( raw_pw[0] )
91 saltedmd5 = hash.digest()
93 for i in range( 1000 ):
99 hash.update( saltedmd5 )
104 hash.update( raw_pw )
106 hash.update( saltedmd5 )
108 hash.update( raw_pw )
110 saltedmd5 = hash.digest()
112 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
115 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
116 v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
119 rearranged += itoa64[v & 0x3f]
122 v = ord( saltedmd5[11] )
125 rearranged += itoa64[v & 0x3f]
128 return magic + salt + '$' + rearranged
130 class users(osv.osv):
134 # Add handlers for 'input_pw' field.
136 def set_pw(self, cr, uid, id, name, value, args, context):
138 raise osv.except_osv(_('Error'), _("Please specify the password !"))
140 obj = pooler.get_pool(cr.dbname).get('res.users')
141 if not hasattr(obj, "_salt_cache"):
144 salt = obj._salt_cache[id] = gen_salt()
145 encrypted = encrypt_md5(value, salt)
146 cr.execute('update res_users set password=%s where id=%s',
147 (encrypted.encode('utf-8'), int(id)))
151 def get_pw( self, cr, uid, ids, name, args, context ):
152 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
153 stored_pws = cr.fetchall()
156 for id, stored_pw in stored_pws:
162 # The column size could be smaller as it is meant to store a hash, but
163 # an existing column cannot be downsized; thus we use the original
165 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
166 method=True, size=64, string='Password', invisible=True,
170 def login(self, db, login, password):
174 raise RuntimeError("Cannot authenticate to False db!")
177 cr = pooler.get_db(db).cursor()
178 return self._login(cr, db, login, password)
181 logging.getLogger('netsvc').exception('Could not authenticate')
182 return Exception('Access Denied')
187 def _login(self, cr, db, login, password):
188 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
189 (login.encode('utf-8'),))
192 stored_pw, id = cr.fetchone()
194 # Return early if no one has a login name like that.
197 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
200 # means couldn't encrypt or user is not active!
203 # Calculate an encrypted password from the user-provided
205 obj = pooler.get_pool(db).get('res.users')
206 if not hasattr(obj, "_salt_cache"):
208 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
209 encrypted_pw = encrypt_md5(password, salt)
211 # Check if the encrypted password matches against the one in the db.
212 cr.execute('UPDATE res_users SET date=now() ' \
213 'WHERE id=%s AND password=%s AND active RETURNING id',
214 (int(id), encrypted_pw.encode('utf-8')))
223 def check(self, db, uid, passwd):
225 # empty passwords disallowed for obvious security reasons
226 raise security.ExceptionNoTb('AccessDenied')
228 # Get a chance to hash all passwords in db before using the uid_cache.
229 obj = pooler.get_pool(db).get('res.users')
230 if not hasattr(obj, "_salt_cache"):
232 self._uid_cache.get(db, {}).clear()
234 cached_pass = self._uid_cache.get(db, {}).get(uid)
235 if (cached_pass is not None) and cached_pass == passwd:
238 cr = pooler.get_db(db).cursor()
240 if uid not in self._salt_cache.get(db, {}):
241 # If we don't have cache, we have to repeat the procedure
242 # through the login function.
243 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
244 stored_login = cr.fetchone()
246 stored_login = stored_login[0]
248 res = self._login(cr, db, stored_login, passwd)
250 raise security.ExceptionNoTb('AccessDenied')
252 salt = self._salt_cache[db][uid]
253 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
254 (int(uid), encrypt_md5(passwd, salt)))
255 res = cr.fetchone()[0]
260 raise security.ExceptionNoTb('AccessDenied')
263 if self._uid_cache.has_key(db):
264 ulist = self._uid_cache[db]
267 self._uid_cache[db] = {uid: passwd}
270 def maybe_encrypt(self, cr, pw, id):
271 """ Return the password 'pw', making sure it is encrypted.
273 If the password 'pw' is not encrypted, then encrypt all active passwords
274 in the db. Returns the (possibly newly) encrypted password for 'id'.
277 if not pw.startswith(magic_md5):
278 cr.execute("SELECT id, password FROM res_users " \
279 "WHERE active=true AND password NOT LIKE '$%'")
280 # Note that we skip all passwords like $.., in anticipation for
281 # more than md5 magic prefixes.
284 encrypted = encrypt_md5(p, gen_salt())
285 cr.execute('UPDATE res_users SET password=%s where id=%s',
288 encrypted_res = encrypted
294 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: