Makefile.am | 3
dirsrvtests/tests/tickets/ticket397_test.py | 151 ++++++++++++++++++
ldap/ldif/template-dse.ldif.in | 9 +
ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c | 216 +++++++++++++++++++++++++++
ldap/servers/plugins/pwdstorage/pwd_init.c | 20 ++
ldap/servers/plugins/pwdstorage/pwdstorage.h | 7
ldap/servers/slapd/pw.c | 7
ldap/servers/slapd/pw.h | 3
8 files changed, 414 insertions(+), 2 deletions(-)
New commits:
commit 542287ce724e4d3bd69d699d3d61c3e640cc1541
Author: William Brown <firstyear(a)redhat.com>
Date: Tue Aug 2 16:54:08 2016 +1000
Ticket 397 - Add PBKDF2 to Directory Server password storage.
Bug Description: We need to improve the cryptographic quality of hashes
available in DS to prevent attacks on hashes both online and offline.
Fix Description: PBKDF2 is a hash that provides "iterations" of complexity
and work time to ensure complexity on the behalf of an attacker. It makes it
harder to create rainbow tables, bruteforce, or hardware accelerate attacks.
https://fedorahosted.org/389/ticket/397
Author: wibrown
Review by: rrelyea, nhosoi (Thanks!)
diff --git a/Makefile.am b/Makefile.am
index b6ebf92..546f89e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1419,7 +1419,8 @@ libpwdstorage_plugin_la_SOURCES =
ldap/servers/plugins/pwdstorage/clear_pwd.c \
ldap/servers/plugins/pwdstorage/pwd_util.c \
ldap/servers/plugins/pwdstorage/sha_pwd.c \
ldap/servers/plugins/pwdstorage/smd5_pwd.c \
- ldap/servers/plugins/pwdstorage/ssha_pwd.c
+ ldap/servers/plugins/pwdstorage/ssha_pwd.c \
+ ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
libpwdstorage_plugin_la_CPPFLAGS = $(PLUGIN_CPPFLAGS)
libpwdstorage_plugin_la_LIBADD = libslapd.la $(NSS_LINK) $(NSPR_LINK) $(LIBCRYPT)
diff --git a/dirsrvtests/tests/tickets/ticket397_test.py
b/dirsrvtests/tests/tickets/ticket397_test.py
new file mode 100644
index 0000000..4bf4eda
--- /dev/null
+++ b/dirsrvtests/tests/tickets/ticket397_test.py
@@ -0,0 +1,151 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+DEBUGGING = False
+USER_DN = 'uid=user,ou=People,%s' % DEFAULT_SUFFIX
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+
+log = logging.getLogger(__name__)
+
+
+class TopologyStandalone(object):
+ """The DS Topology Class"""
+ def __init__(self, standalone):
+ """Init"""
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ """Create DS Deployment"""
+
+ # Creating standalone instance ...
+ if DEBUGGING:
+ standalone = DirSrv(verbose=True)
+ else:
+ standalone = DirSrv(verbose=False)
+ args_instance[SER_HOST] = HOST_STANDALONE
+ args_instance[SER_PORT] = PORT_STANDALONE
+ args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_standalone = args_instance.copy()
+ standalone.allocate(args_standalone)
+ instance_standalone = standalone.exists()
+ if instance_standalone:
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+ def fin():
+ """If we are debugging just stop the instances, otherwise remove
+ them
+ """
+ if DEBUGGING:
+ standalone.stop()
+ else:
+ standalone.delete()
+
+ request.addfinalizer(fin)
+
+ # Clear out the tmp dir
+ standalone.clearTmpDir(__file__)
+
+ return TopologyStandalone(standalone)
+
+def _test_bind(inst, password):
+ result = True
+ userconn = ldap.initialize("ldap://%s:%s" % (HOST_STANDALONE,
PORT_STANDALONE))
+ try:
+ userconn.simple_bind_s(USER_DN, password)
+ userconn.unbind_s()
+ except ldap.INVALID_CREDENTIALS:
+ result = False
+ return result
+
+def _test_algo(inst, algo_name):
+ inst.config.set('passwordStorageScheme', algo_name)
+
+ if DEBUGGING:
+ print('Testing %s' % algo_name)
+
+ # Create the user with a password
+ inst.add_s(Entry((
+ USER_DN, {
+ 'objectClass': 'top account
simplesecurityobject'.split(),
+ 'uid': 'user',
+ 'userpassword': ['Secret123', ]
+ })))
+
+ # Make sure when we read the userPassword field, it is the correct ALGO
+ pw_field = inst.search_s(USER_DN, ldap.SCOPE_BASE, '(objectClass=*)',
['userPassword'] )[0]
+
+ if DEBUGGING:
+ print(pw_field.getValue('userPassword'))
+
+ if algo_name != 'CLEAR':
+ lalgo_name = algo_name.lower()
+ lpw_algo_name = pw_field.getValue('userPassword').lower()
+ assert(lpw_algo_name.startswith("{%s}" % lalgo_name))
+ # Now make sure a bind works
+ assert(_test_bind(inst, 'Secret123'))
+ # Bind with a wrong shorter password, should fail
+ assert(not _test_bind(inst, 'Wrong'))
+ # Bind with a wrong longer password, should fail
+ assert(not _test_bind(inst, 'This is even more wrong'))
+ # Bind with a password that has the algo in the name
+ assert(not _test_bind(inst, '{%s}SomeValues....' % algo_name))
+ # Bind with a wrong exact length password.
+ assert(not _test_bind(inst, 'Alsowrong'))
+ # Bind with a subset password, should fail
+ assert(not _test_bind(inst, 'Secret'))
+ if algo_name != 'CRYPT':
+ # Bind with a subset password that is 1 char shorter, to detect off by 1 in
clear
+ assert(not _test_bind(inst, 'Secret12'))
+ # Bind with a superset password, should fail
+ assert(not _test_bind(inst, 'Secret123456'))
+ # Delete the user
+ inst.delete_s(USER_DN)
+ # done!
+
+def test_397(topology):
+ """
+ Assert that all of our password algorithms correctly PASS and FAIL varying
+ password conditions.
+
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ log.info("ATTACH NOW")
+ time.sleep(30)
+
+ # Merge this to the password suite in the future
+
+ for algo in ('PBKDF2_SHA256', ):
+ for i in range(0, 10):
+ _test_algo(topology.standalone, algo)
+
+ log.info('Test PASSED')
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
+
diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in
index 8258b70..7e519f2 100644
--- a/ldap/ldif/template-dse.ldif.in
+++ b/ldap/ldif/template-dse.ldif.in
@@ -211,6 +211,15 @@ nsslapd-plugininitfunc: ns_mta_md5_pwd_storage_scheme_init
nsslapd-plugintype: pwdstoragescheme
nsslapd-pluginenabled: on
+dn: cn=PBKDF2_SHA256,cn=Password Storage Schemes,cn=plugins,cn=config
+objectclass: top
+objectclass: nsSlapdPlugin
+cn: PBKDF2_SHA256
+nsslapd-pluginpath: libpwdstorage-plugin
+nsslapd-plugininitfunc: pbkdf2_sha256_pwd_storage_scheme_init
+nsslapd-plugintype: pwdstoragescheme
+nsslapd-pluginenabled: on
+
dn: cn=AES,cn=Password Storage Schemes,cn=plugins,cn=config
objectclass: top
objectclass: nsSlapdPlugin
diff --git a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
new file mode 100644
index 0000000..1b3e555
--- /dev/null
+++ b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
@@ -0,0 +1,216 @@
+/** BEGIN COPYRIGHT BLOCK
+ * Copyright (C) 2016 Red Hat, Inc.
+ * All rights reserved.
+ *
+ * License: GPL (version 3 or any later version).
+ * See LICENSE for details.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+/*
+ * slapd hashed password routines
+ *
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "pwdstorage.h"
+
+#include <pk11pub.h>
+
+/* Need this for htonl and ntohl */
+#include <arpa/inet.h>
+
+/* WB Nist recommend 128 bits (16 bytes) in 2016, may as well go for more to future
proof. */
+/* !!!!!!!! NEVER CHANGE THESE VALUES !!!!!!!! */
+#define PBKDF2_SALT_LENGTH 64
+#define PBKDF2_ITERATIONS_LENGTH 4
+/* If this isn't 256 NSS explodes without setting an error code .... */
+#define PBKDF2_HASH_LENGTH 256
+#define PBKDF2_TOTAL_LENGTH (PBKDF2_ITERATIONS_LENGTH + PBKDF2_SALT_LENGTH +
PBKDF2_HASH_LENGTH)
+/* ======== END NEVER CHANGE THESE VALUES ==== */
+
+/*
+ * WB - It's important we keep this private, and we increment it over time.
+ * Administrators are likely to forget to update it, or they will set it too low.
+ * We therfore keep it private, so we can increase it as our security recomendations
+ * change and improve.
+ *
+ * At the same time we MUST increase this with each version of Directory Server
+ * This value is written into the hash, so it's safe to change.
+ */
+#define PBKDF2_ITERATIONS 30000
+
+static const char *schemeName = PBKDF2_SHA256_SCHEME_NAME;
+static const PRUint32 schemeNameLength = PBKDF2_SHA256_NAME_LEN;
+
+/* For requesting the slot which supports these types */
+static CK_MECHANISM_TYPE mechanism_array[] = {CKM_SHA256_HMAC, CKM_PKCS5_PBKD2};
+
+void
+pbkdf2_sha256_extract(char *hash_in, SECItem *salt, PRUint32 *iterations)
+{
+ /*
+ * This will take the input of hash_in (generated from pbkdf2_sha256_hash) and
+ * populate the hash (output of nss pkbdf2), salt, and iterations.
+ * Enough space should be avaliable in these for the values to fit into.
+ */
+
+ memcpy(iterations, hash_in, PBKDF2_ITERATIONS_LENGTH);
+ /* We use ntohl on this value to make sure it's correct endianess. */
+ *iterations = ntohl(*iterations);
+
+ /* warning: pointer targets in assignment differ in signedness [-Wpointer-sign] */
+ salt->data = (unsigned char *)(hash_in + PBKDF2_ITERATIONS_LENGTH);
+ salt->len = PBKDF2_SALT_LENGTH;
+}
+
+SECStatus
+pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *salt,
PRUint32 iterations)
+{
+ SECItem *result = NULL;
+ SECAlgorithmID *algid = NULL;
+ PK11SlotInfo *slot = NULL;
+ PK11SymKey *symkey = NULL;
+
+ /* We assume that NSS is already started. */
+ algid = PK11_CreatePBEV2AlgorithmID(SEC_OID_PKCS5_PBKDF2, SEC_OID_HMAC_SHA256,
SEC_OID_HMAC_SHA256, hash_out_len, iterations, salt);
+
+ if (algid != NULL) {
+ /* Gets the best slot that provides SHA256HMAC and PBKDF2 (may not be the
default!) */
+ slot = PK11_GetBestSlotMultiple(mechanism_array, 2, NULL);
+ if (slot != NULL) {
+ symkey = PK11_PBEKeyGen(slot, algid, pwd, PR_FALSE, NULL);
+ PK11_FreeSlot(slot);
+ if (symkey == NULL) {
+ /* We try to get the Error here but NSS has two or more error interfaces,
and sometimes it uses none of them. */
+ PRInt32 status = PORT_GetError();
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to retrieve
symkey from NSS. Error code might be %d ???\n", status);
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "The most likely
cause is your system has nss 3.21 or lower. PBKDF2 requires nss 3.22 or higher.\n");
+ return SECFailure;
+ }
+ } else {
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to retrieve
slot from NSS.\n");
+ return SECFailure;
+ }
+ SECOID_DestroyAlgorithmID(algid, PR_TRUE);
+ } else {
+ /* Uh oh! */
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to generate
algorithm ID.\n");
+ return SECFailure;
+ }
+
+ if (PK11_ExtractKeyValue(symkey) == SECSuccess) {
+ result = PK11_GetKeyData(symkey);
+ if (result != NULL && result->len <= hash_out_len) {
+ memcpy(hash_out, result->data, result->len);
+ PK11_FreeSymKey(symkey);
+ } else {
+ PK11_FreeSymKey(symkey);
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to retrieve
(get) hash output.\n");
+ return SECFailure;
+ }
+ } else {
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to extract hash
output.\n");
+ return SECFailure;
+ }
+
+ return SECSuccess;
+}
+
+char *
+pbkdf2_sha256_pw_enc(const char *pwd)
+{
+ char hash[ PBKDF2_TOTAL_LENGTH ];
+ size_t encsize = 3 + schemeNameLength + LDIF_BASE64_LEN(PBKDF2_TOTAL_LENGTH);
+ char *enc = slapi_ch_calloc(encsize, sizeof(char));
+ PRUint32 iterations = PBKDF2_ITERATIONS;
+
+ SECItem saltItem;
+ SECItem passItem;
+ char salt[PBKDF2_SALT_LENGTH];
+
+ memset(hash, 0, PBKDF2_TOTAL_LENGTH);
+ memset(salt, 0, PBKDF2_SALT_LENGTH);
+ saltItem.data = (unsigned char *)salt;
+ saltItem.len = PBKDF2_SALT_LENGTH;
+ passItem.data = (unsigned char *)pwd;
+ passItem.len = strlen(pwd);
+
+ /* make a new random salt */
+ slapi_rand_array(salt, PBKDF2_SALT_LENGTH);
+
+ /*
+ * Preload the salt and iterations to the output.
+ * memcpy the iterations to the hash_out
+ * We use ntohl on this value to make sure it's correct endianess.
+ */
+ iterations = htonl(iterations);
+ memcpy(hash, &iterations, PBKDF2_ITERATIONS_LENGTH);
+ /* memcpy the salt to the hash_out */
+ memcpy(hash + PBKDF2_ITERATIONS_LENGTH, saltItem.data, PBKDF2_SALT_LENGTH);
+
+ /*
+ * This offset is to make the hash function put the values
+ * In the correct part of the memory.
+ */
+ if ( pbkdf2_sha256_hash(hash + PBKDF2_ITERATIONS_LENGTH + PBKDF2_SALT_LENGTH,
PBKDF2_HASH_LENGTH, &passItem, &saltItem, PBKDF2_ITERATIONS) != SECSuccess ) {
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Could not generate
pbkdf2_sha256_hash!\n");
+ return NULL;
+ }
+
+ sprintf(enc, "%c%s%c", PWD_HASH_PREFIX_START, schemeName,
PWD_HASH_PREFIX_END);
+ (void)PL_Base64Encode( hash, PBKDF2_TOTAL_LENGTH, enc + 2 + schemeNameLength);
+ PR_ASSERT(enc[encsize - 1] == '\0');
+
+ slapi_log_err(SLAPI_LOG_PLUGIN, (char *)schemeName, "Generated hash %s\n",
enc);
+
+ return enc;
+}
+
+PRInt32
+pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd)
+{
+ PRInt32 result = 1; /* Default to fail. */
+ char dbhash[ PBKDF2_TOTAL_LENGTH ];
+ char userhash[ PBKDF2_HASH_LENGTH ];
+ PRUint32 dbpwd_len = strlen(dbpwd);
+ SECItem saltItem;
+ SECItem passItem;
+ PRUint32 iterations = 0;
+
+ /* Our hash value is always at a known offset. */
+ char *hash = dbhash + PBKDF2_ITERATIONS_LENGTH + PBKDF2_SALT_LENGTH;
+
+ slapi_log_err(SLAPI_LOG_PLUGIN, (char *)schemeName, "Comparing
password\n");
+
+ memset(dbhash, 0, PBKDF2_TOTAL_LENGTH);
+
+ passItem.data = (unsigned char *)userpwd;
+ passItem.len = strlen(userpwd);
+
+ /* Decode the DBpwd to bytes from b64 */
+ if ( PL_Base64Decode( dbpwd, dbpwd_len, dbhash) == NULL ) {
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to base64 decode
dbpwd value\n");
+ return result;
+ }
+ /* extract the fields */
+ pbkdf2_sha256_extract(dbhash, &saltItem, &iterations);
+
+ /* Now send the userpw to the hash function, with the salt + iter. */
+ if ( pbkdf2_sha256_hash(userhash, PBKDF2_HASH_LENGTH, &passItem, &saltItem,
iterations) != SECSuccess ) {
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to hash userpwd
value\n");
+ return result;
+ }
+ /* Now compare the result of pbkdf2_sha256_hash. */
+ result = memcmp(userhash, hash, PBKDF2_HASH_LENGTH);
+
+ return result;
+}
+
+
diff --git a/ldap/servers/plugins/pwdstorage/pwd_init.c
b/ldap/servers/plugins/pwdstorage/pwd_init.c
index 5efd9ca..d66bb98 100644
--- a/ldap/servers/plugins/pwdstorage/pwd_init.c
+++ b/ldap/servers/plugins/pwdstorage/pwd_init.c
@@ -44,6 +44,8 @@ static Slapi_PluginDesc md5_pdesc = {
"md5-password-storage-scheme", VENDOR, DS_
static Slapi_PluginDesc smd5_pdesc = { "smd5-password-storage-scheme", VENDOR,
DS_PACKAGE_VERSION, "Salted MD5 hash algorithm (SMD5)" };
+static Slapi_PluginDesc pbkdf2_sha256_pdesc = {
"pbkdf2-sha256-password-storage-scheme", VENDOR, DS_PACKAGE_VERSION,
"Salted PBKDF2 SHA256 hash algorithm (PBKDF2_SHA256)" };
+
static char *plugin_name = "NSPwdStoragePlugin";
int
@@ -336,3 +338,21 @@ smd5_pwd_storage_scheme_init( Slapi_PBlock *pb )
slapi_log_err(SLAPI_LOG_PLUGIN, plugin_name, "<= smd5_pwd_storage_scheme_init
%d\n\n", rc );
return( rc );
}
+
+int
+pbkdf2_sha256_pwd_storage_scheme_init(Slapi_PBlock *pb)
+{
+ int rc;
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, plugin_name, "=>
pbkdf2_sha256_pwd_storage_scheme_init\n");
+
+ rc = slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, (void *) SLAPI_PLUGIN_VERSION_01);
+ rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void
*)&pbkdf2_sha256_pdesc);
+ rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_ENC_FN, (void
*)pbkdf2_sha256_pw_enc);
+ rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_CMP_FN, (void
*)pbkdf2_sha256_pw_cmp);
+ rc |= slapi_pblock_set(pb, SLAPI_PLUGIN_PWD_STORAGE_SCHEME_NAME,
PBKDF2_SHA256_SCHEME_NAME);
+
+ slapi_log_error(SLAPI_LOG_PLUGIN, plugin_name, "<=
pbkdf2_sha256_pwd_storage_scheme_init %d\n", rc);
+ return rc;
+}
+
diff --git a/ldap/servers/plugins/pwdstorage/pwdstorage.h
b/ldap/servers/plugins/pwdstorage/pwdstorage.h
index 1e085c7..27e708d 100644
--- a/ldap/servers/plugins/pwdstorage/pwdstorage.h
+++ b/ldap/servers/plugins/pwdstorage/pwdstorage.h
@@ -54,6 +54,9 @@
#define MD5_NAME_LEN 3
#define SALTED_MD5_SCHEME_NAME "SMD5"
#define SALTED_MD5_NAME_LEN 4
+#define PBKDF2_SHA256_SCHEME_NAME "PBKDF2_SHA256"
+#define PBKDF2_SHA256_NAME_LEN 13
+
SECStatus sha_salted_hash(char *hash_out, const char *pwd, struct berval *salt, unsigned
int secOID);
int sha_pw_cmp( const char *userpwd, const char *dbpwd, unsigned int shaLen );
@@ -82,6 +85,10 @@ char *md5_pw_enc( const char *pwd );
int smd5_pw_cmp( const char *userpwd, const char *dbpwd );
char *smd5_pw_enc( const char *pwd );
+SECStatus pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem
*salt, PRUint32 iterations);
+char * pbkdf2_sha256_pw_enc(const char *pwd);
+int pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd);
+
/* Utility functions */
PRUint32 pwdstorage_base64_decode_len(const char *encval, PRUint32 enclen);
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
index 2506a2b..5af04d2 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -262,6 +262,13 @@ pw_val2scheme( char *val, char **valpwdp, int first_is_default )
if (NULL == val) {
return( NULL );
}
+
+ /*
+ * Future implementors of new password mechanisms may find that this function
+ * is causing them trouble. If your hash ends up as {CLEAR}{NEWMECH}.... it
+ * because NEWMECH > PWD_MAX_NAME_LEN. Update pw.h!
+ */
+
if ( *val != PWD_HASH_PREFIX_START ||
( end = strchr( val, PWD_HASH_PREFIX_END )) == NULL ||
( namelen = end - val - 1 ) > PWD_MAX_NAME_LEN ) {
diff --git a/ldap/servers/slapd/pw.h b/ldap/servers/slapd/pw.h
index 58e7441..8e07582 100644
--- a/ldap/servers/slapd/pw.h
+++ b/ldap/servers/slapd/pw.h
@@ -19,7 +19,8 @@
#ifndef _SLAPD_PW_H_
#define _SLAPD_PW_H_
-#define PWD_MAX_NAME_LEN 10
+// Updated to the 13 for PBKDF2_SHA256
+#define PWD_MAX_NAME_LEN 13
#define PWD_HASH_PREFIX_START '{'
#define PWD_HASH_PREFIX_END '}'