[IMP] Clean Modules name: there are 3 modules named Contracts Management.
[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     raw_pw = raw_pw.encode('utf-8')
74     salt = salt.encode('utf-8')
75     hash = hashlib.md5()
76     hash.update( raw_pw + magic + salt )
77     st = hashlib.md5()
78     st.update( raw_pw + salt + raw_pw)
79     stretch = st.digest()
80
81     for i in range( 0, len( raw_pw ) ):
82         hash.update( stretch[i % 16] )
83
84     i = len( raw_pw )
85
86     while i:
87         if i & 1:
88             hash.update('\x00')
89         else:
90             hash.update( raw_pw[0] )
91         i >>= 1
92
93     saltedmd5 = hash.digest()
94
95     for i in range( 1000 ):
96         hash = hashlib.md5()
97
98         if i & 1:
99             hash.update( raw_pw )
100         else:
101             hash.update( saltedmd5 )
102
103         if i % 3:
104             hash.update( salt )
105         if i % 7:
106             hash.update( raw_pw )
107         if i & 1:
108             hash.update( saltedmd5 )
109         else:
110             hash.update( raw_pw )
111
112         saltedmd5 = hash.digest()
113
114     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
115
116     rearranged = ''
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] )
119
120         for i in range(4):
121             rearranged += itoa64[v & 0x3f]
122             v >>= 6
123
124     v = ord( saltedmd5[11] )
125
126     for i in range( 2 ):
127         rearranged += itoa64[v & 0x3f]
128         v >>= 6
129
130     return magic + salt + '$' + rearranged
131
132 class users(osv.osv):
133     _name="res.users"
134     _inherit="res.users"
135     # agi - 022108
136     # Add handlers for 'input_pw' field.
137
138     def set_pw(self, cr, uid, id, name, value, args, context):
139         if not value:
140             raise osv.except_osv(_('Error'), _("Please specify the password !"))
141
142         obj = pooler.get_pool(cr.dbname).get('res.users')
143         if not hasattr(obj, "_salt_cache"):
144             obj._salt_cache = {}
145
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)))
150         cr.commit()
151         del value
152
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()
156         res = {}
157
158         for id, stored_pw in stored_pws:
159             res[id] = stored_pw
160
161         return res
162
163     _columns = {
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
166         # column size.
167         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
168             size=64, string='Password', invisible=True,
169             store=True),
170     }
171
172     def login(self, db, login, password):
173         if not password:
174             return False
175         if db is False:
176             raise RuntimeError("Cannot authenticate to False db!")
177         cr = None
178         try:
179             cr = pooler.get_db(db).cursor()
180             return self._login(cr, db, login, password)
181         except Exception:
182             import logging
183             logging.getLogger('netsvc').exception('Could not authenticate')
184             return Exception('Access Denied')
185         finally:
186             if cr is not None:
187                 cr.close()
188
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'),))
192
193         if cr.rowcount:
194             stored_pw, id = cr.fetchone()
195         else:
196             # Return early if no one has a login name like that.
197             return False
198     
199         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
200         
201         if not stored_pw:
202             # means couldn't encrypt or user is not active!
203             return False
204
205         # Calculate an encrypted password from the user-provided
206         # password.
207         obj = pooler.get_pool(db).get('res.users')
208         if not hasattr(obj, "_salt_cache"):
209             obj._salt_cache = {}
210         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
211         encrypted_pw = encrypt_md5(password, salt)
212     
213         # Check if the encrypted password matches against the one in the db.
214         cr.execute("""UPDATE res_users
215                         SET date=now() AT TIME ZONE 'UTC'
216                         WHERE id=%s AND password=%s AND active
217                         RETURNING id""", 
218                    (int(id), encrypted_pw.encode('utf-8')))
219         res = cr.fetchone()
220         cr.commit()
221     
222         if res:
223             return res[0]
224         else:
225             return False
226
227     def check(self, db, uid, passwd):
228         if not passwd:
229             # empty passwords disallowed for obvious security reasons
230             raise security.ExceptionNoTb('AccessDenied')
231
232         # Get a chance to hash all passwords in db before using the uid_cache.
233         obj = pooler.get_pool(db).get('res.users')
234         if not hasattr(obj, "_salt_cache"):
235             obj._salt_cache = {}
236             self._uid_cache.get(db, {}).clear()
237
238         cached_pass = self._uid_cache.get(db, {}).get(uid)
239         if (cached_pass is not None) and cached_pass == passwd:
240             return True
241
242         cr = pooler.get_db(db).cursor()
243         try:
244             if uid not in self._salt_cache.get(db, {}):
245                 # If we don't have cache, we have to repeat the procedure
246                 # through the login function.
247                 cr.execute( 'SELECT login FROM res_users WHERE id=%s', (uid,) )
248                 stored_login = cr.fetchone()
249                 if stored_login:
250                     stored_login = stored_login[0]
251         
252                 res = self._login(cr, db, stored_login, passwd)
253                 if not res:
254                     raise security.ExceptionNoTb('AccessDenied')
255             else:
256                 salt = self._salt_cache[db][uid]
257                 cr.execute('SELECT COUNT(*) FROM res_users WHERE id=%s AND password=%s AND active', 
258                     (int(uid), encrypt_md5(passwd, salt)))
259                 res = cr.fetchone()[0]
260         finally:
261             cr.close()
262
263         if not bool(res):
264             raise security.ExceptionNoTb('AccessDenied')
265
266         if res:
267             if self._uid_cache.has_key(db):
268                 ulist = self._uid_cache[db]
269                 ulist[uid] = passwd
270             else:
271                 self._uid_cache[db] = {uid: passwd}
272         return bool(res)
273     
274     def maybe_encrypt(self, cr, pw, id):
275         """ Return the password 'pw', making sure it is encrypted.
276         
277         If the password 'pw' is not encrypted, then encrypt all active passwords
278         in the db. Returns the (possibly newly) encrypted password for 'id'.
279         """
280
281         if not pw.startswith(magic_md5):
282             cr.execute("SELECT id, password FROM res_users " \
283                 "WHERE active=true AND password NOT LIKE '$%'")
284             # Note that we skip all passwords like $.., in anticipation for
285             # more than md5 magic prefixes.
286             res = cr.fetchall()
287             for i, p in res:
288                 encrypted = encrypt_md5(p, gen_salt())
289                 cr.execute('UPDATE res_users SET password=%s where id=%s',
290                         (encrypted, i))
291                 if i == id:
292                     encrypted_res = encrypted
293             cr.commit()
294             return encrypted_res
295         return pw
296
297 users()
298 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: