]> jfr.im git - irc/atheme/atheme.git/commitdiff
modules/crypto/: add extended-key-setup blowfish (bcrypt) support
authorAaron Jones <redacted>
Tue, 18 Feb 2020 01:56:46 +0000 (01:56 +0000)
committerAaron Jones <redacted>
Tue, 18 Feb 2020 02:50:33 +0000 (02:50 +0000)
Since this is the algorithm backing the crypt3-openbsd module, remove
that too.

NEWS.md
configure
dist/atheme.conf.example
include/atheme/sysconf.h.in
m4/atheme-check-build-requirements.m4
modules/crypto/Makefile
modules/crypto/bcrypt.c [new file with mode: 0644]
modules/crypto/crypt3-openbsd.c [deleted file]
modules/statserv/pwhashes.c

diff --git a/NEWS.md b/NEWS.md
index 06c1c2ed0bc8a1927cde518838343daf9aea99a0..7e1cfadce5bd3b670eb523d90e817582bbf4a92d 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -70,7 +70,7 @@ POTENTIAL COMPATIBILITY BREAKAGE
   and Argon2id) too. If you were using this module on version 7.2, please see
   the `dist/atheme.conf.example` file for migration instructions. The names of
   the configuration options have changed! You will need libargon2 available at
-  configure-time.
+  configure-time (`--with-argon2`).
 
 Security
 --------
@@ -92,9 +92,6 @@ Security
   (where supported, e.g. in WeeChat) to achieve its purpose of preventing the
   user's account password from persisting in on-disk log files.
 
-- Services is now capable of using OpenBSD `crypt_newhash(3)` to encrypt
-  and verify passwords.
-
 - Services now has a much more rigorous random number generation interface
   and will e.g. refuse to use `arc4random(3)` unless we are actually on
   OpenBSD (which is the only platform that uses a secure algorithm for it).
@@ -214,7 +211,8 @@ Password Cryptography
 ---------------------
 - The existing crypto modules no longer need OpenSSL (or any crypto library)
 - Add support for scrypt password encryption with `modules/crypto/scrypt`.
-  This requires libsodium.
+  The scrypt module requires libsodium (`--with-sodium`).
+- Add support for bcrypt password encryption with `modules/crypto/bcrypt`.
 - `libathemecore/crypto.c`: log current crypto provider on mod(un/re)load
 - `libathemecore/crypto.c`: rip out plaintext fallback implementation
 - Make old modules (`ircservices`, `pbkdf2`, `rawmd5`, `rawsha1`) verify-only
index 16fe5823348ed660d6ceaf865f60e620df4ca444..1fd4d90ef32ff0f15ff752d5644434db67daebed 100755 (executable)
--- a/configure
+++ b/configure
@@ -6057,28 +6057,6 @@ fi
 done
 
 
-    for ac_func in crypt_checkpass
-do :
-  ac_fn_c_check_func "$LINENO" "crypt_checkpass" "ac_cv_func_crypt_checkpass"
-if test "x$ac_cv_func_crypt_checkpass" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_CRYPT_CHECKPASS 1
-_ACEOF
-
-fi
-done
-
-    for ac_func in crypt_newhash
-do :
-  ac_fn_c_check_func "$LINENO" "crypt_newhash" "ac_cv_func_crypt_newhash"
-if test "x$ac_cv_func_crypt_newhash" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_CRYPT_NEWHASH 1
-_ACEOF
-
-fi
-done
-
     for ac_func in consttime_memequal
 do :
   ac_fn_c_check_func "$LINENO" "consttime_memequal" "ac_cv_func_consttime_memequal"
index 8ba849c9ae1be4723cddd9578e20b45a570ceb8b..de639f74ebb9b27049615588d606b55055e3bde3 100644 (file)
@@ -108,7 +108,7 @@ loadmodule "modules/backend/opensex";
  *   Argon2 (Password Hashing Competition 2015)    modules/crypto/argon2
  *   scrypt (Tarsnap Online Backup Service)        modules/crypto/scrypt
  *   PBKDF2 (Including support for SASL SCRAM-SHA) modules/crypto/pbkdf2v2
- *   OpenBSD crypt_newhash(3) a la '$2b$...$...'   modules/crypto/crypt3-openbsd
+ *   bcrypt (EksBlowfish; from Niels Provos etc.)  modules/crypto/bcrypt
  *   SHA2-512 crypt(3) a la '$6$...'               modules/crypto/crypt3-sha2-512
  *   SHA2-256 crypt(3) a la '$5$...'               modules/crypto/crypt3-sha2-256
  *
@@ -163,6 +163,14 @@ loadmodule "modules/backend/opensex";
  * hashes. However, you should also load PBKDF2 v2 (if you don't decide to use
  * anything else), because the PBKDF2 v1 module is now verify-only.
  *
+ * The bcrypt module will truncate passwords greater than 72 characters. It is
+ * also capable of verifying the older $2a$ digests that contain an integer
+ * wrap-around bug, as used on e.g. Anope. It is not capable of verifying the
+ * PHP-bcrypt $2x$ and $2y$ digests; but $2y$ can simply be changed to $2b$.
+ * All successfully-verified passwords not using $2b$ will be converted to it.
+ * This is an encryption-capable module, but its use is discouraged unless you
+ * need to use it for interoperability with some other piece of software.
+ *
  * The crypt3-* modules depend on your platform crypt(3) supporting the
  * respective algorithms. This is not guaranteed to be the case. If you used
  * modules/crypto/posix on Linux, you need crypt3-md5. If you used
@@ -186,8 +194,8 @@ loadmodule "modules/backend/opensex";
  */
 #loadmodule "modules/crypto/argon2";            /* --with-argon2 */
 #loadmodule "modules/crypto/scrypt";            /* --with-sodium */
-#loadmodule "modules/crypto/crypt3-openbsd";    /* Needs OpenBSD */
 loadmodule "modules/crypto/pbkdf2v2";
+#loadmodule "modules/crypto/bcrypt";            /* See notes above */
 loadmodule "modules/crypto/pbkdf2";             /* Verify-only, see prev. */
 #loadmodule "modules/crypto/crypt3-sha2-512";   /* Needs crypt(3) support */
 #loadmodule "modules/crypto/crypt3-sha2-256";   /* Needs crypt(3) support */
@@ -1191,13 +1199,17 @@ crypto {
         */
        #pbkdf2v2_saltlen = 32;
 
-       /* (*) crypt3_openbsd_newhash_pref
+       /* (*) bcrypt_cost
+        *
+        * Amount of rounds to perform for new passwords (as a power of 2).
+        * You should raise this as high as is reasonable. A benchmark
+        * program is available alongside this software to aid in this
+        * process.
         *
-        * The "pref" parameter for OpenBSD crypt_newhash(3).
-        * Please see <https://man.openbsd.org/crypt_checkpass.3>
-        * The default is "bcrypt,a"
+        * Valid values are 4 to 31 (inclusive)
+        * The default is 7
         */
-       #crypt3_openbsd_newhash_pref = "bcrypt,a";
+       #bcrypt_cost = 7;
 
        /* (*) crypt3_sha2_256_rounds
         * (*) crypt3_sha2_512_rounds
index 837321c39a5146e5be983c4a085c1e74c6c2a680..0bf642fd83d466fa7e325bc2e3e886da3af66fae 100644 (file)
 /* Define to 1 if crypt(3) appears to be usable */
 #undef HAVE_CRYPT
 
-/* Define to 1 if you have the `crypt_checkpass' function. */
-#undef HAVE_CRYPT_CHECKPASS
-
 /* Define to 1 if you have the <crypt.h> header file. */
 #undef HAVE_CRYPT_H
 
-/* Define to 1 if you have the `crypt_newhash' function. */
-#undef HAVE_CRYPT_NEWHASH
-
 /* Define to 1 if you have the <ctype.h> header file. */
 #undef HAVE_CTYPE_H
 
index 2c3ec931696d87e96d3ebc5efc74f38623b94c47..99555d97dc48fc6890e70931944c7bca989679f3 100644 (file)
@@ -45,8 +45,6 @@ AC_DEFUN([ATHEME_CHECK_BUILD_REQUIREMENTS], [
     AC_CHECK_HEADERS([time.h], [], [], [])
     AC_CHECK_HEADERS([unistd.h], [], [], [])
 
-    AC_CHECK_FUNCS([crypt_checkpass], [], [])
-    AC_CHECK_FUNCS([crypt_newhash], [], [])
     AC_CHECK_FUNCS([consttime_memequal], [], [])
     AC_CHECK_FUNCS([dup2], [], [ATHEME_REQUIRED_FUNC_MISSING])
     AC_CHECK_FUNCS([execve], [], [ATHEME_REQUIRED_FUNC_MISSING])
index 9627c420749dc0ce9c520e54f04fdf42da4f7f60..cc4e1ed1fedbf2762401a197e36b525b5d207a1f 100644 (file)
@@ -15,7 +15,7 @@ SUBDIRS = ${LEGACY_PWCRYPTO_COND_D}
 MODULE  = crypto
 SRCS    =                           \
     argon2.c                        \
-    crypt3-openbsd.c                \
+    bcrypt.c                        \
     crypt3-sha2-256.c               \
     crypt3-sha2-512.c               \
     main.c                          \
diff --git a/modules/crypto/bcrypt.c b/modules/crypto/bcrypt.c
new file mode 100644 (file)
index 0000000..0bd492f
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * SPDX-License-Identifier: ISC
+ * SPDX-URL: https://spdx.org/licenses/ISC.html
+ *
+ * Copyright (C) 2020 Aaron M. D. Jones <aaronmdjones@gmail.com>
+ */
+
+#include <atheme.h>
+
+#define ATHEME_BCRYPT_LOADSALT_FORMAT   "$2%1[ab]$%2u$%22[" BASE64_ALPHABET_CRYPT3_BLOWFISH "]"
+#define ATHEME_BCRYPT_LOADHASH_FORMAT   ATHEME_BCRYPT_LOADSALT_FORMAT "%32[" BASE64_ALPHABET_CRYPT3_BLOWFISH "]"
+#define ATHEME_BCRYPT_PARAMLEN_MIN      60U // The fixed-length salt and output guarantees a minimum MCF length
+
+// The algorithm generates a 24-byte result but almost every implementation only uses 23 of them
+#define ATHEME_BCRYPT_HASHLEN_TRUNC     23U
+
+#define ATHEME_BCRYPT_PREFIXLEN         7U
+#define ATHEME_BCRYPT_SALTLEN_B64       22U // base64-encoded 16 bytes *without padding* == 22 bytes
+#define ATHEME_BCRYPT_HASHLEN_B64       31U // base64-encoded 23 bytes *without padding* == 31 bytes
+
+#define CRYPTO_MODULE_NAME              "crypto/bcrypt"
+
+static mowgli_list_t **crypto_conf_table = NULL;
+
+static unsigned int atheme_bcrypt_cost = ATHEME_BCRYPT_ROUNDS_DEF;
+
+static const char *
+atheme_bcrypt_crypt(const char *const restrict password, const char ATHEME_VATTR_UNUSED *const restrict parameters)
+{
+       static char result[PASSLEN + 1];
+       unsigned char salt[ATHEME_BCRYPT_SALTLEN];
+       unsigned char hash[ATHEME_BCRYPT_HASHLEN];
+
+       (void) memset(result, 0x00, sizeof result);
+       (void) atheme_random_buf(salt, sizeof salt);
+
+       if (! atheme_eks_bf_compute(password, ATHEME_BCRYPT_VERSION_MINOR, atheme_bcrypt_cost, salt, hash))
+       {
+               (void) slog(LG_ERROR, "%s: atheme_eks_bf_compute() failed (BUG)", MOWGLI_FUNC_NAME);
+               return NULL;
+       }
+
+       if (snprintf(result, sizeof result, "$2%c$%2.2u$", ATHEME_BCRYPT_VERSION_MINOR, atheme_bcrypt_cost) !=
+             ATHEME_BCRYPT_PREFIXLEN)
+       {
+               (void) slog(LG_ERROR, "%s: snprintf(3) did not write an acceptable amount of data (BUG)",
+                                     MOWGLI_FUNC_NAME);
+               (void) smemzero(hash, sizeof hash);
+               return NULL;
+       }
+
+       if (base64_encode_table(salt, ATHEME_BCRYPT_SALTLEN, result + ATHEME_BCRYPT_PREFIXLEN,
+             ATHEME_BCRYPT_SALTLEN_B64, BASE64_ALPHABET_CRYPT3_BLOWFISH) != ATHEME_BCRYPT_SALTLEN_B64)
+       {
+               (void) slog(LG_ERROR, "%s: base64_encode_table(salt) failed (BUG)", MOWGLI_FUNC_NAME);
+               (void) smemzero(hash, sizeof hash);
+               return NULL;
+       }
+
+       // We only encode 23 of the 24 bytes for wider compatibility with other implementations
+       if (base64_encode_table(hash, ATHEME_BCRYPT_HASHLEN_TRUNC, result + ATHEME_BCRYPT_PREFIXLEN +
+             ATHEME_BCRYPT_SALTLEN_B64, ATHEME_BCRYPT_HASHLEN_B64, BASE64_ALPHABET_CRYPT3_BLOWFISH) !=
+             ATHEME_BCRYPT_HASHLEN_B64)
+       {
+               (void) slog(LG_ERROR, "%s: base64_encode_table(hash) failed (BUG)", MOWGLI_FUNC_NAME);
+               (void) smemzero(hash, sizeof hash);
+               (void) smemzero(result, sizeof result);
+               return NULL;
+       }
+
+       (void) smemzero(hash, sizeof hash);
+       return result;
+}
+
+static bool ATHEME_FATTR_WUR
+atheme_bcrypt_verify(const char *const restrict password, const char *const restrict parameters,
+                     unsigned int *const restrict flags)
+{
+       char minor[2];
+       unsigned int cost;
+       char salt64[BUFSIZE];
+       char hash64[BUFSIZE];
+
+       unsigned char salt[ATHEME_BCRYPT_SALTLEN];
+       unsigned char hash[ATHEME_BCRYPT_HASHLEN];
+       unsigned char cmphash[ATHEME_BCRYPT_HASHLEN];
+
+       bool retval = false;
+
+       if (strlen(parameters) < ATHEME_BCRYPT_PARAMLEN_MIN)
+       {
+               (void) slog(LG_DEBUG, "%s: parameters are not the correct length", MOWGLI_FUNC_NAME);
+               return false;
+       }
+       if (sscanf(parameters, ATHEME_BCRYPT_LOADHASH_FORMAT, minor, &cost, salt64, hash64) != 4)
+       {
+               (void) slog(LG_DEBUG, "%s: sscanf(3) was unsuccessful", MOWGLI_FUNC_NAME);
+               goto cleanup;
+       }
+       if (cost < ATHEME_BCRYPT_ROUNDS_MIN || cost > ATHEME_BCRYPT_ROUNDS_MAX)
+       {
+               (void) slog(LG_DEBUG, "%s: cost parameter '%u' unacceptable", MOWGLI_FUNC_NAME, cost);
+               goto cleanup;
+       }
+       if (base64_decode_table(salt64, salt, sizeof salt, BASE64_ALPHABET_CRYPT3_BLOWFISH) != ATHEME_BCRYPT_SALTLEN)
+       {
+               (void) slog(LG_DEBUG, "%s: base64_decode_table(salt) failed", MOWGLI_FUNC_NAME);
+               goto cleanup;
+       }
+
+       // We can verify 23-byte or 24-byte digests for wider compatibility with other implementations
+       const size_t hashlen = base64_decode_table(hash64, hash, sizeof hash, BASE64_ALPHABET_CRYPT3_BLOWFISH);
+
+       if (hashlen == BASE64_FAIL || hashlen < ATHEME_BCRYPT_HASHLEN_TRUNC)
+       {
+               (void) slog(LG_DEBUG, "%s: base64_decode_table(hash) failed", MOWGLI_FUNC_NAME);
+               goto cleanup;
+       }
+
+       *flags |= PWVERIFY_FLAG_MYMODULE;
+
+       if (! atheme_eks_bf_compute(password, (unsigned int) minor[0], cost, salt, cmphash))
+       {
+               (void) slog(LG_ERROR, "%s: atheme_eks_bf_compute() failed (BUG)", MOWGLI_FUNC_NAME);
+               goto cleanup;
+       }
+       if (smemcmp(hash, cmphash, hashlen) != 0)
+       {
+               (void) slog(LG_DEBUG, "%s: smemcmp() mismatch; incorrect password?", MOWGLI_FUNC_NAME);
+               goto cleanup;
+       }
+
+       retval = true;
+
+       (void) slog(LG_DEBUG, "%s: authentication successful", MOWGLI_FUNC_NAME);
+
+       if (minor[0] != ATHEME_BCRYPT_VERSION_MINOR)
+       {
+               (void) slog(LG_DEBUG, "%s: minor version (%s) is not current (%c)", MOWGLI_FUNC_NAME, minor,
+                                     ATHEME_BCRYPT_VERSION_MINOR);
+
+               *flags |= PWVERIFY_FLAG_RECRYPT;
+       }
+       if (cost != atheme_bcrypt_cost)
+       {
+               (void) slog(LG_DEBUG, "%s: cost (%u) is not the default (%u)", MOWGLI_FUNC_NAME, cost,
+                                     atheme_bcrypt_cost);
+
+               *flags |= PWVERIFY_FLAG_RECRYPT;
+       }
+
+cleanup:
+       (void) smemzero(hash, sizeof hash);
+       (void) smemzero(hash64, sizeof hash64);
+       (void) smemzero(cmphash, sizeof cmphash);
+
+       return retval;
+}
+
+static const struct crypt_impl crypto_bcrypt_impl = {
+
+       .id        = CRYPTO_MODULE_NAME,
+       .crypt     = &atheme_bcrypt_crypt,
+       .verify    = &atheme_bcrypt_verify,
+};
+
+static void
+mod_init(struct module *const restrict m)
+{
+       if (! atheme_eks_bf_testsuite_run())
+       {
+               (void) slog(LG_ERROR, "%s: self-test failed (BUG)", m->name);
+
+               m->mflags |= MODFLAG_FAIL;
+               return;
+       }
+
+       MODULE_TRY_REQUEST_SYMBOL(m, crypto_conf_table, "crypto/main", "crypto_conf_table")
+
+       (void) add_uint_conf_item("bcrypt_cost", *crypto_conf_table, 0, &atheme_bcrypt_cost,
+                                 ATHEME_BCRYPT_ROUNDS_MIN, ATHEME_BCRYPT_ROUNDS_MAX, ATHEME_BCRYPT_ROUNDS_DEF);
+
+       (void) crypt_register(&crypto_bcrypt_impl);
+
+       (void) slog(LG_INFO, "%s: WARNING: Passwords greater than 72 characters are truncated!", m->name);
+
+       m->mflags |= MODFLAG_DBCRYPTO;
+}
+
+static void
+mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
+{
+       (void) del_conf_item("bcrypt_cost", *crypto_conf_table);
+
+       (void) crypt_unregister(&crypto_bcrypt_impl);
+}
+
+SIMPLE_DECLARE_MODULE_V1(CRYPTO_MODULE_NAME, MODULE_UNLOAD_CAPABILITY_OK)
diff --git a/modules/crypto/crypt3-openbsd.c b/modules/crypto/crypt3-openbsd.c
deleted file mode 100644 (file)
index 799b55c..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SPDX-License-Identifier: ISC
- * SPDX-URL: https://spdx.org/licenses/ISC.html
- *
- * Copyright (C) 2019 Atheme Development Group (https://atheme.github.io/)
- *
- * OpenBSD-style crypt_checkpass(3)/crypt_newhash(3) wrapper.
- */
-
-#include <atheme.h>
-
-#define CRYPTO_MODULE_NAME "crypto/crypt3-openbsd"
-
-#if defined(__OpenBSD__) && defined(HAVE_CRYPT_CHECKPASS) && defined(HAVE_CRYPT_NEWHASH)
-
-#define CRYPT3_PREF_DEF "bcrypt,a"
-
-static char *crypt3_pref = NULL;
-
-static mowgli_list_t **crypto_conf_table = NULL;
-
-static const char * ATHEME_FATTR_WUR
-atheme_crypt3_openbsd_crypt(const char *const restrict password,
-                            const char ATHEME_VATTR_UNUSED *const restrict parameters)
-{
-       static char result[PASSLEN + 1];
-
-       if (crypt_newhash(password, crypt3_pref, result, sizeof result) != 0)
-       {
-               (void) slog(LG_ERROR, "%s: crypt_newhash(3): %s", MOWGLI_FUNC_NAME, strerror(errno));
-               return NULL;
-       }
-
-       return result;
-}
-
-static bool ATHEME_FATTR_WUR
-atheme_crypt3_openbsd_verify(const char *const restrict password, const char *const restrict parameters,
-                             unsigned int ATHEME_VATTR_UNUSED *const restrict flags)
-{
-       if (crypt_checkpass(password, parameters) != 0)
-       {
-               (void) slog(LG_DEBUG, "%s: crypt_checkpass(3): %s", MOWGLI_FUNC_NAME, strerror(errno));
-               return false;
-       }
-
-       return true;
-}
-
-static const struct crypt_impl crypto_crypt3_impl = {
-
-       .id        = CRYPTO_MODULE_NAME,
-       .crypt     = &atheme_crypt3_openbsd_crypt,
-       .verify    = &atheme_crypt3_openbsd_verify,
-};
-
-static void
-mod_init(struct module *const restrict m)
-{
-       MODULE_TRY_REQUEST_SYMBOL(m, crypto_conf_table, "crypto/main", "crypto_conf_table")
-
-       (void) add_dupstr_conf_item("crypt3_openbsd_newhash_pref", *crypto_conf_table, 0, &crypt3_pref, CRYPT3_PREF_DEF);
-
-       (void) crypt_register(&crypto_crypt3_impl);
-
-       m->mflags |= MODFLAG_DBCRYPTO;
-}
-
-static void
-mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
-{
-       (void) del_conf_item("crypt3_openbsd_newhash_pref", *crypto_conf_table);
-
-       (void) crypt_unregister(&crypto_crypt3_impl);
-}
-
-#else /* __OpenBSD__ && HAVE_CRYPT_CHECKPASS && HAVE_CRYPT_NEWHASH */
-
-static void
-mod_init(struct module *const restrict m)
-{
-       (void) slog(LG_ERROR, "Module %s requires a recent OpenBSD system; refusing to load.", m->name);
-
-       m->mflags |= MODFLAG_FAIL;
-}
-
-static void
-mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
-{
-       // Nothing To Do
-}
-
-#endif /* !(__OpenBSD__ && HAVE_CRYPT_CHECKPASS && HAVE_CRYPT_NEWHASH) */
-
-SIMPLE_DECLARE_MODULE_V1(CRYPTO_MODULE_NAME, MODULE_UNLOAD_CAPABILITY_OK)
index 1569a358e4c80b4cb068a0c51f54952d81f18117..d1b6d3dda5e43870bc03a0ff38d793ef9aa33ac9 100644 (file)
@@ -15,6 +15,7 @@
 #define SCANFMT_ANOPE_ENC_SHA256    "$anope$enc_sha256$" SCANFMT_BASE64_RFC4648 "$" SCANFMT_BASE64_RFC4648
 #define SCANFMT_ARGON2              "$%[A-Za-z0-9]$v=%u$m=%u,t=%u,p=%u$" SCANFMT_BASE64_RFC4648 "$" SCANFMT_BASE64_RFC4648
 #define SCANFMT_BASE64              "$base64$" SCANFMT_BASE64_RFC4648
+#define SCANFMT_BCRYPT              "$2%1[ab]$%2u$%22[" BASE64_ALPHABET_CRYPT3 "]%31[" BASE64_ALPHABET_CRYPT3 "]"
 #define SCANFMT_CRYPT3_DES          SCANFMT_BASE64_CRYPT3
 #define SCANFMT_CRYPT3_MD5          "$1$" SCANFMT_BASE64_CRYPT3 "$" SCANFMT_BASE64_CRYPT3
 #define SCANFMT_CRYPT3_SHA2_256     "$5$" SCANFMT_BASE64_CRYPT3 "$" SCANFMT_BASE64_CRYPT3
@@ -40,6 +41,7 @@ enum crypto_type
        TYPE_ARGON2I,
        TYPE_ARGON2ID,
        TYPE_BASE64,
+       TYPE_BCRYPT,
        TYPE_CRYPT3_DES,
        TYPE_CRYPT3_MD5,
        TYPE_CRYPT3_SHA2_256,
@@ -81,6 +83,8 @@ crypto_type_to_name(const enum crypto_type type)
                        return "crypto/argon2 (Argon2id)";
                case TYPE_BASE64:
                        return "crypto/base64 (\00304PLAIN-TEXT!\003)";
+               case TYPE_BCRYPT:
+                       return "crypto/bcrypt (EksBlowfish)";
                case TYPE_CRYPT3_DES:
                        return "crypto/crypt3-des (DES)";
                case TYPE_CRYPT3_MD5:
@@ -156,6 +160,7 @@ ss_cmd_pwhashes_func(struct sourceinfo *const restrict si, const int ATHEME_VATT
                continue_if_fail(mu != NULL);
 
                const char *const pw = mu->pass;
+               const size_t pwlen = strlen(pw);
 
                if (! (mu->flags & MU_CRYPTPASS))
                {
@@ -180,7 +185,11 @@ ss_cmd_pwhashes_func(struct sourceinfo *const restrict si, const int ATHEME_VATT
                {
                        pwhashes[TYPE_BASE64]++;
                }
-               else if (sscanf(pw, SCANFMT_CRYPT3_DES, s1) == 1 && strlen(s1) == 13U && strcmp(s1, pw) == 0)
+               else if (pwlen >= 60U && sscanf(pw, SCANFMT_BCRYPT, s1, &i1, s2, s3) == 4)
+               {
+                       pwhashes[TYPE_BCRYPT]++;
+               }
+               else if (pwlen == 13U && sscanf(pw, SCANFMT_CRYPT3_DES, s1) == 1 && strcmp(s1, pw) == 0)
                {
                        // Fuzzy (no rigid format)
                        pwhashes[TYPE_CRYPT3_DES]++;
@@ -209,7 +218,7 @@ ss_cmd_pwhashes_func(struct sourceinfo *const restrict si, const int ATHEME_VATT
                {
                        pwhashes[TYPE_IRCSERVICES]++;
                }
-               else if (sscanf(pw, SCANFMT_PBKDF2, s1) == 1 && strlen(s1) == 140U && strcmp(s1, pw) == 0)
+               else if (pwlen == 140U && sscanf(pw, SCANFMT_PBKDF2, s1) == 1 && strcmp(s1, pw) == 0)
                {
                        // Fuzzy (no rigid format)
                        pwhashes[TYPE_PBKDF2]++;