[MERGE] addons 16 survey
[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 import logging
46
47 magic_md5 = '$1$'
48 _logger = logging.getLogger(__name__)
49
50 def gen_salt( length=8, symbols=None):
51     if symbols is None:
52         symbols = ascii_letters + digits
53     seed()
54     return ''.join( sample( symbols, length ) )
55
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
59 # Poul and Mark. -agi
60 #
61 # Original license:
62 #
63 # * "THE BEER-WARE LICENSE" (Revision 42):
64 # *
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
68 # * return.
69 # *
70 # * Poul-Henning Kamp
71
72
73 #TODO: py>=2.6: from hashlib import md5
74 import hashlib
75
76 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
77     raw_pw = raw_pw.encode('utf-8')
78     salt = salt.encode('utf-8')
79     hash = hashlib.md5()
80     hash.update( raw_pw + magic + salt )
81     st = hashlib.md5()
82     st.update( raw_pw + salt + raw_pw)
83     stretch = st.digest()
84
85     for i in range( 0, len( raw_pw ) ):
86         hash.update( stretch[i % 16] )
87
88     i = len( raw_pw )
89
90     while i:
91         if i & 1:
92             hash.update('\x00')
93         else:
94             hash.update( raw_pw[0] )
95         i >>= 1
96
97     saltedmd5 = hash.digest()
98
99     for i in range( 1000 ):
100         hash = hashlib.md5()
101
102         if i & 1:
103             hash.update( raw_pw )
104         else:
105             hash.update( saltedmd5 )
106
107         if i % 3:
108             hash.update( salt )
109         if i % 7:
110             hash.update( raw_pw )
111         if i & 1:
112             hash.update( saltedmd5 )
113         else:
114             hash.update( raw_pw )
115
116         saltedmd5 = hash.digest()
117
118     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
119
120     rearranged = ''
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] )
123
124         for i in range(4):
125             rearranged += itoa64[v & 0x3f]
126             v >>= 6
127
128     v = ord( saltedmd5[11] )
129
130     for i in range( 2 ):
131         rearranged += itoa64[v & 0x3f]
132         v >>= 6
133
134     return magic + salt + '$' + rearranged
135
136 class users(osv.osv):
137     _name="res.users"
138     _inherit="res.users"
139     # agi - 022108
140     # Add handlers for 'input_pw' field.
141
142     def set_pw(self, cr, uid, id, name, value, args, context):
143         if value:
144             obj = pooler.get_pool(cr.dbname).get('res.users')
145             if not hasattr(obj, "_salt_cache"):
146                 obj._salt_cache = {}
147
148             salt = obj._salt_cache[id] = gen_salt()
149             encrypted = encrypt_md5(value, salt)
150
151         else:
152             #setting a password to '' is allowed. It can be used to inactivate the classic log-in of the user
153             #while the access can still be granted by another login method (openid...)
154             encrypted = ''
155         cr.execute('update res_users set password=%s where id=%s',
156             (encrypted.encode('utf-8'), int(id)))
157         del value
158
159     def get_pw( self, cr, uid, ids, name, args, context ):
160         cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
161         stored_pws = cr.fetchall()
162         res = {}
163
164         for id, stored_pw in stored_pws:
165             res[id] = stored_pw
166
167         return res
168
169     _columns = {
170         # The column size could be smaller as it is meant to store a hash, but
171         # an existing column cannot be downsized; thus we use the original
172         # column size.
173         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
174             size=64, string='Password', invisible=True,
175             store=True),
176     }
177
178     def login(self, db, login, password):
179         if not password:
180             return False
181         if db is False:
182             raise RuntimeError("Cannot authenticate to False db!")
183         cr = None
184         try:
185             cr = pooler.get_db(db).cursor()
186             return self._login(cr, db, login, password)
187         except Exception:
188             _logger.exception('Cannot authenticate.')
189             return Exception('Access denied.')
190         finally:
191             if cr is not None:
192                 cr.close()
193
194     def _login(self, cr, db, login, password):
195         cr.execute( 'SELECT password, id FROM res_users WHERE login=%s AND active',
196             (login.encode('utf-8'),))
197
198         if cr.rowcount:
199             stored_pw, id = cr.fetchone()
200         else:
201             # Return early if no one has a login name like that.
202             return False
203
204         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
205
206         if not stored_pw:
207             # means couldn't encrypt or user is not active!
208             return False
209
210         # Calculate an encrypted password from the user-provided
211         # password.
212         obj = pooler.get_pool(db).get('res.users')
213         if not hasattr(obj, "_salt_cache"):
214             obj._salt_cache = {}
215         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
216         encrypted_pw = encrypt_md5(password, salt)
217
218         # Check if the encrypted password matches against the one in the db.
219         cr.execute("""UPDATE res_users
220                         SET login_date=now() AT TIME ZONE 'UTC'
221                         WHERE id=%s AND password=%s AND active
222                         RETURNING id""",
223                    (int(id), encrypted_pw.encode('utf-8')))
224         res = cr.fetchone()
225         cr.commit()
226
227         if res:
228             return res[0]
229         else:
230             return False
231
232     def check(self, db, uid, passwd):
233         if not passwd:
234             # empty passwords disallowed for obvious security reasons
235             raise security.ExceptionNoTb('AccessDenied')
236
237         # Get a chance to hash all passwords in db before using the uid_cache.
238         obj = pooler.get_pool(db).get('res.users')
239         if not hasattr(obj, "_salt_cache"):
240             obj._salt_cache = {}
241             self._uid_cache.get(db, {}).clear()
242
243         cached_pass = self._uid_cache.get(db, {}).get(uid)
244         if (cached_pass is not None) and cached_pass == passwd:
245             return True
246
247         cr = pooler.get_db(db).cursor()
248         try:
249             if uid not in self._salt_cache.get(db, {}):
250                 # If we don't have cache, we have to repeat the procedure
251                 # through the login function.
252                 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
253                 stored_login = cr.fetchone()
254                 if stored_login:
255                     stored_login = stored_login[0]
256
257                 res = self._login(cr, db, stored_login, passwd)
258                 if not res:
259                     raise security.ExceptionNoTb('AccessDenied')
260             else:
261                 salt = self._salt_cache[db][uid]
262                 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active',
263                     (int(uid), encrypt_md5(passwd, salt)))
264                 res = cr.fetchone()[0]
265         finally:
266             cr.close()
267
268         if not bool(res):
269             raise security.ExceptionNoTb('AccessDenied')
270
271         if res:
272             if self._uid_cache.has_key(db):
273                 ulist = self._uid_cache[db]
274                 ulist[uid] = passwd
275             else:
276                 self._uid_cache[db] = {uid: passwd}
277         return bool(res)
278
279     def maybe_encrypt(self, cr, pw, id):
280         """ Return the password 'pw', making sure it is encrypted.
281
282         If the password 'pw' is not encrypted, then encrypt all active passwords
283         in the db. Returns the (possibly newly) encrypted password for 'id'.
284         """
285
286         if not pw.startswith(magic_md5):
287             cr.execute("SELECT id, password FROM res_users " \
288                 "WHERE active=true AND password NOT LIKE '$%'")
289             # Note that we skip all passwords like $.., in anticipation for
290             # more than md5 magic prefixes.
291             res = cr.fetchall()
292             for i, p in res:
293                 encrypted = encrypt_md5(p, gen_salt())
294                 cr.execute('UPDATE res_users SET password=%s where id=%s',
295                         (encrypted, i))
296                 if i == id:
297                     encrypted_res = encrypted
298             cr.commit()
299             return encrypted_res
300         return pw
301
302 users()
303
304 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: