[IMP] Checked manifests again.
[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
45 magic_md5 = '$1$'
46
47 def gen_salt( length=8, symbols=ascii_letters + digits ):
48     seed()
49     return ''.join( sample( symbols, length ) )
50
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
54 # Poul and Mark. -agi
55 #
56 # Original license:
57 #
58 # * "THE BEER-WARE LICENSE" (Revision 42):
59 # *
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
63 # * return.
64 # *
65 # * Poul-Henning Kamp
66
67
68 #TODO: py>=2.6: from hashlib import md5
69 import hashlib
70
71 def encrypt_md5( raw_pw, salt, magic=magic_md5 ):
72     hash = hashlib.md5()
73     hash.update( raw_pw + magic + salt )
74     st = hashlib.md5()
75     st.update( raw_pw + salt + raw_pw)
76     stretch = st.digest()
77
78     for i in range( 0, len( raw_pw ) ):
79         hash.update( stretch[i % 16] )
80
81     i = len( raw_pw )
82
83     while i:
84         if i & 1:
85             hash.update('\x00')
86         else:
87             hash.update( raw_pw[0] )
88         i >>= 1
89
90     saltedmd5 = hash.digest()
91
92     for i in range( 1000 ):
93         hash = hashlib.md5()
94
95         if i & 1:
96             hash.update( raw_pw )
97         else:
98             hash.update( saltedmd5 )
99
100         if i % 3:
101             hash.update( salt )
102         if i % 7:
103             hash.update( raw_pw )
104         if i & 1:
105             hash.update( saltedmd5 )
106         else:
107             hash.update( raw_pw )
108
109         saltedmd5 = hash.digest()
110
111     itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
112
113     rearranged = ''
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] )
116
117         for i in range(4):
118             rearranged += itoa64[v & 0x3f]
119             v >>= 6
120
121     v = ord( saltedmd5[11] )
122
123     for i in range( 2 ):
124         rearranged += itoa64[v & 0x3f]
125         v >>= 6
126
127     return magic + salt + '$' + rearranged
128
129 class users(osv.osv):
130     _name="res.users"
131     _inherit="res.users"
132     # agi - 022108
133     # Add handlers for 'input_pw' field.
134
135     def set_pw(self, cr, uid, id, name, value, args, context):
136         if not value:
137             raise osv.except_osv(_('Error'), _("Please specify the password !"))
138
139         obj = pooler.get_pool(cr.dbname).get('res.users')
140         if not hasattr(obj, "_salt_cache"):
141             obj._salt_cache = {}
142
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)))
147         cr.commit()
148         del value
149
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()
153         res = {}
154
155         for id, stored_pw in stored_pws:
156             res[id] = stored_pw
157
158         return res
159
160     _columns = {
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
163         # column size.
164         'password': fields.function(get_pw, fnct_inv=set_pw, type='char',
165             method=True, size=64, string='Password', invisible=True,
166             store=True),
167     }
168
169     def login(self, db, login, password):
170         if not password:
171             return False
172         if db is False:
173             raise RuntimeError("Cannot authenticate to False db!")
174         cr = None
175         try:
176             cr = pooler.get_db(db).cursor()
177             return self._login(cr, db, login, password)
178         except Exception:
179             import logging
180             logging.getLogger('netsvc').exception('Could not authenticate')
181             return Exception('Access Denied')
182         finally:
183             if cr is not None:
184                 cr.close()
185
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'),))
189
190         if cr.rowcount:
191             stored_pw, id = cr.fetchone()
192         else:
193             # Return early if no one has a login name like that.
194             return False
195     
196         stored_pw = self.maybe_encrypt(cr, stored_pw, id)
197         
198         if not stored_pw:
199             # means couldn't encrypt or user is not active!
200             return False
201
202         # Calculate an encrypted password from the user-provided
203         # password.
204         obj = pooler.get_pool(db).get('res.users')
205         if not hasattr(obj, "_salt_cache"):
206             obj._salt_cache = {}
207         salt = obj._salt_cache[id] = stored_pw[len(magic_md5):11]
208         encrypted_pw = encrypt_md5(password, salt)
209     
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')))
214         res = cr.fetchone()
215         cr.commit()
216     
217         if res:
218             return res[0]
219         else:
220             return False
221
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"):
226             obj._salt_cache = {}
227             self._uid_cache.get(db, {}).clear()
228
229         cached_pass = self._uid_cache.get(db, {}).get(uid)
230         if (cached_pass is not None) and cached_pass == passwd:
231             return True
232
233         cr = pooler.get_db(db).cursor()
234         try:
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()
240                 if stored_login:
241                     stored_login = stored_login[0]
242         
243                 res = self._login(cr, db, stored_login, passwd)
244                 if not res:
245                     raise security.ExceptionNoTb('AccessDenied')
246             else:
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]
251         finally:
252             cr.close()
253
254         if not bool(res):
255             raise security.ExceptionNoTb('AccessDenied')
256
257         if res:
258             if self._uid_cache.has_key(db):
259                 ulist = self._uid_cache[db]
260                 ulist[uid] = passwd
261             else:
262                 self._uid_cache[db] = {uid: passwd}
263         return bool(res)
264     
265     def maybe_encrypt(self, cr, pw, id):
266         """ Return the password 'pw', making sure it is encrypted.
267         
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'.
270         """
271
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.
277             res = cr.fetchall()
278             for i, p in res:
279                 encrypted = encrypt_md5(p, gen_salt())
280                 cr.execute('UPDATE res_users SET password=%s where id=%s',
281                         (encrypted, i))
282                 if i == id:
283                     encrypted_res = encrypted
284             cr.commit()
285             return encrypted_res
286         return pw
287
288 users()
289 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: