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 raise osv.except_osv(_('Error!'), _("You have to specify a password."))
146 obj = pooler.get_pool(cr.dbname).get('res.users')
147 if not hasattr(obj, "_salt_cache"):
150 salt = obj._salt_cache[id] = gen_salt()
151 encrypted = encrypt_md5(value, salt)
152 cr.execute('update res_users set password=%s where id=%s',
153 (encrypted.encode('utf-8'), int(id)))
157 def get_pw( self, cr, uid, ids, name, args, context ):
158 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
159 stored_pws = cr.fetchall()
162 for id, stored_pw in stored_pws:
168 # The column size could be smaller as it is meant to store a hash, but
169 # an existing column cannot be downsized; thus we use the original
171 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
172 size=64, string='Password', invisible=True,
176 def login(self, db, login, password):
180 raise RuntimeError("Cannot authenticate to False db!")
183 cr = pooler.get_db(db).cursor()
184 return self._login(cr, db, login, password)
186 _logger.exception('Cannot authenticate.')
187 return Exception('Access denied.')
192 def _login(self, cr, db, login, password):
193 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
194 (login.encode('utf-8'),))
197 stored_pw, id = cr.fetchone()
199 # Return early if no one has a login name like that.
202 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
205 # means couldn't encrypt or user is not active!
208 # Calculate an encrypted password from the user-provided
210 obj = pooler.get_pool(db).get('res.users')
211 if not hasattr(obj, "_salt_cache"):
213 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
214 encrypted_pw = encrypt_md5(password, salt)
216 # Check if the encrypted password matches against the one in the db.
217 cr.execute("""UPDATE res_users
218 SET login_date=now() AT TIME ZONE 'UTC'
219 WHERE id=%s AND password=%s AND active
221 (int(id), encrypted_pw.encode('utf-8')))
230 def check(self, db, uid, passwd):
232 # empty passwords disallowed for obvious security reasons
233 raise security.ExceptionNoTb('AccessDenied')
235 # Get a chance to hash all passwords in db before using the uid_cache.
236 obj = pooler.get_pool(db).get('res.users')
237 if not hasattr(obj, "_salt_cache"):
239 self._uid_cache.get(db, {}).clear()
241 cached_pass = self._uid_cache.get(db, {}).get(uid)
242 if (cached_pass is not None) and cached_pass == passwd:
245 cr = pooler.get_db(db).cursor()
247 if uid not in self._salt_cache.get(db, {}):
248 # If we don't have cache, we have to repeat the procedure
249 # through the login function.
250 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
251 stored_login = cr.fetchone()
253 stored_login = stored_login[0]
255 res = self._login(cr, db, stored_login, passwd)
257 raise security.ExceptionNoTb('AccessDenied')
259 salt = self._salt_cache[db][uid]
260 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
261 (int(uid), encrypt_md5(passwd, salt)))
262 res = cr.fetchone()[0]
267 raise security.ExceptionNoTb('AccessDenied')
270 if self._uid_cache.has_key(db):
271 ulist = self._uid_cache[db]
274 self._uid_cache[db] = {uid: passwd}
277 def maybe_encrypt(self, cr, pw, id):
278 """ Return the password 'pw', making sure it is encrypted.
280 If the password 'pw' is not encrypted, then encrypt all active passwords
281 in the db. Returns the (possibly newly) encrypted password for 'id'.
284 if not pw.startswith(magic_md5):
285 cr.execute("SELECT id, password FROM res_users " \
286 "WHERE active=true AND password NOT LIKE '$%'")
287 # Note that we skip all passwords like $.., in anticipation for
288 # more than md5 magic prefixes.
291 encrypted = encrypt_md5(p, gen_salt())
292 cr.execute('UPDATE res_users SET password=%s where id=%s',
295 encrypted_res = encrypted
302 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: