PEP8: fix E303: too many blank lines (2)
[samba.git] / python / samba / tests / password_hash.py
1 # Tests for Tests for source4/dsdb/samdb/ldb_modules/password_hash.c
2 #
3 # Copyright (C) Catalyst IT Ltd. 2017
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 """
20 Base class for tests for source4/dsdb/samdb/ldb_modules/password_hash.c
21 """
22
23 from samba.credentials import Credentials
24 from samba.samdb import SamDB
25 from samba.auth import system_session
26 from samba.tests import TestCase
27 from samba.ndr import ndr_unpack
28 from samba.dcerpc import drsblobs
29 from samba.dcerpc.samr import DOMAIN_PASSWORD_STORE_CLEARTEXT
30 from samba.dsdb import UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
31 from samba.tests import delete_force
32 from samba.tests.password_test import PasswordCommon
33 import ldb
34 import samba
35 import binascii
36 from hashlib import md5
37 import crypt
38 from samba.compat import text_type
39
40
41 USER_NAME = "PasswordHashTestUser"
42 USER_PASS = samba.generate_random_password(32, 32)
43 UPN       = "PWHash@User.Principle"
44
45 # Get named package from the passed supplemental credentials
46 #
47 # returns the package and it's position within the supplemental credentials
48
49
50 def get_package(sc, name):
51     if sc is None:
52         return None
53
54     idx = 0
55     for p in sc.sub.packages:
56         idx += 1
57         if name == p.name:
58             return (idx, p)
59
60     return None
61
62 # Calculate the MD5 password digest from the supplied user, realm and password
63 #
64
65
66 def calc_digest(user, realm, password):
67
68     data = "%s:%s:%s" % (user, realm, password)
69     if isinstance(data, text_type):
70         data = data.encode('utf8')
71
72     return md5(data).hexdigest()
73
74
75 class PassWordHashTests(TestCase):
76
77     def setUp(self):
78         self.lp = samba.tests.env_loadparm()
79         super(PassWordHashTests, self).setUp()
80
81     def set_store_cleartext(self, cleartext):
82         # get the current pwdProperties
83         pwdProperties = self.ldb.get_pwdProperties()
84         # update the clear-text properties flag
85         props = int(pwdProperties)
86         if cleartext:
87             props |= DOMAIN_PASSWORD_STORE_CLEARTEXT
88         else:
89             props &= ~DOMAIN_PASSWORD_STORE_CLEARTEXT
90         self.ldb.set_pwdProperties(str(props))
91
92     # Add a user to ldb, this will exercise the password_hash code
93     # and calculate the appropriate supplemental credentials
94     def add_user(self, options=None, clear_text=False, ldb=None):
95         # set any needed options
96         if options is not None:
97             for (option, value) in options:
98                 self.lp.set(option, value)
99
100         if ldb is None:
101             self.creds = Credentials()
102             self.session = system_session()
103             self.creds.guess(self.lp)
104             self.session = system_session()
105             self.ldb = SamDB(session_info=self.session,
106                              credentials=self.creds,
107                              lp=self.lp)
108         else:
109             self.ldb = ldb
110
111         res = self.ldb.search(base=self.ldb.get_config_basedn(),
112                               expression="ncName=%s" % self.ldb.get_default_basedn(),
113                               attrs=["nETBIOSName"])
114         self.netbios_domain = res[0]["nETBIOSName"][0]
115         self.dns_domain = self.ldb.domain_dns_name()
116
117         # Gets back the basedn
118         base_dn = self.ldb.domain_dn()
119
120         # Gets back the configuration basedn
121         configuration_dn = self.ldb.get_config_basedn().get_linearized()
122
123         # permit password changes during this test
124         PasswordCommon.allow_password_changes(self, self.ldb)
125
126         self.base_dn = self.ldb.domain_dn()
127
128         account_control = 0
129         if clear_text:
130             # Restore the current domain setting on exit.
131             pwdProperties = self.ldb.get_pwdProperties()
132             self.addCleanup(self.ldb.set_pwdProperties, pwdProperties)
133             # Update the domain setting
134             self.set_store_cleartext(clear_text)
135             account_control |= UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
136
137         # (Re)adds the test user USER_NAME with password USER_PASS
138         # and userPrincipalName UPN
139         delete_force(self.ldb, "cn=" + USER_NAME + ",cn=users," + self.base_dn)
140         self.ldb.add({
141              "dn": "cn=" + USER_NAME + ",cn=users," + self.base_dn,
142              "objectclass": "user",
143              "sAMAccountName": USER_NAME,
144              "userPassword": USER_PASS,
145              "userPrincipalName": UPN,
146              "userAccountControl": str(account_control)
147         })
148
149     # Get the supplemental credentials for the user under test
150     def get_supplemental_creds(self):
151         base = "cn=" + USER_NAME + ",cn=users," + self.base_dn
152         res = self.ldb.search(scope=ldb.SCOPE_BASE,
153                               base=base,
154                               attrs=["supplementalCredentials"])
155         self.assertIs(True, len(res) > 0)
156         obj = res[0]
157         sc_blob = obj["supplementalCredentials"][0]
158         sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
159         return sc
160
161     # Calculate and validate a Wdigest value
162     def check_digest(self, user, realm, password, digest):
163         expected = calc_digest(user, realm, password)
164         actual = binascii.hexlify(bytearray(digest))
165         error = "Digest expected[%s], actual[%s], " \
166                 "user[%s], realm[%s], pass[%s]" % \
167                 (expected, actual, user, realm, password)
168         self.assertEquals(expected, actual, error)
169
170     # Check all of the 29 expected WDigest values
171     #
172     def check_wdigests(self, digests):
173
174         self.assertEquals(29, digests.num_hashes)
175
176         # Using the n-1 pattern in the array indexes to make it easier
177         # to check the tests against the spec and the samba-tool user tests.
178         self.check_digest(USER_NAME,
179                           self.netbios_domain,
180                           USER_PASS,
181                           digests.hashes[1 - 1].hash)
182         self.check_digest(USER_NAME.lower(),
183                           self.netbios_domain.lower(),
184                           USER_PASS,
185                           digests.hashes[2 - 1].hash)
186         self.check_digest(USER_NAME.upper(),
187                           self.netbios_domain.upper(),
188                           USER_PASS,
189                           digests.hashes[3 - 1].hash)
190         self.check_digest(USER_NAME,
191                           self.netbios_domain.upper(),
192                           USER_PASS,
193                           digests.hashes[4 - 1].hash)
194         self.check_digest(USER_NAME,
195                           self.netbios_domain.lower(),
196                           USER_PASS,
197                           digests.hashes[5 - 1].hash)
198         self.check_digest(USER_NAME.upper(),
199                           self.netbios_domain.lower(),
200                           USER_PASS,
201                           digests.hashes[6 - 1].hash)
202         self.check_digest(USER_NAME.lower(),
203                           self.netbios_domain.upper(),
204                           USER_PASS,
205                           digests.hashes[7 - 1].hash)
206         self.check_digest(USER_NAME,
207                           self.dns_domain,
208                           USER_PASS,
209                           digests.hashes[8 - 1].hash)
210         self.check_digest(USER_NAME.lower(),
211                           self.dns_domain.lower(),
212                           USER_PASS,
213                           digests.hashes[9 - 1].hash)
214         self.check_digest(USER_NAME.upper(),
215                           self.dns_domain.upper(),
216                           USER_PASS,
217                           digests.hashes[10 - 1].hash)
218         self.check_digest(USER_NAME,
219                           self.dns_domain.upper(),
220                           USER_PASS,
221                           digests.hashes[11 - 1].hash)
222         self.check_digest(USER_NAME,
223                           self.dns_domain.lower(),
224                           USER_PASS,
225                           digests.hashes[12 - 1].hash)
226         self.check_digest(USER_NAME.upper(),
227                           self.dns_domain.lower(),
228                           USER_PASS,
229                           digests.hashes[13 - 1].hash)
230         self.check_digest(USER_NAME.lower(),
231                           self.dns_domain.upper(),
232                           USER_PASS,
233                           digests.hashes[14 - 1].hash)
234         self.check_digest(UPN,
235                           "",
236                           USER_PASS,
237                           digests.hashes[15 - 1].hash)
238         self.check_digest(UPN.lower(),
239                           "",
240                           USER_PASS,
241                           digests.hashes[16 - 1].hash)
242         self.check_digest(UPN.upper(),
243                           "",
244                           USER_PASS,
245                           digests.hashes[17 - 1].hash)
246
247         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
248         self.check_digest(name,
249                           "",
250                           USER_PASS,
251                           digests.hashes[18 - 1].hash)
252
253         name = "%s\\%s" % (self.netbios_domain.lower(), USER_NAME.lower())
254         self.check_digest(name,
255                           "",
256                           USER_PASS,
257                           digests.hashes[19 - 1].hash)
258
259         name = "%s\\%s" % (self.netbios_domain.upper(), USER_NAME.upper())
260         self.check_digest(name,
261                           "",
262                           USER_PASS,
263                           digests.hashes[20 - 1].hash)
264         self.check_digest(USER_NAME,
265                           "Digest",
266                           USER_PASS,
267                           digests.hashes[21 - 1].hash)
268         self.check_digest(USER_NAME.lower(),
269                           "Digest",
270                           USER_PASS,
271                           digests.hashes[22 - 1].hash)
272         self.check_digest(USER_NAME.upper(),
273                           "Digest",
274                           USER_PASS,
275                           digests.hashes[23 - 1].hash)
276         self.check_digest(UPN,
277                           "Digest",
278                           USER_PASS,
279                           digests.hashes[24 - 1].hash)
280         self.check_digest(UPN.lower(),
281                           "Digest",
282                           USER_PASS,
283                           digests.hashes[25 - 1].hash)
284         self.check_digest(UPN.upper(),
285                           "Digest",
286                           USER_PASS,
287                           digests.hashes[26 - 1].hash)
288         name = "%s\\%s" % (self.netbios_domain, USER_NAME)
289         self.check_digest(name,
290                           "Digest",
291                           USER_PASS,
292                           digests.hashes[27 - 1].hash)
293
294         name = "%s\\%s" % (self.netbios_domain.lower(), USER_NAME.lower())
295         self.check_digest(name,
296                           "Digest",
297                           USER_PASS,
298                           digests.hashes[28 - 1].hash)
299
300         name = "%s\\%s" % (self.netbios_domain.upper(), USER_NAME.upper())
301         self.check_digest(name,
302                           "Digest",
303                           USER_PASS,
304                           digests.hashes[29 - 1].hash)
305
306     def checkUserPassword(self, up, expected):
307
308         # Check we've received the correct number of hashes
309         self.assertEquals(len(expected), up.num_hashes)
310
311         i = 0
312         for (tag, alg, rounds) in expected:
313             self.assertEquals(tag, up.hashes[i].scheme)
314
315             data = up.hashes[i].value.split("$")
316             # Check we got the expected crypt algorithm
317             self.assertEquals(alg, data[1])
318
319             if rounds is None:
320                 cmd = "$%s$%s" % (alg, data[2])
321             else:
322                 cmd = "$%s$rounds=%d$%s" % (alg, rounds, data[3])
323
324             # Calculate the expected hash value
325             expected = crypt.crypt(USER_PASS, cmd)
326             self.assertEquals(expected, up.hashes[i].value)
327             i += 1
328
329     # Check that the correct nt_hash was stored for userPassword
330     def checkNtHash(self, password, nt_hash):
331         creds = Credentials()
332         creds.set_anonymous()
333         creds.set_password(password)
334         expected = creds.get_nt_hash()
335         actual = bytearray(nt_hash)
336         self.assertEquals(expected, actual)