1 | # -*- coding: utf8 -*- |
---|
2 | # |
---|
3 | # Copyright (C) 2007 Matthew Good <trac@matt-good.net> |
---|
4 | # |
---|
5 | # "THE BEER-WARE LICENSE" (Revision 42): |
---|
6 | # <trac@matt-good.net> wrote this file. As long as you retain this notice you |
---|
7 | # can do whatever you want with this stuff. If we meet some day, and you think |
---|
8 | # this stuff is worth it, you can buy me a beer in return. Matthew Good |
---|
9 | # |
---|
10 | # Author: Matthew Good <trac@matt-good.net> |
---|
11 | |
---|
12 | from binascii import hexlify |
---|
13 | import md5, sha |
---|
14 | |
---|
15 | from trac.core import * |
---|
16 | from trac.config import Option |
---|
17 | |
---|
18 | from md5crypt import md5crypt |
---|
19 | |
---|
20 | class IPasswordHashMethod(Interface): |
---|
21 | def generate_hash(user, password): |
---|
22 | pass |
---|
23 | |
---|
24 | def test_hash(user, password, hash): |
---|
25 | pass |
---|
26 | |
---|
27 | |
---|
28 | class HtPasswdHashMethod(Component): |
---|
29 | implements(IPasswordHashMethod) |
---|
30 | |
---|
31 | def generate_hash(self, user, password): |
---|
32 | password = password.encode('utf-8') |
---|
33 | return htpasswd(password) |
---|
34 | |
---|
35 | def check_hash(self, user, password, hash): |
---|
36 | password = password.encode('utf-8') |
---|
37 | return hash == htpasswd(password, hash) |
---|
38 | |
---|
39 | |
---|
40 | class HtDigestHashMethod(Component): |
---|
41 | implements(IPasswordHashMethod) |
---|
42 | |
---|
43 | realm = Option('account-manager', 'htdigest_realm') |
---|
44 | |
---|
45 | def generate_hash(self, user, password): |
---|
46 | user,password,realm = _encode(user, password, self.realm) |
---|
47 | return ':'.join([realm, htdigest(user, realm, password)]) |
---|
48 | |
---|
49 | def check_hash(self, user, password, hash): |
---|
50 | user,password,realm = _encode(user, password, self.realm) |
---|
51 | return hash == self.generate_hash(user, password) |
---|
52 | |
---|
53 | |
---|
54 | def _encode(*args): |
---|
55 | return [a.encode('utf-8') for a in args] |
---|
56 | |
---|
57 | # check for the availability of the "crypt" module for checking passwords on |
---|
58 | # Unix-like platforms |
---|
59 | # MD5 is still used when adding/updating passwords |
---|
60 | try: |
---|
61 | from crypt import crypt |
---|
62 | except ImportError: |
---|
63 | crypt = None |
---|
64 | |
---|
65 | # os.urandom was added in Python 2.4 |
---|
66 | # try to fall back on reading from /dev/urandom on older Python versions |
---|
67 | try: |
---|
68 | from os import urandom |
---|
69 | except ImportError: |
---|
70 | from random import randrange |
---|
71 | def urandom(n): |
---|
72 | return ''.join([chr(randrange(256)) for _ in xrange(n)]) |
---|
73 | |
---|
74 | def salt(): |
---|
75 | s = '' |
---|
76 | v = long(hexlify(urandom(4)), 16) |
---|
77 | itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' |
---|
78 | for i in range(8): |
---|
79 | s += itoa64[v & 0x3f]; v >>= 6 |
---|
80 | return s |
---|
81 | |
---|
82 | def htpasswd(password, salt_=None): |
---|
83 | # TODO need unit test of generating new hash |
---|
84 | if salt_ is None: |
---|
85 | salt_ = salt() |
---|
86 | if crypt is None: |
---|
87 | salt_ = '$apr1$' + salt_ |
---|
88 | if salt_.startswith('$apr1$'): |
---|
89 | return md5crypt(password, salt_[6:].split('$')[0], '$apr1$') |
---|
90 | elif salt_.startswith('{SHA}'): |
---|
91 | return '{SHA}' + sha.new(password).digest().encode('base64')[:-1] |
---|
92 | elif crypt is None: |
---|
93 | # crypt passwords are only supported on Unix-like systems |
---|
94 | raise NotImplementedError(u'Le module "crypt" n\'est pas disponible ' |
---|
95 | u'sur cette plateforme.') |
---|
96 | else: |
---|
97 | return crypt(password, salt_) |
---|
98 | |
---|
99 | def htdigest(user, realm, password): |
---|
100 | p = ':'.join([user, realm, password]) |
---|
101 | return md5.new(p).hexdigest() |
---|