ctdb-scripts: Improve update and listing code
[samba4-gss.git] / python / samba / netcmd / user / readpasswords / common.py
blob1bf29fe3eca8fb30936bcb52d884e34d25e61e43
1 # user management
3 # common code
5 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
6 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 import base64
23 import builtins
24 import binascii
25 import datetime
26 import errno
27 import io
28 import os
30 import ldb
31 from samba import credentials, nttime2float
32 from samba.auth import system_session
33 from samba.common import get_bytes, get_string
34 from samba.dcerpc import drsblobs, security, gmsa
35 from samba.ndr import ndr_unpack
36 from samba.netcmd import Command, CommandError
37 from samba.samdb import SamDB
38 from samba.nt_time import timedelta_from_nt_time_delta, nt_time_from_datetime
39 from samba.gkdi import MAX_CLOCK_SKEW
41 # python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
42 # have to use python[3]-gpg instead
43 # The API is different, need to adapt.
45 def _gpgme_decrypt(encrypted_bytes):
46 """
47 Use python[3]-gpgme to decrypt GPG.
48 """
49 ctx = gpgme.Context()
50 ctx.armor = True # use ASCII-armored
51 out = io.BytesIO()
52 ctx.decrypt(io.BytesIO(encrypted_bytes), out)
53 return out.getvalue()
56 def _gpg_decrypt(encrypted_bytes):
57 """
58 Use python[3]-gpg to decrypt GPG.
59 """
60 ciphertext = gpg.Data(string=encrypted_bytes)
61 ctx = gpg.Context(armor=True)
62 # plaintext, result, verify_result
63 plaintext, _, _ = ctx.decrypt(ciphertext)
64 return plaintext
67 gpg_decrypt = None
69 if not gpg_decrypt:
70 try:
71 import gpgme
72 gpg_decrypt = _gpgme_decrypt
73 except ImportError:
74 pass
76 if not gpg_decrypt:
77 try:
78 import gpg
79 gpg_decrypt = _gpg_decrypt
80 except ImportError:
81 pass
83 if gpg_decrypt:
84 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
85 "cleartext source")
86 else:
87 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, "
88 "python[3]-gpgme or python[3]-gpg required")
91 disabled_virtual_attributes = {
94 virtual_attributes = {
95 "virtualClearTextUTF8": {
96 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
98 "virtualClearTextUTF16": {
99 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
101 "virtualSambaGPG": {
102 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
104 "unicodePwd": {
105 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
107 "virtualManagedPasswordQueryTime": {
112 def get_crypt_value(alg, utf8pw, rounds=0):
113 algs = {
114 "5": {"length": 43},
115 "6": {"length": 86},
117 if alg not in algs:
118 raise ValueError(f"invalid algorithm code: {alg}"
119 f"(expected one of {','.join(algs.keys())})")
121 salt = os.urandom(16)
122 # The salt needs to be in [A-Za-z0-9./]
123 # base64 is close enough and as we had 16
124 # random bytes but only need 16 characters
125 # we can ignore the possible == at the end
126 # of the base64 string
127 # we just need to replace '+' by '.'
128 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
129 crypt_salt = ""
130 if rounds != 0:
131 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
132 else:
133 crypt_salt = "$%s$%s$" % (alg, b64salt)
135 crypt_value = crypt.crypt(utf8pw, crypt_salt)
136 if crypt_value is None:
137 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
138 expected_len = len(crypt_salt) + algs[alg]["length"]
139 if len(crypt_value) != expected_len:
140 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
141 crypt_salt, len(crypt_value), expected_len))
142 return crypt_value
145 try:
146 import hashlib
147 hashlib.sha1()
148 virtual_attributes["virtualSSHA"] = {
150 except ImportError as e:
151 reason = "hashlib.sha1()"
152 reason += " required"
153 disabled_virtual_attributes["virtualSSHA"] = {
154 "reason": reason,
157 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
158 try:
159 import crypt
160 get_crypt_value(alg, "")
161 virtual_attributes[attr] = {
163 except ImportError as e:
164 reason = "crypt"
165 reason += " required"
166 disabled_virtual_attributes[attr] = {
167 "reason": reason,
169 except NotImplementedError as e:
170 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
171 disabled_virtual_attributes[attr] = {
172 "reason": reason,
175 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
176 for x in range(1, 30):
177 virtual_attributes["virtualWDigest%02d" % x] = {}
179 # Add Kerberos virtual attributes
180 virtual_attributes["virtualKerberosSalt"] = {}
182 virtual_attributes_help = "The attributes to display (comma separated). "
183 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
184 if len(disabled_virtual_attributes) != 0:
185 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
188 class GetPasswordCommand(Command):
190 def __init__(self):
191 super().__init__()
192 self.lp = None
194 def inject_virtual_attributes(self, samdb):
195 # We use sort here in order to have a predictable processing order
196 # this might not be strictly needed, but also doesn't hurt here
197 for a in sorted(virtual_attributes.keys()):
198 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
199 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
201 def connect_for_passwords(self, url,
202 creds=None,
203 require_ldapi=True,
204 verbose=False):
206 # using anonymous here, results in no authentication
207 # which means we can get system privileges via
208 # the privileged ldapi socket
209 anon_creds = credentials.Credentials()
210 anon_creds.set_anonymous()
212 if url is None and not require_ldapi:
213 pass
214 elif url.lower().startswith("ldapi://"):
215 creds = anon_creds
216 pass
217 elif require_ldapi:
218 raise CommandError("--url requires an ldapi:// url for this command")
220 if verbose:
221 self.outf.write("Connecting to '%s'\n" % url)
223 samdb = SamDB(url=url, session_info=system_session(),
224 credentials=creds, lp=self.lp)
226 if require_ldapi or url is None:
227 try:
229 # Make sure we're connected as SYSTEM
231 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
232 assert len(res) == 1
233 sids = res[0].get("tokenGroups")
234 assert len(sids) == 1
235 sid = ndr_unpack(security.dom_sid, sids[0])
236 assert str(sid) == security.SID_NT_SYSTEM
237 except Exception as msg:
238 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
239 (security.SID_NT_SYSTEM))
241 self.inject_virtual_attributes(samdb)
243 return samdb
245 def get_account_attributes(self, samdb, username, basedn, filter, scope,
246 attrs, decrypt, support_pw_attrs=True):
248 def get_option(opts, name):
249 if not opts:
250 return None
251 for o in opts:
252 if o.lower().startswith("%s=" % name.lower()):
253 (key, _, val) = o.partition('=')
254 return val
255 return None
257 def get_virtual_attr_definition(attr):
258 for van in sorted(virtual_attributes.keys()):
259 if van.lower() != attr.lower():
260 continue
261 return virtual_attributes[van]
262 return None
264 formats = [
265 "GeneralizedTime",
266 "UnixTime",
267 "TimeSpec",
270 def get_virtual_format_definition(opts):
271 formatname = get_option(opts, "format")
272 if formatname is None:
273 return None
274 for fm in formats:
275 if fm.lower() != formatname.lower():
276 continue
277 return fm
278 return None
280 def parse_raw_attr(raw_attr, is_hidden=False):
281 (attr, _, fullopts) = raw_attr.partition(';')
282 if fullopts:
283 opts = fullopts.split(';')
284 else:
285 opts = []
286 a = {}
287 a["raw_attr"] = raw_attr
288 a["attr"] = attr
289 a["opts"] = opts
290 a["vattr"] = get_virtual_attr_definition(attr)
291 a["vformat"] = get_virtual_format_definition(opts)
292 a["is_hidden"] = is_hidden
293 return a
295 raw_attrs = attrs[:]
296 has_wildcard_attr = "*" in raw_attrs
297 has_virtual_attrs = False
298 requested_attrs = []
299 implicit_attrs = []
301 for raw_attr in raw_attrs:
302 a = parse_raw_attr(raw_attr)
303 requested_attrs.append(a)
305 search_attrs = []
306 has_virtual_attrs = False
307 for a in requested_attrs:
308 if a["vattr"] is not None:
309 has_virtual_attrs = True
310 continue
311 if a["vformat"] is not None:
312 # also add it as implicit attr,
313 # where we just do
314 # search_attrs.append(a["attr"])
315 # later on
316 implicit_attrs.append(a)
317 continue
318 if a["raw_attr"] in search_attrs:
319 continue
320 search_attrs.append(a["raw_attr"])
322 if not has_wildcard_attr:
323 required_attrs = [
324 "sAMAccountName",
325 "userPrincipalName"
327 for required_attr in required_attrs:
328 a = parse_raw_attr(required_attr)
329 implicit_attrs.append(a)
331 if has_virtual_attrs:
332 if support_pw_attrs:
333 required_attrs = [
334 "supplementalCredentials",
335 "unicodePwd",
336 "msDS-ManagedPassword",
338 for required_attr in required_attrs:
339 a = parse_raw_attr(required_attr, is_hidden=True)
340 implicit_attrs.append(a)
342 for a in implicit_attrs:
343 if a["attr"] in search_attrs:
344 continue
345 search_attrs.append(a["attr"])
347 if scope == ldb.SCOPE_BASE:
348 search_controls = ["show_deleted:1", "show_recycled:1"]
349 else:
350 search_controls = []
351 try:
352 res = samdb.search(base=basedn, expression=filter,
353 scope=scope, attrs=search_attrs,
354 controls=search_controls)
355 if len(res) == 0:
356 raise Exception('Unable to find user "%s"' % (username or filter))
357 if len(res) > 1:
358 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
359 except Exception as msg:
360 # FIXME: catch more specific exception
361 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
362 obj = res[0]
364 calculated = {}
366 sc = None
367 unicodePwd = None
368 if "supplementalCredentials" in obj:
369 sc_blob = obj["supplementalCredentials"][0]
370 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
371 if "unicodePwd" in obj:
372 unicodePwd = obj["unicodePwd"][0]
373 if "msDS-ManagedPassword" in obj:
374 # unpack a GMSA managed password as if we could read the
375 # hidden password attributes.
376 managed_password = obj["msDS-ManagedPassword"][0]
377 unpacked_managed_password = ndr_unpack(gmsa.MANAGEDPASSWORD_BLOB,
378 managed_password)
379 calculated["OLDCLEARTEXT"] = \
380 unpacked_managed_password.passwords.previous
381 query_interval = unpacked_managed_password.passwords.query_interval
382 calculated["GMSA:query_interval"] = \
383 query_interval
385 query_time_datetime = \
386 timedelta_from_nt_time_delta(query_interval) + datetime.datetime.now(tz=datetime.timezone.utc)
388 query_time_nttime = nt_time_from_datetime(query_time_datetime)
390 calculated["GMSA:query_time"] = query_time_nttime
392 # This password is useful for a keytab, but not for
393 # authentication, so don't show or provide it as the new password
394 # just yet
395 if calculated["GMSA:query_interval"] <= MAX_CLOCK_SKEW:
396 calculated["Primary:CLEARTEXT"] = \
397 unpacked_managed_password.passwords.previous
398 else:
399 calculated["Primary:CLEARTEXT"] = \
400 unpacked_managed_password.passwords.current
402 account_name = str(obj["sAMAccountName"][0])
403 if "userPrincipalName" in obj:
404 account_upn = str(obj["userPrincipalName"][0])
405 else:
406 realm = samdb.domain_dns_name()
407 account_upn = "%s@%s" % (account_name, realm.lower())
409 def get_package(name, min_idx=0):
410 if name in calculated:
411 return calculated[name]
412 if sc is None:
413 return None
414 if min_idx < 0:
415 min_idx = len(sc.sub.packages) + min_idx
416 idx = 0
417 for p in sc.sub.packages:
418 idx += 1
419 if idx <= min_idx:
420 continue
421 if name != p.name:
422 continue
424 return binascii.a2b_hex(p.data)
425 return None
427 def get_cleartext(attr_opts):
428 param = get_option(attr_opts, "previous")
429 if param:
430 if param != "1":
431 raise CommandError(
432 f"Invalid attribute parameter ;previous={param}, "
433 "only supported value is previous=1")
434 return calculated.get("OLDCLEARTEXT")
435 else:
436 return get_package("Primary:CLEARTEXT")
438 def get_kerberos_ctr():
439 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
440 if primary_krb5 is None:
441 primary_krb5 = get_package("Primary:Kerberos")
442 if primary_krb5 is None:
443 return (0, None)
444 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
445 primary_krb5)
446 return (krb5_blob.version, krb5_blob.ctr)
448 aes256_key = None
449 kerberos_salt = None
451 (krb5_v, krb5_ctr) = get_kerberos_ctr()
452 if krb5_v in [3, 4]:
453 kerberos_salt = krb5_ctr.salt.string
455 if krb5_ctr.keys:
456 def is_aes256(k):
457 return k.keytype == 18
458 aes256_key = next(builtins.filter(is_aes256, krb5_ctr.keys),
459 None)
461 if decrypt:
463 # Samba adds 'Primary:SambaGPG' at the end.
464 # When Windows sets the password it keeps
465 # 'Primary:SambaGPG' and rotates it to
466 # the beginning. So we can only use the value,
467 # if it is the last one.
469 # In order to get more protection we verify
470 # the nthash of the decrypted utf16 password
471 # against the stored nthash in unicodePwd if
472 # available, otherwise against the first 16
473 # bytes of the AES256 key.
475 sgv = get_package("Primary:SambaGPG", min_idx=-1)
476 if sgv is not None:
477 try:
478 cv = gpg_decrypt(sgv)
480 # We only use the password if it matches
481 # the current nthash stored in the unicodePwd
482 # attribute, or the current AES256 key.
484 tmp = credentials.Credentials()
485 tmp.set_anonymous()
486 tmp.set_utf16_password(cv)
488 decrypted = None
489 current_hash = None
491 if unicodePwd is not None:
492 decrypted = tmp.get_nt_hash()
493 current_hash = unicodePwd
494 elif aes256_key is not None and kerberos_salt is not None:
495 tmp.set_kerberos_salt_principal(kerberos_salt)
496 decrypted = tmp.get_kerberos_key(credentials.ENCTYPE_AES256_CTS_HMAC_SHA1_96)
497 current_hash = aes256_key.value
499 if current_hash is not None and current_hash == decrypted:
500 calculated["Primary:CLEARTEXT"] = cv
502 except Exception as e:
503 if gpg_decrypt is None:
504 message = decrypt_samba_gpg_help
505 else:
506 message = str(e)
507 self.outf.write(
508 "WARNING: '%s': SambaGPG can't be decrypted "
509 "into CLEARTEXT: %s\n" % (
510 username or account_name, message))
512 def get_utf8(a, b, username):
513 creds_for_charcnv = credentials.Credentials()
514 creds_for_charcnv.set_anonymous()
515 creds_for_charcnv.set_utf16_password(get_bytes(b))
517 # This can't fail due to character conversion issues as it
518 # includes a built-in fallback (UTF16_MUNGED) matching
519 # exactly what we need.
520 return creds_for_charcnv.get_password().encode()
522 # Extract the WDigest hash for the value specified by i.
523 # Builds an htdigest compatible value
524 DIGEST = "Digest"
526 def get_wDigest(i, primary_wdigest, account_name, account_upn,
527 domain, dns_domain):
528 if i == 1:
529 user = account_name
530 realm = domain
531 elif i == 2:
532 user = account_name.lower()
533 realm = domain.lower()
534 elif i == 3:
535 user = account_name.upper()
536 realm = domain.upper()
537 elif i == 4:
538 user = account_name
539 realm = domain.upper()
540 elif i == 5:
541 user = account_name
542 realm = domain.lower()
543 elif i == 6:
544 user = account_name.upper()
545 realm = domain.lower()
546 elif i == 7:
547 user = account_name.lower()
548 realm = domain.upper()
549 elif i == 8:
550 user = account_name
551 realm = dns_domain.lower()
552 elif i == 9:
553 user = account_name.lower()
554 realm = dns_domain.lower()
555 elif i == 10:
556 user = account_name.upper()
557 realm = dns_domain.upper()
558 elif i == 11:
559 user = account_name
560 realm = dns_domain.upper()
561 elif i == 12:
562 user = account_name
563 realm = dns_domain.lower()
564 elif i == 13:
565 user = account_name.upper()
566 realm = dns_domain.lower()
567 elif i == 14:
568 user = account_name.lower()
569 realm = dns_domain.upper()
570 elif i == 15:
571 user = account_upn
572 realm = ""
573 elif i == 16:
574 user = account_upn.lower()
575 realm = ""
576 elif i == 17:
577 user = account_upn.upper()
578 realm = ""
579 elif i == 18:
580 user = "%s\\%s" % (domain, account_name)
581 realm = ""
582 elif i == 19:
583 user = "%s\\%s" % (domain.lower(), account_name.lower())
584 realm = ""
585 elif i == 20:
586 user = "%s\\%s" % (domain.upper(), account_name.upper())
587 realm = ""
588 elif i == 21:
589 user = account_name
590 realm = DIGEST
591 elif i == 22:
592 user = account_name.lower()
593 realm = DIGEST
594 elif i == 23:
595 user = account_name.upper()
596 realm = DIGEST
597 elif i == 24:
598 user = account_upn
599 realm = DIGEST
600 elif i == 25:
601 user = account_upn.lower()
602 realm = DIGEST
603 elif i == 26:
604 user = account_upn.upper()
605 realm = DIGEST
606 elif i == 27:
607 user = "%s\\%s" % (domain, account_name)
608 realm = DIGEST
609 elif i == 28:
610 # Differs from spec, see tests
611 user = "%s\\%s" % (domain.lower(), account_name.lower())
612 realm = DIGEST
613 elif i == 29:
614 # Differs from spec, see tests
615 user = "%s\\%s" % (domain.upper(), account_name.upper())
616 realm = DIGEST
617 else:
618 user = ""
620 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
621 primary_wdigest)
622 try:
623 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
624 return "%s:%s:%s" % (user, realm, get_string(digest))
625 except IndexError:
626 return None
628 # get the value for a virtualCrypt attribute.
629 # look for an exact match on algorithm and rounds in supplemental creds
630 # if not found calculate using Primary:CLEARTEXT
631 # if no Primary:CLEARTEXT return the first supplementalCredential
632 # that matches the algorithm.
633 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
634 sv = None
635 fb = None
636 b = get_package("Primary:userPassword")
637 if b is not None:
638 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
639 if sv is None:
640 # No exact match on algorithm and number of rounds
641 # try and calculate one from the Primary:CLEARTEXT
642 b = get_cleartext(attr_opts)
643 if b is not None:
644 u8 = get_utf8(a, b, username or account_name)
645 if u8 is not None:
646 # in py2 using get_bytes should ensure u8 is unmodified
647 # in py3 it will be decoded
648 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
649 if sv is None:
650 # Unable to calculate a hash with the specified
651 # number of rounds, fall back to the first hash using
652 # the specified algorithm
653 sv = fb
654 if sv is None:
655 return None
656 return "{CRYPT}" + sv
658 def get_userPassword_hash(blob, algorithm, rounds):
659 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
660 SCHEME = "{CRYPT}"
662 # Check that the NT hash or AES256 key have not been changed
663 # without updating the user password hashes. This indicates that
664 # password has been changed without updating the supplemental
665 # credentials.
666 if unicodePwd is not None:
667 current_hash = unicodePwd
668 elif aes256_key is not None:
669 current_hash = aes256_key.value[:16]
670 else:
671 return None, None
673 if current_hash != bytearray(up.current_nt_hash.hash):
674 return None, None
676 scheme_prefix = "$%d$" % algorithm
677 prefix = scheme_prefix
678 if rounds > 0:
679 prefix = "$%d$rounds=%d" % (algorithm, rounds)
680 scheme_match = None
682 for h in up.hashes:
683 # in PY2 this should just do nothing and in PY3 if bytes
684 # it will decode them
685 h_value = get_string(h.value)
686 if (scheme_match is None and
687 h.scheme == SCHEME and
688 h_value.startswith(scheme_prefix)):
689 scheme_match = h_value
690 if h.scheme == SCHEME and h_value.startswith(prefix):
691 return (h_value, scheme_match)
693 # No match on the number of rounds, return the value of the
694 # first matching scheme
695 return (None, scheme_match)
697 # Extract the rounds value from the options of a virtualCrypt attribute
698 # i.e. options = "rounds=20;other=ignored;" will return 20
699 # if the rounds option is not found or the value is not a number, 0 is returned
700 # which indicates that the default number of rounds should be used.
701 def get_rounds(opts):
702 val = get_option(opts, "rounds")
703 if val is None:
704 return 0
705 try:
706 return int(val)
707 except ValueError:
708 return 0
710 def get_unicode_pwd_hash(pwd):
711 # We can't read unicodePwd directly, but we can regenerate
712 # it from msDS-ManagedPassword
713 tmp = credentials.Credentials()
714 tmp.set_anonymous()
715 tmp.set_utf16_password(pwd)
716 return tmp.get_nt_hash()
718 # We use sort here in order to have a predictable processing order
719 for a in sorted(virtual_attributes.keys()):
720 vattr = None
721 for ra in requested_attrs:
722 if ra["vattr"] is None:
723 continue
724 if ra["attr"].lower() != a.lower():
725 continue
726 vattr = ra
727 break
728 if vattr is None:
729 continue
730 attr_opts = vattr["opts"]
732 if a == "virtualClearTextUTF8":
733 b = get_cleartext(attr_opts)
734 if b is None:
735 continue
736 u8 = get_utf8(a, b, username or account_name)
737 if u8 is None:
738 continue
739 v = u8
740 elif a == "virtualClearTextUTF16":
741 v = get_cleartext(attr_opts)
742 if v is None:
743 continue
744 elif a == "virtualSSHA":
745 b = get_cleartext(attr_opts)
746 if b is None:
747 continue
748 u8 = get_utf8(a, b, username or account_name)
749 if u8 is None:
750 continue
751 salt = os.urandom(4)
752 h = hashlib.sha1()
753 h.update(u8)
754 h.update(salt)
755 bv = h.digest() + salt
756 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
757 elif a == "virtualCryptSHA256":
758 rounds = get_rounds(attr_opts)
759 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
760 if x is None:
761 continue
762 v = x
763 elif a == "virtualCryptSHA512":
764 rounds = get_rounds(attr_opts)
765 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
766 if x is None:
767 continue
768 v = x
769 elif a == "virtualSambaGPG":
770 # Samba adds 'Primary:SambaGPG' at the end.
771 # When Windows sets the password it keeps
772 # 'Primary:SambaGPG' and rotates it to
773 # the beginning. So we can only use the value,
774 # if it is the last one.
775 v = get_package("Primary:SambaGPG", min_idx=-1)
776 if v is None:
777 continue
778 elif a == "virtualKerberosSalt":
779 v = kerberos_salt
780 if v is None:
781 continue
782 elif a == "unicodePwd" and unicodePwd is None:
783 if "Primary:CLEARTEXT" in calculated and not get_option(attr_opts, "previous"):
784 v = get_unicode_pwd_hash(calculated["Primary:CLEARTEXT"])
785 elif "OLDCLEARTEXT" in calculated and get_option(attr_opts, "previous"):
786 v = get_unicode_pwd_hash(calculated["OLDCLEARTEXT"])
787 else:
788 continue
789 elif a.startswith("virtualWDigest"):
790 primary_wdigest = get_package("Primary:WDigest")
791 if primary_wdigest is None:
792 continue
793 x = a[len("virtualWDigest"):]
794 try:
795 i = int(x)
796 except ValueError:
797 continue
798 domain = samdb.domain_netbios_name()
799 dns_domain = samdb.domain_dns_name()
800 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
801 if v is None:
802 continue
803 elif a == "virtualManagedPasswordQueryTime":
804 if "GMSA:query_time" not in calculated:
805 continue
806 v = str(calculated["GMSA:query_time"])
807 else:
808 continue
809 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, vattr["raw_attr"])
811 def get_src_attrname(srcattrg):
812 srcattrl = srcattrg.lower()
813 srcattr = None
814 for k in obj.keys():
815 if srcattrl != k.lower():
816 continue
817 srcattr = k
818 break
819 return srcattr
821 def get_src_time_float(srcattr):
822 if srcattr not in obj:
823 return None
824 vstr = str(obj[srcattr][0])
825 if vstr.endswith(".0Z"):
826 vut = ldb.string_to_time(vstr)
827 vfl = float(vut)
828 return vfl
830 try:
831 vnt = int(vstr)
832 except ValueError as e:
833 return None
834 # 0 or 9223372036854775807 mean no value too
835 if vnt == 0:
836 return None
837 if vnt >= 0x7FFFFFFFFFFFFFFF:
838 return None
839 vfl = nttime2float(vnt)
840 return vfl
842 def get_generalizedtime(srcattr):
843 vfl = get_src_time_float(srcattr)
844 if vfl is None:
845 return None
846 vut = int(vfl)
847 try:
848 v = "%s" % ldb.timestring(vut)
849 except OSError as e:
850 if e.errno == errno.EOVERFLOW:
851 return None
852 raise
853 return v
855 def get_unixepoch(srcattr):
856 vfl = get_src_time_float(srcattr)
857 if vfl is None:
858 return None
859 vut = int(vfl)
860 v = "%d" % vut
861 return v
863 def get_timespec(srcattr):
864 vfl = get_src_time_float(srcattr)
865 if vfl is None:
866 return None
867 v = "%.9f" % vfl
868 return v
870 generated_formats = {}
871 for fm in formats:
872 for ra in requested_attrs:
873 if ra["vformat"] is None:
874 continue
875 if ra["vformat"] != fm:
876 continue
878 srcattr = get_src_attrname(ra["attr"])
879 if srcattr is not None:
880 an = "%s;format=%s" % (srcattr, fm)
881 else:
882 srcattr = an = get_src_attrname(ra["raw_attr"])
883 if srcattr is None:
884 continue
885 if an in generated_formats:
886 continue
887 generated_formats[an] = fm
889 v = None
890 if fm == "GeneralizedTime":
891 v = get_generalizedtime(srcattr)
892 elif fm == "UnixTime":
893 v = get_unixepoch(srcattr)
894 elif fm == "TimeSpec":
895 v = get_timespec(srcattr)
896 if v is None:
897 continue
898 obj[an] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, an)
900 # Now filter out implicit attributes
901 for delname in obj.keys():
902 keep = False
903 for ra in requested_attrs:
904 if delname.lower() != ra["raw_attr"].lower():
905 continue
906 keep = True
907 break
908 if keep:
909 continue
911 dattr = None
912 for ia in implicit_attrs:
913 if delname.lower() != ia["attr"].lower():
914 continue
915 dattr = ia
916 break
917 if dattr is None:
918 continue
920 if has_wildcard_attr and not dattr["is_hidden"]:
921 continue
922 del obj[delname]
923 return obj
925 def parse_attributes(self, attributes):
927 if attributes is None:
928 raise CommandError("Please specify --attributes")
929 attrs = attributes.split(',')
930 password_attrs = []
931 for pa in attrs:
932 pa = pa.lstrip().rstrip()
933 for da in disabled_virtual_attributes.keys():
934 if pa.lower() == da.lower():
935 r = disabled_virtual_attributes[da]["reason"]
936 raise CommandError("Virtual attribute '%s' not supported: %s" % (
937 da, r))
938 for va in virtual_attributes.keys():
939 if pa.lower() == va.lower():
940 # Take the real name
941 pa = va
942 break
943 password_attrs += [pa]
945 return password_attrs