[MRG] From trunk
[odoo/odoo.git] / addons / base_crypt / crypt.py
1 # Notice:
2 # ------
3 #
4 # Implements encrypting functions.
5 #
6 # Copyright (c) 2008, F S 3 Consulting Inc.
7 #
8 # Maintainer:
9 # Alec Joseph Rivera (agi<at>fs3.ph)
10 #
11 #
12 # Warning:
13 # -------
14 #
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.
20 #
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.
25 #
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.
30 #
31 # You should  have received a copy of the GNU General  Public License along
32 # with this program; if not, write to the:
33 #
34 # Free Software Foundation, Inc.
35 # 59 Temple Place - Suite 330
36 # Boston, MA  02111-1307
37 # USA.
38
39 from random import seed, sample
40 from string import ascii_letters, digits
41 from osv import fields,osv
42 import pooler
43 from tools.translate import _
44 from service import security
45
46 magic_md5 = '$1$'
47
48 def gen_salt( length=8, symbols=ascii_letters + digits ):
49     seed()
50     return ''.join( sample( symbols, length ) )
51
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
55 # Poul and Mark. -agi
56 #
57 # Original license:
58 #
59 # * "THE BEER-WARE LICENSE" (Revision 42):
60 # *
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
64 # * return.
65 # *
66 # * Poul-Henning Kamp
67
68
69 #TODO: py>=2.6: from hashlib import md5
70 import hashlib
71
72 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
73     hash = hashlib.md5()
74     hash.update( raw_pw + magic + salt )
75     st = hashlib.md5()
76     st.update( raw_pw + salt + raw_pw)
77     stretch = st.digest()
78
79     for i in range( 0, len( raw_pw ) ):
80         hash.update( stretch[i % 16] )
81
82     i = len( raw_pw )
83
84     while i:
85         if i & 1:
86             hash.update('\x00')
87         else:
88             hash.update( raw_pw[0] )
89         i >>= 1
90
91     saltedmd5 = hash.digest()
92
93     for i in range( 1000 ):
94         hash = hashlib.md5()
95
96         if i & 1:
97             hash.update( raw_pw )
98         else:
99             hash.update( saltedmd5 )
100
101         if i % 3:
102             hash.update( salt )
103         if i % 7:
104             hash.update( raw_pw )
105         if i & 1:
106             hash.update( saltedmd5 )
107         else:
108             hash.update( raw_pw )
109
110         saltedmd5 = hash.digest()
111
112     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
113
114     rearranged = ''
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] )
117
118         for i in range(4):
119             rearranged += itoa64[v & 0x3f]
120             v >>= 6
121
122     v = ord( saltedmd5[11] )
123
124     for i in range( 2 ):
125         rearranged += itoa64[v & 0x3f]
126         v >>= 6
127
128     return magic + salt + '$' + rearranged
129
130 class users(osv.osv):
131     _name="res.users"
132     _inherit="res.users"
133     # agi - 022108
134     # Add handlers for 'input_pw' field.
135
136     def set_pw(self, cr, uid, id, name, value, args, context):
137         if not value:
138             raise osv.except_osv(_('Error'), _("Please specify the password !"))
139
140         obj = pooler.get_pool(cr.dbname).get('res.users')
141         if not hasattr(obj, "_salt_cache"):
142             obj._salt_cache = {}
143
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)))
148         cr.commit()
149         del value
150
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()
154         res = {}
155
156         for id, stored_pw in stored_pws:
157             res[id] = stored_pw
158
159         return res
160
161     _columns = {
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
164         # column size.
165         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
166             size=64, string='Password', invisible=True,
167             store=True),
168     }
169
170     def login(self, db, login, password):
171         if not password:
172             return False
173         if db is False:
174             raise RuntimeError("Cannot authenticate to False db!")
175         cr = None
176         try:
177             cr = pooler.get_db(db).cursor()
178             return self._login(cr, db, login, password)
179         except Exception:
180             import logging
181             logging.getLogger('netsvc').exception('Could not authenticate')
182             return Exception('Access Denied')
183         finally:
184             if cr is not None:
185                 cr.close()
186
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'),))
190
191         if cr.rowcount:
192             stored_pw, id = cr.fetchone()
193         else:
194             # Return early if no one has a login name like that.
195             return False
196     
197         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
198         
199         if not stored_pw:
200             # means couldn't encrypt or user is not active!
201             return False
202
203         # Calculate an encrypted password from the user-provided
204         # password.
205         obj = pooler.get_pool(db).get('res.users')
206         if not hasattr(obj, "_salt_cache"):
207             obj._salt_cache = {}
208         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
209         encrypted_pw = encrypt_md5(password, salt)
210     
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')))
215         res = cr.fetchone()
216         cr.commit()
217     
218         if res:
219             return res[0]
220         else:
221             return False
222
223     def check(self, db, uid, passwd):
224         if not passwd:
225             # empty passwords disallowed for obvious security reasons
226             raise security.ExceptionNoTb('AccessDenied')
227
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"):
231             obj._salt_cache = {}
232             self._uid_cache.get(db, {}).clear()
233
234         cached_pass = self._uid_cache.get(db, {}).get(uid)
235         if (cached_pass is not None) and cached_pass == passwd:
236             return True
237
238         cr = pooler.get_db(db).cursor()
239         try:
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()
245                 if stored_login:
246                     stored_login = stored_login[0]
247         
248                 res = self._login(cr, db, stored_login, passwd)
249                 if not res:
250                     raise security.ExceptionNoTb('AccessDenied')
251             else:
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]
256         finally:
257             cr.close()
258
259         if not bool(res):
260             raise security.ExceptionNoTb('AccessDenied')
261
262         if res:
263             if self._uid_cache.has_key(db):
264                 ulist = self._uid_cache[db]
265                 ulist[uid] = passwd
266             else:
267                 self._uid_cache[db] = {uid: passwd}
268         return bool(res)
269     
270     def maybe_encrypt(self, cr, pw, id):
271         """ Return the password 'pw', making sure it is encrypted.
272         
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'.
275         """
276
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.
282             res = cr.fetchall()
283             for i, p in res:
284                 encrypted = encrypt_md5(p, gen_salt())
285                 cr.execute('UPDATE res_users SET password=%s where id=%s',
286                         (encrypted, i))
287                 if i == id:
288                     encrypted_res = encrypted
289             cr.commit()
290             return encrypted_res
291         return pw
292
293 users()
294 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: