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 _
47 def gen_salt( length=8, symbols=ascii_letters + digits ):
49 return ''.join( sample( symbols, length ) )
51 # The encrypt_md5 is based on Mark Johnson's md5crypt.py, which in turn is
52 # based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
53 # Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
58 # * "THE BEER-WARE LICENSE" (Revision 42):
60 # * <phk@login.dknet.dk> wrote this file. As long as you retain this
61 # * notice you can do whatever you want with this stuff. If we meet some
62 # * day, and you think this stuff is worth it, you can buy me a beer in
68 #TODO: py>=2.6: from hashlib import md5
71 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
73 hash.update( raw_pw + magic + salt )
75 st.update( raw_pw + salt + raw_pw)
78 for i in range( 0, len( raw_pw ) ):
79 hash.update( stretch[i % 16] )
87 hash.update( raw_pw[0] )
90 saltedmd5 = hash.digest()
92 for i in range( 1000 ):
98 hash.update( saltedmd5 )
103 hash.update( raw_pw )
105 hash.update( saltedmd5 )
107 hash.update( raw_pw )
109 saltedmd5 = hash.digest()
111 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
114 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
115 v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
118 rearranged += itoa64[v & 0x3f]
121 v = ord( saltedmd5[11] )
124 rearranged += itoa64[v & 0x3f]
127 return magic + salt + '$' + rearranged
129 class users(osv.osv):
133 # Add handlers for 'input_pw' field.
135 def set_pw(self, cr, uid, id, name, value, args, context):
137 raise osv.except_osv(_('Error'), _("Please specify the password !"))
139 obj = pooler.get_pool(cr.dbname).get('res.users')
140 if not hasattr(obj, "_salt_cache"):
143 salt = obj._salt_cache[id] = gen_salt()
144 encrypted = encrypt_md5(value, salt)
145 cr.execute('update res_users set password=%s where id=%s',
146 (encrypted.encode('utf-8'), int(id)))
150 def get_pw( self, cr, uid, ids, name, args, context ):
151 cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
152 stored_pws = cr.fetchall()
155 for id, stored_pw in stored_pws:
161 # The column size could be smaller as it is meant to store a hash, but
162 # an existing column cannot be downsized; thus we use the original
164 'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
165 method=True, size=64, string='Password', invisible=True,
169 def login(self, db, login, password):
173 raise RuntimeError("Cannot authenticate to False db!")
176 cr = pooler.get_db(db).cursor()
177 return self._login(cr, db, login, password)
180 logging.getLogger('netsvc').exception('Could not authenticate')
181 return Exception('Access Denied')
186 def _login(self, cr, db, login, password):
187 cr.execute( 'SELECT password, id FROM res_users WHERE login=%s',
188 (login.encode('utf-8'),))
191 stored_pw, id = cr.fetchone()
193 # Return early if no one has a login name like that.
196 stored_pw = self.maybe_encrypt(cr, stored_pw, id)
199 # means couldn't encrypt or user is not active!
202 # Calculate an encrypted password from the user-provided
204 obj = pooler.get_pool(db).get('res.users')
205 if not hasattr(obj, "_salt_cache"):
207 salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
208 encrypted_pw = encrypt_md5(password, salt)
210 # Check if the encrypted password matches against the one in the db.
211 cr.execute('UPDATE res_users SET date=now() ' \
212 'WHERE id=%s AND password=%s AND active RETURNING id',
213 (int(id), encrypted_pw.encode('utf-8')))
222 def check(self, db, uid, passwd):
223 # Get a chance to hash all passwords in db before using the uid_cache.
224 obj = pooler.get_pool(db).get('res.users')
225 if not hasattr(obj, "_salt_cache"):
227 self._uid_cache.get(db, {}).clear()
229 cached_pass = self._uid_cache.get(db, {}).get(uid)
230 if (cached_pass is not None) and cached_pass == passwd:
233 cr = pooler.get_db(db).cursor()
235 if uid not in self._salt_cache.get(db, {}):
236 # If we don't have cache, we have to repeat the procedure
237 # through the login function.
238 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
239 stored_login = cr.fetchone()
241 stored_login = stored_login[0]
243 res = self._login(cr, db, stored_login, passwd)
245 raise security.ExceptionNoTb('AccessDenied')
247 salt = self._salt_cache[db][uid]
248 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s',
249 (int(uid), encrypt_md5(passwd, salt)))
250 res = cr.fetchone()[0]
255 raise security.ExceptionNoTb('AccessDenied')
258 if self._uid_cache.has_key(db):
259 ulist = self._uid_cache[db]
262 self._uid_cache[db] = {uid: passwd}
265 def maybe_encrypt(self, cr, pw, id):
266 """ Return the password 'pw', making sure it is encrypted.
268 If the password 'pw' is not encrypted, then encrypt all active passwords
269 in the db. Returns the (possibly newly) encrypted password for 'id'.
272 if not pw.startswith(magic_md5):
273 cr.execute("SELECT id, password FROM res_users " \
274 "WHERE active=true AND password NOT LIKE '$%'")
275 # Note that we skip all passwords like $.., in anticipation for
276 # more than md5 magic prefixes.
279 encrypted = encrypt_md5(p, gen_salt())
280 cr.execute('UPDATE res_users SET password=%s where id=%s',
283 encrypted_res = encrypted
289 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: