PATCHES: Password sync as active directory domain controller

Stefan Metzmacher metze at samba.org
Wed Feb 17 16:15:00 UTC 2016


Hi,

here's a patchset that adds some features which
help to sync user account passwords with other authentication
stores.


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.

Please review and push:-)

Thanks!
metze
-------------- next part --------------
From ebe9f4d19af7f3e65aaa2f892772f828ae348618 Mon Sep 17 00:00:00 2001
From: Stefan Metzmacher <metze at samba.org>
Date: Wed, 17 Feb 2016 10:00:57 +0100
Subject: [PATCH 01/22] WHATSNEW: Clear release notes for Samba 4.5.0pre1.

Signed-off-by: Stefan Metzmacher <metze at samba.org>
---
 WHATSNEW.txt | 203 ++---------------------------------------------------------
 1 file changed, 4 insertions(+), 199 deletions(-)

diff --git a/WHATSNEW.txt b/WHATSNEW.txt
index 7c748c2..07241db 100644
--- a/WHATSNEW.txt
+++ b/WHATSNEW.txt
@@ -1,12 +1,12 @@
 Release Announcements
 =====================
 
-This is the first release candidate of Samba 4.4.  This is *not*
+This is the first preview release of Samba 4.5.  This is *not*
 intended for production environments and is designed for testing
 purposes only.  Please report any defects via the Samba bug reporting
 system at https://bugzilla.samba.org/.
 
-Samba 4.4 will be the next version of the Samba suite.
+Samba 4.5 will be the next version of the Samba suite.
 
 
 UPGRADING
@@ -18,215 +18,20 @@ Nothing special.
 NEW FEATURES/CHANGES
 ====================
 
-Asynchronous flush requests
----------------------------
-
-Flush requests from SMB2/3 clients are handled asynchronously and do
-not block the processing of other requests. Note that 'strict sync'
-has to be set to 'yes' for Samba to honor flush requests from SMB
-clients.
-
-s3: smbd
---------
-
-Remove '--with-aio-support' configure option. We no longer would ever prefer
-POSIX-RT aio, use pthread_aio instead.
-
-samba-tool sites
-----------------
-
-The 'samba-tool sites' subcommand can now be run against another server by
-specifying an LDB URL using the '-H' option and not against the local database
-only (which is still the default when no URL is given).
-
-samba-tool domain demote
-------------------------
-
-Add '--remove-other-dead-server' option to 'samba-tool domain demote'
-subcommand. The new version of this tool now can remove another DC that is
-itself offline.  The '--remove-other-dead-server' removes as many references
-to the DC as possible.
-
-samba-tool drs clone-dc-database
---------------------------------
-
-Replicate an initial clone of domain, but do not join it.
-This is developed for debugging purposes, but not for setting up another DC.
-
-pdbedit
--------
-
-Add '--set-nt-hash' option to pdbedit to update user password from nt-hash
-hexstring. 'pdbedit -vw' shows also password hashes.
-
-smbstatus
----------
-
-'smbstatus' was enhanced to show the state of signing and encryption for
-sessions and shares.
-
-smbget
-------
-The -u and -p options for user and password were replaced by the -U option that
-accepts username[%password] as in many other tools of the Samba suite.
-Similary, smbgetrc files do not accept username and password options any more,
-only a single "user" option which also accepts user%password combinations.
-
-s4-rpc_server
--------------
-
-Add a GnuTLS based backupkey implementation.
-
-ntlm_auth
----------
-
-Using the '--offline-logon' enables ntlm_auth to use cached passwords when the
-DC is offline.
-
-Allow '--password' force a local password check for ntlm-server-1 mode.
-
-vfs_offline
------------
-
-A new VFS module called vfs_offline has been added to mark all files in the
-share as offline. It can be useful for shares mounted on top of a remote file
-system (either through a samba VFS module or via FUSE).
-
-KCC
----
-
-The Samba KCC has been improved, but is still disabled by default.
-
-DNS
----
-
-There were several improvements concerning the Samba DNS server.
-
-Active Directory
-----------------
-
-There were some improvements in the Active Directory area.
-
-WINS nsswitch module
---------------------
-
-The WINS nsswitch module has been rewritten to address memory issues and to
-simplify the code. The module now uses libwbclient to do WINS queries. This
-means that winbind needs to be running in order to resolve WINS names using
-the nss_wins module. This does not affect smbd.
-
-CTDB changes
-------------
-
-* CTDB now uses a newly implemented parallel database recovery scheme
-  that avoids deadlocks with smbd.
-
-  In certain circumstances CTDB and smbd could deadlock.  The new
-  recovery implementation avoid this.  It also provides improved
-  recovery performance.
-
-* All files are now installed into and referred to by the paths
-  configured at build time.  Therefore, CTDB will now work properly
-  when installed into the default location at /usr/local.
-
-* Public CTDB header files are no longer installed, since Samba and
-  CTDB are built from within the same source tree.
-
-* CTDB_DBDIR can now be set to tmpfs[:<tmpfs-options>]
-
-  This will cause volatile TDBs to be located in a tmpfs.  This can
-  help to avoid performance problems associated with contention on the
-  disk where volatile TDBs are usually stored.  See ctdbd.conf(5) for
-  more details.
-
-* Configuration variable CTDB_NATGW_SLAVE_ONLY is no longer used.
-  Instead, nodes should be annotated with the "slave-only" option in
-  the CTDB NAT gateway nodes file.  This file must be consistent
-  across nodes in a NAT gateway group.  See ctdbd.conf(5) for more
-  details.
-
-* New event script 05.system allows various system resources to be
-  monitored
-
-  This can be helpful for explaining poor performance or unexpected
-  behaviour.  New configuration variables are
-  CTDB_MONITOR_FILESYSTEM_USAGE, CTDB_MONITOR_MEMORY_USAGE and
-  CTDB_MONITOR_SWAP_USAGE.  Default values cause warnings to be
-  logged.  See the SYSTEM RESOURCE MONITORING CONFIGURATION in
-  ctdbd.conf(5) for more information.
-
-  The memory, swap and filesystem usage monitoring previously found in
-  00.ctdb and 40.fs_use is no longer available.  Therefore,
-  configuration variables CTDB_CHECK_FS_USE, CTDB_MONITOR_FREE_MEMORY,
-  CTDB_MONITOR_FREE_MEMORY_WARN and CTDB_CHECK_SWAP_IS_NOT_USED are
-  now ignored.
-
-* The 62.cnfs eventscript has been removed.  To get a similar effect
-  just do something like this:
-
-      mmaddcallback ctdb-disable-on-quorumLoss \
-        --command /usr/bin/ctdb \
-        --event quorumLoss --parms "disable"
-
-      mmaddcallback ctdb-enable-on-quorumReached \
-        --command /usr/bin/ctdb \
-        --event quorumReached --parms "enable"
-
-* The CTDB tunable parameter EventScriptTimeoutCount has been renamed
-  to MonitorTimeoutCount
-
-  It has only ever been used to limit timed-out monitor events.
-
-  Configurations containing CTDB_SET_EventScriptTimeoutCount=<n> will
-  cause CTDB to fail at startup.  Useful messages will be logged.
-
-* The commandline option "-n all" to CTDB tool has been removed.
-
-  The option was not uniformly implemented for all the commands.
-  Instead of command "ctdb ip -n all", use "ctdb ip all".
-
-* All CTDB current manual pages are now correctly installed
+TODO...
 
 
 REMOVED FEATURES
 ================
 
-Public headers
---------------
-
-Several public headers are not installed any longer. They are made for internal
-use only. More public headers will very likely be removed in future releases.
-
-The following headers are not installed any longer:
-dlinklist.h, gen_ndr/epmapper.h, gen_ndr/mgmt.h, gen_ndr/ndr_atsvc_c.h,
-gen_ndr/ndr_epmapper_c.h, gen_ndr/ndr_epmapper.h, gen_ndr/ndr_mgmt_c.h,
-gen_ndr/ndr_mgmt.h,gensec.h, ldap_errors.h, ldap_message.h, ldap_ndr.h,
-ldap-util.h, pytalloc.h, read_smb.h, registry.h, roles.h, samba_util.h,
-smb2_constants.h, smb2_create_blob.h, smb2.h, smb2_lease.h, smb2_signing.h,
-smb_cli.h, smb_cliraw.h, smb_common.h, smb_composite.h, smb_constants.h,
-smb_raw.h, smb_raw_interfaces.h, smb_raw_signing.h, smb_raw_trans2.h,
-smb_request.h, smb_seal.h, smb_signing.h, smb_unix_ext.h, smb_util.h,
-torture.h, tstream_smbXcli_np.h.
-
-vfs_smb_traffic_analyzer
-------------------------
-
-The SMB traffic analyzer VFS module has been removed, because it is not
-maintained any longer and not widely used.
-
-vfs_scannedonly
----------------
+TODO...
 
-The scannedonly VFS module has been removed, because it is not maintained
-any longer.
 
 smb.conf changes
 ----------------
 
   Parameter Name		Description		Default
   --------------		-----------		-------
-  aio max threads               New                     100
-  ldap page size		Changed default		1000
 
 
 KNOWN ISSUES
-- 
1.9.1


From a6050d2a2eff855b7f3bff38dc7a8eac037b2d8c Mon Sep 17 00:00:00 2001
From: Stefan Metzmacher <metze at samba.org>
Date: Wed, 17 Feb 2016 11:29:16 +0100
Subject: [PATCH 02/22] WHATSNEW: add 'Support for
 LDAP_SERVER_NOTIFICATION_OID'

Signed-off-by: Stefan Metzmacher <metze at samba.org>
---
 WHATSNEW.txt | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/WHATSNEW.txt b/WHATSNEW.txt
index 07241db..2883f5e 100644
--- a/WHATSNEW.txt
+++ b/WHATSNEW.txt
@@ -18,6 +18,14 @@ Nothing special.
 NEW FEATURES/CHANGES
 ====================
 
+Support for LDAP_SERVER_NOTIFICATION_OID
+----------------------------------------
+
+The ldap server has support for the LDAP_SERVER_NOTIFICATION_OID
+control. This can be used to monitor the active directory database
+for changes.
+
+
 TODO...
 
 
-- 
1.9.1


From e4ed8d856b1f4f7245eff68578ad04c683b284b5 Mon Sep 17 00:00:00 2001
From: Stefan Metzmacher <metze at samba.org>
Date: Thu, 11 Feb 2016 17:51:29 +0100
Subject: [PATCH 03/22] python:samba: move netcmd/time.py to
 python/samba/netcmd/nettime.py

This allows 'import time' to work.

Signed-off-by: Stefan Metzmacher <metze at samba.org>
---
 python/samba/netcmd/main.py    |  2 +-
 python/samba/netcmd/nettime.py | 59 ++++++++++++++++++++++++++++++++++++++++++
 python/samba/netcmd/time.py    | 59 ------------------------------------------
 3 files changed, 60 insertions(+), 60 deletions(-)
 create mode 100644 python/samba/netcmd/nettime.py
 delete mode 100644 python/samba/netcmd/time.py

diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index 471c6b4..498702c 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -35,7 +35,7 @@ from samba.netcmd.rodc import cmd_rodc
 from samba.netcmd.sites import cmd_sites
 from samba.netcmd.spn import cmd_spn
 from samba.netcmd.testparm import cmd_testparm
-from samba.netcmd.time import cmd_time
+from samba.netcmd.nettime import cmd_time
 from samba.netcmd.user import cmd_user
 from samba.netcmd.processes import cmd_processes
 
diff --git a/python/samba/netcmd/nettime.py b/python/samba/netcmd/nettime.py
new file mode 100644
index 0000000..694b6ad
--- /dev/null
+++ b/python/samba/netcmd/nettime.py
@@ -0,0 +1,59 @@
+# time
+#
+# Copyright Jelmer Vernooij 2010 <jelmer at samba.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import samba.getopt as options
+import common
+from samba.net import Net
+
+from samba.netcmd import (
+    Command,
+    )
+
+class cmd_time(Command):
+    """Retrieve the time on a server.
+
+This command returns the date and time of the Active Directory server specified on the command.  The server name specified may be the local server or a remote server.  If the servername is not specified, the command returns the time and date of the local AD server.
+
+Example1:
+samba-tool time samdom.example.com
+
+Example1 returns the date and time of the server samdom.example.com.
+
+Example2:
+samba-tool time
+
+Example2 return the date and time of the local server.
+"""
+    synopsis = "%prog [server-name] [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    takes_args = ["server_name?"]
+
+    def run(self, server_name=None, credopts=None, sambaopts=None,
+            versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        net = Net(creds, lp, server=credopts.ipaddress)
+        if server_name is None:
+            server_name = common.netcmd_dnsname(lp)
+        self.outf.write(net.time(server_name)+"\n")
diff --git a/python/samba/netcmd/time.py b/python/samba/netcmd/time.py
deleted file mode 100644
index 694b6ad..0000000
--- a/python/samba/netcmd/time.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# time
-#
-# Copyright Jelmer Vernooij 2010 <jelmer at samba.org>
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-
-import samba.getopt as options
-import common
-from samba.net import Net
-
-from samba.netcmd import (
-    Command,
-    )
-
-class cmd_time(Command):
-    """Retrieve the time on a server.
-
-This command returns the date and time of the Active Directory server specified on the command.  The server name specified may be the local server or a remote server.  If the servername is not specified, the command returns the time and date of the local AD server.
-
-Example1:
-samba-tool time samdom.example.com
-
-Example1 returns the date and time of the server samdom.example.com.
-
-Example2:
-samba-tool time
-
-Example2 return the date and time of the local server.
-"""
-    synopsis = "%prog [server-name] [options]"
-
-    takes_optiongroups = {
-        "sambaopts": options.SambaOptions,
-        "credopts": options.CredentialsOptions,
-        "versionopts": options.VersionOptions,
-        }
-
-    takes_args = ["server_name?"]
-
-    def run(self, server_name=None, credopts=None, sambaopts=None,
-            versionopts=None):
-        lp = sambaopts.get_loadparm()
-        creds = credopts.get_credentials(lp, fallback_machine=True)
-        net = Net(creds, lp, server=credopts.ipaddress)
-        if server_name is None:
-            server_name = common.netcmd_dnsname(lp)
-        self.outf.write(net.time(server_name)+"\n")
-- 
1.9.1


From f70e6c379bfd9cd80a8ad2e5e4e41e47bd76b9ce 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..87fb181 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()
+    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
+
+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
+except NotImplementedError as e:
+    reason = "modern '$5$' salt in crypt(3) required"
+    disabled_virtual_attributes["virtualCryptSHA256"] = {
+        "reason" : reason,
+        }
+    pass
+
+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
+except NotImplementedError as e:
+    reason = "modern '$6$' salt in crypt(3) required"
+    disabled_virtual_attributes["virtualCryptSHA512"] = {
+        "reason" : reason,
+        }
+    pass
+
+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)
+        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:
+            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:
+            # 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 $5$... 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)
+
+    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 e650cdb88b3da5a02e61016a7cae9f9d6d2f3af1 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 7b69a4a8481b70ffd8a34b6d85ffd6dcd2085569 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 5798e2aaf07f0c259d4c7e046113a44c455e3003 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 87fb181..897c89a 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 $5$... 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 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 67040a800eb344078d5e025433595b7985118aad 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 032165df853652768d62934d712514fca042c9a2 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 ae49a8848f8be29763c8aa409c758b23f65ad072 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 7edb362fc308279828dc547bb3938c3287885b3f 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 a6c0d6cde4bfd514d6a63f07751237e33f759318 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 b4d16282d3b10df697142025642d316c79136459 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 007dc64ed276217e721a4e0fe71cdbdefe4e00a7 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 3a44e118b9a16c950709790491e7f98fae190945 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 5e5f0a659f2e32149a98afa37394fb221b6ccb49 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 413447c65325ecf5fb7e713ae2b9c8013889badc 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 897c89a..652a6a6 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 $5$... 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 $5$... 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 c6f499feb985acc4f64ea5fd2f2a02b5401ace5c 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 0156da1464fc596e07b460214bf3ee6741ee57e7 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 d76eda6..967c048 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 75f95ceb072dd0fbd629847e499adac0b49a70dc 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 2343cec..79f146c 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 2f211eae7506520e837d00ab8e921fdafddd6bf0 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 499ab08d0947fecd4adddba7113320e23d71d5a5 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 | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/WHATSNEW.txt b/WHATSNEW.txt
index 2883f5e..7de2d72 100644
--- a/WHATSNEW.txt
+++ b/WHATSNEW.txt
@@ -25,6 +25,18 @@ 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...
 
@@ -40,6 +52,7 @@ smb.conf changes
 
   Parameter Name		Description		Default
   --------------		-----------		-------
+  password hash gpg key ids     New
 
 
 KNOWN ISSUES
-- 
1.9.1

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 836 bytes
Desc: OpenPGP digital signature
URL: <http://lists.samba.org/pipermail/samba-technical/attachments/20160217/cfa47bfa/signature.sig>


More information about the samba-technical mailing list