PATCHES: Password sync as active directory domain controller

Alexander Bokovoy ab at samba.org
Wed Mar 2 07:50:26 UTC 2016


Hi Metze,

thanks for these patches. They are an impressive amount of work. I have
few comments below mostly related to Python 2/3 compatibility and some
idiomatic use of Python.


On Mon, 29 Feb 2016, Stefan Metzmacher wrote:
> From 9e74157546880f003f87a39de3b973c45b6df1df Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 04/22] samba-tool: add 'user getpassword' command
> 
> This provides an easy way to get the passwords of a user
> including the cleartext passwords (if stored) and derived
> hashes. This is done by providing virtual attributes like:
> virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
> 
> This is much easier than using ldbsearch and manually parsing
> the supplementalCredentials attribute.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/netcmd/user.py | 432 ++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 432 insertions(+)
> 
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index cf640b0..04e1a50 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -20,10 +20,20 @@
>  import samba.getopt as options
>  import ldb
>  import pwd
> +import os
> +import sys
> +import errno
> +import base64
> +import binascii
>  from getpass import getpass
>  from samba.auth import system_session
>  from samba.samdb import SamDB
> +from samba.dcerpc import misc
> +from samba.dcerpc import security
> +from samba.dcerpc import drsblobs
> +from samba.ndr import ndr_unpack, ndr_pack, ndr_print
>  from samba import (
> +    credentials,
>      dsdb,
>      gensec,
>      generate_random_password,
> @@ -37,6 +47,141 @@ from samba.netcmd import (
>      Option,
>      )
>  
> +disabled_virtual_attributes = {
> +    }
> +
> +virtual_attributes = {
> +    "virtualClearTextUTF8": {
> +        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> +        },
> +    "virtualClearTextUTF16": {
> +        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> +        },
> +    }
> +
> +def check_random():
> +    try:
> +        import Crypto.Random
> +        return None
> +    except ImportError as e:
> +        pass
> +    try:
> +        import M2Crypto.Rand
> +        return None
> +    except ImportError as e:
> +        pass
> +    return "Crypto.Random or M2Crypto.Rand required"
> +
> +def get_random_bytes(num):
> +    try:
> +        import Crypto.Random
> +        return Crypto.Random.get_random_bytes(num)
> +    except ImportError as e:
> +        pass
> +    try:
> +        import M2Crypto.Rand
> +        return M2Crypto.Rand.rand_bytes(num)
> +    except ImportError as e:
> +        pass
> +    raise ImportError("Crypto.Random or M2Crypto.Rand required")
> +
> +def get_crypt_value(alg, utf8pw):
> +    algs = {
> +        "5": {"length": 43},
> +        "6": {"length": 86},
> +    }
> +    assert alg in algs.keys()

Please replace it with 
     assert alg in algs

It is more Python idiomatic and also avoids creating a list every time
only to drop it.

> +    salt = get_random_bytes(16)
> +    # The salt needs to be in [A-Za-z0-9./]
> +    # base64 is close enough and as we had 16
> +    # random bytes but only need 16 characters
> +    # we can ignore the possible == at the end
> +    # of the base64 string
> +    # we just need to replace '+' by '.'
> +    b64salt = base64.b64encode(salt)
> +    crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
> +    crypt_value = crypt.crypt(utf8pw, crypt_salt)
> +    if crypt_value is None:
> +        raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
> +    expected_len = len(crypt_salt) + algs[alg]["length"]
> +    if len(crypt_value) != expected_len:
> +        raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
> +            crypt_salt, len(crypt_value), expected_len))
> +    return crypt_value
> +
> +try:
> +    random_reason = check_random()
> +    if random_reason is not None:
> +        raise ImportError(random_reason)
> +    import hashlib
> +    h = hashlib.sha1()
> +    h = None
> +    virtual_attributes["virtualSSHA"] = {
> +        }
> +except ImportError as e:
> +    reason = "hashlib.sha1()"
> +    if random_reason:
> +        reason += " and " + random_reason
> +    reason += " required"
> +    disabled_virtual_attributes["virtualSSHA"] = {
> +        "reason" : reason,
> +        }
> +    pass

You don't need 'pass' here as there are already other statements under
'except'.

> +
> +try:
> +    random_reason = check_random()
> +    if random_reason is not None:
> +        raise ImportError(random_reason)
> +    import crypt
> +    v = get_crypt_value("5", "")
> +    v = None
> +    virtual_attributes["virtualCryptSHA256"] = {
> +        }
> +except ImportError as e:
> +    reason = "crypt"
> +    if random_reason:
> +        reason += " and " + random_reason
> +    reason += " required"
> +    disabled_virtual_attributes["virtualCryptSHA256"] = {
> +        "reason" : reason,
> +        }
> +    pass

Same here.

> +except NotImplementedError as e:
> +    reason = "modern '$5$' salt in crypt(3) required"
> +    disabled_virtual_attributes["virtualCryptSHA256"] = {
> +        "reason" : reason,
> +        }
> +    pass

Same here.

> +
> +try:
> +    random_reason = check_random()
> +    if random_reason is not None:
> +        raise ImportError(random_reason)
> +    import crypt
> +    v = get_crypt_value("6", "")
> +    v = None
> +    virtual_attributes["virtualCryptSHA512"] = {
> +        }
> +except ImportError as e:
> +    reason = "crypt"
> +    if random_reason is not None:
> +        reason += " and " + random_reason
> +    reason += " required"
> +    disabled_virtual_attributes["virtualCryptSHA512"] = {
> +        "reason" : reason,
> +        }
> +    pass

Same here.

> +except NotImplementedError as e:
> +    reason = "modern '$6$' salt in crypt(3) required"
> +    disabled_virtual_attributes["virtualCryptSHA512"] = {
> +        "reason" : reason,
> +        }
> +    pass

Same here.

> +
> +virtual_attributes_help  = "The attributes to display (comma separated). "
> +virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
> +if len(disabled_virtual_attributes) != 0:
> +    virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
>  
>  class cmd_user_create(Command):
>      """Create a new user.
> @@ -610,6 +755,292 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
>              raise CommandError("Failed to set password for user '%s': %s" % (username or filter, msg))
>          self.outf.write("Changed password OK\n")
>  
> +class GetPasswordCommand(Command):
> +
> +    def __init__(self):
> +        Command.__init__(self)
A nit-pick:
It would be good to write all constructors in idiomatic way for 'new'
classes (derived from 'object')
           super(GetPasswordCommand, self).__init__()


> +        self.lp = None
> +
> +    def connect_system_samdb(self, url, allow_local=False, verbose=False):
> +
> +        # using anonymous here, results in no authentication
> +        # which means we can get system privileges via
> +        # the privileged ldapi socket
> +        creds = credentials.Credentials()
> +        creds.set_anonymous()
> +
> +        if url is None and allow_local:
> +            pass
> +        elif url.lower().startswith("ldapi://"):
> +            pass
> +        elif url.lower().startswith("ldap://"):
> +            raise CommandError("--url ldap:// is not supported for this command")
> +        elif url.lower().startswith("ldaps://"):
> +            raise CommandError("--url ldaps:// is not supported for this command")
> +        elif not allow_local:
> +            raise CommandError("--url requires an ldapi:// url for this command")
> +
> +        if verbose:
> +            self.outf.write("Connecting to '%s'\n" % url)
> +
> +        samdb = SamDB(url=url, session_info=system_session(),
> +                      credentials=creds, lp=self.lp)
> +
> +        try:
> +            #
> +            # Make sure we're connected as SYSTEM
> +            #
> +            res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
> +            assert len(res) == 1
> +            sids = res[0].get("tokenGroups")
> +            assert len(sids) == 1
> +            sid = ndr_unpack(security.dom_sid, sids[0])
> +            assert str(sid) == security.SID_NT_SYSTEM
> +        except Exception, msg:
Please use 'except Exception as e' like in the other code above because
this makes the code Python 2 and 3 compatible ('except Exception, msg'
is not compatible with Python 3).

> +            raise CommandError("You need to speficy an URL that gives privileges as SID_NT_SYSTEM(%s)" %
> +                               (security.SID_NT_SYSTEM))
> +
> +        for a in sorted(virtual_attributes.keys()):
> +            flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
> +            samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
> +
> +        return samdb
> +
> +    def get_account_attributes(self, samdb, username,
> +                               basedn, filter, scope, attrs):
> +
> +        require_supplementalCredentials = False
> +        search_attrs = attrs[:]
> +        lower_attrs = [x.lower() for x in search_attrs]
> +        for a in virtual_attributes.keys():
> +            if a.lower() in lower_attrs:
> +                require_supplementalCredentials = True
> +        add_supplementalCredentials = False
> +        if require_supplementalCredentials:
> +            a = "supplementalCredentials"
> +            if a.lower() not in lower_attrs:
> +                search_attrs += [a]
> +                add_supplementalCredentials = True
> +        add_sAMAcountName = False
> +        a = "sAMAccountName"
> +        if a.lower() not in lower_attrs:
> +            search_attrs += [a]
> +            add_sAMAcountName = True
> +
> +        if scope == ldb.SCOPE_BASE:
> +            search_controls = ["show_deleted:1", "show_recycled:1"]
> +        else:
> +            search_controls = []
> +        try:
> +            res = samdb.search(base=basedn, expression=filter,
> +                               scope=scope, attrs=search_attrs,
> +                               controls=search_controls)
> +            if len(res) == 0:
> +                raise Exception('Unable to find user "%s"' % (username or filter))
> +            if len(res) > 1:
> +                raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
> +        except Exception, msg:
Same here.

> +            # FIXME: catch more specific exception
> +            raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
> +        obj = res[0]
> +
> +        sc = None
> +        if "supplementalCredentials" in obj:
> +            sc_blob = obj["supplementalCredentials"][0]
> +            sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
> +        if add_supplementalCredentials:
> +            del obj["supplementalCredentials"]
> +        account_name = obj["sAMAccountName"][0]
> +        if add_sAMAcountName:
> +            del obj["sAMAccountName"]
> +
> +        def get_package(name):
> +            if sc is None:
> +                return None
> +            for p in sc.sub.packages:
> +                if name != p.name:
> +                    continue
> +
> +                return binascii.a2b_hex(p.data)
> +            return None
> +
> +        def get_utf8(a, b, username):
> +            try:
> +                u = unicode(b, 'utf-16-le')
> +            except UnicodeDecodeError as e:
> +                self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
> +                                username, a))
> +                return None
> +            u8 = u.encode('utf-8')
> +            return u8
> +
> +        for a in sorted(virtual_attributes.keys()):
> +            if not a.lower() in lower_attrs:
> +                continue
> +
> +            if a == "virtualClearTextUTF8":
> +                b = get_package("Primary:CLEARTEXT")
> +                if b is None:
> +                    continue
> +                u8 = get_utf8(a, b, username or account_name)
> +                if u8 is None:
> +                    continue
> +                v = u8
> +            elif a == "virtualClearTextUTF16":
> +                v = get_package("Primary:CLEARTEXT")
> +                if v is None:
> +                    continue
> +            elif a == "virtualSSHA":
> +                b = get_package("Primary:CLEARTEXT")
> +                if b is None:
> +                    continue
> +                u8 = get_utf8(a, b, username or account_name)
> +                if u8 is None:
> +                    continue
> +                salt = get_random_bytes(4)
> +                h = hashlib.sha1()
> +                h.update(u8)
> +                h.update(salt)
> +                bv = h.digest() + salt
> +                v = "{SSHA}" + base64.b64encode(bv)
> +            elif a == "virtualCryptSHA256":
> +                b = get_package("Primary:CLEARTEXT")
> +                if b is None:
> +                    continue
> +                u8 = get_utf8(a, b, username or account_name)
> +                if u8 is None:
> +                    continue
> +                sv = get_crypt_value("5", u8)
> +                v = "{CRYPT}" + sv
> +            elif a == "virtualCryptSHA512":
> +                b = get_package("Primary:CLEARTEXT")
> +                if b is None:
> +                    continue
> +                u8 = get_utf8(a, b, username or account_name)
> +                if u8 is None:
> +                    continue
> +                sv = get_crypt_value("6", u8)
> +                v = "{CRYPT}" + sv
> +            else:
> +                continue
> +            obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
> +        return obj
> +
> +    def parse_attributes(self, attributes):
> +
> +        if attributes is None:
> +            raise CommandError("Please specify --attributes")
> +        attrs = attributes.split(',')
> +        password_attrs = []
> +        for pa in attrs:
> +            for da in disabled_virtual_attributes.keys():
> +                if pa.lower() == da.lower():
> +                    r = disabled_virtual_attributes[da]["reason"]
> +                    raise CommandError("Virtual attribute '%s' not supported: %s" % (
> +                                       da, r))
> +            for va in virtual_attributes.keys():
> +                if pa.lower() == va.lower():
> +                    # Take the real name
> +                    pa = va
> +                    break
> +            password_attrs += [pa]
> +
> +        return password_attrs
> +
> +class cmd_user_getpassword(GetPasswordCommand):
> +    """Get the password fields of a user/computer account.
> +
> +This command gets the logon password for a user/computer account.
> +
> +The username specified on the command is the sAMAccountName.
> +The username may also be specified using the --filter option.
> +
> +The command must be run from the root user id or another authorized user id.
> +The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
> +used to adjust the local path. By default tdb:// is used by default.
> +
> +The '--attributes' parameter takes a comma separated list of attributes,
> +which will be printed or given to the script specified by '--script'. If a
> +specified attribute is not available on an object it's silently omitted.
> +All attributes defined in the schema (e.g. the unicodePwd attribute holds
> +the NTHASH) and the following virtual attributes are possible (see --help
> +for which virtual attributes are supported in your environment):
> +
> +   virtualClearTextUTF16: The raw cleartext as stored in the
> +                          'Primary:CLEARTEXT' buffer inside of the
> +                          supplementalCredentials attribute. This typically
> +                          contains valid UTF-16-LE, but may contain random
> +                          bytes, e.g. for computer accounts.
> +
> +   virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
> +                          (only from valid UTF-16-LE)
> +
> +   virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
> +                          checksum, useful for OpenLDAP's '{SSHA}' algorithm.
> +
> +   virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
> +                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> +                          with a $5$... salt, see crypt(3) on modern systems.
> +
> +   virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
> +                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> +                          with a $6$... salt, see crypt(3) on modern systems.
> +
> +Example1:
> +samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
> +
> +Example2:
> +samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
> +
> +"""
> +    def __init__(self):
> +        GetPasswordCommand.__init__(self)
Again, better to use 
    super(cmd_user_getpassword, self).__init__()
here

> +
> +    synopsis = "%prog (<username>|--filter <filter>) [options]"
> +
> +    takes_optiongroups = {
> +        "sambaopts": options.SambaOptions,
> +        "versionopts": options.VersionOptions,
> +    }
> +
> +    takes_options = [
> +        Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
> +               metavar="URL", dest="H"),
> +        Option("--filter", help="LDAP Filter to set password on", type=str),
> +        Option("--attributes", type=str,
> +               help=virtual_attributes_help,
> +               metavar="ATTRIBUTELIST", dest="attributes"),
> +        ]
> +
> +    takes_args = ["username?"]
> +
> +    def run(self, username=None, H=None, filter=None,
> +            attributes=None,
> +            sambaopts=None, versionopts=None):
> +        self.lp = sambaopts.get_loadparm()
> +
> +        if filter is None and username is None:
> +            raise CommandError("Either the username or '--filter' must be specified!")
> +
> +        if filter is None:
> +            filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
> +
> +        if attributes is None:
> +            raise CommandError("Please specify --attributes")
> +
> +        password_attrs = self.parse_attributes(attributes)
> +
> +        samdb = self.connect_system_samdb(url=H, allow_local=True)
> +
> +        obj = self.get_account_attributes(samdb, username,
> +                                          basedn=None,
> +                                          filter=filter,
> +                                          scope=ldb.SCOPE_SUBTREE,
> +                                          attrs=password_attrs)
> +
> +        ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
> +        self.outf.write("%s" % ldif)
> +        self.outf.write("Got password OK\n")
>  
>  class cmd_user(SuperCommand):
>      """User management."""
> @@ -624,3 +1055,4 @@ class cmd_user(SuperCommand):
>      subcommands["setexpiry"] = cmd_user_setexpiry()
>      subcommands["password"] = cmd_user_password()
>      subcommands["setpassword"] = cmd_user_setpassword()
> +    subcommands["getpassword"] = cmd_user_getpassword()
> -- 
> 1.9.1
> 
> 
> From c42135b4cd0393bdf4b922b42db82eead7cc0b3b Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 05/22] python:samba/tests: add simple 'samba-tool user
>  getpassword' test
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/tests/samba_tool/user.py | 24 +++++++++++++++++++++++-
>  1 file changed, 23 insertions(+), 1 deletion(-)
> 
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index 645eb40..2542a73 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -17,9 +17,11 @@
>  
>  import os
>  import time
> +import base64
>  import ldb
>  from samba.tests.samba_tool.base import SambaToolCmdTest
>  from samba import (
> +        credentials,
>          nttime2unix,
>          dsdb
>          )
> @@ -114,13 +116,33 @@ class UserCmdTestCase(SambaToolCmdTest):
>  
>          for user in self.users:
>              newpasswd = self.randomPass()
> +            creds = credentials.Credentials()
> +            creds.set_anonymous()
> +            creds.set_password(newpasswd)
> +            nthash = creds.get_nt_hash()
> +            attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> +            unicodePwd = base64.b64encode(creds.get_nt_hash())
> +
>              (result, out, err) = self.runsubcmd("user", "setpassword",
>                                                  user["name"],
>                                                  "--newpassword=%s" % newpasswd)
> -            # self.assertCmdSuccess(result, "Ensure setpassword runs")
> +            self.assertCmdSuccess(result, "Ensure setpassword runs")
>              self.assertEquals(err,"","setpassword without url")
>              self.assertMatch(out, "Changed password OK", "setpassword without url")
>  
> +            (result, out, err) = self.runsubcmd("user", "getpassword",
> +                                                user["name"],
> +                                                "--attributes=%s" % attributes)
> +            self.assertCmdSuccess(result, "Ensure getpassword runs")
> +            self.assertEqual(err,"","getpassword without url")
> +            self.assertMatch(out, "Got password OK", "getpassword without url")
> +            self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> +                "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> +            self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
> +                    "getpassword unicodePwd: out[%s]" % out)
> +            self.assertMatch(out, "supplementalCredentials:: ",
> +                    "getpassword supplementalCredentials: out[%s]" % out)
> +
>          for user in self.users:
>              newpasswd = self.randomPass()
>              (result, out, err) = self.runsubcmd("user", "setpassword",
> -- 
> 1.9.1
> 
> 
> From deb1cf71b6a8907fd118ef5e9ad6f70a4f7bf654 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:15:38 +0100
> Subject: [PATCH 06/22] docs-xml:samba-tool.8: document "user getpassword"
>  command
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  docs-xml/manpages/samba-tool.8.xml | 5 +++++
>  1 file changed, 5 insertions(+)
> 
> diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
> index 3416ecf..024ffb6 100644
> --- a/docs-xml/manpages/samba-tool.8.xml
> +++ b/docs-xml/manpages/samba-tool.8.xml
> @@ -587,6 +587,11 @@
>  	<para>Sets or resets the password of an user account.</para>
>  </refsect3>
>  
> +<refsect3>
> +	<title>user getpassword <replaceable>username</replaceable> [options]</title>
> +	<para>Gets the password of an user account.</para>
> +</refsect3>
> +
>  <refsect2>
>  	<title>vampire [options] <replaceable>domain</replaceable></title>
>  	<para>Join and synchronise a remote AD domain to the local server.
> -- 
> 1.9.1
> 
> 
> From 41af9a5379c1dde2f142bf6dab94911c4d61890d Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 07/22] samba-tool: add 'user syncpasswords' command
> 
> This provides an easy way to keep passwords in sync with
> another account database, e.g. an OpenLDAP server.
> 
> It provides a functionality like the "passwd program"
> for the "unix password sync" feature of a standalone, member
> and classic (NT4) server, but for an active directory domain
> controller.
> 
> The provided script is called for each account/password related
> change.
> 
> Like the 'user getpassword' command it allows virtual attributes like:
> virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
> 
> Note that this command should just run on a single domain controller
> (typically the PDC-emulator).
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/netcmd/user.py | 688 ++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 688 insertions(+)
> 
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index 04e1a50..0fe7c54 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -23,8 +23,10 @@ import pwd
>  import os
>  import sys
>  import errno
> +import time
>  import base64
>  import binascii
> +from subprocess import call, check_call, Popen, PIPE, STDOUT
>  from getpass import getpass
>  from samba.auth import system_session
>  from samba.samdb import SamDB
> @@ -37,6 +39,7 @@ from samba import (
>      dsdb,
>      gensec,
>      generate_random_password,
> +    Ldb,
>      )
>  from samba.net import Net
>  
> @@ -1042,6 +1045,690 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
>          self.outf.write("%s" % ldif)
>          self.outf.write("Got password OK\n")
>  
> +class cmd_user_syncpasswords(GetPasswordCommand):
> +    """Sync the password of user accounts.
> +
> +This syncs logon passwords for user accounts.
> +
> +Note that this command should run on a single domain controller only
> +(typically the PDC-emulator).
> +
> +The command must be run from the root user id or another authorized user id.
> +The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
> +local path.  By default, ldapi:// is used with the default path to the
> +privileged ldapi socket.
> +
> +This command has three modes: "Cache Initialization", "Sync Loop Run" and
> +"Sync Loop Terminate".
> +
> +
> +Cache Initialization
> +====================
> +
> +The first time, this command needs to be called with
> +'--cache-ldb-initialize' in order to initialize its cache.
> +
> +The cache initialization requires '--attributes' and allows the following
> +optional options: '--script', '--filter' or
> +'-H/--URL'.
> +
> +The '--attributes' parameter takes a comma separated list of attributes,
> +which will be printed or given to the script specified by '--script'. If a
> +specified attribute is not available on an object it will be silently omitted.
> +All attributes defined in the schema (e.g. the unicodePwd attribute holds
> +the NTHASH) and the following virtual attributes are possible (see '--help'
> +for supported virtual attributes in your environment):
> +
> +   virtualClearTextUTF16: The raw cleartext as stored in the
> +                          'Primary:CLEARTEXT' buffer inside of the
> +                          supplementalCredentials attribute. This typically
> +                          contains valid UTF-16-LE, but may contain random
> +                          bytes, e.g. for computer accounts.
> +
> +   virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
> +                          (only from valid UTF-16-LE)
> +
> +   virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
> +                          checksum, useful for OpenLDAP's '{SSHA}' algorithm.
> +
> +   virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
> +                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> +                          with a $5$... salt, see crypt(3) on modern systems.
> +
> +   virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
> +                          checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> +                          with a $6$... salt, see crypt(3) on modern systems.
> +
> +The '--script' option specifies a custom script that is called whenever any
> +of the dirsyncAttributes (see below) was changed. The script is called
> +without any arguments. It gets the LDIF for exactly one object on STDIN.
> +If the script processed the object successfully it has to respond with a
> +single line starting with 'DONE-EXIT: ' followed by an optional message.
> +
> +Note that the script might be called without any password change, e.g. if
> +the account was disabled (an userAccountControl change) or the
> +sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
> +are always returned as unique identifier of the account. It might be useful
> +to also ask for non-password attributes like: objectSid, sAMAccountName,
> +userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
> +Depending on the object, some attributes may not be present/available,
> +but you always get the current state (and not a diff).
> +
> +If no '--script' option is specified, the LDIF will be printed on STDOUT or
> +into the logfile.
> +
> +The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
> +(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
> +    (!(sAMAccountName=krbtgt*)))
> +This means only normal (non-krbtgt) user
> +accounts are monitored.  The '--filter' can modify that, e.g. if it's
> +required to also sync computer accounts.
> +
> +
> +Sync Loop Run
> +=============
> +
> +This (default) mode runs in an endless loop waiting for password related
> +changes in the active directory database. It makes use of the
> +LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
> +get changes in a reliable fashion. Objects are monitored for changes of the
> +following dirsyncAttributes:
> +
> +  unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
> +  userPrincipalName and userAccountControl.
> +
> +It recovers from LDAP disconnects and updates the cache in conservative way
> +(in single steps after each succesfully processed change).  An error from
> +the script (specified by '--script') will result in fatal error and this
> +command will exit.  But the cache state should be still valid and can be
> +resumed in the next "Sync Loop Run".
> +
> +The '--logfile' option specifies an optional (required if '--daemon' is
> +specified) logfile that takes all output of the command. The logfile is
> +automatically reopened if fstat returns st_nlink == 0.
> +
> +The optional '--daemon' option will put the command into the background.
> +
> +You can stop the command without the '--daemon' option, also by hitting
> +strg+c.
> +
> +If you specify the '--no-wait' option the command skips the
> +LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
> +all LDAP_SERVER_DIRSYNC_OID changes are consumed.
> +
> +Sync Loop Terminate
> +===================
> +
> +In order to terminate an already running command (likely as daemon) the
> +'--terminate' option can be used. This also requires the '--logfile' option
> +to be specified.
> +
> +
> +Example1:
> +samba-tool user syncpasswords --cache-ldb-initialize \\
> +    --attributes=virtualClearTextUTF8
> +samba-tool user syncpasswords
> +
> +Example2:
> +samba-tool user syncpasswords --cache-ldb-initialize \\
> +    --attributes=objectGUID,objectSID,sAMAccountName,\\
> +    userPrincipalName,userAccountControl,pwdLastSet,\\
> +    msDS-KeyVersionNumber,virtualCryptSHA512 \\
> +    --script=/path/to/my-custom-syncpasswords-script.py
> +samba-tool user syncpasswords --daemon \\
> +    --logfile=/var/log/samba/user-syncpasswords.log
> +samba-tool user syncpasswords --terminate \\
> +    --logfile=/var/log/samba/user-syncpasswords.log
> +
> +"""
> +    synopsis = "%prog [--cache-ldb-initialize] [options]"
> +
> +    takes_optiongroups = {
> +        "sambaopts": options.SambaOptions,
> +        "versionopts": options.VersionOptions,
> +    }
> +
> +    takes_options = [
> +        Option("--cache-ldb-initialize",
> +               help="Initialize the cache for the first time",
> +               dest="cache_ldb_initialize", action="store_true"),
> +        Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
> +               metavar="CACHE-LDB-PATH", dest="cache_ldb"),
> +        Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
> +               metavar="URL", dest="H"),
> +        Option("--filter", help="optional LDAP filter to set password on", type=str,
> +               metavar="LDAP-SEARCH-FILTER", dest="filter"),
> +        Option("--attributes", type=str,
> +               help=virtual_attributes_help,
> +               metavar="ATTRIBUTELIST", dest="attributes"),
> +        Option("--script", help="Script that is called for each password change", type=str,
> +               metavar="/path/to/syncpasswords.script", dest="script"),
> +        Option("--no-wait", help="Don't block waiting for changes",
> +               action="store_true", default=False, dest="nowait"),
> +        Option("--logfile", type=str,
> +               help="The logfile to use (required in --daemon mode).",
> +               metavar="/path/to/syncpasswords.log", dest="logfile"),
> +        Option("--daemon", help="daemonize after initial setup",
> +               action="store_true", default=False, dest="daemon"),
> +        Option("--terminate",
> +               help="Send a SIGTERM to an already running (daemon) process",
> +               action="store_true", default=False, dest="terminate"),
> +        ]
> +
> +    def run(self, cache_ldb_initialize=False, cache_ldb=None,
> +            H=None, filter=None,
> +            attributes=None,
> +            script=None, nowait=None, logfile=None, daemon=None, terminate=None,
> +            sambaopts=None, versionopts=None):
> +
> +        self.lp = sambaopts.get_loadparm()
> +        self.logfile = None
> +        self.samdb_url = None
> +        self.samdb = None
> +        self.cache = None
> +
> +        if not cache_ldb_initialize:
> +            if attributes is not None:
> +                raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
> +            if script is not None:
> +                raise CommandError("--script is only allowed together with --cache-ldb-initialize")
> +            if filter is not None:
> +                raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
> +            if H is not None:
> +                raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
> +        else:
> +            if nowait is not False:
> +                raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
> +            if logfile is not None:
> +                raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
> +            if daemon is not False:
> +                raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
> +            if terminate is not False:
> +                raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
> +
> +        if nowait is True:
> +            if daemon is True:
> +                raise CommandError("--daemon is not allowed together with --no-wait")
> +            if terminate is not False:
> +                raise CommandError("--terminate is not allowed together with --no-wait")
> +
> +        if terminate is True and daemon is True:
> +            raise CommandError("--terminate is not allowed together with --daemon")
> +
> +        if daemon is True and logfile is None:
> +            raise CommandError("--daemon is only allowed together with --logfile")
> +
> +        if terminate is True and logfile is None:
> +            raise CommandError("--terminate is only allowed together with --logfile")
> +
> +        if script is not None:
> +            if not os.path.exists(script):
> +                raise CommandError("script[%s] does not exist!" % script)
> +
> +            sync_command = "%s" % os.path.abspath(script)
> +        else:
> +            sync_command = None
> +
> +        dirsync_filter = filter
> +        if dirsync_filter is None:
> +            dirsync_filter = "(&" + \
> +                               "(objectClass=user)" + \
> +                               "(userAccountControl:%s:=%u)" % (
> +                                ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
> +                               "(!(sAMAccountName=krbtgt*))" + \
> +                             ")"
> +
> +        dirsync_secret_attrs = [
> +            "unicodePwd",
> +            "dBCSPwd",
> +            "supplementalCredentials",
> +        ]
> +
> +        dirsync_attrs = dirsync_secret_attrs + [
> +            "pwdLastSet",
> +            "sAMAccountName",
> +            "userPrincipalName",
> +            "userAccountControl",
> +            "isDeleted",
> +            "isRecycled",
> +        ]
> +
> +        password_attrs = None
> +
> +        if cache_ldb_initialize:
> +            if H is None:
> +                H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
> +
> +            password_attrs = self.parse_attributes(attributes)
> +            lower_attrs = [x.lower() for x in password_attrs]
> +            # We always return these in order to track deletions
> +            for a in ["objectGUID", "isDeleted", "isRecycled"]:
> +                if a.lower() not in lower_attrs:
> +                    password_attrs += [a]
> +
> +        if cache_ldb is not None:
> +            if cache_ldb.lower().startswith("ldapi://"):
> +                raise CommandError("--cache_ldb ldapi:// is not supported")
> +            elif cache_ldb.lower().startswith("ldap://"):
> +                raise CommandError("--cache_ldb ldap:// is not supported")
> +            elif cache_ldb.lower().startswith("ldaps://"):
> +                raise CommandError("--cache_ldb ldaps:// is not supported")
> +            elif cache_ldb.lower().startswith("tdb://"):
> +                pass
> +            else:
> +                if not os.path.exists(cache_ldb):
> +                    cache_ldb = self.lp.private_path(cache_ldb)
> +        else:
> +            cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
> +
> +        def log_msg(msg):
> +            if self.logfile is not None:
> +                info = os.fstat(0)
> +                if info.st_nlink == 0:
> +                    logfile = self.logfile
> +                    self.logfile = None
> +                    log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
> +                    logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
> +                    os.dup2(logfd, 0)
> +                    os.dup2(logfd, 1)
> +                    os.dup2(logfd, 2)
> +                    os.close(logfd)
> +                    log_msg("Reopened logfile[%s]\n" % (logfile))
> +                    self.logfile = logfile
> +            msg = "%s: pid[%d]: %s" % (
> +                    time.ctime(),
> +                    os.getpid(),
> +                    msg)
> +            self.outf.write(msg)
> +            return
> +
> +        def load_cache():
> +            cache_attrs = [
> +                "samdbUrl",
> +                "dirsyncFilter",
> +                "dirsyncAttribute",
> +                "dirsyncControl",
> +                "passwordAttribute",
> +                "syncCommand",
> +                "currentPid",
> +            ]
> +
> +            self.cache = Ldb(cache_ldb)
> +            self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
> +            res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
> +                                    attrs=cache_attrs)
> +            if len(res) == 1:
> +                try:
> +                    self.samdb_url = res[0]["samdbUrl"][0]
> +                except KeyError as e:
> +                    self.samdb_url = None
> +            else:
> +                self.samdb_url = None
> +            if self.samdb_url is None and not cache_ldb_initialize:
> +                raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
> +                                   cache_ldb))
> +            if self.samdb_url is not None and cache_ldb_initialize:
> +                raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
> +                                   cache_ldb))
> +            if self.samdb_url is None:
> +                self.samdb_url = H
> +                self.dirsync_filter = dirsync_filter
> +                self.dirsync_attrs = dirsync_attrs
> +                self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
> +                self.password_attrs = password_attrs
> +                self.sync_command = sync_command
> +                add_ldif  = "dn: %s\n" % self.cache_dn
> +                add_ldif += "objectClass: userSyncPasswords\n"
> +                add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
> +                add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
> +                for a in self.dirsync_attrs:
> +                    add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
> +                add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
> +                for a in self.password_attrs:
> +                    add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
> +                if self.sync_command is not None:
> +                    add_ldif += "syncCommand: %s\n" % self.sync_command
> +                add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> +                self.cache.add_ldif(add_ldif)
> +                self.current_pid = None
> +                self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
> +                msgs = self.cache.parse_ldif(add_ldif)
> +                changetype,msg = msgs.next()
> +                ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
> +                self.outf.write("%s" % ldif)
> +            else:
> +                self.dirsync_filter = res[0]["dirsyncFilter"][0]
> +                self.dirsync_attrs = []
> +                for a in res[0]["dirsyncAttribute"]:
> +                    self.dirsync_attrs.append(a)
> +                self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
> +                self.password_attrs = []
> +                for a in res[0]["passwordAttribute"]:
> +                    self.password_attrs.append(a)
> +                if "syncCommand" in res[0]:
> +                    self.sync_command = res[0]["syncCommand"][0]
> +                else:
> +                    self.sync_command = None
> +                if "currentPid" in res[0]:
> +                    self.current_pid = int(res[0]["currentPid"][0])
> +                else:
> +                    self.current_pid = None
> +                log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
> +
> +            return
> +
> +        def run_sync_command(dn, ldif):
> +            log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command))
> +            sync_command_p = Popen(self.sync_command,
> +                                   stdin=PIPE,
> +                                   stdout=PIPE,
> +                                   stderr=STDOUT)
> +
> +            res = sync_command_p.poll()
> +            assert res is None
> +
> +            input = "%s" % (ldif)
> +            reply = sync_command_p.communicate(input)[0]
> +            log_msg("%s\n" % (reply))
> +            res = sync_command_p.poll()
> +            if res is None:
> +                sync_command_p.terminate()
> +            res = sync_command_p.wait()
> +
> +            if reply.startswith("DONE-EXIT: "):
> +                return
> +
> +            log_msg("RESULT: %s\n" % (res))
> +            raise Exception("ERROR: %s - %s\n" % (res, reply))
> +
> +        def handle_object(idx, dirsync_obj):
> +            binary_guid = dirsync_obj.dn.get_extended_component("GUID")
> +            guid = ndr_unpack(misc.GUID, binary_guid)
> +            binary_sid = dirsync_obj.dn.get_extended_component("SID")
> +            sid = ndr_unpack(security.dom_sid, binary_sid)
> +            domain_sid, rid = sid.split()
> +            if rid == security.DOMAIN_RID_KRBTGT:
> +                log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
> +                return
> +            for a in list(dirsync_obj.keys()):
For iteration over keys it is better to use iterator objects which don't
allocate the whole list at the same time:

   for a in dirsync_obj:
       ....

> +                for h in dirsync_secret_attrs:
> +                    if a.lower() == h.lower():
> +                        del dirsync_obj[a]
> +                        dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
> +            dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
> +            log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
> +            obj = self.get_account_attributes(self.samdb,
> +                                              username="%s" % sid,
> +                                              basedn="<GUID=%s>" % guid,
> +                                              filter="(objectClass=user)",
> +                                              scope=ldb.SCOPE_BASE,
> +                                              attrs=self.password_attrs)
> +            ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
> +            log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
> +            if self.sync_command is None:
> +                self.outf.write("%s" % (ldif))
> +                return
> +            self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
> +            run_sync_command(obj.dn, ldif)
> +
> +        def check_current_pid_conflict(pid):
> +            if pid is None:
> +                return False
> +
> +            mypid = os.getpid()
> +            if pid == mypid:
> +                return False
> +
> +            try:
> +                os.kill(pid, 0)
> +            except OSError as (num, msg):
> +                if num != errno.ESRCH:
> +                    raise
> +                return False
> +
> +            p = Popen("grep -q 'syncpasswords' /proc/%d/cmdline" % pid, shell=True)
> +            res = p.wait()
> +            if res != 0:
> +                return False
> +            return True
> +
> +        def update_pid(pid):
> +            self.current_pid = pid
> +            if self.current_pid is not None:
> +                log_msg("currentPid: %d\n" % self.current_pid)
> +
> +            modify_ldif =  "dn: %s\n" % (self.cache_dn)
> +            modify_ldif += "changetype: modify\n"
> +            modify_ldif += "replace: currentPid\n"
> +            if self.current_pid is not None:
> +                modify_ldif += "currentPid: %d\n" % (self.current_pid)
> +            modify_ldif += "replace: currentTime\n"
> +            modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> +            self.cache.modify_ldif(modify_ldif)
> +            return
> +
> +        def update_cache(res_controls):
> +            assert len(res_controls) > 0
> +            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
> +            res_controls[0].critical = True
> +            self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
> +            log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
> +
> +            modify_ldif =  "dn: %s\n" % (self.cache_dn)
> +            modify_ldif += "changetype: modify\n"
> +            modify_ldif += "replace: dirsyncControl\n"
> +            modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
> +            modify_ldif += "replace: currentTime\n"
> +            modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> +            self.cache.modify_ldif(modify_ldif)
> +            return
> +
> +        def check_object(dirsync_obj, res_controls):
> +            assert len(res_controls) > 0
> +            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
> +
> +            binary_sid = dirsync_obj.dn.get_extended_component("SID")
> +            sid = ndr_unpack(security.dom_sid, binary_sid)
> +            dn = "KEY=%s" % sid
> +            lastCookie = str(res_controls[0])
> +
> +            res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
> +                                    expression="(lastCookie=%s)" % (
> +                                        ldb.binary_encode(lastCookie)),
> +                                    attrs=[])
> +            if len(res) == 1:
> +                return True
> +            return False
> +
> +        def update_object(dirsync_obj, res_controls):
> +            assert len(res_controls) > 0
> +            assert res_controls[0].oid == "1.2.840.113556.1.4.841"
> +
> +            binary_sid = dirsync_obj.dn.get_extended_component("SID")
> +            sid = ndr_unpack(security.dom_sid, binary_sid)
> +            dn = "KEY=%s" % sid
> +            lastCookie = str(res_controls[0])
> +
> +            self.cache.transaction_start()
> +            try:
> +                res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
> +                                        expression="(objectClass=*)",
> +                                        attrs=["lastCookie"])
> +                if len(res) == 0:
> +                    add_ldif  = "dn: %s\n" % (dn)
> +                    add_ldif += "objectClass: userCookie\n"
> +                    add_ldif += "lastCookie: %s\n" % (lastCookie)
> +                    add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> +                    self.cache.add_ldif(add_ldif)
> +                else:
> +                    modify_ldif =  "dn: %s\n" % (dn)
> +                    modify_ldif += "changetype: modify\n"
> +                    modify_ldif += "replace: lastCookie\n"
> +                    modify_ldif += "lastCookie: %s\n" % (lastCookie)
> +                    modify_ldif += "replace: currentTime\n"
> +                    modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> +                    self.cache.modify_ldif(modify_ldif)
> +                self.cache.transaction_commit()
> +            except Exception as e:
> +                self.cache.transaction_cancel()
> +
> +            return
> +
> +        def dirsync_loop():
> +            while True:
> +                res = self.samdb.search(expression=self.dirsync_filter,
> +                                        scope=ldb.SCOPE_SUBTREE,
> +                                        attrs=self.dirsync_attrs,
> +                                        controls=self.dirsync_controls)
> +                log_msg("dirsync_loop(): results %d\n" % len(res))
> +                ri = 0
> +                for r in res:
> +                    done = check_object(r, res.controls)
> +                    if not done:
> +                        handle_object(ri, r)
> +                        update_object(r, res.controls)
> +                    ri += 1
> +                update_cache(res.controls)
> +                if len(res) == 0:
> +                    break
> +
> +        def sync_loop(wait):
> +            notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
> +            notify_controls = ["notification:1"]
> +            notify_handle = self.samdb.search_iterator(expression="objectClass=*",
> +                                                       scope=ldb.SCOPE_SUBTREE,
> +                                                       attrs=notify_attrs,
> +                                                       controls=notify_controls,
> +                                                       timeout=-1)
> +
> +            if wait is True:
> +                log_msg("Resuming monitoring\n")
> +            else:
> +                log_msg("Getting changes\n")
> +            self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
> +            self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
> +            self.outf.write("syncCommand: %s\n" % self.sync_command)
> +            dirsync_loop()
> +
> +            if wait is not True:
> +                return
> +
> +            for msg in notify_handle:
> +                if not isinstance(msg, ldb.Message):
> +                    self.outf.write("referal: %s\n" % msg)
> +                    continue
> +                created = msg.get("uSNCreated")[0]
> +                changed = msg.get("uSNChanged")[0]
> +                log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
> +                        (msg.dn, created, changed))
> +
> +                dirsync_loop()
> +
> +            res = notify_handle.result()
> +
> +        def daemonize():
> +            self.samdb = None
> +            self.cache = None
> +            orig_pid = os.getpid()
> +            pid = os.fork()
> +            if pid == 0:
> +                os.setsid()
> +                pid = os.fork()
> +                if pid == 0: # Actual daemon
> +                    pid = os.getpid()
> +                    log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
> +                    load_cache()
> +                    update_pid(os.getpid())
> +                    return
> +            os._exit(0)
> +
> +        if cache_ldb_initialize:
> +            self.samdb_url = H
> +            self.samdb = self.connect_system_samdb(url=self.samdb_url,
> +                                                   verbose=True)
> +            load_cache()
> +            return
> +
> +        if logfile is not None:
> +            import resource      # Resource usage information.
> +            maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
> +            if maxfd == resource.RLIM_INFINITY:
> +                maxfd = 1024 # Rough guess at maximum number of open file descriptors.
> +            logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
> +            self.outf.write("Using logfile[%s]\n" % logfile)
> +            for fd in range(0, maxfd):
> +                if fd == logfd:
> +                    continue
> +                try:
> +                    os.close(fd)
> +                except OSError:
> +                    pass
> +            os.dup2(logfd, 0)
> +            os.dup2(logfd, 1)
> +            os.dup2(logfd, 2)
> +            os.close(logfd)
> +            log_msg("Attached to logfile[%s]\n" % (logfile))
> +            self.logfile = logfile
> +
> +        load_cache()
> +        conflict = check_current_pid_conflict(self.current_pid)
> +        if terminate:
> +            if self.current_pid is None:
> +                log_msg("No process running.\n")
> +                return
> +            if not conflict:
> +                log_msg("Proccess %d is not running anymore.\n" % (
> +                        self.current_pid))
> +                update_pid(None)
> +                return
> +            log_msg("Sending SIGTERM to proccess %d.\n" % (
> +                    self.current_pid))
> +            os.kill(self.current_pid, signal.SIGTERM)
> +            return
> +        if conflict:
> +            raise CommandError("Exiting pid %d, command is already running as pid %d" % (
> +                               os.getpid(), self.current_pid))
> +        update_pid(os.getpid())
> +        if daemon is True:
> +            daemonize()
> +
> +        wait = True
> +        while wait is True:
> +            retry_sleep_min = 1
> +            retry_sleep_max = 600
> +            if nowait is True:
> +                wait = False
> +                retry_sleep = 0
> +            else:
> +                retry_sleep = retry_sleep_min
> +
> +            while self.samdb is None:
> +                if retry_sleep != 0:
> +                    log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
> +                    time.sleep(retry_sleep)
> +                retry_sleep = retry_sleep * 2
> +                if retry_sleep >= retry_sleep_max:
> +                    retry_sleep = retry_sleep_max
> +                log_msg("Connecting to '%s'\n" % self.samdb_url)
> +                try:
> +                    self.samdb = self.connect_system_samdb(url=self.samdb_url)
> +                except Exception as msg:
> +                    self.samdb = None
> +                    log_msg("Connect to samdb Exception => (%s)\n" % msg)
> +                    if wait is not True:
> +                        raise
> +                    pass
> +
> +            try:
> +                sync_loop(wait)
> +            except ldb.LdbError as (enum, estr):
> +                self.samdb = None
> +                log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
> +                pass
> +
> +        # not reached
> +        return
> +
>  class cmd_user(SuperCommand):
>      """User management."""
>  
> @@ -1056,3 +1743,4 @@ class cmd_user(SuperCommand):
>      subcommands["password"] = cmd_user_password()
>      subcommands["setpassword"] = cmd_user_setpassword()
>      subcommands["getpassword"] = cmd_user_getpassword()
> +    subcommands["syncpasswords"] = cmd_user_syncpasswords()
> -- 
> 1.9.1
> 
> 
> From 941197011381493a604ecff1b4f77dd8ad338404 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 08/22] python:samba/tests: add simple 'samba-tool user
>  syncpasswords' test
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/tests/samba_tool/user.py | 46 ++++++++++++++++++++++++++++++++++-
>  1 file changed, 45 insertions(+), 1 deletion(-)
> 
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index 2542a73..aad323d 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -114,13 +114,41 @@ class UserCmdTestCase(SambaToolCmdTest):
>              self.assertEquals(err,"","setpassword with url")
>              self.assertMatch(out, "Changed password OK", "setpassword with url")
>  
> +        attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> +        (result, out, err) = self.runsubcmd("user", "syncpasswords",
> +                                            "--cache-ldb-initialize",
> +                                            "--attributes=%s" % attributes)
> +        self.assertCmdSuccess(result, "Ensure syncpasswords --cache-ldb-initialize runs")
> +        self.assertEqual(err,"","getpassword without url")
> +        cache_attrs = {
> +            "objectClass": { "value": "userSyncPasswords" },
> +            "samdbUrl": { },
> +            "dirsyncFilter": { },
> +            "dirsyncAttribute": { },
> +            "dirsyncControl": { "value": "dirsync:1:0:0"},
> +            "passwordAttribute": { },
> +            "currentTime": { },
> +        }
> +        for a in cache_attrs.keys():
> +            v = cache_attrs[a].get("value", "")
> +            self.assertMatch(out, "%s: %s" % (a, v),
> +                "syncpasswords --cache-ldb-initialize: %s: %s out[%s]" % (a, v, out))
> +
> +        (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
> +        self.assertCmdSuccess(result, "Ensure syncpasswords --no-wait runs")
> +        self.assertEqual(err,"","syncpasswords --no-wait")
> +        self.assertMatch(out, "dirsync_loop(): results 0",
> +            "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
> +        for user in self.users:
> +            self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> +                "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> +
>          for user in self.users:
>              newpasswd = self.randomPass()
>              creds = credentials.Credentials()
>              creds.set_anonymous()
>              creds.set_password(newpasswd)
>              nthash = creds.get_nt_hash()
> -            attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
>              unicodePwd = base64.b64encode(creds.get_nt_hash())
>  
>              (result, out, err) = self.runsubcmd("user", "setpassword",
> @@ -130,6 +158,22 @@ class UserCmdTestCase(SambaToolCmdTest):
>              self.assertEquals(err,"","setpassword without url")
>              self.assertMatch(out, "Changed password OK", "setpassword without url")
>  
> +            (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
> +            self.assertCmdSuccess(result, "Ensure syncpasswords --no-wait runs")
> +            self.assertEqual(err,"","syncpasswords --no-wait")
> +            self.assertMatch(out, "dirsync_loop(): results 0",
> +                "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
> +            self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> +                "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> +            self.assertMatch(out, "# unicodePwd::: REDACTED SECRET ATTRIBUTE",
> +                    "getpassword '# unicodePwd::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
> +            self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
> +                    "getpassword unicodePwd: out[%s]" % out)
> +            self.assertMatch(out, "# supplementalCredentials::: REDACTED SECRET ATTRIBUTE",
> +                    "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
> +            self.assertMatch(out, "supplementalCredentials:: ",
> +                    "getpassword supplementalCredentials: out[%s]" % out)
> +
>              (result, out, err) = self.runsubcmd("user", "getpassword",
>                                                  user["name"],
>                                                  "--attributes=%s" % attributes)
> -- 
> 1.9.1
> 
> 
> From cb6f2a7d60f6b9ba074f56131c667889874648ac Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:15:38 +0100
> Subject: [PATCH 09/22] docs-xml:samba-tool.8: document "user syncpasswords"
>  command
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  docs-xml/manpages/samba-tool.8.xml | 7 +++++++
>  1 file changed, 7 insertions(+)
> 
> diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
> index 024ffb6..dea984f 100644
> --- a/docs-xml/manpages/samba-tool.8.xml
> +++ b/docs-xml/manpages/samba-tool.8.xml
> @@ -592,6 +592,13 @@
>  	<para>Gets the password of an user account.</para>
>  </refsect3>
>  
> +<refsect3>
> +	<title>user syncpasswords <replaceable>--cache-ldb-initialize</replaceable> [options]</title>
> +	<para>Syncs the passwords of all user accounts, using an optional script.</para>
> +	<para>Note that this command should run on a single domain controller only
> +	(typically the PDC-emulator).</para>
> +</refsect3>
> +
>  <refsect2>
>  	<title>vampire [options] <replaceable>domain</replaceable></title>
>  	<para>Join and synchronise a remote AD domain to the local server.
> -- 
> 1.9.1
> 
> 
> From 97b8b98ed8faf202f94ba4685ed11391b4bf7ed8 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:56:03 +0100
> Subject: [PATCH 10/22] docs-xml/smbdotconf: reference "unix password sync"
>  with "samba-tool user syncpasswords"
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  docs-xml/smbdotconf/security/unixpasswordsync.xml | 8 ++++++--
>  1 file changed, 6 insertions(+), 2 deletions(-)
> 
> diff --git a/docs-xml/smbdotconf/security/unixpasswordsync.xml b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> index 321ece5..75c8916 100644
> --- a/docs-xml/smbdotconf/security/unixpasswordsync.xml
> +++ b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> @@ -9,8 +9,12 @@
>      If this is set to <constant>yes</constant> the program specified in the <parameter moreinfo="none">passwd
>      program</parameter> parameter is called <emphasis>AS ROOT</emphasis> -
>      to allow the new UNIX password to be set without access to the 
> -    old UNIX password (as the SMB password change code has no 
> -	access to the old password cleartext, only the new).</para>
> +    old UNIX password (as the SMB password change code has no
> +    access to the old password cleartext, only the new).</para>
> +
> +    <para>This option has no effect if <command moreinfo="none">samba</command>
> +    is running as an active directory domain controller, in that case have a
> +    look at the <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
>  </description>
>  
>  <related>passwd program</related>
> -- 
> 1.9.1
> 
> 
> From 3273fc157363a73d58e93992abb730843775391c Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 07:01:18 +0100
> Subject: [PATCH 11/22] .travis.yml: install libgpgme11-dev python[3]-gpgme
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  .travis.yml | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/.travis.yml b/.travis.yml
> index b930cfe..6c6934e 100644
> --- a/.travis.yml
> +++ b/.travis.yml
> @@ -24,7 +24,7 @@ matrix:
>  
>  before_install:
>   - sudo apt-get update -qq
> - - sudo apt-get install --assume-yes acl attr autoconf bison build-essential debhelper dnsutils docbook-xml docbook-xsl flex gdb git krb5-user libacl1-dev libaio-dev libattr1-dev libblkid-dev libbsd-dev libcap-dev libcups2-dev libgnutls-dev libldap2-dev libncurses5-dev libpam0g-dev libparse-yapp-perl libpopt-dev libreadline-dev perl perl-modules pkg-config python-crypto python-dev python-dnspython python3-crypto python3-dev python3-dnspython realpath screen xsltproc zlib1g-dev
> + - sudo apt-get install --assume-yes acl attr autoconf bison build-essential debhelper dnsutils docbook-xml docbook-xsl flex gdb git krb5-user libacl1-dev libaio-dev libattr1-dev libblkid-dev libbsd-dev libcap-dev libcups2-dev libgnutls-dev libgpgme11-dev libldap2-dev libncurses5-dev libpam0g-dev libparse-yapp-perl libpopt-dev libreadline-dev perl perl-modules pkg-config python-crypto python-dev python-dnspython python-gpgme python3-crypto python3-dev python3-dnspython python3-gpgme realpath screen xsltproc zlib1g-dev
>  
>  script:
>   - git fetch --unshallow
> -- 
> 1.9.1
> 
> 
> From 0127066d0f34737d1752e2ad7811ff55fc144b76 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:10:54 +0100
> Subject: [PATCH 12/22] docs-xml/smbdotconf: add "password hash gpg key ids"
>  option
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  .../smbdotconf/security/passwordhashgpgkeyids.xml  | 45 ++++++++++++++++++++++
>  1 file changed, 45 insertions(+)
>  create mode 100644 docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
> 
> diff --git a/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml b/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
> new file mode 100644
> index 0000000..48fbc79
> --- /dev/null
> +++ b/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
> @@ -0,0 +1,45 @@
> +<samba:parameter name="password hash gpg key ids"
> +                 context="G"
> +                 type="cmdlist"
> +                 xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
> +<description>
> +	<para>If <command moreinfo="none">samba</command> is running as an
> +	active directory domain controller, it is possible to store the
> +	cleartext password of accounts in a PGP/OpenGPG encrypted form.</para>
> +
> +	<para>You can specify one or more recipients by key id or user id.</para>
> +
> +	<para>The value is stored as 'Primary:SambaGPG' in the
> +	<command moreinfo="none">supplementalCredentials</command> attribute.</para>
> +
> +	<para>As password changes can occur on any domain controller,
> +	you should configure this on each of them. Note that this feature is currently
> +	available only on Samba domain controllers.</para>
> +
> +	<para>This option is only available if <command moreinfo="none">samba</command>
> +	was compiled with <command moreinfo="none">gpgme</command> support.</para>
> +
> +	<para>You may need to export the <command moreinfo="none">GNUPGHOME</command>
> +	environment variable before starting <command moreinfo="none">samba</command>.
> +	<emphasis>It is strongly recommended to only store the public key in this
> +	location. The private key is not used for encryption and should be
> +	only stored where decryption is required.</emphasis></para>
> +
> +	<para>Being able to restore the cleartext password helps, when they need to be imported
> +	into other authentication systems later (see <command moreinfo="none">samba-tool user getpassword</command>)
> +	or you want to keep the passwords in sync with another system, e.g. an OpenLDAP server
> +	(see <command moreinfo="none">samba-tool user syncpasswords</command>).</para>
> +
> +	<para>While this option needs to be configured on all domain controllers, the
> +	<command moreinfo="none">samba-tool user syncpasswords</command> command should
> +	run on a single domain controller only (typically the PDC-emulator).</para>
> +</description>
> +
> +<related>unix password sync</related>
> +
> +<value type="default"></value>
> +<value type="example">01FAB41A</value>
> +<value type="example">4952E40301FAB41A</value>
> +<value type="example">selftest at samba.example.com</value>
> +<value type="example">selftest at samba.example.com, 4952E40301FAB41A</value>
> +</samba:parameter>
> -- 
> 1.9.1
> 
> 
> From 270ef6c74bd9929f31f1d8eb8b3485197f305101 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:56:03 +0100
> Subject: [PATCH 13/22] docs-xml/smbdotconf: reference "unix password sync"
>  with "password hash gpg key ids"
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  docs-xml/smbdotconf/security/unixpasswordsync.xml | 4 +++-
>  1 file changed, 3 insertions(+), 1 deletion(-)
> 
> diff --git a/docs-xml/smbdotconf/security/unixpasswordsync.xml b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> index 75c8916..89b0158 100644
> --- a/docs-xml/smbdotconf/security/unixpasswordsync.xml
> +++ b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> @@ -14,11 +14,13 @@
>  
>      <para>This option has no effect if <command moreinfo="none">samba</command>
>      is running as an active directory domain controller, in that case have a
> -    look at the <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
> +    look at the <smbconfoption name="password hash gpg key ids"/> option and the
> +    <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
>  </description>
>  
>  <related>passwd program</related>
>  <related>passwd chat</related>
> +<related>password hash gpg key ids</related>
>  
>  <value type="default">no</value>
>  </samba:parameter>
> -- 
> 1.9.1
> 
> 
> From 3ff64ac992474ee31425b4735d713be3f4037249 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 14/22] s4:dsdb/samdb: add configure checks for libgpgme
> 
> This will be used to store the cleartext utf16 password
> GPG encrypted as 'Primary:SambaGPG' in the
> supplementalCredentials attribute.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  source4/dsdb/samdb/ldb_modules/wscript | 28 ++++++++++++++++++++++++++++
>  wscript                                |  2 ++
>  2 files changed, 30 insertions(+)
>  create mode 100644 source4/dsdb/samdb/ldb_modules/wscript
> 
> diff --git a/source4/dsdb/samdb/ldb_modules/wscript b/source4/dsdb/samdb/ldb_modules/wscript
> new file mode 100644
> index 0000000..2aed0a0
> --- /dev/null
> +++ b/source4/dsdb/samdb/ldb_modules/wscript
> @@ -0,0 +1,28 @@
> +
> +import Logs, Options, sys
> +import samba3
> +
> +def set_options(opt):
> +    opt.SAMBA3_ADD_OPTION('gpgme', default=None)
> +
> +    return
> +
> +def configure(conf):
> +    conf.SET_TARGET_TYPE('gpgme', 'EMPTY')
> +
> +    if Options.options.with_gpgme != False:
> +        conf.find_program('gpgme-config', var='GPGME_CONFIG')
> +
> +        if conf.env.GPGME_CONFIG:
> +            conf.CHECK_CFG(path=conf.env.GPGME_CONFIG, args="--cflags --libs",
> +                           package="", uselib_store="gpgme",
> +                           msg='Checking for gpgme support')
> +
> +        if conf.CHECK_FUNCS_IN('gpgme_new', 'gpgme', headers='gpgme.h'):
> +            conf.DEFINE('ENABLE_GPGME', '1')
> +
> +        if not conf.CONFIG_SET('ENABLE_GPGME'):
> +            if Options.options.with_gpgme == True:
> +                conf.fatal('GPGME support requested, but no suitable GPGME library found')
> +            else:
> +                Logs.warn('no suitable GPGME library found')
> diff --git a/wscript b/wscript
> index 41ed5da..ef6eb7a 100644
> --- a/wscript
> +++ b/wscript
> @@ -39,6 +39,7 @@ def set_options(opt):
>      opt.RECURSE('lib/ldb')
>      opt.RECURSE('selftest')
>      opt.RECURSE('source4/lib/tls')
> +    opt.RECURSE('source4/dsdb/samdb/ldb_modules')
>      opt.RECURSE('pidl')
>      opt.RECURSE('source3')
>      opt.RECURSE('lib/util')
> @@ -149,6 +150,7 @@ def configure(conf):
>      if conf.CONFIG_GET('KRB5_VENDOR') in (None, 'heimdal'):
>          conf.RECURSE('source4/heimdal_build')
>      conf.RECURSE('source4/lib/tls')
> +    conf.RECURSE('source4/dsdb/samdb/ldb_modules')
>      conf.RECURSE('source4/ntvfs/sysdep')
>      conf.RECURSE('lib/util')
>      conf.RECURSE('lib/util/charset')
> -- 
> 1.9.1
> 
> 
> From aa6956ddea7a74f9eab2efec9680755cda57d9a9 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 15/22] drsblobs.idl: add package_PrimarySambaGPGBlob
> 
> This will be used to store the cleartext utf16 password
> GPG encrypted in the supplementalCredentials attribute.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  librpc/idl/drsblobs.idl | 8 ++++++++
>  1 file changed, 8 insertions(+)
> 
> diff --git a/librpc/idl/drsblobs.idl b/librpc/idl/drsblobs.idl
> index 499febb..0f421a0 100644
> --- a/librpc/idl/drsblobs.idl
> +++ b/librpc/idl/drsblobs.idl
> @@ -445,6 +445,14 @@ interface drsblobs {
>  		[in] package_PrimaryWDigestBlob blob
>  		);
>  
> +	typedef [public] struct {
> +		[flag(NDR_REMAINING)] DATA_BLOB gpg_blob;
> +	} package_PrimarySambaGPGBlob;
> +
> +	void decode_PrimarySambaGPG(
> +		[in] package_PrimarySambaGPGBlob blob
> +		);
> +
>  	typedef struct {
>  		[value(0)] uint32 size;
>  	} AuthInfoNone;
> -- 
> 1.9.1
> 
> 
> From 47e26c134dc77f4da4dd24c420af3ae11c05f50b Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 16/22] s4:dsdb/samdb: optionally store
>  package_PrimarySambaGPGBlob in supplementalCredentials
> 
> It's important that Primary:SambaGPG is added as the last element.
> This is the indication that it matches the current password.
> When a password change happens on a Windows DC,
> it will keep the old Primary:SambaGPG value, but as the first element.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  source4/dsdb/samdb/ldb_modules/password_hash.c     | 226 ++++++++++++++++++++-
>  .../dsdb/samdb/ldb_modules/wscript_build_server    |   2 +-
>  2 files changed, 221 insertions(+), 7 deletions(-)
> 
> diff --git a/source4/dsdb/samdb/ldb_modules/password_hash.c b/source4/dsdb/samdb/ldb_modules/password_hash.c
> index 05b0854..14dc268 100644
> --- a/source4/dsdb/samdb/ldb_modules/password_hash.c
> +++ b/source4/dsdb/samdb/ldb_modules/password_hash.c
> @@ -45,6 +45,11 @@
>  #include "param/param.h"
>  #include "lib/krb5_wrap/krb5_samba.h"
>  
> +#ifdef ENABLE_GPGME
> +#undef class
> +#include <gpgme.h>
> +#endif
> +
>  /* If we have decided there is a reason to work on this request, then
>   * setup all the password hash types correctly.
>   *
> @@ -97,6 +102,8 @@ struct ph_context {
>  	bool hash_values;
>  	bool userPassword;
>  	bool pwd_last_set_bypass;
> +
> +	const char **gpg_key_ids;
>  };
>  
>  
> @@ -1393,17 +1400,137 @@ static int setup_primary_wdigest(struct setup_password_fields_io *io,
>  	return LDB_SUCCESS;
>  }
>  
> +static int setup_primary_samba_gpg(struct setup_password_fields_io *io,
> +				   struct package_PrimarySambaGPGBlob *pgb)
> +{
> +#ifdef ENABLE_GPGME
> +	struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
> +	gpgme_error_t gret;
> +	gpgme_ctx_t ctx = NULL;
> +	size_t num_keys = str_list_length(io->ac->gpg_key_ids);
> +	gpgme_key_t keys[num_keys+1];
> +	size_t ki = 0;
> +	size_t kr = 0;
> +	gpgme_data_t plain_data = NULL;
> +	gpgme_data_t crypt_data = NULL;
> +	size_t crypt_length = 0;
> +	char *crypt_mem = NULL;
> +
> +	gret = gpgme_new(&ctx);
> +	if (gret != GPG_ERR_NO_ERROR) {
> +		ldb_debug(ldb, LDB_DEBUG_ERROR,
> +			  "%s:%s: gret[%u] %s\n",
> +			  __location__, __func__,
> +			  gret, gpgme_strerror(gret));
> +		return ldb_module_operr(io->ac->module);
> +	}
> +
> +	gpgme_set_armor(ctx, 1);
> +
> +	gret = gpgme_data_new_from_mem(&plain_data,
> +				       (const char *)io->n.cleartext_utf16->data,
> +				       io->n.cleartext_utf16->length,
> +				       0 /* no copy */);
> +	if (gret != GPG_ERR_NO_ERROR) {
> +		ldb_debug(ldb, LDB_DEBUG_ERROR,
> +			  "%s:%s: gret[%u] %s\n",
> +			  __location__, __func__,
> +			  gret, gpgme_strerror(gret));
> +		gpgme_release(ctx);
> +		return ldb_module_operr(io->ac->module);
> +	}
> +	gret = gpgme_data_new(&crypt_data);
> +	if (gret != GPG_ERR_NO_ERROR) {
> +		ldb_debug(ldb, LDB_DEBUG_ERROR,
> +			  "%s:%s: gret[%u] %s\n",
> +			  __location__, __func__,
> +			  gret, gpgme_strerror(gret));
> +		gpgme_data_release(plain_data);
> +		gpgme_release(ctx);
> +		return ldb_module_operr(io->ac->module);
> +	}
> +
> +	for (ki = 0; ki < num_keys; ki++) {
> +		const char *key_id = io->ac->gpg_key_ids[ki];
> +
> +		keys[ki] = NULL;
> +
> +		gret = gpgme_get_key(ctx, key_id, &keys[ki], 0 /* public key */);
> +		if (gret != GPG_ERR_NO_ERROR) {
> +			keys[ki] = NULL;
> +			ldb_debug(ldb, LDB_DEBUG_ERROR,
> +				  "%s:%s: ki[%zu] key_id[%s] gret[%u] %s\n",
> +				  __location__, __func__,
> +				  ki, io->ac->gpg_key_ids[ki],
> +				  gret, gpgme_strerror(gret));
> +			for (kr = 0; keys[kr] != NULL; kr++) {
> +				gpgme_key_release(keys[kr]);
> +			}
> +			gpgme_data_release(crypt_data);
> +			gpgme_data_release(plain_data);
> +			gpgme_release(ctx);
> +			return ldb_module_operr(io->ac->module);
> +		}
> +	}
> +	keys[ki] = NULL;
> +
> +	gret = gpgme_op_encrypt(ctx, keys,
> +				GPGME_ENCRYPT_ALWAYS_TRUST,
> +				plain_data, crypt_data);
> +	gpgme_data_release(plain_data);
> +	plain_data = NULL;
> +	for (kr = 0; keys[kr] != NULL; kr++) {
> +		gpgme_key_release(keys[kr]);
> +		keys[kr] = NULL;
> +	}
> +	gpgme_release(ctx);
> +	ctx = NULL;
> +	if (gret != GPG_ERR_NO_ERROR) {
> +		ldb_debug(ldb, LDB_DEBUG_ERROR,
> +			  "%s:%s: gret[%u] %s\n",
> +			  __location__, __func__,
> +			  gret, gpgme_strerror(gret));
> +		gpgme_data_release(crypt_data);
> +		return ldb_module_operr(io->ac->module);
> +	}
> +
> +	crypt_mem = gpgme_data_release_and_get_mem(crypt_data, &crypt_length);
> +	crypt_data = NULL;
> +	if (crypt_mem == NULL) {
> +		return ldb_module_oom(io->ac->module);
> +	}
> +
> +	pgb->gpg_blob = data_blob_talloc(io->ac,
> +					 (const uint8_t *)crypt_mem,
> +					 crypt_length);
> +	gpgme_free(crypt_mem);
> +	crypt_mem = NULL;
> +	crypt_length = 0;
> +	if (pgb->gpg_blob.data == NULL) {
> +		return ldb_module_oom(io->ac->module);
> +	}
> +
> +	return LDB_SUCCESS;
> +#else /* ENABLE_GPGME */
> +	ldb_debug_set(ldb, LDB_DEBUG_FATAL,
> +		      "You configured 'password hash gpg key ids', "
> +		      "but GPGME support is missing. (%s:%d)",
> +		      __FILE__, __LINE__);
> +	return LDB_ERR_UNWILLING_TO_PERFORM;
> +#endif /* else ENABLE_GPGME */
> +}
> +
>  static int setup_supplemental_field(struct setup_password_fields_io *io)
>  {
>  	struct ldb_context *ldb;
>  	struct supplementalCredentialsBlob scb;
>  	struct supplementalCredentialsBlob _old_scb;
>  	struct supplementalCredentialsBlob *old_scb = NULL;
> -	/* Packages + (Kerberos-Newer-Keys, Kerberos, WDigest and CLEARTEXT) */
> +	/* Packages + (Kerberos-Newer-Keys, Kerberos, WDigest, CLEARTEXT, SambaGPG) */
>  	uint32_t num_names = 0;
> -	const char *names[1+4];
> +	const char *names[1+5];
>  	uint32_t num_packages = 0;
> -	struct supplementalCredentialsPackage packages[1+4];
> +	struct supplementalCredentialsPackage packages[1+5];
>  	/* Packages */
>  	struct supplementalCredentialsPackage *pp = NULL;
>  	struct package_PackagesBlob pb;
> @@ -1433,14 +1560,22 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  	struct package_PrimaryCLEARTEXTBlob pcb;
>  	DATA_BLOB pcb_blob;
>  	char *pcb_hexstr;
> +	/* Primary:SambaGPG */
> +	const char **ng = NULL;
> +	struct supplementalCredentialsPackage *pg = NULL;
> +	struct package_PrimarySambaGPGBlob pgb;
> +	DATA_BLOB pgb_blob;
> +	char *pgb_hexstr;
>  	int ret;
>  	enum ndr_err_code ndr_err;
>  	uint8_t zero16[16];
>  	bool do_newer_keys = false;
>  	bool do_cleartext = false;
> +	bool do_samba_gpg = false;
>  
>  	ZERO_STRUCT(zero16);
>  	ZERO_STRUCT(names);
> +	ZERO_STRUCT(packages);
>  
>  	ldb = ldb_module_get_ctx(io->ac->module);
>  
> @@ -1483,6 +1618,10 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  		do_cleartext = true;
>  	}
>  
> +	if (io->ac->gpg_key_ids != NULL) {
> +		do_samba_gpg = true;
> +	}
> +
>  	/*
>  	 * The ordering is this
>  	 *
> @@ -1490,9 +1629,16 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  	 * Primary:Kerberos
>  	 * Primary:WDigest
>  	 * Primary:CLEARTEXT (optional)
> +	 * Primary:SambaGPG (optional)
>  	 *
>  	 * And the 'Packages' package is insert before the last
>  	 * other package.
> +	 *
> +	 * Note: it's important that Primary:SambaGPG is added as
> +	 * the last element. This is the indication that it matches
> +	 * the current password. When a password change happens on
> +	 * a Windows DC, it will keep the old Primary:SambaGPG value,
> +	 * but as the first element.
>  	 */
>  	if (do_newer_keys) {
>  		/* Primary:Kerberos-Newer-Keys */
> @@ -1504,7 +1650,7 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  	nk = &names[num_names++];
>  	pk = &packages[num_packages++];
>  
> -	if (!do_cleartext) {
> +	if (!do_cleartext && !do_samba_gpg) {
>  		/* Packages */
>  		pp = &packages[num_packages++];
>  	}
> @@ -1514,14 +1660,25 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  	pd = &packages[num_packages++];
>  
>  	if (do_cleartext) {
> -		/* Packages */
> -		pp = &packages[num_packages++];
> +		if (!do_samba_gpg) {
> +			/* Packages */
> +			pp = &packages[num_packages++];
> +		}
>  
>  		/* Primary:CLEARTEXT */
>  		nc = &names[num_names++];
>  		pc = &packages[num_packages++];
>  	}
>  
> +	if (do_samba_gpg) {
> +		/* Packages */
> +		pp = &packages[num_packages++];
> +
> +		/* Primary:SambaGPG */
> +		ng = &names[num_names++];
> +		pg = &packages[num_packages++];
> +	}
> +
>  	if (pkn) {
>  		/*
>  		 * setup 'Primary:Kerberos-Newer-Keys' element
> @@ -1640,6 +1797,36 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
>  	}
>  
>  	/*
> +	 * setup 'Primary:SambaGPG' element
> +	 */
> +	if (pg) {
> +		*ng		= "SambaGPG";
> +
> +		ret = setup_primary_samba_gpg(io, &pgb);
> +		if (ret != LDB_SUCCESS) {
> +			return ret;
> +		}
> +
> +		ndr_err = ndr_push_struct_blob(&pgb_blob, io->ac, &pgb,
> +			(ndr_push_flags_fn_t)ndr_push_package_PrimarySambaGPGBlob);
> +		if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
> +			NTSTATUS status = ndr_map_error2ntstatus(ndr_err);
> +			ldb_asprintf_errstring(ldb,
> +					"setup_supplemental_field: failed to "
> +					"push package_PrimarySambaGPGBlob: %s",
> +					nt_errstr(status));
> +			return LDB_ERR_OPERATIONS_ERROR;
> +		}
> +		pgb_hexstr = data_blob_hex_string_upper(io->ac, &pgb_blob);
> +		if (!pgb_hexstr) {
> +			return ldb_oom(ldb);
> +		}
> +		pg->name	= "Primary:SambaGPG";
> +		pg->reserved	= 1;
> +		pg->data	= pgb_hexstr;
> +	}
> +
> +	/*
>  	 * setup 'Packages' element
>  	 */
>  	pb.names = names;
> @@ -2595,6 +2782,7 @@ static struct ph_context *ph_init_context(struct ldb_module *module,
>  {
>  	struct ldb_context *ldb;
>  	struct ph_context *ac;
> +	struct loadparm_context *lp_ctx = NULL;
>  
>  	ldb = ldb_module_get_ctx(module);
>  
> @@ -2608,6 +2796,10 @@ static struct ph_context *ph_init_context(struct ldb_module *module,
>  	ac->req = req;
>  	ac->userPassword = userPassword;
>  
> +	lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"),
> +				       struct loadparm_context);
> +	ac->gpg_key_ids = lpcfg_password_hash_gpg_key_ids(lp_ctx);
> +
>  	return ac;
>  }
>  
> @@ -3518,6 +3710,28 @@ static const struct ldb_module_ops ldb_password_hash_module_ops = {
>  
>  int ldb_password_hash_module_init(const char *version)
>  {
> +#ifdef ENABLE_GPGME
> +	const char *gversion = NULL;
> +#endif /* ENABLE_GPGME */
> +
>  	LDB_MODULE_CHECK_VERSION(version);
> +
> +#ifdef ENABLE_GPGME
> +	/*
> +	 * Note: this sets a SIGPIPE handler
> +	 * if none is active already. See:
> +	 * https://www.gnupg.org/documentation/manuals/gpgme/Signal-Handling.html#Signal-Handling
> +	 */
> +	gversion = gpgme_check_version(GPGME_VERSION);
> +	if (gversion == NULL) {
> +		fprintf(stderr, "%s() in %s version[%s]: "
> +			"gpgme_check_version(%s) not available, "
> +			"gpgme_check_version(NULL) => '%s'\n",
> +			__func__, __FILE__, version,
> +			GPGME_VERSION, gpgme_check_version(NULL));
> +		return LDB_ERR_UNAVAILABLE;
> +	}
> +#endif /* ENABLE_GPGME */
> +
>  	return ldb_register_module(&ldb_password_hash_module_ops);
>  }
> diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build_server b/source4/dsdb/samdb/ldb_modules/wscript_build_server
> index aba2d87..bc6903b 100755
> --- a/source4/dsdb/samdb/ldb_modules/wscript_build_server
> +++ b/source4/dsdb/samdb/ldb_modules/wscript_build_server
> @@ -116,7 +116,7 @@ bld.SAMBA_MODULE('ldb_password_hash',
>  	init_function='ldb_password_hash_module_init',
>  	module_init_name='ldb_init_module',
>  	internal_module=False,
> -	deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 DSDB_MODULE_HELPERS'
> +	deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 gpgme DSDB_MODULE_HELPERS'
>  	)
>  
>  
> -- 
> 1.9.1
> 
> 
> From ccfa0938bc4994633f52a33caf17803f55fef3aa Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 17/22] samba-tool: add --decrypt-samba-gpg support to 'user
>  getpasswords' and 'user syncpasswords'
> 
> This get's the cleartext passwords by decrypting
> the 'Primary:SambaGPG' value in order to provide the
> virtual attributes: virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
> 
> The virtual attribute virtualSambaGPG provides the raw
> (encrypted) value of the 'Primary:SambaGPG' value.
> 
> See the "password hash gpg key ids" option for the encryption part
> of this feature.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/netcmd/user.py | 139 ++++++++++++++++++++++++++++++++++++++++----
>  1 file changed, 128 insertions(+), 11 deletions(-)
> 
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index 0fe7c54..0378c5b 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -50,6 +50,18 @@ from samba.netcmd import (
>      Option,
>      )
>  
> +
> +try:
> +    import io
> +    import gpgme
> +    gpgme_support = True
> +    decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
> +except ImportError as e:
> +    gpgme_support = False
> +    decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
> +            "python-gpgme requires"
> +    pass
> +
>  disabled_virtual_attributes = {
>      }
>  
> @@ -60,6 +72,9 @@ virtual_attributes = {
>      "virtualClearTextUTF16": {
>          "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
>          },
> +    "virtualSambaGPG": {
> +        "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> +        },
>      }
>  
>  def check_random():
> @@ -809,8 +824,8 @@ class GetPasswordCommand(Command):
>  
>          return samdb
>  
> -    def get_account_attributes(self, samdb, username,
> -                               basedn, filter, scope, attrs):
> +    def get_account_attributes(self, samdb, username, basedn, filter, scope,
> +                               attrs, decrypt):
>  
>          require_supplementalCredentials = False
>          search_attrs = attrs[:]
> @@ -857,16 +872,50 @@ class GetPasswordCommand(Command):
>          if add_sAMAcountName:
>              del obj["sAMAccountName"]
>  
> -        def get_package(name):
> +        calculated = {}
> +        def get_package(name, min_idx=0):
> +            if name in calculated:
> +                return calculated[name]
>              if sc is None:
>                  return None
> +            if min_idx < 0:
> +                min_idx = len(sc.sub.packages) + min_idx
> +            idx = 0
>              for p in sc.sub.packages:
> +                idx += 1
> +                if idx <= min_idx:
> +                    continue
>                  if name != p.name:
>                      continue
>  
>                  return binascii.a2b_hex(p.data)
>              return None
>  
> +        if decrypt:
> +            # Samba add 'Primary:SambaGPG' at the end.
> +            # When Windows sets the password it keeps
> +            # 'Primary:SambaGPG' and rotates it to
> +            # the begining. So we can only use the value,
> +            # if it is the last one.
> +            sgv = get_package("Primary:SambaGPG", min_idx=-1)
> +            if sgv is not None:
> +                ctx = gpgme.Context()
> +                ctx.armor = True
> +                cipher_io = io.BytesIO(sgv)
> +                plain_io = io.BytesIO()
> +                try:
> +                    ctx.decrypt(cipher_io, plain_io)
> +                    cv = plain_io.getvalue()
> +                    calculated["Primary:CLEARTEXT"] = cv
> +                except gpgme.GpgmeError as (major, minor, msg):
> +                    if major == gpgme.ERR_BAD_SECKEY:
> +                        msg = "ERR_BAD_SECKEY: " + msg
> +                    else:
> +                        msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
> +                    self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
> +                                    username or account_name, msg))
> +                    pass
> +
>          def get_utf8(a, b, username):
>              try:
>                  u = unicode(b, 'utf-16-le')
> @@ -924,6 +973,15 @@ class GetPasswordCommand(Command):
>                      continue
>                  sv = get_crypt_value("6", u8)
>                  v = "{CRYPT}" + sv
> +            elif a == "virtualSambaGPG":
> +                # Samba add 'Primary:SambaGPG' at the end.
> +                # When Windows sets the password it keeps
> +                # 'Primary:SambaGPG' and rotates it to
> +                # the begining. So we can only use the value,
> +                # if it is the last one.
> +                v = get_package("Primary:SambaGPG", min_idx=-1)
> +                if v is None:
> +                    continue
>              else:
>                  continue
>              obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
> @@ -970,7 +1028,8 @@ the NTHASH) and the following virtual attributes are possible (see --help
>  for which virtual attributes are supported in your environment):
>  
>     virtualClearTextUTF16: The raw cleartext as stored in the
> -                          'Primary:CLEARTEXT' buffer inside of the
> +                          'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
> +                          with '--decrypt-samba-gpg') buffer inside of the
>                            supplementalCredentials attribute. This typically
>                            contains valid UTF-16-LE, but may contain random
>                            bytes, e.g. for computer accounts.
> @@ -989,6 +1048,20 @@ for which virtual attributes are supported in your environment):
>                            checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
>                            with a $6$... salt, see crypt(3) on modern systems.
>  
> +   virtualSambaGPG:       The raw cleartext as stored in the
> +                          'Primary:SambaGPG' buffer inside of the
> +                          supplementalCredentials attribute.
> +                          See the 'password hash gpg key ids' option in
> +                          smb.conf.
> +
> +The '--decrypt-samba-gpg' option triggers decryption of the
> +Primary:SambaGPG buffer. Check with '--help' if this feature is available
> +in your environment or not (the python-gpgme package is required).  Please
> +note that you might need to set the GNUPGHOME environment variable.  If the
> +decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
> +environment variable has been set correctly and the passphrase is already
> +known by the gpg-agent.
> +
>  Example1:
>  samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
>  
> @@ -1013,15 +1086,21 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
>          Option("--attributes", type=str,
>                 help=virtual_attributes_help,
>                 metavar="ATTRIBUTELIST", dest="attributes"),
> +        Option("--decrypt-samba-gpg",
> +               help=decrypt_samba_gpg_help,
> +               action="store_true", default=False, dest="decrypt_samba_gpg"),
>          ]
>  
>      takes_args = ["username?"]
>  
>      def run(self, username=None, H=None, filter=None,
> -            attributes=None,
> +            attributes=None, decrypt_samba_gpg=None,
>              sambaopts=None, versionopts=None):
>          self.lp = sambaopts.get_loadparm()
>  
> +        if decrypt_samba_gpg and not gpgme_support:
> +            raise CommandError(decrypt_samba_gpg_help)
> +
>          if filter is None and username is None:
>              raise CommandError("Either the username or '--filter' must be specified!")
>  
> @@ -1039,7 +1118,8 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
>                                            basedn=None,
>                                            filter=filter,
>                                            scope=ldb.SCOPE_SUBTREE,
> -                                          attrs=password_attrs)
> +                                          attrs=password_attrs,
> +                                          decrypt=decrypt_samba_gpg)
>  
>          ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
>          self.outf.write("%s" % ldif)
> @@ -1051,7 +1131,8 @@ class cmd_user_syncpasswords(GetPasswordCommand):
>  This syncs logon passwords for user accounts.
>  
>  Note that this command should run on a single domain controller only
> -(typically the PDC-emulator).
> +(typically the PDC-emulator). However the "password hash gpg key ids"
> +option should to be configured on all domain controllers.
>  
>  The command must be run from the root user id or another authorized user id.
>  The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
> @@ -1069,7 +1150,7 @@ The first time, this command needs to be called with
>  '--cache-ldb-initialize' in order to initialize its cache.
>  
>  The cache initialization requires '--attributes' and allows the following
> -optional options: '--script', '--filter' or
> +optional options: '--decrypt-samba-gpg', '--script', '--filter' or
>  '-H/--URL'.
>  
>  The '--attributes' parameter takes a comma separated list of attributes,
> @@ -1080,7 +1161,8 @@ the NTHASH) and the following virtual attributes are possible (see '--help'
>  for supported virtual attributes in your environment):
>  
>     virtualClearTextUTF16: The raw cleartext as stored in the
> -                          'Primary:CLEARTEXT' buffer inside of the
> +                          'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
> +                          with '--decrypt-samba-gpg') buffer inside of the
>                            supplementalCredentials attribute. This typically
>                            contains valid UTF-16-LE, but may contain random
>                            bytes, e.g. for computer accounts.
> @@ -1099,6 +1181,20 @@ for supported virtual attributes in your environment):
>                            checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
>                            with a $6$... salt, see crypt(3) on modern systems.
>  
> +   virtualSambaGPG:       The raw cleartext as stored in the
> +                          'Primary:SambaGPG' buffer inside of the
> +                          supplementalCredentials attribute.
> +                          See the 'password hash gpg key ids' option in
> +                          smb.conf.
> +
> +The '--decrypt-samba-gpg' option triggers decryption of the
> +Primary:SambaGPG buffer. Check with '--help' if this feature is available
> +in your environment or not (the python-gpgme package is required).  Please
> +note that you might need to set the GNUPGHOME environment variable.  If the
> +decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
> +environment variable has been set correctly and the passphrase is already
> +known by the gpg-agent.
> +
>  The '--script' option specifies a custom script that is called whenever any
>  of the dirsyncAttributes (see below) was changed. The script is called
>  without any arguments. It gets the LDIF for exactly one object on STDIN.
> @@ -1201,6 +1297,9 @@ samba-tool user syncpasswords --terminate \\
>          Option("--attributes", type=str,
>                 help=virtual_attributes_help,
>                 metavar="ATTRIBUTELIST", dest="attributes"),
> +        Option("--decrypt-samba-gpg",
> +               help=decrypt_samba_gpg_help,
> +               action="store_true", default=False, dest="decrypt_samba_gpg"),
>          Option("--script", help="Script that is called for each password change", type=str,
>                 metavar="/path/to/syncpasswords.script", dest="script"),
>          Option("--no-wait", help="Don't block waiting for changes",
> @@ -1217,7 +1316,7 @@ samba-tool user syncpasswords --terminate \\
>  
>      def run(self, cache_ldb_initialize=False, cache_ldb=None,
>              H=None, filter=None,
> -            attributes=None,
> +            attributes=None, decrypt_samba_gpg=None,
>              script=None, nowait=None, logfile=None, daemon=None, terminate=None,
>              sambaopts=None, versionopts=None):
>  
> @@ -1230,6 +1329,8 @@ samba-tool user syncpasswords --terminate \\
>          if not cache_ldb_initialize:
>              if attributes is not None:
>                  raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
> +            if decrypt_samba_gpg:
> +                raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
>              if script is not None:
>                  raise CommandError("--script is only allowed together with --cache-ldb-initialize")
>              if filter is not None:
> @@ -1299,6 +1400,9 @@ samba-tool user syncpasswords --terminate \\
>              if H is None:
>                  H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
>  
> +            if decrypt_samba_gpg and not gpgme_support:
> +                raise CommandError(decrypt_samba_gpg_help)
> +
>              password_attrs = self.parse_attributes(attributes)
>              lower_attrs = [x.lower() for x in password_attrs]
>              # We always return these in order to track deletions
> @@ -1349,6 +1453,7 @@ samba-tool user syncpasswords --terminate \\
>                  "dirsyncAttribute",
>                  "dirsyncControl",
>                  "passwordAttribute",
> +                "decryptSambaGPG",
>                  "syncCommand",
>                  "currentPid",
>              ]
> @@ -1376,6 +1481,7 @@ samba-tool user syncpasswords --terminate \\
>                  self.dirsync_attrs = dirsync_attrs
>                  self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
>                  self.password_attrs = password_attrs
> +                self.decrypt_samba_gpg = decrypt_samba_gpg
>                  self.sync_command = sync_command
>                  add_ldif  = "dn: %s\n" % self.cache_dn
>                  add_ldif += "objectClass: userSyncPasswords\n"
> @@ -1386,6 +1492,10 @@ samba-tool user syncpasswords --terminate \\
>                  add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
>                  for a in self.password_attrs:
>                      add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
> +                if self.decrypt_samba_gpg == True:
> +                    add_ldif += "decryptSambaGPG: TRUE\n"
> +                else:
> +                    add_ldif += "decryptSambaGPG: FALSE\n"
>                  if self.sync_command is not None:
>                      add_ldif += "syncCommand: %s\n" % self.sync_command
>                  add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
> @@ -1405,6 +1515,12 @@ samba-tool user syncpasswords --terminate \\
>                  self.password_attrs = []
>                  for a in res[0]["passwordAttribute"]:
>                      self.password_attrs.append(a)
> +                decrypt_string = res[0]["decryptSambaGPG"][0]
> +                assert(decrypt_string in ["TRUE", "FALSE"])
> +                if decrypt_string == "TRUE":
> +                    self.decrypt_samba_gpg = True
> +                else:
> +                    self.decrypt_samba_gpg = False
>                  if "syncCommand" in res[0]:
>                      self.sync_command = res[0]["syncCommand"][0]
>                  else:
> @@ -1462,7 +1578,8 @@ samba-tool user syncpasswords --terminate \\
>                                                basedn="<GUID=%s>" % guid,
>                                                filter="(objectClass=user)",
>                                                scope=ldb.SCOPE_BASE,
> -                                              attrs=self.password_attrs)
> +                                              attrs=self.password_attrs,
> +                                              decrypt=self.decrypt_samba_gpg)
>              ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
>              log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
>              if self.sync_command is None:
> -- 
> 1.9.1
> 
> 
> From b5e6f0b273d3431388a4e206998ee50a1723d188 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 13:51:00 +0100
> Subject: [PATCH 18/22] selftest:gnupg: add a gpg key for Samba Selftest
>  <selftest at samba.example.com>
> 
> This key doesn't have a passphrase and allows automatic testing
> of decryption.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  .gitignore                 |   1 +
>  selftest/gnupg/gpg.conf    |   4 ++++
>  selftest/gnupg/pubring.gpg | Bin 0 -> 1214 bytes
>  selftest/gnupg/secring.gpg | Bin 0 -> 2516 bytes
>  selftest/gnupg/trustdb.gpg | Bin 0 -> 1280 bytes
>  5 files changed, 5 insertions(+)
>  create mode 100644 selftest/gnupg/gpg.conf
>  create mode 100644 selftest/gnupg/pubring.gpg
>  create mode 100644 selftest/gnupg/secring.gpg
>  create mode 100644 selftest/gnupg/trustdb.gpg
> 
> diff --git a/.gitignore b/.gitignore
> index a4c2a69..5870140 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -16,6 +16,7 @@ source3/.clang_complete
>  *.patch
>  *.pyc
>  semantic.cache
> +selftest/gnupg/random_seed
>  pidl/blib
>  pidl/cover_db
>  pidl/Makefile
> diff --git a/selftest/gnupg/gpg.conf b/selftest/gnupg/gpg.conf
> new file mode 100644
> index 0000000..33b9f9f
> --- /dev/null
> +++ b/selftest/gnupg/gpg.conf
> @@ -0,0 +1,4 @@
> +
> +keyid-format long
> +fingerprint
> +default-key 4952E40301FAB41A
> diff --git a/selftest/gnupg/pubring.gpg b/selftest/gnupg/pubring.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..b3fa9ccc547cd125ddae5fc00a2e2075ef669964
> GIT binary patch
> literal 1214
> zc-jHJ1VQ_m0SyFJm1KMY2ms-X8f0hnqKH(%6n51&uP3(6+vtkL2;*?1DyswNmT~eI
> zE0Kjt7tO%@7h}c<IeIf9OYxcgsg|f>J$~2Bs+|JIzRsidEo7<Qp?ijk$+QGtOTIZL
> z;-)ql`Lh~H<Kr*AttnVgZdHEm4WSD8+nAn$=^KW8S#GRW1Y8QxYNcf0SFSP&L9h2n
> z^lTW}6N+;We1FFeUIPuF_svb}15Xv#xAk`3L6qva?GUYQ%VRjmAAWqf+Ry)PA2G53
> zw1q?a{_cuS27>Fzm>k-tolUY9k#dnB at 1>D>GTP02BT4;5qG?lSGz|xm25tAJ9fzia
> zS*1YoCkdP)j4>lM)HeVT0RRECD^p=@VqqXtWo%}2Wpi{OJac7iW^`q9bU<@qZDL_A
> zWq4t2aBO8RV{dIfi2*(Y69EDMC<Ovmm1KMZ8v_LiF8&9A1`7!Y2Ll2I6$k<e3JU}l
> z0s{d89svRufB*^!5J^(x0|EN98hfM&0Ebl;?X7H@<<^6Nfj3=<D5_;pcxvM6a!S#M
> za9rS#d4?Det$A)hG+n?-R`Yx1z*K;-6sqhuVsrphMT-<?MJSuzcRXE#{AwvcCJp~g
> zYY3RqifioJe#CV&jz at Q3g$^E&8y at MHe6&^8#b!teLqI=Ee>+~Grh(*TA|DQe7*odR
> z3&spq3+?1<k9dEgEBlTr{zDab6y(<{g|=%Zxi=c)BcHokpEnHvc}K4TL^xI+QPL(0
> zzU00vK4_(jRH+tc+p9((SoJ&va6Bk|%uwQszx1f&oq5|v8cG5vgLCI2Ma07l*XI<d
> z{m`%LwQ`RyzGe+Sa?Ppy7hKcSX|MtS1GxbW1Xh(~d;tgm=X at b)M5Uv*vGqB8c+_OP
> z0NFlSx9Je^m%Q*U3BeVP8HkgMw+R~4oXp}0iz_#wrd~<uSU+MVBkXe|J={-gn`Gb}
> zYP(F%+m53*<=12L0(H{mo|_xn4%FMQyF(gRdh2trF_?g<ZNCmkO*bxC1T~nnq5T0o
> z*4IQJ53f=5BB)$ermWYsHXR!m_YuvsG29|UzHa!l7_zSmTQ23#%#0T=6y+3?FxCi%
> zj|m4&QlRIEco<l$$)gB4_(aW at Y;`%bZ35E7s_0=WA|XA#9rn8B0n#IL<F>SPSyO4K
> z2{BC_ZJed?GWO1V at pb5I$l^92Z6iJ6!A|0n at F!W>01*KI0f_-61Q-DV01pKMR+VIY
> z0vikk2`>HzfB*^!5J^(x0|EN98XOS^`U6>eKq2q%Uiu}q$vxpjk#NGYTmh_ at hBvsp
> zy^ebF=y%PnZJePn+hJjksXjcAh1(<6O#Nkngqc3YnN7iTgHQtpLaoV%o#vvC7xm4i
> zoOen+#${$}vG9?~NX at 5DTO-R&W_5|qRK~M&=fYx;;{I<;2A9s={!@IO9DMxsLg7Ru
> zhevTy3Co~w_W9#DV(*f){B1R18Zw>Wwt&-9Im2h4NYAGrMt%w(EEdfG9cw*#X^{{G
> z;0uLtjd5(Dr&%WO7Jln5<f_qQz63|oP+CZw<lG9@&zXHRVo_c^c75%FEKp7!UenV{
> cJdkshVP{+3B`5IdDqLdAgp#ju39teH11G#8&Hw-a
> 
> literal 0
> Hc-jL100001
> 
> diff --git a/selftest/gnupg/secring.gpg b/selftest/gnupg/secring.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..09dd9fd9fa75494fdad1ff9b39fc29ccbb58755c
> GIT binary patch
> literal 2516
> zc-jHf2`l!M1DFI>m1KMY2ms-X8f0hnqKH(%6n51&uP3(6+vtkL2;*?1DyswNmT~eI
> zE0Kjt7tO%@7h}c<IeIf9OYxcgsg|f>J$~2Bs+|JIzRsidEo7<Qp?ijk$+QGtOTIZL
> z;-)ql`Lh~H<Kr*AttnVgZdHEm4WSD8+nAn$=^KW8S#GRW1Y8QxYNcf0SFSP&L9h2n
> z^lTW}6N+;We1FFeUIPuF_svb}15Xv#xAk`3L6qva?GUYQ%VRjmAAWqf+Ry)PA2G53
> zw1q?a{_cuS27>Fzm>k-tolUY9k#dnB at 1>D>GTP02BT4;5qG?lSGz|xm25tAJ9fzia
> zS*1YoCkdP)j4>lM)HeVT0RRC22mT;956!|@tMEZjEsq8snh@>7s2I^>MsP);(2d`}
> z<h+04fN=7&JO!82pl1)rXZ$bx84%867Ium>-4a%_3b&9Q at qnc7kw{0#c0cAcZH^yt
> z8Y!U?G_Z1-fHm#|nsroy3J;aOi0oHO%&8o7#}}OW7z~lMvI%<=$=;)C)T5LKr`Xe-
> z;(I`ciVV*jI4$yE#Gd1Mf1%ioFM7Gfq5I^qCmn^RlD?8VZ>?IHzEeVG45i!1l|TlG
> zb{hxwKNf$Y)vwmVbTm8p9M`&QD4 at Uuos<!f+><VM4g at H?X7!+jg(}tc2B<m$f6bCY
> zE~1pHKw65tLrW*vkc6hcE`0<5<gvnTNx}3>6OnFk9h4Q*R^6`y3f;GM6`;_un)1{Y
> zn4vko;>Y3VnI$#Hy7a<v`270L1P00-FzQ!s0%h4XNClYh-*u`B{|6T>tvub8w;%I7
> zLs0WOzxPvTU1i&8KC_4w!H1ZYQdyl;>y%j1H4r4Oj?#d`bON3bBbZ1X1OWVcCSfS@
> zpL+6?fK&NAZt5-<FV%&h8CCi-b=)B9UUUw$qV%L_=$Tci3 at -OzzfjSWNdb_q#c1;E
> z(vaU)R&yXJt_cEzx~v;4dyEd;siLjeMSj3C<EFLHU}D~LH-0OomX}B at RC791dF&WK
> z&#;nhpOMRJDS=tGt4Api<zoZ>Gcxu5Jvp_jQes5^F2+o&s^fu at izPNmM7Rs{7qUGx
> z2$gj7914BcY!qW)V6W#Xid;#N3d3<&Jvq6>$63AQ^@KU|$wzb}wd|xnhOk=a;&wKY
> zt!Tt{W?mo))g$)nqr%IuDwm`cLxkAFn4y5G?Dpj{aEfJdQvLAUEDX6BLJzbnQ(<jl
> zVIWgwY-V(2b95j)b7gF1bY*jNKyzVjVqq?2cwudDY-KKEZ*4w_0X_s10RjLh1p-!;
> zWPAb}0|f~#{s({t3ke7Z0|EvW2m%QT3j`Jd0|5da0Rk6*0162ZNmAqk0s6EWd!z^e
> zhgBBst!$d*)`Nk8H(iJ*s%21kYU1j0O3{aKT;P#;h8Pg7d2T>7UBF6K^LypMRDiJ*
> zs_ZvnbO2OEixg)?D4X7QJY9qQYAHY_4gXAQ2$<4}YwX&7#C0@|M|WU_4jzyj9_g5T
> zv{lx{W=IJ`KtD@=J6 at rtf#hW(9}a^UQ^x2E#tc>q?c{5Zcz>cR`;IF9Llt-w<ku^O
> zwreK2HyYz3pSxP0Hw^%JN3Q}zI947}(k2VO<i0FEXr+r(sTOD3t41JL^*jV{JSct4
> zP~wZf^r+;WdD})BN&+Z at bLS&P#KR2N=M<>@(68&Ya*r>*W(_}b&8hqsT+`HPumS)8
> zodcKzR+VIY0SEx+d?9H>rK7j8^*MZa)MUH>**;mf=@9Xkyznjw!4-}fh?9%A2^!O!
> z%;E`)D>tF0UP<X#KVl^#>~kbN+)rzpWZ)fYyG+j8j-xl_*JJYnb<*XYn;Y8>)Z4JT
> zLmF3l>vOO%n1HEmzYa)EH!fKOHJG%a{Q*4I*F+!>uTk_Os9aX2tk<<R9UB+-5zVwQ
> z+#*B1ZuqkpvabtUF6Gb6j2ABy<rI at J)(D4>2?tG5py!Bq7+9>yqX;?pM9r6Mbvd+c
> z0 at B2)=wT}&Aw9kw_PXW)(j#- at wzPCvQ)#FPF-;w9oTc$H_Rf6qb?9u!;x-^{BR%55
> zPU4gBCt2A55di=J00;dTQ6XPq4>oEOA1oP1#v};c+w@^iVFTYKE&E#%x;{<qpS#2-
> z2c=R<d-+(vPFrg?LtAH1R%A4Lm>8y8dpo+7ncTA~WuKLCS_BGMvMx0?a&se`>@`|v
> zE%YE3Bj4@?qZ>txh&3nII0ZwJ9Q0W9h`!Kf-Qktz3AqK=FZUluscw)R=0Fc|`+QK|
> z!$l_7*w<$u{ie8aC3Y-$=5lPaXtr(=^uk0_le%G7*Y>0Dn)Gss40zJ at o=Hm(xsJ?T
> zN++L-p%0sgkY~%S##^0m at LPnqp)8%Hf=j%iQApsyI$x1EfN%qW7AZw%g50(1A}i;?
> zX75T6mN|MvIT~vO0P-bY-qO`m(A)k0u8!NuqK2yGXI(-(Q%}oK(s0{1x#B1tF!A|D
> zqLS_d2hxab%w`;@yhp*?$Es?0Feu0a0&G<1F~3<#TG9dpiz^SN6&F->|ETk$t`J>?
> zID`u1^7&GS8Zn+(YUN0nisOm{@W<Q#^_}T97I~MQ`tx$gPXqw;x|>Lcl$CFwb)X!X
> zJM%bFlG==doVuw2$Rn*$_T)3}7y0(3+Y4C)`zM_dBz`um$s8m;H6uQp82T_c&oMx*
> zCbz3ITU>e6ma at J5M)|%x>90C5n+_Hla=mj;q{Lm)bXLx_KWN0x!xN~LzmH8n>sSzF
> zj`;CSX}lo{9f?;20HV$9ZV&MrqW at 8awWFpo{cP7sX<;lX%nu-^CiCw><nW<<TxgM-
> zD!-<?adqfeTNpuf-&B98(pgHJdf6LA+yV*K at -FXhxO4mROOz`D)`7yWH#lopE<txZ
> zXx<ZRTpWJH^CJcv9ol~bvN#hxAxnwn_Wm5q0!sT at H}MrwtvP#%0VM<&0RjLI1p-!;
> zWPAb}3<U`;{s({n3JDNNQse^x`m`Dx5eNDMS$jYs at 9$pvCAG;t;Y5*e!m?ZeteA#3
> zxV*iNdh+OZ&8=;mp)lKFVUMXkJdlOkBi2m)Wr2j5KE;_$!E}RA0|!E_$%mchqK_B#
> z&8D1pN<GG9W^1wVk;+KTr%ziW%S~o=iOy8UvvcRdVvyqgZ%hW4&fWe~e4ZS9{PjZN
> zL?wquaZw4&pl|m1<2YjPlC=D7HDMYuo#3{B(^EOaXP-#Vryxdt3Lh*M%>W&1J$Y%7
> z5Cz~1g>a2=Y at w%FCh!)1>o4T0(PO>@N77JQNSx%{3f9k=eKcZGUOaYv?Sd>&P99#<
> e(@Q*%bCqFdTizun at aZaCV#|b*uW<>m0ssItu%B-L
> 
> literal 0
> Hc-jL100001
> 
> diff --git a/selftest/gnupg/trustdb.gpg b/selftest/gnupg/trustdb.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..bfe8f0689daf367f65de4e9054107d9c9c74a1c3
> GIT binary patch
> literal 1280
> zc-mu3FGy!*W at Ke#VqgfHnyS}2Ir|R-CSc at ZAP$VG8&x-|Zd4sv>f{Kk<6-#Yv6{<s
> zvCXUNUp-PigPt%m{@NnN29cCUsN-e05F6a)zQJJY#s0`D?vRZVU(Awf*)Yrl01lrS
> A<^TWy
> 
> literal 0
> Hc-jL100001
> 
> -- 
> 1.9.1
> 
> 
> From 7ce8c7c7bf4ebbe94ad09ca0a42c1e7e701c3387 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 10:04:40 +0100
> Subject: [PATCH 19/22] s4:selftest: run samba.tests.samba_tool.user also
>  against ad_dc:local
> 
> In future ad_dc_ntvfs and ad_dc will differ regarding the Primary:SambaGPG
> password feature. So we should test both.
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  source4/selftest/tests.py | 1 +
>  1 file changed, 1 insertion(+)
> 
> diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
> index 32d47a0..633e31f 100755
> --- a/source4/selftest/tests.py
> +++ b/source4/selftest/tests.py
> @@ -486,6 +486,7 @@ planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.gpo")
>  
>  planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.processes")
>  planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user")
> +planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.user")
>  planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group")
>  planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
>  
> -- 
> 1.9.1
> 
> 
> From 7a7b76ae4b3d99b8d5135c7f54341437ab3122d6 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 13:51:00 +0100
> Subject: [PATCH 20/22] selftest:Samba4: configure "password hash gpg key ids"
>  for ad_dc (if available)
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  selftest/selftest.pl      |  1 +
>  selftest/target/Samba4.pm | 32 ++++++++++++++++++++++++++++++++
>  2 files changed, 33 insertions(+)
> 
> diff --git a/selftest/selftest.pl b/selftest/selftest.pl
> index 0827376..012c4fc 100755
> --- a/selftest/selftest.pl
> +++ b/selftest/selftest.pl
> @@ -320,6 +320,7 @@ $ENV{KRB5CCNAME} = "$prefix/krb5ticket";
>  $ENV{PREFIX_ABS} = $prefix_abs;
>  $ENV{SRCDIR} = $srcdir;
>  $ENV{SRCDIR_ABS} = $srcdir_abs;
> +$ENV{GNUPGHOME} = "$srcdir_abs/selftest/gnupg";
>  $ENV{BINDIR} = $bindir_abs;
>  
>  my $tls_enabled = not $opt_quick;
> diff --git a/selftest/target/Samba4.pm b/selftest/target/Samba4.pm
> index c1c3967..7df8a27 100755
> --- a/selftest/target/Samba4.pm
> +++ b/selftest/target/Samba4.pm
> @@ -1784,6 +1784,27 @@ sub provision_rodc($$$)
>  	return $ret;
>  }
>  
> +sub read_config_h($)
> +{
> +	my ($name) = @_;
> +	my %ret = {};
> +	open(LF, "<$name") or die("unable to read $name: $!");
> +	while (<LF>) {
> +		chomp;
> +		next if not (/^#define /);
> +		if (/^#define (.*?)[ \t]+(.*?)$/) {
> +			$ret{$1} = $2;
> +			next;
> +		}
> +		if (/^#define (.*?)[ \t]+$/) {
> +			$ret{$1} = 1;;
> +			next;
> +		}
> +	}
> +	close(LF);
> +	return \%ret;
> +}
> +
>  sub provision_ad_dc($$)
>  {
>  	my ($self, $prefix) = @_;
> @@ -1797,6 +1818,15 @@ sub provision_ad_dc($$)
>  	my $require_mutexes = "dbwrap_tdb_require_mutexes:* = yes";
>  	$require_mutexes = "" if ($ENV{SELFTEST_DONT_REQUIRE_TDB_MUTEX_SUPPORT} eq "1");
>  
> +	my $config_h = {};
> +
> +	if (defined($ENV{CONFIG_H})) {
> +		$config_h = read_config_h($ENV{CONFIG_H});
> +	}
> +
> +	my $password_hash_gpg_key_ids = "password hash gpg key ids = selftest\@samba.example.com";
> +	$password_hash_gpg_key_ids = "" unless defined($config_h->{HAVE_GPGME});
> +
>  	my $extra_smbconf_options = "
>          server services = -smb +s3fs
>          xattr_tdb:file = $prefix_abs/statedir/xattr.tdb
> @@ -1804,6 +1834,8 @@ sub provision_ad_dc($$)
>  	dbwrap_tdb_mutexes:* = yes
>  	${require_mutexes}
>  
> +	${password_hash_gpg_key_ids}
> +
>  	kernel oplocks = no
>  	kernel change notify = no
>  
> -- 
> 1.9.1
> 
> 
> From 0f2058630330dae690749e5f12c9868f9abf420f Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 21/22] python:samba/tests: use 'samba-tool user
>  {getpassword,syncpasswords}' with --decrypt-samba-gpg
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  python/samba/tests/samba_tool/user.py | 25 ++++++++++++++++++++++---
>  1 file changed, 22 insertions(+), 3 deletions(-)
> 
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index aad323d..b9f318e 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -114,10 +114,11 @@ class UserCmdTestCase(SambaToolCmdTest):
>              self.assertEquals(err,"","setpassword with url")
>              self.assertMatch(out, "Changed password OK", "setpassword with url")
>  
> -        attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> +        attributes = "sAMAccountName,unicodePwd,supplementalCredentials,virtualClearTextUTF8,virtualClearTextUTF16,virtualSSHA,virtualSambaGPG"
>          (result, out, err) = self.runsubcmd("user", "syncpasswords",
>                                              "--cache-ldb-initialize",
> -                                            "--attributes=%s" % attributes)
> +                                            "--attributes=%s" % attributes,
> +                                            "--decrypt-samba-gpg")
>          self.assertCmdSuccess(result, "Ensure syncpasswords --cache-ldb-initialize runs")
>          self.assertEqual(err,"","getpassword without url")
>          cache_attrs = {
> @@ -127,6 +128,7 @@ class UserCmdTestCase(SambaToolCmdTest):
>              "dirsyncAttribute": { },
>              "dirsyncControl": { "value": "dirsync:1:0:0"},
>              "passwordAttribute": { },
> +            "decryptSambaGPG": { },
>              "currentTime": { },
>          }
>          for a in cache_attrs.keys():
> @@ -150,6 +152,8 @@ class UserCmdTestCase(SambaToolCmdTest):
>              creds.set_password(newpasswd)
>              nthash = creds.get_nt_hash()
>              unicodePwd = base64.b64encode(creds.get_nt_hash())
> +            virtualClearTextUTF8 = base64.b64encode(newpasswd)
> +            virtualClearTextUTF16 = base64.b64encode(unicode(newpasswd, 'utf-8').encode('utf-16-le'))
>  
>              (result, out, err) = self.runsubcmd("user", "setpassword",
>                                                  user["name"],
> @@ -173,10 +177,18 @@ class UserCmdTestCase(SambaToolCmdTest):
>                      "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
>              self.assertMatch(out, "supplementalCredentials:: ",
>                      "getpassword supplementalCredentials: out[%s]" % out)
> +            if "virtualSambaGPG:: " in out:
> +                self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
> +                    "getpassword virtualClearTextUTF8: out[%s]" % out)
> +                self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
> +                    "getpassword virtualClearTextUTF16: out[%s]" % out)
> +                self.assertMatch(out, "virtualSSHA: ",
> +                    "getpassword virtualSSHA: out[%s]" % out)
>  
>              (result, out, err) = self.runsubcmd("user", "getpassword",
>                                                  user["name"],
> -                                                "--attributes=%s" % attributes)
> +                                                "--attributes=%s" % attributes,
> +                                                "--decrypt-samba-gpg")
>              self.assertCmdSuccess(result, "Ensure getpassword runs")
>              self.assertEqual(err,"","getpassword without url")
>              self.assertMatch(out, "Got password OK", "getpassword without url")
> @@ -186,6 +198,13 @@ class UserCmdTestCase(SambaToolCmdTest):
>                      "getpassword unicodePwd: out[%s]" % out)
>              self.assertMatch(out, "supplementalCredentials:: ",
>                      "getpassword supplementalCredentials: out[%s]" % out)
> +            if "virtualSambaGPG:: " in out:
> +                self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
> +                    "getpassword virtualClearTextUTF8: out[%s]" % out)
> +                self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
> +                    "getpassword virtualClearTextUTF16: out[%s]" % out)
> +                self.assertMatch(out, "virtualSSHA: ",
> +                    "getpassword virtualSSHA: out[%s]" % out)
>  
>          for user in self.users:
>              newpasswd = self.randomPass()
> -- 
> 1.9.1
> 
> 
> From d3252f6b41506f456ddcf4e17c13259ca95539bf Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Wed, 17 Feb 2016 10:07:27 +0100
> Subject: [PATCH 22/22] WHATSNEW: add 'Password sync as active directory domain
>  controller'
> 
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
>  WHATSNEW.txt | 14 ++++++++++++++
>  1 file changed, 14 insertions(+)
> 
> diff --git a/WHATSNEW.txt b/WHATSNEW.txt
> index e7579b9..37b2d05 100644
> --- a/WHATSNEW.txt
> +++ b/WHATSNEW.txt
> @@ -33,6 +33,19 @@ The ldap server has support for the LDAP_SERVER_NOTIFICATION_OID
>  control. This can be used to monitor the active directory database
>  for changes.
>  
> +Password sync as active directory domain controller
> +---------------------------------------------------
> +
> +The new commands 'samba-tool user getpassword'
> +and 'samba-tool user syncpasswords' provide
> +access and syncing of various password fields.
> +
> +If compiled with GPGME support (--with-gpgme) it's
> +possible to store cleartext passwords in a PGP/OpenGPG
> +encrypted form by configuring the new "password hash gpg key ids"
> +option.
> +
> +
>  TODO...
>  
>  
> @@ -47,6 +60,7 @@ smb.conf changes
>  
>    Parameter Name		Description		Default
>    --------------		-----------		-------
> +  password hash gpg key ids     New
>  
>  
>  KNOWN ISSUES
> -- 
> 1.9.1
> 


-- 
/ Alexander Bokovoy



More information about the samba-technical mailing list