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 SET date=now() ' \
215 'WHERE id=%s AND password=%s AND active RETURNING id',
216 (int(id), encrypted_pw.encode('utf-8')))
225 def check(self, db, uid, passwd):
227 # empty passwords disallowed for obvious security reasons
228 raise security.ExceptionNoTb('AccessDenied')
230 # Get a chance to hash all passwords in db before using the uid_cache.
231 obj = pooler.get_pool(db).get('res.users')
232 if not hasattr(obj, "_salt_cache"):
234 self._uid_cache.get(db, {}).clear()
236 cached_pass = self._uid_cache.get(db, {}).get(uid)
237 if (cached_pass is not None) and cached_pass == passwd:
240 cr = pooler.get_db(db).cursor()
242 if uid not in self._salt_cache.get(db, {}):
243 # If we don't have cache, we have to repeat the procedure
244 # through the login function.
245 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
246 stored_login = cr.fetchone()
248 stored_login = stored_login[0]
250 res = self._login(cr, db, stored_login, passwd)
252 raise security.ExceptionNoTb('AccessDenied')
254 salt = self._salt_cache[db][uid]
255 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
256 (int(uid), encrypt_md5(passwd, salt)))
257 res = cr.fetchone()[0]
262 raise security.ExceptionNoTb('AccessDenied')
265 if self._uid_cache.has_key(db):
266 ulist = self._uid_cache[db]
269 self._uid_cache[db] = {uid: passwd}
272 def maybe_encrypt(self, cr, pw, id):
273 """ Return the password 'pw', making sure it is encrypted.
275 If the password 'pw' is not encrypted, then encrypt all active passwords
276 in the db. Returns the (possibly newly) encrypted password for 'id'.
279 if not pw.startswith(magic_md5):
280 cr.execute("SELECT id, password FROM res_users " \
281 "WHERE active=true AND password NOT LIKE '$%'")
282 # Note that we skip all passwords like $.., in anticipation for
283 # more than md5 magic prefixes.
286 encrypted = encrypt_md5(p, gen_salt())
287 cr.execute('UPDATE res_users SET password=%s where id=%s',
290 encrypted_res = encrypted
296 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: