diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py
index 95597898e6..da67ed244c 100644
--- a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py
+++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py
@@ -9,7 +9,6 @@
import pytest
import os
from lib389.topologies import topology_st
-from lib389.password_plugins import PBKDF2Plugin
from lib389.utils import ds_is_older
from lib389.migrate.openldap.config import olConfig
from lib389.migrate.openldap.config import olOverlayType
diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py
index 4092bb36df..e5f749c01a 100644
--- a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py
+++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py
@@ -9,7 +9,6 @@
import pytest
import os
from lib389.topologies import topology_st
-from lib389.password_plugins import PBKDF2Plugin
from lib389.utils import ds_is_older
from lib389.migrate.openldap.config import olConfig
from lib389.migrate.openldap.config import olOverlayType
diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py
index bf056f0e05..6935900a55 100644
--- a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py
+++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py
@@ -9,7 +9,6 @@
import pytest
import os
from lib389.topologies import topology_st
-from lib389.password_plugins import PBKDF2Plugin
from lib389.utils import ds_is_older
from lib389.migrate.openldap.config import olConfig
from lib389.migrate.openldap.config import olOverlayType
diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py
index 492c94fc8f..fa41a9daf3 100644
--- a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py
+++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py
@@ -9,7 +9,6 @@
import pytest
import os
from lib389.topologies import topology_st
-from lib389.password_plugins import PBKDF2Plugin
from lib389.utils import ds_is_older
from lib389.migrate.openldap.config import olConfig
from lib389.migrate.openldap.config import olOverlayType
diff --git a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py
index 90dae36ec7..6652013b15 100644
--- a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py
+++ b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py
@@ -8,7 +8,7 @@
#
import pytest
from lib389.topologies import topology_st
-from lib389.password_plugins import PBKDF2Plugin
+from lib389.password_plugins import PBKDF2SHA256Plugin
from lib389.utils import ds_is_older
pytestmark = pytest.mark.tier1
@@ -35,18 +35,18 @@ def test_pbkdf2_upgrade(topology_st):
"""
# Remove the pbkdf2 plugin config
- p1 = PBKDF2Plugin(topology_st.standalone)
+ p1 = PBKDF2SHA256Plugin(topology_st.standalone)
assert(p1.exists())
p1._protected = False
p1.delete()
# Restart
topology_st.standalone.restart()
# check it's been readded.
- p2 = PBKDF2Plugin(topology_st.standalone)
+ p2 = PBKDF2SHA256Plugin(topology_st.standalone)
assert(p2.exists())
# Now restart to make sure we still work from the non-bootstrap form
topology_st.standalone.restart()
- p3 = PBKDF2Plugin(topology_st.standalone)
+ p3 = PBKDF2SHA256Plugin(topology_st.standalone)
assert(p3.exists())
diff --git a/dirsrvtests/tests/suites/pwp_storage/storage_test.py b/dirsrvtests/tests/suites/pwp_storage/storage_test.py
index ed0dd9e533..4ee7e896be 100644
--- a/dirsrvtests/tests/suites/pwp_storage/storage_test.py
+++ b/dirsrvtests/tests/suites/pwp_storage/storage_test.py
@@ -18,13 +18,54 @@
from lib389.topologies import topology_st as topo
from lib389.idm.user import UserAccounts, UserAccount
-from lib389._constants import DEFAULT_SUFFIX
+from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD, ErrorLog
from lib389.config import Config
-from lib389.password_plugins import PBKDF2Plugin, SSHA512Plugin
+from lib389.password_plugins import (
+ SSHA512Plugin,
+ PBKDF2SHA1Plugin,
+ PBKDF2SHA256Plugin,
+ PBKDF2SHA512Plugin
+)
from lib389.utils import ds_is_older
pytestmark = pytest.mark.tier1
+PBKDF2_SCHEMES = [
+ ('PBKDF2-SHA1', PBKDF2SHA1Plugin, 70000),
+ ('PBKDF2-SHA256', PBKDF2SHA256Plugin, 30000),
+ ('PBKDF2-SHA512', PBKDF2SHA512Plugin, 10000),
+]
+
+
+@pytest.fixture(scope="function")
+def new_user(request, topo):
+ """Fixture to create and clean up a test user for each test"""
+ # Generate unique user ID based on test name
+ uid = f'new_user_{request.node.name[:20]}'
+
+ # Create user
+ users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+ user = users.create(properties={
+ 'uid': uid,
+ 'cn': 'Test User',
+ 'sn': 'User',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': f'/home/{uid}'
+ })
+
+ def fin():
+ try:
+ # Ensure we're bound as DM before cleanup
+ topo.standalone.simple_bind_s(DN_DM, PASSWORD)
+ if user.exists():
+ user.delete()
+ except Exception as e:
+ log.error(f"Error during user cleanup: {e}")
+
+ request.addfinalizer(fin)
+ return user
+
def user_config(topo, field_value):
"""
@@ -62,6 +103,248 @@ def test_check_password_scheme(topo, value):
user.delete()
+@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES)
+def test_pbkdf2_default_rounds(topo, new_user, scheme_name, plugin_class, default_rounds):
+ """Test PBKDF2 schemes with default iteration rounds.
+
+ :id: bd58cd76-14f9-4d54-9793-ee7bba8e5369
+ :parametrized: yes
+ :setup: Standalone
+ :steps:
+ 1. Remove any existing rounds configuration
+ 2. Verify default rounds are used
+ 3. Set password and verify hash format
+ 4. Test authentication
+ :expectedresults:
+ 1. Pass
+ 2. Pass
+ 3. Pass
+ 4. Pass
+ """
+ try:
+ # Flush logs
+ topo.standalone.restart()
+ topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))
+ topo.standalone.deleteErrorLogs()
+
+ plugin = plugin_class(topo.standalone)
+ plugin.remove_all('nsslapd-pwdpbkdf2numiterations')
+ topo.standalone.restart()
+
+ topo.standalone.config.replace('passwordStorageScheme', scheme_name)
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == default_rounds, \
+ f"Expected default {default_rounds} rounds, got {current_rounds}"
+
+ new_user.set('userPassword', 'Secret123')
+ pwd_hash = new_user.get_attr_val_utf8('userPassword')
+ assert pwd_hash.startswith('{' + scheme_name.upper() + '}')
+ assert str(default_rounds) in pwd_hash
+
+ topo.standalone.simple_bind_s(new_user.dn, 'Secret123')
+
+ assert topo.standalone.searchErrorsLog(
+ f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default'
+ )
+ finally:
+ topo.standalone.simple_bind_s(DN_DM, PASSWORD)
+
+
+@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES)
+def test_pbkdf2_rounds_reset(topo, new_user, scheme_name, plugin_class, default_rounds):
+ """Test PBKDF2 schemes rounds reset to defaults.
+
+ :id: 59bf95c5-6a07-4db1-81eb-d59b54436826
+ :parametrized: yes
+ :setup: Standalone
+ :steps:
+ 1. Set custom rounds for PBKDF2 plugin
+ 2. Verify custom rounds are used
+ 3. Remove rounds configuration
+ 4. Verify defaults are restored
+ 5. Test password operations with default rounds
+ :expectedresults:
+ 1. Pass
+ 2. Pass
+ 3. Pass
+ 4. Pass
+ 5. Pass
+ """
+ try:
+ # Flush logs
+ topo.standalone.restart()
+ topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))
+ topo.standalone.deleteErrorLogs()
+
+ test_rounds = 25000
+ plugin = plugin_class(topo.standalone)
+ plugin.set_rounds(test_rounds)
+ topo.standalone.restart()
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == test_rounds, \
+ f"Expected {test_rounds} rounds, got {current_rounds}"
+
+ plugin.remove_all('nsslapd-pwdpbkdf2numiterations')
+ topo.standalone.restart()
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == default_rounds, \
+ f"Expected default {default_rounds} rounds after reset, got {current_rounds}"
+
+ topo.standalone.config.replace('passwordStorageScheme', scheme_name)
+
+ new_user.set('userPassword', 'Secret123')
+ pwd_hash = new_user.get_attr_val_utf8('userPassword')
+ assert pwd_hash.startswith('{' + scheme_name.upper() + '}')
+ assert str(default_rounds) in pwd_hash
+
+ topo.standalone.simple_bind_s(new_user.dn, 'Secret123')
+
+ assert topo.standalone.searchErrorsLog(
+ f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default'
+ )
+ finally:
+ topo.standalone.simple_bind_s(DN_DM, PASSWORD)
+
+
+@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES)
+@pytest.mark.parametrize('rounds', [10000, 20000, 50000])
+def test_pbkdf2_custom_rounds(topo, new_user, scheme_name, plugin_class, _, rounds):
+ """Test PBKDF2 schemes with custom iteration rounds.
+
+ :id: 6bec6542-ed8d-4a0e-89d6-e047757767c2
+ :parametrized: yes
+ :setup: Standalone
+ :steps:
+ 1. Set custom rounds for PBKDF2 plugin
+ 2. Verify rounds are set correctly
+ 3. Set password and verify hash format
+ 4. Test authentication
+ 5. Verify rounds in password hash
+ :expectedresults:
+ 1. Pass
+ 2. Pass
+ 3. Pass
+ 4. Pass
+ 5. Pass
+ """
+ try:
+ # Flush logs
+ topo.standalone.restart()
+ topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))
+ topo.standalone.deleteErrorLogs()
+
+ plugin = plugin_class(topo.standalone)
+ plugin.set_rounds(rounds)
+ topo.standalone.restart()
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == rounds, \
+ f"Expected {rounds} rounds, got {current_rounds}"
+
+ topo.standalone.config.replace('passwordStorageScheme', scheme_name)
+
+ new_user.set('userPassword', 'Secret123')
+ pwd_hash = new_user.get_attr_val_utf8('userPassword')
+ assert pwd_hash.startswith('{' + scheme_name.upper() + '}')
+ assert str(rounds) in pwd_hash
+
+ topo.standalone.simple_bind_s(new_user.dn, 'Secret123')
+
+ assert topo.standalone.searchErrorsLog(
+ f'Number of iterations for {scheme_name}'
+ )
+ finally:
+ topo.standalone.simple_bind_s(DN_DM, PASSWORD)
+
+
+@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES)
+def test_pbkdf2_invalid_rounds(topo, scheme_name, plugin_class, _):
+ """Test PBKDF2 schemes with invalid iteration rounds.
+
+ :id: 4e5b4f37-c97b-4f58-b5c5-726495d9fa4e
+ :parametrized: yes
+ :setup: Standalone
+ :steps:
+ 1. Try to set invalid rounds (too low and too high)
+ 2. Verify appropriate errors are raised
+ 3. Verify original rounds are maintained
+ :expectedresults:
+ 1. Pass
+ 2. Pass
+ 3. Pass
+ """
+ # Flush logs
+ topo.standalone.restart()
+ topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))
+ topo.standalone.deleteErrorLogs()
+
+ plugin = plugin_class(topo.standalone)
+ plugin.enable()
+
+ original_rounds = plugin.get_rounds()
+
+ with pytest.raises(ValueError) as excinfo:
+ plugin.set_rounds(5000)
+ assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value)
+
+ with pytest.raises(ValueError) as excinfo:
+ plugin.set_rounds(20000000)
+ assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value)
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == original_rounds, \
+ f"Rounds changed from {original_rounds} to {current_rounds}"
+
+
+@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES)
+def test_pbkdf2_rounds_persistence(topo, new_user, scheme_name, plugin_class, _):
+ """Test PBKDF2 rounds persistence across server restarts.
+
+ :id: b15de1ae-53ac-429f-991b-cea5e6a7b383
+ :parametrized: yes
+ :setup: Standalone
+ :steps:
+ 1. Set custom rounds for PBKDF2 plugin
+ 2. Restart server
+ 3. Verify rounds are maintained
+ 4. Set password and verify hash
+ 5. Test authentication
+ :expectedresults:
+ 1. Pass
+ 2. Pass
+ 3. Pass
+ 4. Pass
+ 5. Pass
+ """
+ try:
+ # Flush logs
+ topo.standalone.restart()
+ topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN))
+ topo.standalone.deleteErrorLogs()
+
+ test_rounds = 15000
+ plugin = plugin_class(topo.standalone)
+ plugin.set_rounds(test_rounds)
+ topo.standalone.restart()
+
+ current_rounds = plugin.get_rounds()
+ assert current_rounds == test_rounds, \
+ f"Expected {test_rounds} rounds after restart, got {current_rounds}"
+
+ topo.standalone.config.replace('passwordStorageScheme', scheme_name)
+
+ new_user.set('userPassword', 'Secret123')
+ pwd_hash = new_user.get_attr_val_utf8('userPassword')
+ assert str(test_rounds) in pwd_hash
+
+ topo.standalone.simple_bind_s(new_user.dn, 'Secret123')
+ finally:
+ topo.standalone.simple_bind_s(DN_DM, PASSWORD)
+
+
def test_clear_scheme(topo):
"""Check clear password scheme.
@@ -106,27 +389,27 @@ def test_check_two_scheme(topo):
user.delete()
@pytest.mark.skipif(ds_is_older('1.4'), reason="Not implemented")
-def test_check_pbkdf2_sha256(topo):
- """Check password scheme PBKDF2_SHA256.
+def test_check_pbkdf2_sha512(topo):
+ """Check password scheme PBKDF2-SHA512 is restored after deletion
:id: 31612e7e-33a6-11ea-a750-8c16451d917b
:setup: Standalone
:steps:
- 1. Try to delete PBKDF2_SHA256.
- 2. Should not deleted PBKDF2_SHA256 and server should up.
+ 1. Try to delete PBKDF2-SHA512.
+ 2. Should not deleted PBKDF2-SHA512 and server should up.
:expectedresults:
1. Pass
2. Pass
"""
- value = 'PBKDF2_SHA256'
+ value = 'PBKDF2-SHA512'
user = user_config(topo, value)
assert '{' + f'{value.lower()}' + '}' in \
UserAccount(topo.standalone, user.dn).get_attr_val_utf8('userpassword').lower()
- plg = PBKDF2Plugin(topo.standalone)
+ plg = PBKDF2SHA512Plugin(topo.standalone)
plg._protected = False
plg.delete()
topo.standalone.restart()
- assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == 'PBKDF2_SHA256'
+ assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == value
assert topo.standalone.status()
user.delete()
diff --git a/ldap/ldif/template-dse-minimal.ldif.in b/ldap/ldif/template-dse-minimal.ldif.in
index d2b02f8be9..264cc51e06 100644
--- a/ldap/ldif/template-dse-minimal.ldif.in
+++ b/ldap/ldif/template-dse-minimal.ldif.in
@@ -195,6 +195,7 @@ nsslapd-pluginenabled: on
dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init
@@ -208,6 +209,7 @@ nsslapd-pluginDescription: PBKDF2
dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA1
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init
@@ -221,6 +223,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\
dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA256
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init
@@ -234,6 +237,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\
dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA512
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init
diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in
index ce44d75cd1..ccb736eb86 100644
--- a/ldap/ldif/template-dse.ldif.in
+++ b/ldap/ldif/template-dse.ldif.in
@@ -252,6 +252,7 @@ nsslapd-pluginenabled: on
dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init
@@ -265,6 +266,7 @@ nsslapd-pluginDescription: PBKDF2
dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA1
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init
@@ -278,6 +280,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\
dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA256
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init
@@ -291,6 +294,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\
dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
+objectClass: pwdPBKDF2PluginConfig
cn: PBKDF2-SHA512
nsslapd-pluginpath: libpwdchan-plugin
nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init
diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif
index c98e5b34b3..bfe8259f82 100644
--- a/ldap/schema/01core389.ldif
+++ b/ldap/schema/01core389.ldif
@@ -332,6 +332,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2391 NAME 'dsEntryDN' DESC '389 Director
attributeTypes: ( 2.16.840.1.113730.3.1.2392 NAME 'nsslapd-return-original-entrydn' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
attributeTypes: ( 2.16.840.1.113730.3.1.2393 NAME 'nsslapd-auditlog-display-attrs' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
attributeTypes: ( 2.16.840.1.113730.3.1.2398 NAME 'nsslapd-haproxy-trusted-ip' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2400 NAME 'nsslapd-pwdPBKDF2NumIterations' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Directory Server' )
#
# objectclasses
#
@@ -353,3 +354,4 @@ objectClasses: ( 2.16.840.1.113730.3.2.327 NAME 'rootDNPluginConfig' DESC 'Netsc
objectClasses: ( 2.16.840.1.113730.3.2.328 NAME 'nsSchemaPolicy' DESC 'Netscape defined objectclass' SUP top MAY ( cn $ schemaUpdateObjectclassAccept $ schemaUpdateObjectclassReject $ schemaUpdateAttributeAccept $ schemaUpdateAttributeReject) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.332 NAME 'nsChangelogConfig' DESC 'Configuration of the changelog5 object' SUP top MUST ( cn $ nsslapd-changelogdir ) MAY ( nsslapd-changelogmaxage $ nsslapd-changelogtrim-interval $ nsslapd-changelogmaxentries $ nsslapd-changelogsuffix $ nsslapd-changelogcompactdb-interval $ nsslapd-encryptionalgorithm $ nsSymmetricKey ) X-ORIGIN '389 Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.337 NAME 'rewriterEntry' DESC '' SUP top MUST ( nsslapd-libPath ) MAY ( cn $ nsslapd-filterrewriter $ nsslapd-returnedAttrRewriter ) X-ORIGIN '389 Directory Server' )
+objectClasses: ( 2.16.840.1.113730.3.2.340 NAME 'pwdPBKDF2PluginConfig' DESC 'PBKDF2 Password Storage Plugin configuration' SUP top MAY ( nsslapd-pwdPBKDF2NumIterations ) X-ORIGIN '389 Directory Server' )
diff --git a/ldap/servers/slapd/config.c b/ldap/servers/slapd/config.c
index eb8c9a4fee..3db7b7840c 100644
--- a/ldap/servers/slapd/config.c
+++ b/ldap/servers/slapd/config.c
@@ -43,6 +43,7 @@ static char *bootstrap_plugins[] = {
"dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config\n"
"objectclass: top\n"
"objectclass: nsSlapdPlugin\n"
+ "objectClass: pwdPBKDF2PluginConfig\n"
"cn: PBKDF2-SHA512\n"
"nsslapd-pluginpath: libpwdchan-plugin\n"
"nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init\n"
diff --git a/ldap/servers/slapd/fedse.c b/ldap/servers/slapd/fedse.c
index 74f7e6cf99..85bcf063c8 100644
--- a/ldap/servers/slapd/fedse.c
+++ b/ldap/servers/slapd/fedse.c
@@ -231,6 +231,7 @@ static const char *internal_entries[] =
"dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config\n"
"objectclass: top\n"
"objectclass: nsSlapdPlugin\n"
+ "objectClass: pwdPBKDF2PluginConfig\n"
"cn: PBKDF2\n"
"nsslapd-pluginpath: libpwdchan-plugin\n"
"nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init\n"
@@ -244,6 +245,7 @@ static const char *internal_entries[] =
"dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config\n"
"objectclass: top\n"
"objectclass: nsSlapdPlugin\n"
+ "objectClass: pwdPBKDF2PluginConfig\n"
"cn: PBKDF2-SHA1\n"
"nsslapd-pluginpath: libpwdchan-plugin\n"
"nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init\n"
@@ -257,6 +259,7 @@ static const char *internal_entries[] =
"dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config\n"
"objectclass: top\n"
"objectclass: nsSlapdPlugin\n"
+ "objectClass: pwdPBKDF2PluginConfig\n"
"cn: PBKDF2-SHA256\n"
"nsslapd-pluginpath: libpwdchan-plugin\n"
"nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init\n"
diff --git a/src/cockpit/389-console/src/lib/database/globalPwp.jsx b/src/cockpit/389-console/src/lib/database/globalPwp.jsx
index a757361ec3..8681584677 100644
--- a/src/cockpit/389-console/src/lib/database/globalPwp.jsx
+++ b/src/cockpit/389-console/src/lib/database/globalPwp.jsx
@@ -89,6 +89,16 @@ const tpr_attrs = [
"passwordtprdelayvalidfrom",
];
+const password_storage_attrs = [
+ "nsslapd-pwdpbkdf2numiterations"
+];
+
+const PBKDF2_SCHEMES = ['pbkdf2', 'pbkdf2-sha1', 'pbkdf2-sha256', 'pbkdf2-sha512'];
+
+const isPBKDF2Scheme = (scheme) => {
+ return PBKDF2_SCHEMES.includes(scheme.toLowerCase());
+};
+
export class GlobalPwPolicy extends React.Component {
constructor(props) {
super(props);
@@ -102,6 +112,7 @@ export class GlobalPwPolicy extends React.Component {
// each field, so we can loop over them to efficently
// check for changes, and updating/saving the config.
saveGeneralDisabled: true,
+ savePasswordStorageDisabled: true,
saveExpDisabled: true,
saveLockoutDisabled: true,
saveSyntaxDisabled: true,
@@ -118,6 +129,8 @@ export class GlobalPwPolicy extends React.Component {
this.handleGeneralChange = this.handleGeneralChange.bind(this);
this.handleSaveGeneral = this.handleSaveGeneral.bind(this);
+ this.handlePasswordStorageChange = this.handlePasswordStorageChange.bind(this);
+ this.handleSavePasswordStorage = this.handleSavePasswordStorage.bind(this);
this.handleExpChange = this.handleExpChange.bind(this);
this.handleSaveExp = this.handleSaveExp.bind(this);
this.handleLockoutChange = this.handleLockoutChange.bind(this);
@@ -127,6 +140,7 @@ export class GlobalPwPolicy extends React.Component {
this.handleTPRChange = this.handleTPRChange.bind(this);
this.handleSaveTPR = this.handleSaveTPR.bind(this);
this.handleLoadGlobal = this.handleLoadGlobal.bind(this);
+ this.handleLoadPasswordStorage = this.handleLoadPasswordStorage.bind(this);
// Select Typeahead
this.handleSelectToggle = this.handleSelectToggle.bind(this);
this.handleSelectClear = this.handleSelectClear.bind(this);
@@ -145,6 +159,68 @@ export class GlobalPwPolicy extends React.Component {
this.setState({ activeKey: key });
}
+ handlePasswordStorageChange(e) {
+ const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
+ const attr = e.target.id.toLowerCase();
+ let disableSaveBtn = true;
+
+ for (const password_storage_attr of password_storage_attrs) {
+ const storageAttr = password_storage_attr.toLowerCase();
+ const oldValue = String(this.state['_' + storageAttr] || '');
+ const newValue = String(value || '');
+
+ if (attr === storageAttr && oldValue !== newValue) {
+ disableSaveBtn = false;
+ break;
+ }
+ }
+
+ this.setState({
+ [attr]: value || '',
+ savePasswordStorageDisabled: disableSaveBtn,
+ });
+ }
+
+ handleSavePasswordStorage() {
+ if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) {
+ return;
+ }
+ this.setState({
+ saving: true
+ });
+
+ const cmd = [
+ 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(),
+ 'set-num-iterations', this.state[password_storage_attrs[0]]
+ ];
+
+ log_cmd("handleSavePasswordStorage", "Saving password storage settings", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ this.handleLoadGlobal();
+ this.setState({
+ saving: false
+ });
+ this.props.addNotification(
+ "success",
+ _("Successfully updated number of iterations for password storage scheme")
+ );
+ })
+ .fail(err => {
+ const errMsg = JSON.parse(err);
+ this.handleLoadGlobal();
+ this.setState({
+ saving: false
+ });
+ this.props.addNotification(
+ "error",
+ cockpit.format(_("Error updating number of iterations for password storage scheme - $0"), errMsg.desc)
+ );
+ });
+ }
+
handleGeneralChange(e) {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
const attr = e.target.id;
@@ -166,9 +242,18 @@ export class GlobalPwPolicy extends React.Component {
}
}
- this.setState({
+ // Create state update object
+ const stateUpdate = {
[attr]: value,
saveGeneralDisabled: disableSaveBtn,
+ };
+
+ this.setState(stateUpdate, () => {
+ // If passwordstoragescheme was changed and it's a PBKDF2 scheme,
+ // load the iterations value
+ if (attr === 'passwordstoragescheme' && isPBKDF2Scheme(value)) {
+ this.handleLoadPasswordStorage(true);
+ }
});
}
@@ -176,6 +261,15 @@ export class GlobalPwPolicy extends React.Component {
this.setState({
saving: true
});
+ if (!this.state.savePasswordStorageDisabled) {
+ this.handleSavePasswordStorage();
+ }
+ if (this.state.saveGeneralDisabled) {
+ this.setState({
+ saving: false
+ });
+ return;
+ }
const cmd = [
'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
@@ -594,10 +688,74 @@ export class GlobalPwPolicy extends React.Component {
});
}
+ handleLoadPasswordStorage(skipLoading = false) {
+ if (!skipLoading) {
+ this.setState({
+ loading: true
+ });
+ }
+
+ if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) {
+ this.setState({
+ loading: false,
+ 'nsslapd-pwdpbkdf2numiterations': '',
+ '_nsslapd-pwdpbkdf2numiterations': ''
+ });
+ return;
+ }
+
+ const cmd = [
+ 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
+ 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(),
+ 'get-num-iterations'
+ ];
+
+ log_cmd("handleLoadPasswordStorage", "Load password storage settings", cmd);
+ cockpit
+ .spawn(cmd, { superuser: true, err: "message" })
+ .done(content => {
+ const config = JSON.parse(content);
+ const attrs = config.attrs;
+
+ const stateUpdates = {
+ 'nsslapd-pwdpbkdf2numiterations': '',
+ '_nsslapd-pwdpbkdf2numiterations': ''
+ };
+
+ if (!skipLoading) {
+ stateUpdates["loading"] = false
+ }
+ password_storage_attrs.forEach(attr => {
+ const attrLower = attr.toLowerCase();
+ const attrValue = attrs[attr] || attrs[attrLower];
+
+ if (attrValue && attrValue[0]) {
+ stateUpdates[attrLower] = attrValue[0];
+ stateUpdates['_' + attrLower] = attrValue[0];
+ }
+ });
+
+ this.setState(stateUpdates);
+ })
+ .fail(err => {
+ const errMsg = JSON.parse(err);
+ this.setState({
+ loading: false,
+ 'nsslapd-pwdpbkdf2numiterations': '',
+ '_nsslapd-pwdpbkdf2numiterations': ''
+ });
+ this.props.addNotification(
+ "error",
+ cockpit.format(_("Error loading password storage settings - $0"), errMsg.desc)
+ );
+ });
+ }
+
handleLoadGlobal() {
this.setState({
loading: true
});
+
const cmd = [
"dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
"config", "get"
@@ -700,6 +858,7 @@ export class GlobalPwPolicy extends React.Component {
loaded: true,
loading: false,
saveGeneralDisabled: true,
+ savePasswordStorageDisabled: true,
saveUserDisabled: true,
saveExpDisabled: true,
saveLockoutDisabled: true,
@@ -795,8 +954,10 @@ export class GlobalPwPolicy extends React.Component {
_passwordtprmaxuse: attrs.passwordtprmaxuse[0],
_passwordtprdelayexpireat: attrs.passwordtprdelayexpireat[0],
_passwordtprdelayvalidfrom: attrs.passwordtprdelayvalidfrom[0],
- }), this.props.enableTree()
- );
+ }), () => {
+ this.props.enableTree();
+ this.handleLoadPasswordStorage();
+ });
})
.fail(err => {
const errMsg = JSON.parse(err);
@@ -1385,6 +1546,25 @@ export class GlobalPwPolicy extends React.Component {
+ {isPBKDF2Scheme(this.state.passwordstoragescheme) && (
+
+
+ {_("PBKDF2 Iterations")}
+
+
+ {
+ this.handlePasswordStorageChange(e);
+ }}
+ />
+
+
+ )}
@@ -1439,7 +1619,7 @@ export class GlobalPwPolicy extends React.Component {