├── CHANGES ├── INSTALL ├── INTERNALS ├── LICENSE ├── Makefile ├── PLATFORMS ├── README ├── concat.c ├── concat.h ├── libpasswdqc.3 ├── libpasswdqc.map ├── md4.c ├── md4.h ├── pam_macros.h ├── pam_passwdqc.8 ├── pam_passwdqc.c ├── pam_passwdqc.map ├── passwdqc.conf ├── passwdqc.conf.5 ├── passwdqc.h ├── passwdqc.pc.in ├── passwdqc.spec ├── passwdqc_check.3 ├── passwdqc_check.c ├── passwdqc_filter.c ├── passwdqc_filter.h ├── passwdqc_i18n.h ├── passwdqc_load.c ├── passwdqc_memzero.c ├── passwdqc_params_free.3 ├── passwdqc_params_load.3 ├── passwdqc_params_parse.3 ├── passwdqc_params_reset.3 ├── passwdqc_parse.c ├── passwdqc_random.3 ├── passwdqc_random.c ├── po └── ru.po ├── pwqcheck.1 ├── pwqcheck.c ├── pwqcheck.php ├── pwqfilter.1 ├── pwqfilter.c ├── pwqgen.1 ├── pwqgen.c ├── wordset_4k.c └── wordset_4k.h /CHANGES: -------------------------------------------------------------------------------- 1 | Significant changes between 2.0.2 and 2.0.3. 2 | 3 | Added Cygwin support. 4 | 5 | Added pkg-config file. 6 | 7 | Changed enforce=users to support "chpasswd" PAM service in addition to 8 | traditionally supported "passwd". 9 | 10 | 11 | Significant changes between 2.0.1 and 2.0.2. 12 | 13 | Improved pam_passwdqc's auto-generated policy descriptions further, so 14 | that lines are wrapped at a more consistent length. 15 | 16 | Added the libpasswdqc(3) manual page and links to it for all functions 17 | documented in there. 18 | 19 | Added scripts to support Continuous Integration on GitHub (included in 20 | the git repository, but excluded from release tarballs). 21 | 22 | 23 | Significant changes between 2.0.0 and 2.0.1. 24 | 25 | Improved pam_passwdqc's auto-generated policy descriptions, which were 26 | slightly misformatted since the introduction of i18n support in 1.4.0. 27 | Now they not only look prettier, but also make it clearer that the 28 | mentioned lengths are merely the minimums and not the recommended ones. 29 | 30 | Updated Russian translation for consistency with the above and to cover 31 | messages added in 1.9.0+. 32 | 33 | Increased maximum size of randomly-generated passphrases to 136 bits. 34 | This was already the limit in the underlying API, but the tools' limit 35 | was set to 85. This increase is to allow for a wider variety of use 36 | cases for the tools. 37 | 38 | In the Makefile, use CPPFLAGS and LDFLAGS consistently to be friendlier 39 | to packaging by distros. 40 | 41 | Added this file CHANGES based on two latest release announcements, and 42 | started to maintain it. 43 | 44 | 45 | Significant changes between 1.9.0 and 2.0.0. 46 | 47 | Introduce and use passwdqc_params_free(). This is a minor addition to 48 | the libpasswdqc API related to the addition of external files support. 49 | 50 | 51 | Significant changes between 1.5.0 and 1.9.0. 52 | 53 | Added support for external wordlist, denylist, and binary filter. With 54 | these, passwdqc can be configured to deny passwords and passphrases that 55 | are based on lines of a tiny external text file (the "wordlist" option), 56 | directly appear in a tiny external text file (the "denylist" option), 57 | or/and directly appear in a maybe huge binary filter file (the "filter" 58 | option). While usage of larger external text files is inefficient, the 59 | binary filters are very efficient. 60 | 61 | The binary filters can be created and otherwise managed with the newly 62 | added pwqfilter(1) program. It can create a binary filter from a list 63 | of plaintexts or from MD4 or NTLM hashes. The latter are supported in a 64 | way that enables importing of HIBP (Pwned Passwords) database revisions 65 | into passwdqc binary filters. pwqfilter works on arbitrary plain text 66 | strings or hex-encoded hashes, and it can also be reused in lieu of 67 | grep(1) for many purposes, even unrelated to passphrases and security. 68 | 69 | Merged changes needed for building with Visual Studio on Windows. This 70 | includes a refactoring of the random passphrase generator code to make 71 | it shared between platforms. 72 | 73 | 74 | Significant changes between 1.4.1 and 1.5.0. 75 | 76 | Updated the included wordlist to avoid some inappropriate words in 77 | randomly generated passphrases while not removing any words from the 78 | "word-based" check, and also to have plenty of extra words for 79 | subsequent removal of more words that might be considered inappropriate 80 | from the initial 4096 that are used for randomly generated passphrases. 81 | Most of the added words came from EFF Diceware, BIP-0039, and our own 82 | processing of Project Gutenberg Australia books. 83 | 84 | 85 | Significant changes between 1.4.0 and 1.4.1. 86 | 87 | Set default for "max" to 72 (was 40). The previous setting was based on 88 | a reading of RFC 1939, which in practice did not matter. The new one is 89 | based on bcrypt's truncation at 72, which actually still matters. 90 | 91 | Documented "similar" in pwqcheck(1) help message and manual page. This 92 | is a setting that was supported before and documented for other passwdqc 93 | components before, but was apparently erroneously omitted from here. 94 | 95 | 96 | Significant changes between 1.3.2 and 1.4.0. 97 | 98 | Implemented i18n support in pam_passwdqc, contributed by Oleg Solovyov, 99 | Andrey Cherepanov, and Dmitry V. Levin. The i18n support is off by 100 | default, it can be enabled if Linux-PAM is built using --enable-nls 101 | configure option. 102 | 103 | Implemented audit support in pam_passwdqc, contributed by Oleg Solovyov 104 | and Dmitry V. Levin. The audit support is off by default, it can be 105 | enabled if Linux-PAM is built using --enable-audit configure option. 106 | 107 | Both of these optional new features had been introduced and are enabled 108 | in ALT Linux distributions, so this version is effectively upstreaming 109 | the changes from there. 110 | 111 | 112 | Significant changes between 1.3.1 and 1.3.2. 113 | 114 | Compatibility for building with newer versions of glibc, where we now 115 | have to define _DEFAULT_SOURCE for our use of crypt(3). The problem was 116 | identified and this change tested by Dmitry V. Levin. 117 | 118 | Clarified in the man pages that /etc/passwdqc.conf is not read unless 119 | this suggested file location is specified with the config= option. 120 | 121 | Clarified the OpenBSD configuration example. 122 | 123 | Escape the minus sign in the OpenBSD configuration example to make the 124 | man page linter happy, patch by Jackson Doak via Unit 193. 125 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Many Linux distributions, FreeBSD 5.0+, and DragonFly BSD 2.2+ include 2 | pam_passwdqc or the full-blown passwdqc package in the distribution or 3 | provide it as a "native" package to be installed - so you may just use 4 | that. The instructions below apply if your distribution lacks passwdqc 5 | or if you prefer to build and install passwdqc on your own (such as to 6 | get a newer version of it than one available in/for the distribution). 7 | 8 | On a system with the PAM (Pluggable Authentication Modules) framework, 9 | you may build all components of passwdqc (the library, the PAM module, 10 | and two command-line programs) by simply running "make". To install, 11 | run "make install". To uninstall, run "make uninstall". 12 | 13 | On a system with the PAM framework built with i18n support enabled 14 | you may also build pam_passwdqc with i18n support by adding 15 | -DENABLE_NLS=1 to CPPFLAGS. To compile translation files, run 16 | "make locales". To install them, run "make install_locales". 17 | 18 | On a system with the PAM framework built with Linux audit support 19 | enabled you may also build pam_passwdqc with audit support by adding 20 | -DHAVE_LIBAUDIT=1 to CPPFLAGS. 21 | 22 | On a system without PAM, you may build everything but the PAM module 23 | with "make utils". To install, run "make install_lib install_utils". 24 | To uninstall, run "make remove_lib remove_utils". 25 | 26 | Please note that currently passwdqc's default is to install right into 27 | system directories such as /etc, /lib, /usr/lib, /usr/include, 28 | /usr/share/man, /usr/bin. If desired, these pathnames may be overridden 29 | on make's command-line (please see Makefile for the available macro 30 | names and passwdqc.spec for some examples). 31 | 32 | Since passwdqc installs a new shared library, you may need to run the 33 | ldconfig(8) program to update the dynamic linker cache. 34 | 35 | Alternatively, on a Red Hat'ish Linux system and under an account 36 | configured to build RPM packages (perhaps with ~/.rpmmacros specifying 37 | the proper pathnames for %_topdir, %_tmppath, and %buildroot), you may 38 | build RPM packages by running "rpmbuild -tb passwdqc-2.0.3.tar.gz", then 39 | install the two binary subpackages with "rpm -Uvh passwdqc*-2.0.3*.rpm". 40 | This works due to the RPM spec file included in the tarball. 41 | 42 | Please refer to README and PLATFORMS for information on configuring your 43 | system to use the PAM module. You may also refer to the pam_passwdqc(8) 44 | and passwdqc.conf(5) manual pages. 45 | 46 | Please refer to the pwqcheck(1), pwqfilter(1), and pwqgen(1) manual 47 | pages for information on using the command-line programs. 48 | -------------------------------------------------------------------------------- /INTERNALS: -------------------------------------------------------------------------------- 1 | The functions defined in passwdqc.h may be used without PAM at all, and 2 | all of them are in fact exported by libpasswdqc. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Two manual pages (pam_passwdqc.8 and passwdqc.conf.5) are under the 2 | 3-clause BSD-style license as specified within the files themselves. 3 | 4 | concat.c, wordset_4k.c, wordset_4k.h, pam_macros.h, and pwqcheck.php 5 | are in the public domain, but at your option they may also be used under 6 | this package's license below. 7 | 8 | Files in ci directory (install-dependencies.sh and run-build-and-tests.sh) 9 | are provided under the terms of the GNU General Public License version 2 or 10 | later. These files are not included in passwdqc release tarballs. 11 | 12 | The rest of the files in this package fall under the following terms 13 | (heavily cut-down "BSD license"): 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28 | SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2000-2003,2005,2009,2010,2020 by Solar Designer 3 | # Copyright (c) 2008,2009,2017 by Dmitry V. Levin 4 | # Copyright (c) 2017 by Oleg Solovyov 5 | # See LICENSE 6 | # 7 | 8 | PACKAGE = passwdqc 9 | VERSION = 2.0.3 10 | TITLE = pam_passwdqc 11 | SHARED_LIB = libpasswdqc.so.1 12 | DEVEL_LIB = libpasswdqc.so 13 | SHARED_LIB_DARWIN = libpasswdqc.0.dylib 14 | SHARED_LIB_CYGWIN = cygpasswdqc-0.dll 15 | DEVEL_LIB_DARWIN = libpasswdqc.dylib 16 | DEVEL_LIB_CYGWIN = libpasswdqc.dll.a 17 | MAP_LIB = libpasswdqc.map 18 | PAM_SO_SUFFIX = 19 | SHARED_PAM = $(TITLE).so$(PAM_SO_SUFFIX) 20 | MAP_PAM = pam_passwdqc.map 21 | SHLIBMODE = 755 22 | HEADER = passwdqc.h 23 | INCMODE = 644 24 | PKGCONFIG = passwdqc.pc 25 | PKGCONFMODE = 644 26 | MAN1 = pwqgen.1 pwqcheck.1 pwqfilter.1 27 | MAN3 = libpasswdqc.3 \ 28 | passwdqc_params_reset.3 \ 29 | passwdqc_params_load.3 \ 30 | passwdqc_params_parse.3 \ 31 | passwdqc_params_free.3 \ 32 | passwdqc_check.3 \ 33 | passwdqc_random.3 34 | MAN5 = passwdqc.conf.5 35 | MAN8 = $(TITLE).8 36 | MANMODE = 644 37 | BINDIR = /usr/bin 38 | BINMODE = 755 39 | CONFDIR = /etc 40 | CONFMODE = 644 41 | SHARED_LIBDIR = /lib 42 | SHARED_LIBDIR_CYGWIN = /usr/bin 43 | SHARED_LIBDIR_SUN = /usr/lib 44 | SHARED_LIBDIR_REL = ../..$(SHARED_LIBDIR) 45 | DEVEL_LIBDIR = /usr/lib 46 | SECUREDIR = /lib/security 47 | SECUREDIR_SUN = /usr/lib/security 48 | SECUREDIR_DARWIN = /usr/lib/pam 49 | INCLUDEDIR = /usr/include 50 | PKGCONFIGDIR = $(DEVEL_LIBDIR)/pkgconfig 51 | MANDIR = /usr/share/man 52 | DESTDIR = 53 | LOCALEDIR = /usr/share/locale 54 | LOCALEMODE = 644 55 | 56 | LANGUAGES = ru 57 | 58 | CC = gcc 59 | LD = $(CC) 60 | LD_lib = $(LD) 61 | RM = rm -f 62 | LN_s = ln -s -f 63 | MKDIR = umask 022 && mkdir -p 64 | INSTALL = install -c 65 | # We support Sun's older /usr/ucb/install, but not the newer /usr/sbin/install. 66 | INSTALL_SUN = /usr/ucb/install -c 67 | CFLAGS = -Wall -W -O2 68 | CFLAGS_lib = $(CFLAGS) -fPIC 69 | CFLAGS_bin = $(CFLAGS) -fomit-frame-pointer 70 | CPPFLAGS = 71 | CPPFLAGS_bin = $(CPPFLAGS) 72 | CPPFLAGS_lib = $(CPPFLAGS) -DPACKAGE=\\\"$(PACKAGE)\\\" 73 | MSGFMT = msgfmt 74 | XGETTEXT = xgettext 75 | XGETTEXT_OPTS = --keyword=_ --keyword=P2_:1,1 --keyword=P3_:1,2 --language=C --add-comments 76 | MSGMERGE = msgmerge 77 | 78 | LDFLAGS = 79 | LDFLAGS_shared = $(LDFLAGS) --shared 80 | LDFLAGS_shared_LINUX = $(LDFLAGS) --shared 81 | LDFLAGS_shared_SUN = $(LDFLAGS) -G 82 | LDFLAGS_shared_HP = $(LDFLAGS) -b 83 | LDFLAGS_lib = $(LDFLAGS_shared) 84 | LDFLAGS_lib_LINUX = $(LDFLAGS_shared_LINUX) \ 85 | -Wl,--soname,$(SHARED_LIB),--version-script,$(MAP_LIB) 86 | LDFLAGS_lib_SUN = $(LDFLAGS_shared_SUN) 87 | LDFLAGS_lib_HP = $(LDFLAGS_shared_HP) 88 | LDFLAGS_lib_CYGWIN = $(LDFLAGS_shared) \ 89 | -Wl,--out-implib=$(DEVEL_LIB_CYGWIN) \ 90 | -Wl,--export-all-symbols \ 91 | -Wl,--enable-auto-import 92 | LDFLAGS_pam = $(LDFLAGS_shared) 93 | LDFLAGS_pam_LINUX = $(LDFLAGS_shared_LINUX) \ 94 | -Wl,--version-script,$(MAP_PAM) 95 | LDFLAGS_pam_SUN = $(LDFLAGS_shared_SUN) 96 | LDFLAGS_pam_HP = $(LDFLAGS_shared_HP) 97 | 98 | LDLIBS_lib = 99 | LDLIBS_pam = -lpam -lcrypt 100 | LDLIBS_pam_LINUX = -lpam -lcrypt 101 | LDLIBS_pam_SUN = -lpam -lcrypt 102 | LDLIBS_pam_HP = -lpam -lsec 103 | LDLIBS_pam_DARWIN = -lpam -lSystem 104 | 105 | # Uncomment this to use cc instead of gcc 106 | #CC = cc 107 | # Uncomment this to use Sun's C compiler flags 108 | #CFLAGS = -xO2 109 | #CFLAGS_lib = $(CFLAGS) -KPIC 110 | #CFLAGS_bin = $(CFLAGS) 111 | # Uncomment this to use HP's ANSI C compiler flags 112 | #CFLAGS = -Ae +w1 +W 474,486,542 +O2 113 | #CFLAGS_lib = $(CFLAGS) +z 114 | #CFLAGS_bin = $(CFLAGS) 115 | 116 | CONFIGS = passwdqc.conf 117 | BINS = pwqgen pwqcheck pwqfilter 118 | BINS_CYGWIN = $(BINS) $(SHARED_LIB_CYGWIN) 119 | PROJ = $(SHARED_LIB) $(DEVEL_LIB) $(SHARED_PAM) $(BINS) $(PKGCONFIG) 120 | OBJS_LIB = concat.o md4.o passwdqc_check.o passwdqc_filter.o passwdqc_load.o passwdqc_memzero.o passwdqc_parse.o passwdqc_random.o wordset_4k.o 121 | OBJS_PAM = pam_passwdqc.o passwdqc_memzero.o 122 | OBJS_GEN = pwqgen.o passwdqc_memzero.o 123 | OBJS_CHECK = pwqcheck.o passwdqc_memzero.o 124 | OBJS_FILTER = pwqfilter.o md4.o 125 | 126 | default: all 127 | 128 | all locales pam utils install install_lib install_locales install_pam install_utils uninstall remove remove_lib remove_locales remove_pam remove_utils: 129 | case "`uname -s`" in \ 130 | Linux) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ 131 | LDFLAGS_lib="$(LDFLAGS_lib_LINUX)" \ 132 | LDFLAGS_pam="$(LDFLAGS_pam_LINUX)" \ 133 | LDLIBS_pam="$(LDLIBS_pam_LINUX)" \ 134 | $@_wrapped;; \ 135 | SunOS) $(MAKE) -e CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ 136 | LD_lib=/usr/ccs/bin/ld \ 137 | LDFLAGS_lib="$(LDFLAGS_lib_SUN)" \ 138 | LDFLAGS_pam="$(LDFLAGS_pam_SUN)" \ 139 | LDLIBS_pam="$(LDLIBS_pam_SUN)" \ 140 | INSTALL="$(INSTALL_SUN)" \ 141 | SHARED_LIBDIR="$(SHARED_LIBDIR_SUN)" \ 142 | SECUREDIR="$(SECUREDIR_SUN)" \ 143 | $@_wrapped;; \ 144 | HP-UX) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ 145 | LD_lib=ld \ 146 | LDFLAGS_lib="$(LDFLAGS_lib_HP)" \ 147 | LDFLAGS_pam="$(LDFLAGS_pam_HP)" \ 148 | LDLIBS_pam="$(LDLIBS_pam_HP)" \ 149 | $@_wrapped;; \ 150 | Darwin) $(MAKE) \ 151 | SHARED_LIB="$(SHARED_LIB_DARWIN)" \ 152 | DEVEL_LIB="$(DEVEL_LIB_DARWIN)" \ 153 | SECUREDIR="$(SECUREDIR_DARWIN)" \ 154 | LDLIBS_pam="$(LDLIBS_pam_DARWIN)" \ 155 | $@_wrapped;; \ 156 | CYGWIN_NT*) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib)" \ 157 | SHARED_LIB="$(SHARED_LIB_CYGWIN)" \ 158 | SHARED_LIBDIR="$(SHARED_LIBDIR_CYGWIN)" \ 159 | DEVEL_LIB="$(DEVEL_LIB_CYGWIN)" \ 160 | LDFLAGS_lib="$(LDFLAGS_lib_CYGWIN)" \ 161 | BINS="$(BINS_CYGWIN)" \ 162 | CYGWIN=true \ 163 | $@_wrapped;; \ 164 | *) $(MAKE) $@_wrapped;; \ 165 | esac 166 | 167 | all_wrapped: pam_wrapped utils_wrapped $(PKGCONFIG) 168 | 169 | pam_wrapped: $(SHARED_PAM) 170 | 171 | utils_wrapped: $(BINS) 172 | 173 | $(SHARED_LIB): $(OBJS_LIB) $(MAP_LIB) 174 | $(LD_lib) $(LDFLAGS_lib) $(OBJS_LIB) $(LDLIBS_lib) -o $(SHARED_LIB) 175 | 176 | $(DEVEL_LIB): $(SHARED_LIB) 177 | if [ "$(CYGWIN)" != true ]; then \ 178 | $(LN_s) $(SHARED_LIB) $(DEVEL_LIB); \ 179 | fi 180 | 181 | $(SHARED_PAM): $(OBJS_PAM) $(MAP_PAM) $(DEVEL_LIB) 182 | $(LD_lib) $(LDFLAGS_pam) $(OBJS_PAM) $(LDLIBS_pam) -L. -lpasswdqc -o $(SHARED_PAM) 183 | 184 | pwqgen: $(OBJS_GEN) $(DEVEL_LIB) 185 | $(LD) $(LDFLAGS) $(OBJS_GEN) -L. -lpasswdqc -o $@ 186 | 187 | pwqcheck: $(OBJS_CHECK) $(DEVEL_LIB) 188 | $(LD) $(LDFLAGS) $(OBJS_CHECK) -L. -lpasswdqc -o $@ 189 | 190 | pwqfilter: $(OBJS_FILTER) 191 | $(LD) $(LDFLAGS) $(OBJS_FILTER) -o $@ 192 | 193 | pwqgen.o: pwqgen.c passwdqc.h 194 | $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c 195 | 196 | pwqcheck.o: pwqcheck.c passwdqc.h 197 | $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c 198 | 199 | pwqfilter.o: pwqfilter.c passwdqc_filter.h passwdqc.h 200 | $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c 201 | 202 | .c.o: 203 | $(CC) $(CPPFLAGS_lib) $(CFLAGS_lib) -c $*.c 204 | 205 | $(PKGCONFIG): $(PKGCONFIG).in 206 | sed -e "s|@VERSION@|$(VERSION)|g" $(PKGCONFIG).in > $@ 207 | 208 | concat.o: concat.h 209 | pam_passwdqc.o: passwdqc.h pam_macros.h 210 | passwdqc_check.o: passwdqc.h passwdqc_filter.h wordset_4k.h 211 | passwdqc_filter.o: passwdqc.h passwdqc_filter.h 212 | passwdqc_load.o: passwdqc.h concat.h 213 | passwdqc_parse.o: passwdqc.h concat.h 214 | passwdqc_random.o: passwdqc.h wordset_4k.h 215 | wordset_4k.o: wordset_4k.h 216 | 217 | install_wrapped: install_lib_wrapped install_utils_wrapped install_pam_wrapped 218 | @echo 'Consider running ldconfig(8) to update the dynamic linker cache.' 219 | 220 | install_lib_wrapped: 221 | $(MKDIR) $(DESTDIR)$(CONFDIR) 222 | $(INSTALL) -m $(CONFMODE) $(CONFIGS) $(DESTDIR)$(CONFDIR)/ 223 | 224 | $(MKDIR) $(DESTDIR)$(SHARED_LIBDIR) 225 | $(INSTALL) -m $(SHLIBMODE) $(SHARED_LIB) $(DESTDIR)$(SHARED_LIBDIR)/ 226 | 227 | $(MKDIR) $(DESTDIR)$(DEVEL_LIBDIR) 228 | if [ "$(CYGWIN)" != true ]; then \ 229 | $(LN_s) $(SHARED_LIBDIR_REL)/$(SHARED_LIB) \ 230 | $(DESTDIR)$(DEVEL_LIBDIR)/$(DEVEL_LIB); \ 231 | else \ 232 | $(INSTALL) -m $(SHLIBMODE) $(DEVEL_LIB) $(DESTDIR)$(DEVEL_LIBDIR)/; \ 233 | fi 234 | 235 | $(MKDIR) $(DESTDIR)$(INCLUDEDIR) 236 | $(INSTALL) -m $(INCMODE) $(HEADER) $(DESTDIR)$(INCLUDEDIR)/ 237 | 238 | $(MKDIR) $(DESTDIR)$(PKGCONFIGDIR) 239 | $(INSTALL) -m $(PKGCONFMODE) $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/ 240 | 241 | $(MKDIR) $(DESTDIR)$(MANDIR)/man3 242 | $(INSTALL) -m $(MANMODE) $(MAN3) $(DESTDIR)$(MANDIR)/man3/ 243 | 244 | $(MKDIR) $(DESTDIR)$(MANDIR)/man5 245 | $(INSTALL) -m $(MANMODE) $(MAN5) $(DESTDIR)$(MANDIR)/man5/ 246 | 247 | install_utils_wrapped: 248 | $(MKDIR) $(DESTDIR)$(BINDIR) 249 | $(INSTALL) -m $(BINMODE) $(BINS) $(DESTDIR)$(BINDIR)/ 250 | 251 | $(MKDIR) $(DESTDIR)$(MANDIR)/man1 252 | $(INSTALL) -m $(MANMODE) $(MAN1) $(DESTDIR)$(MANDIR)/man1/ 253 | 254 | install_pam_wrapped: 255 | $(MKDIR) $(DESTDIR)$(SECUREDIR) 256 | $(INSTALL) -m $(SHLIBMODE) $(SHARED_PAM) $(DESTDIR)$(SECUREDIR)/ 257 | 258 | $(MKDIR) $(DESTDIR)$(MANDIR)/man8 259 | $(INSTALL) -m $(MANMODE) $(MAN8) $(DESTDIR)$(MANDIR)/man8/ 260 | 261 | POFILES = $(LANGUAGES:%=po/%.po) 262 | MOFILES = $(LANGUAGES:%=po/%.mo) 263 | POTFILE_DEPS = pam_passwdqc.c passwdqc_check.c 264 | POTFILE = po/$(PACKAGE).pot 265 | 266 | $(POTFILE): $(POTFILE_DEPS) 267 | $(XGETTEXT) $(XGETTEXT_OPTS) -o $@-t $^ && mv $@-t $@ 268 | 269 | .SUFFIXES: .po .mo 270 | 271 | .po.mo: 272 | $(MSGFMT) -c -o $@-t $< && mv $@-t $@ 273 | 274 | update_po: $(POTFILE) $(POFILES) 275 | for f in $(POFILES); do $(MSGMERGE) -U $$f $< || exit; done 276 | 277 | update_mo: $(MOFILES) 278 | 279 | locales_wrapped: update_mo 280 | 281 | install_locales_wrapped: 282 | for lang in $(LANGUAGES); do \ 283 | $(MKDIR) $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES && \ 284 | $(INSTALL) -m $(LOCALEMODE) po/$$lang.mo \ 285 | $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/$(PACKAGE).mo || exit; \ 286 | done 287 | 288 | uninstall_wrapped remove_wrapped: remove_pam_wrapped remove_utils_wrapped remove_lib_wrapped remove_locales_wrapped 289 | 290 | remove_pam_wrapped: 291 | $(RM) $(DESTDIR)$(MANDIR)/man8/$(MAN8) 292 | $(RM) $(DESTDIR)$(SECUREDIR)/$(SHARED_PAM) 293 | 294 | remove_utils_wrapped: 295 | for f in $(MAN1); do $(RM) $(DESTDIR)$(MANDIR)/man1/$$f; done 296 | for f in $(BINS); do $(RM) $(DESTDIR)$(BINDIR)/$$f; done 297 | 298 | remove_lib_wrapped: 299 | for f in $(MAN5); do $(RM) $(DESTDIR)$(MANDIR)/man5/$$f; done 300 | for f in $(MAN3); do $(RM) $(DESTDIR)$(MANDIR)/man3/$$f; done 301 | for f in $(HEADER); do $(RM) $(DESTDIR)$(INCLUDEDIR)/$$f; done 302 | for f in $(PKGCONFIG); do $(RM) $(DESTDIR)$(PKGCONFIGDIR)/$$f; done 303 | for f in $(DEVEL_LIB); do $(RM) $(DESTDIR)$(DEVEL_LIBDIR)/$$f; done 304 | for f in $(SHARED_LIB); do $(RM) $(DESTDIR)$(SHARED_LIBDIR)/$$f; done 305 | for f in $(CONFIGS); do $(RM) $(DESTDIR)$(CONFDIR)/$$f; done 306 | 307 | remove_locales_wrapped: 308 | for f in $(LANGUAGES); do $(RM) $(DESTDIR)$(LOCALEDIR)/$$f/LC_MESSAGES/$(PACKAGE).mo; done 309 | 310 | clean: 311 | $(RM) $(PROJ) $(POTFILE) $(MOFILES) *.o 312 | 313 | .PHONY: all all_wrapped clean install install_lib install_locales install_pam install_utils \ 314 | pam pam_wrapped uninstall remove remove_lib remove_pam remove_utils \ 315 | utils utils_wrapped \ 316 | update_mo update_po \ 317 | locales locales_wrapped \ 318 | install_wrapped install_lib_wrapped install_locales_wrapped install_pam_wrapped \ 319 | install_utils_wrapped \ 320 | remove_wrapped remove_lib_wrapped remove_locales_wrapped remove_pam_wrapped \ 321 | remove_utils_wrapped 322 | -------------------------------------------------------------------------------- /PLATFORMS: -------------------------------------------------------------------------------- 1 | Please see the README for instructions common to all platforms and 2 | descriptions of the options mentioned here. 3 | 4 | 5 | Linux. 6 | 7 | Most modern Linux distributions use Linux-PAM with a password changing 8 | module which understands "use_authtok". Thus, you may choose which 9 | module prompts for the old password, things should work either way. 10 | 11 | 12 | FreeBSD 5+, DragonFly BSD 2.2+. 13 | 14 | FreeBSD 5 and newer, as well as DragonFly BSD 2.2 and newer, include 15 | pam_passwdqc in the base system. You should be able to use either the 16 | included or the distributed separately version of pam_passwdqc with 17 | these systems. There's a commented out usage example in the default 18 | /etc/pam.d/passwd. 19 | 20 | FreeBSD 4 and older used a cut down version of Linux-PAM (not OpenPAM) 21 | and didn't use PAM for password changing. 22 | 23 | 24 | OpenBSD. 25 | 26 | OpenBSD does not use PAM, however it is able to use passwdqc's pwqcheck 27 | program. Insert the line ":passwordcheck=/usr/bin/pwqcheck -1:\" 28 | (without the quotes, but with the trailing backslash) into the "default" 29 | section in /etc/login.conf. 30 | 31 | 32 | Solaris, HP-UX 11. 33 | 34 | On Solaris 2.6, 7, and 8 (without patch 108993-18/108994-18 or later) 35 | and on HP-UX 11, pam_passwdqc has to ask for the old password during 36 | the update phase. Use "ask_oldauthtok=update check_oldauthtok" with 37 | pam_passwdqc and "use_first_pass" with pam_unix. 38 | 39 | On Solaris 8 (with patch 108993-18/108994-18 or later), 9, and 10, 40 | use pam_passwdqc instead of both pam_authtok_get and pam_authtok_check, 41 | and set "retry=1" with pam_passwdqc as the passwd command has its own 42 | handling for that. 43 | 44 | You will likely also need to set "max=8" in order to actually enforce 45 | not-so-weak passwords with the obsolete traditional DES-based hashes 46 | that most Solaris systems use and the flawed approach HP-UX uses to 47 | process characters past 8. Of course this way you only get about one 48 | third of the functionality of pam_passwdqc. As a better alternative, 49 | on modern Solaris systems you may edit the "CRYPT_DEFAULT=__unix__" line 50 | in /etc/security/policy.conf to read "CRYPT_DEFAULT=2a" to enable the 51 | OpenBSD-style bcrypt (Blowfish-based) password hashing. 52 | 53 | There's a wiki page with detailed instructions specific to Solaris: 54 | 55 | https://openwall.info/wiki/passwdqc/solaris 56 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | pam_passwdqc is a simple password strength checking module for 2 | PAM-aware password changing programs, such as passwd(1). In addition 3 | to checking regular passwords, it offers support for passphrases and 4 | can provide randomly generated ones. All features are optional and 5 | can be (re-)configured without rebuilding. 6 | 7 | This module should be stacked before your usual password changing 8 | module (such as pam_unix or pam_pwdb) in the password management group 9 | (the "password" lines in /etc/pam.d/passwd or /etc/pam.conf). The 10 | password changing module should then be told to use the provided new 11 | authentication token (new password) rather than request it from the 12 | user. There's usually the "use_authtok" option to do that. If your 13 | password changing module lacks the "use_authtok" option or its prompts 14 | are inconsistent with pam_passwdqc's, you may tell pam_passwdqc to ask 15 | for the old password as well, with "ask_oldauthtok". In that case the 16 | option to use with the password changing module is "use_first_pass". 17 | 18 | There are a number of supported options, which can be used to modify the 19 | behavior of pam_passwdqc (defaults are given in square brackets): 20 | 21 | config=FILE [] 22 | 23 | Load the specified configuration FILE, which must be in the 24 | passwdqc.conf format (described in the passwdqc.conf(5) manual page). 25 | This file may define any options described in here, including load of 26 | yet another configuration file, but loops are not allowed. 27 | 28 | min=N0,N1,N2,N3,N4 [min=disabled,24,11,8,7] 29 | 30 | The minimum allowed password lengths for different kinds of passwords 31 | and passphrases. The keyword "disabled" can be used to disallow 32 | passwords of a given kind regardless of their length. Each subsequent 33 | number is required to be no larger than the preceding one. 34 | 35 | N0 is used for passwords consisting of characters from one character 36 | class only. The character classes are: digits, lower-case letters, 37 | upper-case letters, and other characters. There is also a special 38 | class for non-ASCII characters, which could not be classified, but are 39 | assumed to be non-digits. 40 | 41 | N1 is used for passwords consisting of characters from two character 42 | classes that do not meet the requirements for a passphrase. 43 | 44 | N2 is used for passphrases. Note that besides meeting this length 45 | requirement, a passphrase must also consist of a sufficient number of 46 | words (see the "passphrase" option below). 47 | 48 | N3 and N4 are used for passwords consisting of characters from three 49 | and four character classes, respectively. 50 | 51 | When calculating the number of character classes, upper-case letters 52 | used as the first character and digits used as the last character of a 53 | password are not counted. 54 | 55 | In addition to being sufficiently long, passwords are required to 56 | contain enough different characters for the character classes and 57 | the minimum length they have been checked against. 58 | 59 | max=N [max=72] 60 | 61 | The maximum allowed password length. This can be used to prevent 62 | users from setting passwords that may be too long for some system 63 | services. 64 | 65 | The value 8 is treated specially: with max=8, passwords longer than 8 66 | characters will not be rejected, but will be truncated to 8 characters 67 | for the strength checks and the user will be warned. This is to be 68 | used with the traditional DES-based password hashes, which truncate 69 | the password at 8 characters. 70 | 71 | It is important that you do set max=8 if you are using the traditional 72 | hashes, or some weak passwords will pass the checks. 73 | 74 | passphrase=N [passphrase=3] 75 | 76 | The number of words required for a passphrase, or 0 to disable the 77 | support for user-chosen passphrases. 78 | 79 | match=N [match=4] 80 | 81 | The length of common substring required to conclude that a password is 82 | at least partially based on information found in a character string, 83 | or 0 to disable the substring search. Note that the password will not 84 | be rejected once a weak substring is found; it will instead be 85 | subjected to the usual strength requirements with the weak substring 86 | partially discounted. 87 | 88 | The substring search is case-insensitive and is able to detect and 89 | remove a common substring spelled backwards. 90 | 91 | similar=permit|deny [similar=deny] 92 | 93 | Whether a new password is allowed to be similar to the old one. The 94 | passwords are considered to be similar when there is a sufficiently 95 | long common substring and the new password with the substring partially 96 | discounted would be weak. 97 | 98 | wordlist=FILE [] 99 | 100 | Deny passwords that are based on lines of a tiny external text file, 101 | which can reasonably be e.g. a list of a few thousand common passwords. 102 | Common dictionary words may also reasonably be included, especially in a 103 | local language other than English, or longer yet common English words. 104 | (passwdqc includes a list of a few thousand common English words of 105 | lengths from 3 to 6 built in. Any word list possibly specified with 106 | this option is used in addition to the built-in word list.) 107 | 108 | Substring matching and discounting will be used if the "match" setting 109 | above is non-zero. Please note that this is very inefficient, and isn't 110 | to be used with large wordlists. 111 | 112 | denylist=FILE [] 113 | 114 | Deny passwords or passphrases directly appearing in a tiny external text 115 | file. That file can reasonably be e.g. a list of common passwords if 116 | only a relaxed policy is desired and stricter checks are thus disabled 117 | (using their separate options). Such policy would only be somewhat 118 | effective against online/remote attacks, but not against offline attacks 119 | on hashed passwords. 120 | 121 | filter=FILE [] 122 | 123 | Deny passwords or passphrases directly appearing in a maybe huge binary 124 | filter file created with pwqfilter. This is very efficient, needing at 125 | most two random disk reads per query. A filter created from millions of 126 | leaked passwords can reasonably be used on top of passwdqc's other 127 | checks to further reduce the number of passing yet weak passwords 128 | without causing unreasonable inconvenience (as e.g. higher minimum 129 | lengths and character set requirements could). 130 | 131 | random=N[,only] [random=47] 132 | 133 | The size of randomly-generated passphrases in bits (24 to 136), or 0 to 134 | disable this feature. Any passphrase that contains the offered 135 | randomly-generated string will be allowed regardless of other possible 136 | restrictions. 137 | 138 | The "only" modifier can be used to disallow user-chosen passwords. 139 | 140 | enforce=none|users|everyone [enforce=everyone] 141 | 142 | The module can be configured to warn of weak passwords only, but not 143 | actually enforce strong passwords. The "users" setting is like 144 | "everyone" for all PAM services except "chpasswd" and "passwd". 145 | For these two PAM services "users" will enforce strong passwords 146 | for invocations by non-root users only. 147 | 148 | non-unix [] 149 | 150 | Normally, the module uses getpwnam(3) to obtain the user's personal 151 | login information and use that during the password strength checks. 152 | This behavior can be disabled with the "non-unix" option. 153 | 154 | retry=N [retry=3] 155 | 156 | The number of times the module will ask for a new password if the user 157 | fails to provide a sufficiently strong password and enter it twice the 158 | first time. 159 | 160 | ask_oldauthtok[=update] [] 161 | 162 | Ask for the old password as well. Normally, pam_passwdqc leaves this 163 | task for subsequent modules. With no argument, the "ask_oldauthtok" 164 | option will cause pam_passwdqc to ask for the old password during the 165 | preliminary check phase. With "ask_oldauthtok=update", pam_passwdqc 166 | will do that during the update phase. 167 | 168 | check_oldauthtok [] 169 | 170 | This tells pam_passwdqc to validate the old password before giving a 171 | new password prompt. Normally, this task is left for subsequent 172 | modules. 173 | 174 | The primary use for this option is when "ask_oldauthtok=update" is 175 | also specified, in which case no other module gets a chance to ask 176 | for and validate the password. Of course, this will only work with 177 | Unix passwords. 178 | 179 | use_first_pass [] 180 | use_authtok [] 181 | 182 | Use the new password obtained by modules stacked before pam_passwdqc. 183 | This disables user interaction within pam_passwdqc. With this module, 184 | the only difference between "use_first_pass" and "use_authtok" is that 185 | the former is incompatible with "ask_oldauthtok". 186 | 187 | noaudit [] 188 | 189 | If audit is enabled at build time, the PAM module logs audit events once 190 | user tries to change their credentials. This option disables that audit 191 | logging. 192 | -------------------------------------------------------------------------------- /concat.c: -------------------------------------------------------------------------------- 1 | /* 2 | * concat() - allocate memory and safely concatenate strings in portable C 3 | * (and C++ if you like). 4 | * 5 | * This code deals gracefully with potential integer overflows (perhaps when 6 | * input strings are maliciously long), as well as with input strings changing 7 | * from under it (perhaps because of misbehavior of another thread). It does 8 | * not depend on non-portable functions such as snprintf() and asprintf(). 9 | * 10 | * Written by Solar Designer and placed in the 11 | * public domain. 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "concat.h" 19 | 20 | char *concat(const char *s1, ...) 21 | { 22 | va_list args; 23 | const char *s; 24 | char *p, *result; 25 | size_t l, m, n; 26 | 27 | m = n = strlen(s1); 28 | va_start(args, s1); 29 | while ((s = va_arg(args, char *))) { 30 | l = strlen(s); 31 | if ((m += l) < l) 32 | break; 33 | } 34 | va_end(args); 35 | if (s || m >= INT_MAX) 36 | return NULL; 37 | 38 | result = (char *)malloc(m + 1); 39 | if (!result) 40 | return NULL; 41 | 42 | memcpy(p = result, s1, n); 43 | p += n; 44 | va_start(args, s1); 45 | while ((s = va_arg(args, char *))) { 46 | l = strlen(s); 47 | if ((n += l) < l || n > m) 48 | break; 49 | memcpy(p, s, l); 50 | p += l; 51 | } 52 | va_end(args); 53 | if (s || m != n || p != result + n) { 54 | free(result); 55 | return NULL; 56 | } 57 | 58 | *p = 0; 59 | return result; 60 | } 61 | 62 | #ifdef TEST 63 | #include 64 | 65 | int main(int argc, char **argv) 66 | { 67 | puts(concat(argv[0], argv[1], argv[2], argv[3], NULL)); 68 | return 0; 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /concat.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 by Dmitry V. Levin. See LICENSE. 3 | */ 4 | 5 | #ifndef CONCAT_H__ 6 | #define CONCAT_H__ 7 | 8 | extern char *concat(const char *, ...); 9 | 10 | #endif /* CONCAT_H__ */ 11 | -------------------------------------------------------------------------------- /libpasswdqc.3: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2021 Dmitry V. Levin 2 | .\" All rights reserved. 3 | .\" 4 | .\" Redistribution and use in source and binary forms, with or without 5 | .\" modification, are permitted. 6 | .\" 7 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 8 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 9 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 10 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 11 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 12 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 13 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 14 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 15 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 16 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 17 | .\" SUCH DAMAGE. 18 | .\" 19 | .Dd March 19, 2021 20 | .Dt LIBPASSWDQC 3 21 | .Os "Openwall Project" 22 | .Sh NAME 23 | .Nm passwdqc_params_reset , 24 | .Nm passwdqc_params_load , 25 | .Nm passwdqc_params_parse , 26 | .Nm passwdqc_params_free , 27 | .Nm passwdqc_check , 28 | .Nm passwdqc_random 29 | .Nd password strength checking functions 30 | .Sh LIBRARY 31 | Password strength checking library 32 | .Pq libpasswdqc, -lpasswdqc 33 | .Sh SYNOPSIS 34 | .In passwdqc.h 35 | .Bd -literal 36 | typedef struct { 37 | passwdqc_params_qc_t qc; 38 | passwdqc_params_pam_t pam; 39 | } passwdqc_params_t; 40 | .Ed 41 | .Ft void 42 | .Fn passwdqc_params_reset "passwdqc_params_t *params" 43 | .Ft int 44 | .Fo passwdqc_params_load 45 | .Fa "passwdqc_params_t *params" 46 | .Fa "char **reason" 47 | .Fa "const char *pathname" 48 | .Fc 49 | .Ft int 50 | .Fo passwdqc_params_parse 51 | .Fa "passwdqc_params_t *params" 52 | .Fa "char **reason" 53 | .Fa "int argc" 54 | .Fa "const char *const *argv" 55 | .Fc 56 | .Ft void 57 | .Fn passwdqc_params_free "passwdqc_params_t *params" 58 | .Ft const char * 59 | .Fo passwdqc_check 60 | .Fa "const passwdqc_params_qc_t *params" 61 | .Fa "const char *newpass" 62 | .Fa "const char *oldpass" 63 | .Fa "const struct passwd *pw" 64 | .Fc 65 | .Ft char * 66 | .Fn passwdqc_random "const passwdqc_params_qc_t *params" 67 | .Sh DESCRIPTION 68 | The 69 | .Fn passwdqc_params_reset 70 | function initializes the passwdqc_params_t structure specified by 71 | .Fa params 72 | argument to compile-time defaults. 73 | .Pp 74 | The 75 | .Fn passwdqc_params_load 76 | function fills in the passwdqc_params_t structure specified by 77 | .Fa params 78 | argument according to the configuration options listed in the file specified by 79 | .Fa pathname 80 | argument. When the passwdqc_params_t structure is no longer needed, 81 | the memory allocated by this function should be released using 82 | .Fn passwdqc_params_free . 83 | .Pp 84 | The 85 | .Fn passwdqc_params_parse 86 | function fills in the passwdqc_params_t structure specified by 87 | .Fa params 88 | argument according to the configuration options specified by 89 | .Fa argc 90 | and 91 | .Fa argv 92 | arguments. When the passwdqc_params_t structure is no longer needed, 93 | the memory allocated by this function should be released using 94 | .Fn passwdqc_params_free . 95 | .Pp 96 | The 97 | .Fn passwdqc_params_free 98 | function frees the memory allocated by 99 | .Fn passwdqc_params_load 100 | and 101 | .Fn passwdqc_params_parse 102 | functions when filling in the passwdqc_params_t structure specified by 103 | .Fa params 104 | argument. 105 | .Pp 106 | The 107 | .Fn passwdqc_check 108 | function checks the quality of the passphrase specified by 109 | .Fa newpass 110 | argument according to the configuration specified by 111 | .Fa params 112 | argument. If an optional old passphrase is specified by 113 | .Fa oldpass 114 | argument, 115 | .Fa newpass 116 | is additionally checked against 117 | .Fa oldpass 118 | for similarity. If an optional passwd record is specified by 119 | .Fa pw 120 | argument, 121 | .Fa newpass 122 | is additionally checked whether it is based on the personal login information 123 | in the passwd record. 124 | .Pp 125 | The 126 | .Fn passwdqc_random 127 | function generates a random passphrase according to the configuration 128 | specified by 129 | .Fa params 130 | argument. 131 | .Sh RETURN VALUES 132 | The 133 | .Fn passwdqc_params_reset 134 | and 135 | .Fn passwdqc_params_free 136 | functions do not return a value. 137 | .Pp 138 | Upon successful completion the 139 | .Fn passwdqc_params_load 140 | and 141 | .Fn passwdqc_params_parse 142 | functions return 0. Otherwise, -1 is returned and a pointer to dynamically 143 | allocated memory containing the error string is assigned to 144 | .Fa *reason . 145 | This memory should be released using free(3) when no longer needed. 146 | .Pp 147 | Upon successful completion the 148 | .Fn passwdqc_check 149 | function returns NULL. Otherwise, a string describing the error is returned. 150 | The returned string is statically allocated and valid for the lifetime of the 151 | program. 152 | .Pp 153 | Upon successful completion the 154 | .Fn passwdqc_random 155 | function returns a dynamically allocated string containing the generated 156 | passphrase. Otherwise, NULL is returned. The string should be released using 157 | free(3) when no longer needed. 158 | .Sh FILES 159 | .Pa /etc/passwdqc.conf 160 | (not read unless this suggested file location is specified with the 161 | .Ar pathname 162 | argument or with 163 | .Cm config Ns = Ns Ar /etc/passwdqc.conf 164 | configuration option). 165 | .Sh EXAMPLES 166 | The following example shows how to use the libpasswdqc library with system 167 | configuration options to check a passphrase. 168 | .Bd -literal -offset 2n 169 | #include 170 | #include 171 | #include 172 | #include 173 | 174 | bool 175 | check(const char *newpass, const char *oldpass, const struct passwd *pw) 176 | { 177 | static const char config[] = "/etc/passwdqc.conf"; 178 | char *parse_reason; 179 | const char *check_result = ""; 180 | passwdqc_params_t params; 181 | passwdqc_params_reset(¶ms); 182 | if (passwdqc_params_load(¶ms, &parse_reason, config)) { 183 | fprintf(stderr, "passwdqc_params_load: %s\en", 184 | parse_reason ? parse_reason : "Out of memory"); 185 | free(parse_reason); 186 | goto out; 187 | } 188 | check_result = passwdqc_check(¶ms.qc, newpass, oldpass, pw); 189 | if (check_result) 190 | fprintf(stderr, "passwdqc_check: %s\en", check_result); 191 | out: 192 | passwdqc_params_free(¶ms); 193 | return !check_result; 194 | } 195 | .Ed 196 | .Sh SEE ALSO 197 | .Xr passwdqc.conf 5 , 198 | .Xr pwqcheck 1 , 199 | .Xr pwqgen 1 , 200 | .Xr pam_passwdqc 8 . 201 | .Pp 202 | https://www.openwall.com/passwdqc/ 203 | .Sh HISTORY 204 | The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. 205 | The libpasswdqc library was originally written for ALT GNU/*/Linux 206 | by Dmitry V. Levin, reusing code from pam_passwdqc. 207 | The 208 | .Fn passwdqc_params_free 209 | function was added in version 2.0.0 by Solar Designer. 210 | .Sh AUTHORS 211 | This manual page was written by Dmitry V. Levin. 212 | -------------------------------------------------------------------------------- /libpasswdqc.map: -------------------------------------------------------------------------------- 1 | { 2 | global: 3 | passwdqc_check; 4 | passwdqc_params_load; 5 | passwdqc_params_parse; 6 | passwdqc_params_reset; 7 | passwdqc_params_free; 8 | passwdqc_random; 9 | local: 10 | *; 11 | }; 12 | -------------------------------------------------------------------------------- /md4.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This is an OpenSSL API compatible (but not ABI compatible) implementation 3 | * of the RSA Data Security, Inc. MD4 Message-Digest Algorithm (RFC 1320). 4 | * 5 | * Homepage: 6 | * https://openwall.info/wiki/people/solar/software/public-domain-source-code/md4 7 | * 8 | * Author: 9 | * Alexander Peslyak, better known as Solar Designer 10 | * 11 | * This software was written by Alexander Peslyak in 2001. No copyright is 12 | * claimed, and the software is hereby placed in the public domain. 13 | * In case this attempt to disclaim copyright and place the software in the 14 | * public domain is deemed null and void, then the software is 15 | * Copyright (c) 2001 Alexander Peslyak and it is hereby released to the 16 | * general public under the following terms: 17 | * 18 | * Redistribution and use in source and binary forms, with or without 19 | * modification, are permitted. 20 | * 21 | * There's ABSOLUTELY NO WARRANTY, express or implied. 22 | * 23 | * (This is a heavily cut-down "BSD license".) 24 | * 25 | * This differs from Colin Plumb's older public domain implementation in that 26 | * no exactly 32-bit integer data type is required (any 32-bit or wider 27 | * unsigned integer data type will do), there's no compile-time endianness 28 | * configuration, and the function prototypes match OpenSSL's. No code from 29 | * Colin Plumb's implementation has been reused; this comment merely compares 30 | * the properties of the two independent implementations. 31 | * 32 | * The primary goals of this implementation are portability and ease of use. 33 | * It is meant to be fast, but not as fast as possible. Some known 34 | * optimizations are not included to reduce source code size and avoid 35 | * compile-time configuration. 36 | */ 37 | 38 | #ifndef HAVE_OPENSSL 39 | 40 | #include 41 | 42 | #include "md4.h" 43 | 44 | /* 45 | * The basic MD4 functions. 46 | * 47 | * F and G are optimized compared to their RFC 1320 definitions, with the 48 | * optimization for F borrowed from Colin Plumb's MD5 implementation. 49 | */ 50 | #define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z)))) 51 | #define G(x, y, z) (((x) & ((y) | (z))) | ((y) & (z))) 52 | #define H(x, y, z) ((x) ^ (y) ^ (z)) 53 | 54 | /* 55 | * The MD4 transformation for all three rounds. 56 | */ 57 | #define STEP(f, a, b, c, d, x, s) \ 58 | (a) += f((b), (c), (d)) + (x); \ 59 | (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); 60 | 61 | /* 62 | * SET reads 4 input bytes in little-endian byte order and stores them in a 63 | * properly aligned word in host byte order. 64 | * 65 | * The check for little-endian architectures that tolerate unaligned memory 66 | * accesses is just an optimization. Nothing will break if it fails to detect 67 | * a suitable architecture. 68 | * 69 | * Unfortunately, this optimization may be a C strict aliasing rules violation 70 | * if the caller's data buffer has effective type that cannot be aliased by 71 | * MD4_u32plus. In practice, this problem may occur if these MD4 routines are 72 | * inlined into a calling function, or with future and dangerously advanced 73 | * link-time optimizations. For the time being, keeping these MD4 routines in 74 | * their own translation unit avoids the problem. 75 | */ 76 | #if defined(__i386__) || defined(__x86_64__) || defined(__vax__) 77 | #define SET(n) \ 78 | (*(MD4_u32plus *)&ptr[(n) * 4]) 79 | #define GET(n) \ 80 | SET(n) 81 | #else 82 | #define SET(n) \ 83 | (ctx->block[(n)] = \ 84 | (MD4_u32plus)ptr[(n) * 4] | \ 85 | ((MD4_u32plus)ptr[(n) * 4 + 1] << 8) | \ 86 | ((MD4_u32plus)ptr[(n) * 4 + 2] << 16) | \ 87 | ((MD4_u32plus)ptr[(n) * 4 + 3] << 24)) 88 | #define GET(n) \ 89 | (ctx->block[(n)]) 90 | #endif 91 | 92 | /* 93 | * This processes one or more 64-byte data blocks, but does NOT update the bit 94 | * counters. There are no alignment requirements. 95 | */ 96 | static const void *body(MD4_CTX *ctx, const void *data, size_t size) 97 | { 98 | const unsigned char *ptr; 99 | MD4_u32plus a, b, c, d; 100 | MD4_u32plus saved_a, saved_b, saved_c, saved_d; 101 | const MD4_u32plus ac1 = 0x5a827999, ac2 = 0x6ed9eba1; 102 | 103 | ptr = (const unsigned char *)data; 104 | 105 | a = ctx->a; 106 | b = ctx->b; 107 | c = ctx->c; 108 | d = ctx->d; 109 | 110 | do { 111 | saved_a = a; 112 | saved_b = b; 113 | saved_c = c; 114 | saved_d = d; 115 | 116 | /* Round 1 */ 117 | STEP(F, a, b, c, d, SET(0), 3) 118 | STEP(F, d, a, b, c, SET(1), 7) 119 | STEP(F, c, d, a, b, SET(2), 11) 120 | STEP(F, b, c, d, a, SET(3), 19) 121 | STEP(F, a, b, c, d, SET(4), 3) 122 | STEP(F, d, a, b, c, SET(5), 7) 123 | STEP(F, c, d, a, b, SET(6), 11) 124 | STEP(F, b, c, d, a, SET(7), 19) 125 | STEP(F, a, b, c, d, SET(8), 3) 126 | STEP(F, d, a, b, c, SET(9), 7) 127 | STEP(F, c, d, a, b, SET(10), 11) 128 | STEP(F, b, c, d, a, SET(11), 19) 129 | STEP(F, a, b, c, d, SET(12), 3) 130 | STEP(F, d, a, b, c, SET(13), 7) 131 | STEP(F, c, d, a, b, SET(14), 11) 132 | STEP(F, b, c, d, a, SET(15), 19) 133 | 134 | /* Round 2 */ 135 | STEP(G, a, b, c, d, GET(0) + ac1, 3) 136 | STEP(G, d, a, b, c, GET(4) + ac1, 5) 137 | STEP(G, c, d, a, b, GET(8) + ac1, 9) 138 | STEP(G, b, c, d, a, GET(12) + ac1, 13) 139 | STEP(G, a, b, c, d, GET(1) + ac1, 3) 140 | STEP(G, d, a, b, c, GET(5) + ac1, 5) 141 | STEP(G, c, d, a, b, GET(9) + ac1, 9) 142 | STEP(G, b, c, d, a, GET(13) + ac1, 13) 143 | STEP(G, a, b, c, d, GET(2) + ac1, 3) 144 | STEP(G, d, a, b, c, GET(6) + ac1, 5) 145 | STEP(G, c, d, a, b, GET(10) + ac1, 9) 146 | STEP(G, b, c, d, a, GET(14) + ac1, 13) 147 | STEP(G, a, b, c, d, GET(3) + ac1, 3) 148 | STEP(G, d, a, b, c, GET(7) + ac1, 5) 149 | STEP(G, c, d, a, b, GET(11) + ac1, 9) 150 | STEP(G, b, c, d, a, GET(15) + ac1, 13) 151 | 152 | /* Round 3 */ 153 | STEP(H, a, b, c, d, GET(0) + ac2, 3) 154 | STEP(H, d, a, b, c, GET(8) + ac2, 9) 155 | STEP(H, c, d, a, b, GET(4) + ac2, 11) 156 | STEP(H, b, c, d, a, GET(12) + ac2, 15) 157 | STEP(H, a, b, c, d, GET(2) + ac2, 3) 158 | STEP(H, d, a, b, c, GET(10) + ac2, 9) 159 | STEP(H, c, d, a, b, GET(6) + ac2, 11) 160 | STEP(H, b, c, d, a, GET(14) + ac2, 15) 161 | STEP(H, a, b, c, d, GET(1) + ac2, 3) 162 | STEP(H, d, a, b, c, GET(9) + ac2, 9) 163 | STEP(H, c, d, a, b, GET(5) + ac2, 11) 164 | STEP(H, b, c, d, a, GET(13) + ac2, 15) 165 | STEP(H, a, b, c, d, GET(3) + ac2, 3) 166 | STEP(H, d, a, b, c, GET(11) + ac2, 9) 167 | STEP(H, c, d, a, b, GET(7) + ac2, 11) 168 | STEP(H, b, c, d, a, GET(15) + ac2, 15) 169 | 170 | a += saved_a; 171 | b += saved_b; 172 | c += saved_c; 173 | d += saved_d; 174 | 175 | ptr += 64; 176 | } while (size -= 64); 177 | 178 | ctx->a = a; 179 | ctx->b = b; 180 | ctx->c = c; 181 | ctx->d = d; 182 | 183 | return ptr; 184 | } 185 | 186 | void MD4_Init(MD4_CTX *ctx) 187 | { 188 | ctx->a = 0x67452301; 189 | ctx->b = 0xefcdab89; 190 | ctx->c = 0x98badcfe; 191 | ctx->d = 0x10325476; 192 | 193 | ctx->lo = 0; 194 | ctx->hi = 0; 195 | } 196 | 197 | void MD4_Update(MD4_CTX *ctx, const void *data, size_t size) 198 | { 199 | MD4_u32plus saved_lo; 200 | size_t used, available; 201 | 202 | saved_lo = ctx->lo; 203 | if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) 204 | ctx->hi++; 205 | ctx->hi += (MD4_u32plus)(size >> 29); 206 | 207 | used = saved_lo & 0x3f; 208 | 209 | if (used) { 210 | available = 64 - used; 211 | 212 | if (size < available) { 213 | memcpy(&ctx->buffer[used], data, size); 214 | return; 215 | } 216 | 217 | memcpy(&ctx->buffer[used], data, available); 218 | data = (const unsigned char *)data + available; 219 | size -= available; 220 | body(ctx, ctx->buffer, 64); 221 | } 222 | 223 | if (size >= 64) { 224 | data = body(ctx, data, size & ~(size_t)0x3f); 225 | size &= 0x3f; 226 | } 227 | 228 | memcpy(ctx->buffer, data, size); 229 | } 230 | 231 | #define OUT(dst, src) \ 232 | (dst)[0] = (unsigned char)(src); \ 233 | (dst)[1] = (unsigned char)((src) >> 8); \ 234 | (dst)[2] = (unsigned char)((src) >> 16); \ 235 | (dst)[3] = (unsigned char)((src) >> 24); 236 | 237 | void MD4_Final(unsigned char *result, MD4_CTX *ctx) 238 | { 239 | size_t used, available; 240 | 241 | used = ctx->lo & 0x3f; 242 | 243 | ctx->buffer[used++] = 0x80; 244 | 245 | available = 64 - used; 246 | 247 | if (available < 8) { 248 | memset(&ctx->buffer[used], 0, available); 249 | body(ctx, ctx->buffer, 64); 250 | used = 0; 251 | available = 64; 252 | } 253 | 254 | memset(&ctx->buffer[used], 0, available - 8); 255 | 256 | ctx->lo <<= 3; 257 | OUT(&ctx->buffer[56], ctx->lo) 258 | OUT(&ctx->buffer[60], ctx->hi) 259 | 260 | body(ctx, ctx->buffer, 64); 261 | 262 | OUT(&result[0], ctx->a) 263 | OUT(&result[4], ctx->b) 264 | OUT(&result[8], ctx->c) 265 | OUT(&result[12], ctx->d) 266 | 267 | #if 0 268 | memset(ctx, 0, sizeof(*ctx)); 269 | #endif 270 | } 271 | 272 | #endif 273 | -------------------------------------------------------------------------------- /md4.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This is an OpenSSL API compatible (but not ABI compatible) implementation 3 | * of the RSA Data Security, Inc. MD4 Message-Digest Algorithm (RFC 1320). 4 | * 5 | * Homepage: 6 | * https://openwall.info/wiki/people/solar/software/public-domain-source-code/md4 7 | * 8 | * Author: 9 | * Alexander Peslyak, better known as Solar Designer 10 | * 11 | * This software was written by Alexander Peslyak in 2001. No copyright is 12 | * claimed, and the software is hereby placed in the public domain. 13 | * In case this attempt to disclaim copyright and place the software in the 14 | * public domain is deemed null and void, then the software is 15 | * Copyright (c) 2001 Alexander Peslyak and it is hereby released to the 16 | * general public under the following terms: 17 | * 18 | * Redistribution and use in source and binary forms, with or without 19 | * modification, are permitted. 20 | * 21 | * There's ABSOLUTELY NO WARRANTY, express or implied. 22 | * 23 | * See md4.c for more information. 24 | */ 25 | 26 | #ifdef HAVE_OPENSSL 27 | #include 28 | #elif !defined(_MD4_H) 29 | #define _MD4_H 30 | 31 | #include /* for size_t */ 32 | 33 | /* Any 32-bit or wider unsigned integer data type will do */ 34 | typedef unsigned int MD4_u32plus; 35 | 36 | typedef struct { 37 | MD4_u32plus lo, hi; 38 | MD4_u32plus a, b, c, d; 39 | unsigned char buffer[64]; 40 | #if !(defined(__i386__) || defined(__x86_64__) || defined(__vax__)) 41 | MD4_u32plus block[16]; 42 | #endif 43 | } MD4_CTX; 44 | 45 | extern void MD4_Init(MD4_CTX *ctx); 46 | extern void MD4_Update(MD4_CTX *ctx, const void *data, size_t size); 47 | extern void MD4_Final(unsigned char *result, MD4_CTX *ctx); 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /pam_macros.h: -------------------------------------------------------------------------------- 1 | /* 2 | * These macros are partially based on Linux-PAM's , 3 | * which were organized by Cristian Gafton and I believe are in the public 4 | * domain. 5 | * 6 | * - Solar Designer 7 | */ 8 | 9 | #ifndef PAM_PASSWDQC_MACROS_H__ 10 | #define PAM_PASSWDQC_MACROS_H__ 11 | 12 | #include 13 | #include 14 | 15 | #define pwqc_overwrite_string(x) \ 16 | do { \ 17 | if (x) \ 18 | memset((x), 0, strlen(x)); \ 19 | } while (0) 20 | 21 | #define pwqc_drop_mem(x) \ 22 | do { \ 23 | if (x) { \ 24 | free(x); \ 25 | (x) = NULL; \ 26 | } \ 27 | } while (0) 28 | 29 | #define pwqc_drop_pam_reply(/* struct pam_response* */ reply, /* int */ replies) \ 30 | do { \ 31 | if (reply) { \ 32 | int reply_i; \ 33 | \ 34 | for (reply_i = 0; reply_i < (replies); ++reply_i) { \ 35 | pwqc_overwrite_string((reply)[reply_i].resp); \ 36 | pwqc_drop_mem((reply)[reply_i].resp); \ 37 | } \ 38 | pwqc_drop_mem(reply); \ 39 | } \ 40 | } while (0) 41 | 42 | #endif /* PAM_PASSWDQC_MACROS_H__ */ 43 | -------------------------------------------------------------------------------- /pam_passwdqc.8: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2001 Networks Associates Technology, Inc. 2 | .\" All rights reserved. 3 | .\" Copyright (c) 2009 Dmitry V. Levin 4 | .\" All rights reserved. 5 | .\" Copyright (c) 2009,2019 Solar Designer 6 | .\" All rights reserved. 7 | .\" 8 | .\" Portions of this software were developed for the FreeBSD Project by 9 | .\" ThinkSec AS and NAI Labs, the Security Research Division of Network 10 | .\" Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 11 | .\" ("CBOSS"), as part of the DARPA CHATS research program. 12 | .\" 13 | .\" Redistribution and use in source and binary forms, with or without 14 | .\" modification, are permitted provided that the following conditions 15 | .\" are met: 16 | .\" 1. Redistributions of source code must retain the above copyright 17 | .\" notice, this list of conditions and the following disclaimer. 18 | .\" 2. Redistributions in binary form must reproduce the above copyright 19 | .\" notice, this list of conditions and the following disclaimer in the 20 | .\" documentation and/or other materials provided with the distribution. 21 | .\" 3. The name of the author may not be used to endorse or promote 22 | .\" products derived from this software without specific prior written 23 | .\" permission. 24 | .\" 25 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 26 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 29 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 31 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 32 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 34 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 35 | .\" SUCH DAMAGE. 36 | .\" 37 | .Dd December 9, 2019 38 | .Dt PAM_PASSWDQC 8 39 | .Os "Openwall Project" 40 | .Sh NAME 41 | .Nm pam_passwdqc 42 | .Nd Password quality-control PAM module 43 | .Sh SYNOPSIS 44 | .Op Ar service-name 45 | .Ar module-type 46 | .Ar control-flag 47 | .Pa pam_passwdqc 48 | .Op Ar options 49 | .Sh DESCRIPTION 50 | The 51 | .Nm 52 | module is a simple password strength checking module for 53 | PAM. 54 | In addition to checking regular passwords, it offers support for 55 | passphrases and can provide randomly generated ones. 56 | .Pp 57 | The 58 | .Nm 59 | module provides functionality for only one PAM management group: 60 | password changing. 61 | In terms of the 62 | .Ar module-type 63 | parameter, this is the 64 | .Dq Li password 65 | feature. 66 | .Pp 67 | The 68 | .Fn pam_chauthtok 69 | service function may ask the user for a new password, and verify that 70 | it meets certain minimum standards. 71 | If the chosen password is unsatisfactory, the service function returns 72 | .Dv PAM_AUTHTOK_ERR . 73 | .Pp 74 | The set of options that may be passed to the module is exactly the 75 | same as the set of options that may be specified in the configuration 76 | file (suggested location 77 | .Pa /etc/passwdqc.conf , 78 | to be specified in the 79 | .Cm config=/etc/passwdqc.conf 80 | option). These options are described in 81 | .Xr passwdqc.conf 5 . 82 | .Sh SEE ALSO 83 | .Xr pam.conf 5 , 84 | .Xr passwdqc.conf 5 , 85 | .Xr pam 8 . 86 | .Pp 87 | https://www.openwall.com/passwdqc/ 88 | .Sh AUTHORS 89 | The 90 | .Nm 91 | module was written for Openwall GNU/*/Linux by 92 | .An Solar Designer Aq solar at openwall.com . 93 | This manual page was written for the 94 | .Fx 95 | Project by 96 | ThinkSec AS and NAI Labs, the Security Research Division of Network 97 | Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 98 | .Pq Dq CBOSS , 99 | as part of the DARPA CHATS research program. 100 | It has since been revised, most importantly to refer to 101 | .Xr passwdqc.conf 5 102 | instead of describing the options right on this page. 103 | -------------------------------------------------------------------------------- /pam_passwdqc.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2003,2005,2012,2016,2019,2021 by Solar Designer 3 | * Copyright (c) 2017,2018 by Dmitry V. Levin 4 | * Copyright (c) 2017,2018 by Oleg Solovyov 5 | * See LICENSE 6 | */ 7 | 8 | #define _XOPEN_SOURCE 600 9 | #define _XOPEN_SOURCE_EXTENDED 10 | #define _DEFAULT_SOURCE 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #ifdef HAVE_SHADOW 19 | #include 20 | #endif 21 | #ifdef HAVE_LIBAUDIT 22 | #include 23 | #include 24 | #endif 25 | 26 | #define PAM_SM_PASSWORD 27 | #ifndef LINUX_PAM 28 | #include 29 | #endif 30 | #include 31 | 32 | #include "pam_macros.h" 33 | 34 | #if !defined(PAM_EXTERN) && !defined(PAM_STATIC) 35 | #define PAM_EXTERN extern 36 | #endif 37 | 38 | #if !defined(PAM_AUTHTOK_RECOVERY_ERR) && defined(PAM_AUTHTOK_RECOVER_ERR) 39 | #define PAM_AUTHTOK_RECOVERY_ERR PAM_AUTHTOK_RECOVER_ERR 40 | #endif 41 | 42 | #if (defined(__sun) || defined(__hpux) || defined(_AIX)) && \ 43 | !defined(LINUX_PAM) && !defined(_OPENPAM) 44 | /* Sun's PAM doesn't use const here, while Linux-PAM and OpenPAM do */ 45 | #define lo_const 46 | #else 47 | #define lo_const const 48 | #endif 49 | #ifdef _OPENPAM 50 | /* OpenPAM doesn't use const here, while Linux-PAM does */ 51 | #define l_const 52 | #else 53 | #define l_const lo_const 54 | #endif 55 | typedef lo_const void *pam_item_t; 56 | 57 | #include "passwdqc.h" 58 | 59 | #include "passwdqc_i18n.h" 60 | 61 | #define PROMPT_OLDPASS \ 62 | _("Enter current password: ") 63 | #define PROMPT_NEWPASS1 \ 64 | _("Enter new password: ") 65 | #define PROMPT_NEWPASS2 \ 66 | _("Re-type new password: ") 67 | 68 | #define MESSAGE_MISCONFIGURED \ 69 | _("System configuration error. Please contact your administrator.") 70 | #define MESSAGE_INVALID_OPTION \ 71 | "pam_passwdqc: %s." 72 | #define MESSAGE_INTRO_PASSWORD \ 73 | _("\nYou can now choose the new password.\n") 74 | #define MESSAGE_INTRO_BOTH \ 75 | _("\nYou can now choose the new password or passphrase.\n") 76 | 77 | #define MESSAGE_EXPLAIN_PASSWORD_1_CLASS(count) \ 78 | P3_( \ 79 | "A good password should be a mix of upper and lower case letters, digits, and\n" \ 80 | "other characters. You can use a password containing at least %d character.\n", \ 81 | \ 82 | "A good password should be a mix of upper and lower case letters, digits, and\n" \ 83 | "other characters. You can use a password containing at least %d characters.\n", \ 84 | count), (count) 85 | 86 | #define MESSAGE_EXPLAIN_PASSWORD_N_CLASSES(count) \ 87 | P3_( \ 88 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 89 | "other characters. You can use a password containing at least %d character\n" \ 90 | "from at least %d of these 4 classes.\n" \ 91 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 92 | "count towards the number of character classes used.\n", \ 93 | \ 94 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 95 | "other characters. You can use a password containing at least %d characters\n" \ 96 | "from at least %d of these 4 classes.\n" \ 97 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 98 | "count towards the number of character classes used.\n", \ 99 | count), (count) 100 | 101 | #define MESSAGE_EXPLAIN_PASSWORD_ALL_CLASSES(count) \ 102 | P3_( \ 103 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 104 | "other characters. You can use a password containing at least %d character\n" \ 105 | "from all of these classes.\n" \ 106 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 107 | "count towards the number of character classes used.\n", \ 108 | \ 109 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 110 | "other characters. You can use a password containing at least %d characters\n" \ 111 | "from all of these classes.\n" \ 112 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 113 | "count towards the number of character classes used.\n", \ 114 | count), (count) 115 | 116 | #define MESSAGE_EXPLAIN_PASSWORD_ALL_OR_3_CLASSES(count) \ 117 | P3_( \ 118 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 119 | "other characters. You can use a password containing at least %d character\n" \ 120 | "from all of these classes, or a password containing at least %d characters\n" \ 121 | "from just 3 of these 4 classes.\n" \ 122 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 123 | "count towards the number of character classes used.\n", \ 124 | \ 125 | "A valid password should be a mix of upper and lower case letters, digits, and\n" \ 126 | "other characters. You can use a password containing at least %d characters\n" \ 127 | "from all of these classes, or a password containing at least %d characters\n" \ 128 | "from just 3 of these 4 classes.\n" \ 129 | "An upper case letter that begins the password and a digit that ends it do not\n" \ 130 | "count towards the number of character classes used.\n", \ 131 | count), (count) 132 | 133 | #define MESSAGE_EXPLAIN_PASSPHRASE(count) \ 134 | P3_(\ 135 | "A passphrase should be of at least %d word, %d to %d characters long, and\n" \ 136 | "contain enough different characters.\n", \ 137 | \ 138 | "A passphrase should be of at least %d words, %d to %d characters long, and\n" \ 139 | "contain enough different characters.\n", \ 140 | count), (count) 141 | 142 | #define MESSAGE_RANDOM \ 143 | _("Alternatively, if no one else can see your terminal now, you can pick this as\n" \ 144 | "your password: \"%s\".\n") 145 | #define MESSAGE_RANDOMONLY \ 146 | _("This system is configured to permit randomly generated passwords only.\n" \ 147 | "If no one else can see your terminal now, you can pick this as your\n" \ 148 | "password: \"%s\". Otherwise come back later.\n") 149 | #define MESSAGE_RANDOMFAILED \ 150 | _("This system is configured to use randomly generated passwords only,\n" \ 151 | "but the attempt to generate a password has failed. This could happen\n" \ 152 | "for a number of reasons: you could have requested an impossible password\n" \ 153 | "length, or the access to kernel random number pool could have failed.") 154 | #define MESSAGE_TOOLONG \ 155 | _("This password may be too long for some services. Choose another.") 156 | #define MESSAGE_TRUNCATED \ 157 | _("Warning: your longer password will be truncated to 8 characters.") 158 | #define MESSAGE_WEAKPASS \ 159 | _("Weak password: %s.") 160 | #define MESSAGE_NOTRANDOM \ 161 | _("Sorry, you've mistyped the password that was generated for you.") 162 | #define MESSAGE_MISTYPED \ 163 | _("Sorry, passwords do not match.") 164 | #define MESSAGE_RETRY \ 165 | _("Try again.") 166 | 167 | static int logaudit(pam_handle_t *pamh, int status, passwdqc_params_t *params) 168 | { 169 | #ifdef HAVE_LIBAUDIT 170 | if (!(params->pam.flags & F_NO_AUDIT)) { 171 | int rc = pam_modutil_audit_write(pamh, AUDIT_USER_CHAUTHTOK, "pam_passwdqc", status); 172 | if (status == PAM_SUCCESS) 173 | status = rc; 174 | } 175 | #else /* !HAVE_LIBAUDIT */ 176 | (void) pamh; 177 | #endif 178 | passwdqc_params_free(params); 179 | return status; 180 | } 181 | 182 | static int converse(pam_handle_t *pamh, int style, l_const char *text, 183 | struct pam_response **resp) 184 | { 185 | pam_item_t item; 186 | const struct pam_conv *conv; 187 | struct pam_message msg, *pmsg; 188 | int status; 189 | 190 | *resp = NULL; 191 | status = pam_get_item(pamh, PAM_CONV, &item); 192 | if (status != PAM_SUCCESS) 193 | return status; 194 | conv = item; 195 | 196 | pmsg = &msg; 197 | msg.msg_style = style; 198 | msg.msg = text; 199 | 200 | return conv->conv(1, (lo_const struct pam_message **)&pmsg, resp, 201 | conv->appdata_ptr); 202 | } 203 | 204 | #ifdef __GNUC__ 205 | __attribute__ ((format (printf, 3, 4))) 206 | #endif 207 | static int say(pam_handle_t *pamh, int style, const char *format, ...) 208 | { 209 | va_list args; 210 | char buffer[0x800]; 211 | int needed; 212 | struct pam_response *resp; 213 | int status; 214 | 215 | va_start(args, format); 216 | needed = vsnprintf(buffer, sizeof(buffer), format, args); 217 | va_end(args); 218 | 219 | if ((unsigned int)needed < sizeof(buffer)) { 220 | status = converse(pamh, style, buffer, &resp); 221 | pwqc_drop_pam_reply(resp, 1); 222 | } else { 223 | status = PAM_ABORT; 224 | } 225 | _passwdqc_memzero(buffer, sizeof(buffer)); 226 | 227 | return status; 228 | } 229 | 230 | static int check_max(passwdqc_params_qc_t *qc, pam_handle_t *pamh, 231 | const char *newpass) 232 | { 233 | if (strlen(newpass) > (size_t)qc->max) { 234 | if (qc->max != 8) { 235 | say(pamh, PAM_ERROR_MSG, MESSAGE_TOOLONG); 236 | return -1; 237 | } 238 | say(pamh, PAM_TEXT_INFO, MESSAGE_TRUNCATED); 239 | } 240 | 241 | return 0; 242 | } 243 | 244 | static int check_pass(struct passwd *pw, const char *pass) 245 | { 246 | const char *hash; 247 | int retval; 248 | 249 | #ifdef HAVE_SHADOW 250 | #ifdef __hpux 251 | if (iscomsec()) { 252 | #else 253 | if (!strcmp(pw->pw_passwd, "x")) { 254 | #endif 255 | struct spwd *spw = getspnam(pw->pw_name); 256 | endspent(); 257 | if (!spw) 258 | return -1; 259 | hash = NULL; 260 | if (strlen(spw->sp_pwdp) >= 13) { 261 | #ifdef __hpux 262 | hash = bigcrypt(pass, spw->sp_pwdp); 263 | #else 264 | hash = crypt(pass, spw->sp_pwdp); 265 | #endif 266 | } 267 | retval = (hash && !strcmp(hash, spw->sp_pwdp)) ? 0 : -1; 268 | _passwdqc_memzero(spw->sp_pwdp, strlen(spw->sp_pwdp)); 269 | return retval; 270 | } 271 | #endif 272 | 273 | hash = NULL; 274 | if (strlen(pw->pw_passwd) >= 13) 275 | hash = crypt(pass, pw->pw_passwd); 276 | retval = (hash && !strcmp(hash, pw->pw_passwd)) ? 0 : -1; 277 | _passwdqc_memzero(pw->pw_passwd, strlen(pw->pw_passwd)); 278 | return retval; 279 | } 280 | 281 | static int am_root(pam_handle_t *pamh) 282 | { 283 | pam_item_t item; 284 | const char *service; 285 | 286 | if (getuid() != 0) 287 | return 0; 288 | 289 | if (pam_get_item(pamh, PAM_SERVICE, &item) != PAM_SUCCESS) 290 | return 0; 291 | service = item; 292 | 293 | return !strcmp(service, "passwd") || !strcmp(service, "chpasswd"); 294 | } 295 | 296 | PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, 297 | int argc, const char **argv) 298 | { 299 | passwdqc_params_t params; 300 | struct pam_response *resp; 301 | struct passwd *pw, fake_pw; 302 | pam_item_t item; 303 | const char *user, *oldpass, *newpass; 304 | char *trypass, *randompass; 305 | char *parse_reason; 306 | const char *check_reason; 307 | int ask_oldauthtok; 308 | int randomonly, enforce, retries_left, retry_wanted; 309 | int status; 310 | 311 | passwdqc_params_reset(¶ms); 312 | if (passwdqc_params_parse(¶ms, &parse_reason, argc, argv)) { 313 | say(pamh, PAM_ERROR_MSG, am_root(pamh) ? 314 | MESSAGE_INVALID_OPTION : MESSAGE_MISCONFIGURED, 315 | parse_reason); 316 | free(parse_reason); 317 | return PAM_ABORT; 318 | } 319 | status = PAM_SUCCESS; 320 | 321 | ask_oldauthtok = 0; 322 | if (flags & PAM_PRELIM_CHECK) { 323 | if (params.pam.flags & F_ASK_OLDAUTHTOK_PRELIM) 324 | ask_oldauthtok = 1; 325 | } else if (flags & PAM_UPDATE_AUTHTOK) { 326 | if (params.pam.flags & F_ASK_OLDAUTHTOK_UPDATE) 327 | ask_oldauthtok = 1; 328 | } else { 329 | passwdqc_params_free(¶ms); 330 | return PAM_SERVICE_ERR; 331 | } 332 | 333 | if (ask_oldauthtok && !am_root(pamh)) { 334 | status = converse(pamh, PAM_PROMPT_ECHO_OFF, 335 | PROMPT_OLDPASS, &resp); 336 | 337 | if (status == PAM_SUCCESS) { 338 | if (resp && resp->resp) { 339 | status = pam_set_item(pamh, 340 | PAM_OLDAUTHTOK, resp->resp); 341 | pwqc_drop_pam_reply(resp, 1); 342 | } else 343 | status = PAM_AUTHTOK_RECOVERY_ERR; 344 | } 345 | 346 | if (status != PAM_SUCCESS) 347 | return logaudit(pamh, status, ¶ms); 348 | } 349 | 350 | if (flags & PAM_PRELIM_CHECK) { 351 | passwdqc_params_free(¶ms); 352 | return status; 353 | } 354 | 355 | status = pam_get_item(pamh, PAM_USER, &item); 356 | if (status != PAM_SUCCESS) 357 | return logaudit(pamh, status, ¶ms); 358 | user = item; 359 | 360 | status = pam_get_item(pamh, PAM_OLDAUTHTOK, &item); 361 | if (status != PAM_SUCCESS) 362 | return logaudit(pamh, status, ¶ms); 363 | oldpass = item; 364 | 365 | if (params.pam.flags & F_NON_UNIX) { 366 | pw = &fake_pw; 367 | memset(pw, 0, sizeof(*pw)); 368 | pw->pw_name = (char *)user; 369 | pw->pw_gecos = ""; 370 | pw->pw_dir = ""; 371 | } else { 372 | /* As currently implemented, we don't avoid timing leaks for valid vs. not 373 | * usernames and hashes. Normally, the username would have already been 374 | * checked and determined valid, and the check_oldauthtok option is only needed 375 | * on systems that happen to have similar timing leaks all over the place. */ 376 | pw = getpwnam(user); 377 | endpwent(); 378 | if (!pw) 379 | return logaudit(pamh, PAM_USER_UNKNOWN, ¶ms); 380 | if ((params.pam.flags & F_CHECK_OLDAUTHTOK) && !am_root(pamh) 381 | && (!oldpass || check_pass(pw, oldpass))) 382 | status = PAM_AUTH_ERR; 383 | _passwdqc_memzero(pw->pw_passwd, strlen(pw->pw_passwd)); 384 | if (status != PAM_SUCCESS) 385 | return logaudit(pamh, status, ¶ms); 386 | } 387 | 388 | randomonly = params.qc.min[4] > params.qc.max; 389 | 390 | if (am_root(pamh)) 391 | enforce = params.pam.flags & F_ENFORCE_ROOT; 392 | else 393 | enforce = params.pam.flags & F_ENFORCE_USERS; 394 | 395 | if (params.pam.flags & F_USE_AUTHTOK) { 396 | status = pam_get_item(pamh, PAM_AUTHTOK, &item); 397 | if (status != PAM_SUCCESS) 398 | return logaudit(pamh, status, ¶ms); 399 | newpass = item; 400 | if (!newpass || 401 | (check_max(¶ms.qc, pamh, newpass) && enforce)) 402 | return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); 403 | check_reason = 404 | passwdqc_check(¶ms.qc, newpass, oldpass, pw); 405 | if (check_reason) { 406 | say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, 407 | check_reason); 408 | if (enforce) 409 | status = PAM_AUTHTOK_ERR; 410 | } 411 | return logaudit(pamh, status, ¶ms); 412 | } 413 | 414 | retries_left = params.pam.retry; 415 | 416 | retry: 417 | retry_wanted = 0; 418 | 419 | if (!randomonly && 420 | params.qc.passphrase_words && params.qc.min[2] <= params.qc.max) 421 | status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_BOTH); 422 | else 423 | status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_PASSWORD); 424 | if (status != PAM_SUCCESS) 425 | return logaudit(pamh, status, ¶ms); 426 | 427 | if (!randomonly && params.qc.min[0] == params.qc.min[4]) 428 | status = say(pamh, PAM_TEXT_INFO, 429 | MESSAGE_EXPLAIN_PASSWORD_1_CLASS(params.qc.min[4])); 430 | 431 | else if (!randomonly && params.qc.min[3] == params.qc.min[4]) 432 | status = say(pamh, PAM_TEXT_INFO, 433 | MESSAGE_EXPLAIN_PASSWORD_N_CLASSES(params.qc.min[4]), 434 | params.qc.min[1] != params.qc.min[3] ? 3 : 2); 435 | else if (!randomonly && params.qc.min[3] == INT_MAX) 436 | status = say(pamh, PAM_TEXT_INFO, 437 | MESSAGE_EXPLAIN_PASSWORD_ALL_CLASSES(params.qc.min[4])); 438 | else if (!randomonly) { 439 | status = say(pamh, PAM_TEXT_INFO, 440 | MESSAGE_EXPLAIN_PASSWORD_ALL_OR_3_CLASSES(params.qc.min[4]), 441 | params.qc.min[3]); 442 | } 443 | if (status != PAM_SUCCESS) 444 | return logaudit(pamh, status, ¶ms); 445 | 446 | if (!randomonly && 447 | params.qc.passphrase_words && params.qc.min[2] <= params.qc.max) { 448 | status = say(pamh, PAM_TEXT_INFO, 449 | MESSAGE_EXPLAIN_PASSPHRASE(params.qc.passphrase_words), 450 | params.qc.min[2], params.qc.max); 451 | if (status != PAM_SUCCESS) 452 | return logaudit(pamh, status, ¶ms); 453 | } 454 | 455 | randompass = passwdqc_random(¶ms.qc); 456 | if (randompass) { 457 | status = say(pamh, PAM_TEXT_INFO, randomonly ? 458 | MESSAGE_RANDOMONLY : MESSAGE_RANDOM, randompass); 459 | if (status != PAM_SUCCESS) { 460 | pwqc_overwrite_string(randompass); 461 | pwqc_drop_mem(randompass); 462 | } 463 | } else if (randomonly) { 464 | say(pamh, PAM_ERROR_MSG, am_root(pamh) ? 465 | MESSAGE_RANDOMFAILED : MESSAGE_MISCONFIGURED); 466 | return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); 467 | } 468 | 469 | status = converse(pamh, PAM_PROMPT_ECHO_OFF, PROMPT_NEWPASS1, &resp); 470 | if (status == PAM_SUCCESS && (!resp || !resp->resp)) 471 | status = PAM_AUTHTOK_ERR; 472 | 473 | if (status != PAM_SUCCESS) { 474 | pwqc_overwrite_string(randompass); 475 | pwqc_drop_mem(randompass); 476 | return logaudit(pamh, status, ¶ms); 477 | } 478 | 479 | trypass = strdup(resp->resp); 480 | 481 | pwqc_drop_pam_reply(resp, 1); 482 | 483 | if (!trypass) { 484 | pwqc_overwrite_string(randompass); 485 | pwqc_drop_mem(randompass); 486 | return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); 487 | } 488 | 489 | if (check_max(¶ms.qc, pamh, trypass) && enforce) { 490 | status = PAM_AUTHTOK_ERR; 491 | retry_wanted = 1; 492 | } 493 | 494 | check_reason = NULL; /* unused */ 495 | if (status == PAM_SUCCESS && 496 | (!randompass || !strstr(trypass, randompass)) && 497 | (randomonly || 498 | (check_reason = passwdqc_check(¶ms.qc, trypass, oldpass, pw)))) { 499 | if (randomonly) 500 | say(pamh, PAM_ERROR_MSG, MESSAGE_NOTRANDOM); 501 | else 502 | say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, 503 | check_reason); 504 | if (enforce) { 505 | status = PAM_AUTHTOK_ERR; 506 | retry_wanted = 1; 507 | } 508 | } 509 | 510 | if (status == PAM_SUCCESS) 511 | status = converse(pamh, PAM_PROMPT_ECHO_OFF, 512 | PROMPT_NEWPASS2, &resp); 513 | if (status == PAM_SUCCESS) { 514 | if (resp && resp->resp) { 515 | if (strcmp(trypass, resp->resp)) { 516 | status = say(pamh, 517 | PAM_ERROR_MSG, MESSAGE_MISTYPED); 518 | if (status == PAM_SUCCESS) { 519 | status = PAM_AUTHTOK_ERR; 520 | retry_wanted = 1; 521 | } 522 | } 523 | pwqc_drop_pam_reply(resp, 1); 524 | } else 525 | status = PAM_AUTHTOK_ERR; 526 | } 527 | 528 | if (status == PAM_SUCCESS) 529 | status = pam_set_item(pamh, PAM_AUTHTOK, trypass); 530 | 531 | pwqc_overwrite_string(randompass); 532 | pwqc_drop_mem(randompass); 533 | 534 | pwqc_overwrite_string(trypass); 535 | pwqc_drop_mem(trypass); 536 | 537 | if (retry_wanted && --retries_left > 0) { 538 | status = say(pamh, PAM_TEXT_INFO, MESSAGE_RETRY); 539 | if (status == PAM_SUCCESS) 540 | goto retry; 541 | } 542 | 543 | return logaudit(pamh, status, ¶ms); 544 | } 545 | 546 | #ifdef PAM_MODULE_ENTRY 547 | PAM_MODULE_ENTRY("pam_passwdqc"); 548 | #elif defined(PAM_STATIC) 549 | const struct pam_module _pam_passwdqc_modstruct = { 550 | "pam_passwdqc", 551 | NULL, 552 | NULL, 553 | NULL, 554 | NULL, 555 | NULL, 556 | pam_sm_chauthtok 557 | }; 558 | #endif 559 | -------------------------------------------------------------------------------- /pam_passwdqc.map: -------------------------------------------------------------------------------- 1 | { 2 | global: 3 | pam_sm_chauthtok; 4 | 5 | local: 6 | *; 7 | }; 8 | -------------------------------------------------------------------------------- /passwdqc.conf: -------------------------------------------------------------------------------- 1 | min=disabled,24,11,8,7 2 | max=72 3 | passphrase=3 4 | match=4 5 | similar=deny 6 | random=47 7 | enforce=everyone 8 | retry=3 9 | # The below are just examples, by default none of these are used 10 | #wordlist=/usr/share/john/password.lst 11 | #denylist=/etc/passwdqc.deny 12 | #filter=/opt/passwdqc/hibp.pwq 13 | -------------------------------------------------------------------------------- /passwdqc.conf.5: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2000-2003,2005,2008,2019,2020 Solar Designer 2 | .\" All rights reserved. 3 | .\" Copyright (c) 2001 Networks Associates Technology, Inc. 4 | .\" All rights reserved. 5 | .\" Copyright (c) 2009 Dmitry V. Levin 6 | .\" All rights reserved. 7 | .\" 8 | .\" Portions of this software were developed for the FreeBSD Project by 9 | .\" ThinkSec AS and NAI Labs, the Security Research Division of Network 10 | .\" Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 11 | .\" ("CBOSS"), as part of the DARPA CHATS research program. 12 | .\" 13 | .\" Redistribution and use in source and binary forms, with or without 14 | .\" modification, are permitted provided that the following conditions 15 | .\" are met: 16 | .\" 1. Redistributions of source code must retain the above copyright 17 | .\" notice, this list of conditions and the following disclaimer. 18 | .\" 2. Redistributions in binary form must reproduce the above copyright 19 | .\" notice, this list of conditions and the following disclaimer in the 20 | .\" documentation and/or other materials provided with the distribution. 21 | .\" 3. The name of the author may not be used to endorse or promote 22 | .\" products derived from this software without specific prior written 23 | .\" permission. 24 | .\" 25 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 26 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 29 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 31 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 32 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 34 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 35 | .\" SUCH DAMAGE. 36 | .\" 37 | .Dd March 10, 2021 38 | .Dt PASSWDQC.CONF 5 39 | .Os "Openwall Project" 40 | .Sh NAME 41 | .Nm passwdqc.conf 42 | .Nd libpasswdqc configuration file 43 | .Sh DESCRIPTION 44 | libpasswdqc is a simple password strength checking library. 45 | In addition to checking regular passwords, it offers support for 46 | passphrases and can provide randomly generated ones. 47 | A 48 | .Nm 49 | configuration file may be used to override default libpasswdqc settings. 50 | .Sh FORMAT 51 | A 52 | .Nm 53 | file consists of 0 or more lines of the following format: 54 | .Dl Ar option Ns = Ns Ar value 55 | .Pp 56 | Empty lines and lines beginning with 57 | .Dq Li # 58 | are ignored. 59 | Whitespace characters between the 60 | .Ar option , 61 | .Dq Li = , 62 | and 63 | .Ar value 64 | are not allowed. 65 | .Sh DIRECTIVE OPTIONS 66 | .Bl -tag -width indent 67 | .It Cm config Ns = Ns Ar FILE 68 | Load the specified configuration 69 | .Ar FILE 70 | in the 71 | .Cm passwdqc.conf 72 | format. 73 | This file may define any options described in this manual, 74 | including load of yet another configuration file, but loops are not allowed. 75 | .El 76 | .Sh PASSWORD QUALITY CONTROL OPTIONS 77 | .Bl -tag -width Ds 78 | .Sm off 79 | .It Xo 80 | .Cm min No = 81 | .Ar N0 , N1 , N2 , N3 , N4 82 | .Xc 83 | .Sm on 84 | .Pq default: min=disabled,24,11,8,7 85 | The minimum allowed password lengths for different kinds of 86 | passwords/passphrases. 87 | The keyword 88 | .Cm disabled 89 | can be used to 90 | disallow passwords of a given kind regardless of their length. 91 | Each subsequent number is required to be no larger than the preceding 92 | one. 93 | .Pp 94 | .Ar N0 95 | is used for passwords consisting of characters from one character 96 | class only. 97 | The character classes are: digits, lower-case letters, upper-case 98 | letters, and other characters. 99 | There is also a special class for 100 | .No non- Ns Tn ASCII 101 | characters, which could not be classified, but are assumed to be non-digits. 102 | .Pp 103 | .Ar N1 104 | is used for passwords consisting of characters from two character 105 | classes that do not meet the requirements for a passphrase. 106 | .Pp 107 | .Ar N2 108 | is used for passphrases. 109 | Note that besides meeting this length requirement, 110 | a passphrase must also consist of a sufficient number of words (see the 111 | .Cm passphrase 112 | option below). 113 | .Pp 114 | .Ar N3 115 | and 116 | .Ar N4 117 | are used for passwords consisting of characters from three 118 | and four character classes, respectively. 119 | .Pp 120 | When calculating the number of character classes, upper-case letters 121 | used as the first character and digits used as the last character of a 122 | password are not counted. 123 | .Pp 124 | In addition to being sufficiently long, passwords are required to 125 | contain enough different characters for the character classes and 126 | the minimum length they have been checked against. 127 | .Pp 128 | .It Cm max Ns = Ns Ar N 129 | .Pq default: Cm max Ns = Ns 72 130 | The maximum allowed password length. 131 | This can be used to prevent users from setting passwords that may be 132 | too long for some system services. 133 | The value 8 is treated specially: if 134 | .Cm max 135 | is set to 8, passwords longer than 8 characters will not be rejected, 136 | but will be truncated to 8 characters for the strength checks and the 137 | user will be warned. 138 | This is to be used with the traditional DES-based password hashes, 139 | which truncate the password at 8 characters. 140 | .Pp 141 | It is important that you do set 142 | .Cm max Ns = Ns 8 143 | if you are using the traditional 144 | hashes, or some weak passwords will pass the checks. 145 | .It Cm passphrase Ns = Ns Ar N 146 | .Pq default: Cm passphrase Ns = Ns 3 147 | The number of words required for a passphrase, or 0 to disable the 148 | support for user-chosen passphrases. 149 | .It Cm match Ns = Ns Ar N 150 | .Pq default: Cm match Ns = Ns 4 151 | The length of common substring required to conclude that a password is 152 | at least partially based on information found in a character string, 153 | or 0 to disable the substring search. 154 | Note that the password will not be rejected once a weak substring is 155 | found; it will instead be subjected to the usual strength requirements 156 | with the weak substring partially discounted. 157 | .Pp 158 | The substring search is case-insensitive and is able to detect and 159 | remove a common substring spelled backwards. 160 | .It Xo 161 | .Sm off 162 | .Cm similar No = Cm permit | deny 163 | .Sm on 164 | .Xc 165 | .Pq default: Cm similar Ns = Ns Cm deny 166 | Whether a new password is allowed to be similar to the old one. 167 | The passwords are considered to be similar when there is a sufficiently 168 | long common substring and the new password with the substring partially 169 | discounted would be weak. 170 | .It Cm wordlist Ns = Ns Ar FILE 171 | Deny passwords that are based on lines of the tiny external text 172 | .Ar FILE , 173 | which can reasonably be e.g. a list of a few thousand common passwords. 174 | Common dictionary words may also reasonably be included, especially in a 175 | local language other than English, or longer yet common English words. 176 | (passwdqc includes a list of a few thousand common English words of 177 | lengths from 3 to 6 built in. Any word list possibly specified with 178 | this option is used in addition to the built-in word list.) 179 | .Pp 180 | Substring matching and discounting will be used if the 181 | .Cm match 182 | setting 183 | above is non-zero. Please note that this is very inefficient, and isn't 184 | to be used with large wordlists. 185 | .It Cm denylist Ns = Ns Ar FILE 186 | Deny passwords or passphrases directly appearing in the tiny external text 187 | .Ar FILE . 188 | That file can reasonably be e.g. a list of common passwords if 189 | only a relaxed policy is desired and stricter checks are thus disabled 190 | (using their separate options). Such policy would only be somewhat 191 | effective against online/remote attacks, but not against offline attacks 192 | on hashed passwords. 193 | .It Cm filter Ns = Ns Ar FILE 194 | Deny passwords or passphrases directly appearing in a maybe huge binary 195 | filter 196 | .Ar FILE 197 | created with pwqfilter. This is very efficient, needing at 198 | most two random disk reads per query. A filter created from millions of 199 | leaked passwords can reasonably be used on top of passwdqc's other 200 | checks to further reduce the number of passing yet weak passwords 201 | without causing unreasonable inconvenience (as e.g. higher minimum 202 | lengths and character set requirements could). 203 | .It Xo 204 | .Sm off 205 | .Cm random No = Ar N 206 | .Op , Cm only 207 | .Sm on 208 | .Xc 209 | .Pq default: Cm random Ns = Ns 47 210 | The size of randomly-generated passphrases in bits (24 to 136), 211 | or 0 to disable this feature. 212 | Any passphrase that contains the offered randomly-generated string will be 213 | allowed regardless of other possible restrictions. 214 | .Pp 215 | The 216 | .Cm only 217 | modifier can be used to disallow user-chosen passwords. 218 | .El 219 | .Sh PAM MODULE OPTIONS 220 | .Bl -tag -width indent 221 | .It Xo 222 | .Sm off 223 | .Cm enforce No = Cm none | users | everyone 224 | .Sm on 225 | .Xc 226 | .Pq default: Cm enforce Ns = Ns Cm everyone 227 | The PAM module can be configured to warn of weak passwords only, but not 228 | actually enforce strong passwords. 229 | The 230 | .Cm users 231 | setting is like 232 | .Cm everyone 233 | for all PAM services except 234 | .Cm chpasswd 235 | and 236 | .Cm passwd . 237 | For these two PAM services 238 | .Cm users 239 | will enforce strong passwords for invocations by non-root users only. 240 | .It Cm non-unix 241 | Normally, the PAM module uses 242 | .Xr getpwnam 3 243 | to obtain the user's personal login information and use that during 244 | the password strength checks. 245 | This behavior can be disabled with the 246 | .Cm non-unix 247 | option. 248 | .It Cm retry Ns = Ns Ar N 249 | .Pq default: Cm retry Ns = Ns 3 250 | The number of times the PAM module will ask for a new password if the 251 | user fails to provide a sufficiently strong password and enter it twice 252 | the first time. 253 | .It Cm ask_oldauthtok Ns Op = Ns Cm update 254 | Ask for the old password as well. 255 | Normally, the PAM module leaves this task for subsequent modules. 256 | With no argument, the 257 | .Cm ask_oldauthtok 258 | option will cause the PAM module to ask for the old password during the 259 | preliminary check phase. If the 260 | .Cm ask_oldauthtok 261 | option is specified with the 262 | .Cm update 263 | argument, the PAM module will do that during the update phase. 264 | .It Cm check_oldauthtok 265 | This tells the PAM module to validate the old password before giving a 266 | new password prompt. 267 | Normally, this task is left for subsequent modules. 268 | .Pp 269 | The primary use for this option is when 270 | .Cm ask_oldauthtok Ns = Ns Cm update 271 | is also specified, in which case no other module gets a chance to ask 272 | for and validate the password. 273 | Of course, this will only work with 274 | .Ux 275 | passwords. 276 | .It Cm use_first_pass , use_authtok 277 | Use the new password obtained by other modules stacked before the PAM 278 | module. This disables user interaction within the PAM module. 279 | The only difference between 280 | .Cm use_first_pass 281 | and 282 | .Cm use_authtok 283 | is that the former is incompatible with 284 | .Cm ask_oldauthtok . 285 | .It Cm noaudit 286 | If audit is enabled at build time, the PAM module logs audit events once 287 | user tries to change their credentials. This option disables that audit 288 | logging. 289 | .El 290 | .Sh FILES 291 | .Pa /etc/passwdqc.conf 292 | (not read unless this suggested file location is specified with the 293 | .Cm config=/etc/passwdqc.conf 294 | option). 295 | .Sh SEE ALSO 296 | .Xr getpwnam 3 , 297 | .Xr libpasswdqc 3 , 298 | .Xr pam_passwdqc 8 . 299 | .Pp 300 | https://www.openwall.com/passwdqc/ 301 | .Sh AUTHORS 302 | The pam_passwdqc module was written for Openwall GNU/*/Linux by 303 | .An Solar Designer Aq solar at openwall.com . 304 | This manual page was derived from 305 | .Xr pam_passwdqc 8 . The latter, derived from the author's 306 | documentation, was written for the 307 | .Fx 308 | Project by 309 | ThinkSec AS and NAI Labs, the Security Research Division of Network 310 | Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 311 | .Pq Dq CBOSS , 312 | as part of the DARPA CHATS research program. 313 | -------------------------------------------------------------------------------- /passwdqc.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2002,2016,2019,2020,2021 by Solar Designer 3 | * Copyright (c) 2008,2009 by Dmitry V. Levin 4 | * See LICENSE 5 | */ 6 | 7 | #ifndef PASSWDQC_H__ 8 | #define PASSWDQC_H__ 9 | 10 | #ifndef _MSC_VER 11 | #include 12 | #endif 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | #ifdef _MSC_VER 19 | /* Partial struct passwd just to accommodate passwdqc code's expectations */ 20 | struct passwd { 21 | char *pw_name; 22 | char *pw_passwd; 23 | char *pw_gecos; 24 | char *pw_dir; 25 | char *pw_shell; 26 | }; 27 | #endif 28 | 29 | typedef struct { 30 | int min[5], max; 31 | int passphrase_words; 32 | int match_length; 33 | int similar_deny; 34 | int random_bits; 35 | char *wordlist; 36 | char *denylist; 37 | char *filter; 38 | } passwdqc_params_qc_t; 39 | 40 | typedef struct { 41 | int flags; 42 | int retry; 43 | } passwdqc_params_pam_t; 44 | 45 | typedef struct { 46 | passwdqc_params_qc_t qc; 47 | passwdqc_params_pam_t pam; 48 | } passwdqc_params_t; 49 | 50 | extern const char *passwdqc_check(const passwdqc_params_qc_t *params, 51 | const char *newpass, const char *oldpass, const struct passwd *pw); 52 | extern char *passwdqc_random(const passwdqc_params_qc_t *params); 53 | 54 | extern int passwdqc_params_parse(passwdqc_params_t *params, 55 | char **reason, int argc, const char *const *argv); 56 | extern int passwdqc_params_load(passwdqc_params_t *params, 57 | char **reason, const char *pathname); 58 | extern void passwdqc_params_reset(passwdqc_params_t *params); 59 | extern void passwdqc_params_free(passwdqc_params_t *params); 60 | 61 | #define F_ENFORCE_MASK 0x00000003 62 | #define F_ENFORCE_USERS 0x00000001 63 | #define F_ENFORCE_ROOT 0x00000002 64 | #define F_ENFORCE_EVERYONE F_ENFORCE_MASK 65 | #define F_NON_UNIX 0x00000004 66 | #define F_ASK_OLDAUTHTOK_MASK 0x00000030 67 | #define F_ASK_OLDAUTHTOK_PRELIM 0x00000010 68 | #define F_ASK_OLDAUTHTOK_UPDATE 0x00000020 69 | #define F_CHECK_OLDAUTHTOK 0x00000040 70 | #define F_USE_FIRST_PASS 0x00000100 71 | #define F_USE_AUTHTOK 0x00000200 72 | #define F_NO_AUDIT 0x00000400 73 | 74 | #define PASSWDQC_VERSION "2.0.3" 75 | 76 | extern void (*_passwdqc_memzero)(void *, size_t); 77 | 78 | #ifdef __cplusplus 79 | } 80 | #endif 81 | 82 | #endif /* PASSWDQC_H__ */ 83 | -------------------------------------------------------------------------------- /passwdqc.pc.in: -------------------------------------------------------------------------------- 1 | Name: passwdqc 2 | Description: Password/passphrase strength checking and policy enforcement 3 | Version: @VERSION@ 4 | Libs: -lpasswdqc 5 | Cflags: 6 | -------------------------------------------------------------------------------- /passwdqc.spec: -------------------------------------------------------------------------------- 1 | Summary: A password/passphrase strength checking and policy enforcement toolset. 2 | Name: passwdqc 3 | Version: 2.0.3 4 | Release: owl1 5 | License: BSD-compatible 6 | Group: System Environment/Base 7 | URL: https://www.openwall.com/passwdqc/ 8 | Source: https://www.openwall.com/passwdqc/%name-%version.tar.gz 9 | Provides: pam_passwdqc = %version-%release 10 | Obsoletes: pam_passwdqc < %version-%release 11 | BuildRequires: pam-devel 12 | BuildRoot: /override/%name-%version 13 | 14 | %description 15 | passwdqc is a password/passphrase strength checking and policy 16 | enforcement toolset, including a PAM module (pam_passwdqc), command-line 17 | programs (pwqcheck, pwqfilter, and pwqgen), and a library (libpasswdqc). 18 | 19 | pam_passwdqc is normally invoked on password changes by programs such as 20 | passwd(1). It is capable of checking password or passphrase strength, 21 | enforcing a policy, and offering randomly-generated passphrases, with 22 | all of these features being optional and easily (re-)configurable. 23 | 24 | pwqcheck and pwqgen are standalone password/passphrase strength checking 25 | and random passphrase generator programs, respectively, which are usable 26 | from scripts. 27 | 28 | The pwqfilter program searches, creates, or updates binary passphrase 29 | filter files, which can also be used with pwqcheck and pam_passwdqc. 30 | 31 | libpasswdqc is the underlying library, which may also be used from 32 | third-party programs. 33 | 34 | %package devel 35 | Summary: Libraries and header files for building passwdqc-aware applications. 36 | Group: Development/Libraries 37 | Requires: %name = %version-%release 38 | 39 | %description devel 40 | This package contains development libraries and header files needed for 41 | building passwdqc-aware applications. 42 | 43 | %prep 44 | %setup -q 45 | 46 | %{expand:%%define optflags_lib %{?optflags_lib:%optflags_lib}%{!?optflags_lib:%optflags}} 47 | 48 | %build 49 | %__make \ 50 | CPPFLAGS='-DLINUX_PAM' \ 51 | CFLAGS_bin='-Wall -W %optflags' \ 52 | CFLAGS_lib='-Wall -W -fPIC %optflags_lib' 53 | 54 | %install 55 | rm -rf %buildroot 56 | %__make install DESTDIR=%buildroot MANDIR=%_mandir \ 57 | SHARED_LIBDIR=/%_lib DEVEL_LIBDIR=%_libdir \ 58 | SECUREDIR=/%_lib/security 59 | 60 | %post -p /sbin/ldconfig 61 | %postun -p /sbin/ldconfig 62 | 63 | %files 64 | %defattr(-,root,root) 65 | %doc CHANGES LICENSE README pwqcheck.php 66 | %config(noreplace) /etc/passwdqc.conf 67 | /%_lib/lib*.so* 68 | %_bindir/* 69 | /%_lib/security/pam_passwdqc.so 70 | %_mandir/man[158]/* 71 | 72 | %files devel 73 | %defattr(-,root,root) 74 | %_includedir/*.h 75 | %_libdir/pkgconfig/passwdqc.pc 76 | %_libdir/lib*.so 77 | %_mandir/man3/* 78 | 79 | %changelog 80 | * Fri Jun 23 2023 Dmitry V. Levin 2.0.3-owl1 81 | - wordset_4k: Move "enroll" to the multiple spellings list (by Solar Designer) 82 | - Don't #include on macOS (by Solar Designer) 83 | - pwqfilter: Allow --pre-hashed after --hash* (by Solar Designer) 84 | - Add pkg-config file (by Egor Ignatov) 85 | - Makefile: add Cygwin support (by Chad Dougherty) 86 | - Remove non-existent symbols from the linker version script 87 | to fix -Wl,--no-undefined-version (by Fangrui Song) 88 | - pam_passwdqc: extend enforce=users to support chpasswd PAM service 89 | in addition to traditionally supported passwd 90 | 91 | * Sun Apr 04 2021 Solar Designer 2.0.2-owl1 92 | - Changes by Dmitry V. Levin: 93 | - pam_passwdqc: enhance formatting of auto-generated policy descriptions 94 | - Add libpasswdqc(3) manual page 95 | - Add manual page links for all functions documented in libpasswdqc(3) 96 | - Package section 3 manual pages into devel subpackage 97 | - LICENSE: mention the license of CI scripts (which are not packaged) 98 | - Update CHANGES 99 | 100 | * Wed Mar 10 2021 Solar Designer 2.0.1-owl1 101 | - Changes by Dmitry V. Levin: 102 | - pam_passwdqc: enhance auto-generated policy descriptions 103 | - Makefile: use CPPFLAGS and LDFLAGS consistently 104 | - Makefile: remove *.po dependence on passwdqc.pot 105 | - Remove generated passwdqc.pot from the repository 106 | - po/ru.po: regenerate using "make update_po" 107 | - po/ru.po: translate new messages added in 1.9.0+ 108 | - wordset_4k: Move "whisky" to the multiple spellings list 109 | - Increase maximum size of randomly-generated passphrases to 136 bits 110 | - Add CHANGES based on two latest release announcements, start to maintain it 111 | 112 | * Wed Feb 17 2021 Solar Designer 2.0.0-owl2 113 | - Update the package description to include pwqfilter. 114 | 115 | * Tue Feb 16 2021 Solar Designer 2.0.0-owl1 116 | - Introduce and use passwdqc_params_free(). 117 | 118 | * Fri Jan 29 2021 Solar Designer 1.9.0-owl1 119 | - Add support for external wordlist, denylist, and binary filter. 120 | - passwdqc_random(): Obtain all of the random bytes before the loop. 121 | - Merge changes needed for building with Visual Studio on Windows. 122 | 123 | * Mon Jan 25 2021 Solar Designer 1.5.0-owl1 124 | - Updated the included wordlist to avoid some inappropriate words in randomly 125 | generated passphrases while not removing any words from the "word-based" check, 126 | and also to have plenty of extra words for subsequent removal of more words 127 | that might be considered inappropriate from the initial 4096 that are used for 128 | randomly generated passphrases. 129 | 130 | * Mon Jan 25 2021 Solar Designer 1.4.1-owl1 131 | - Set default for "max" to 72 (was 40). 132 | - Document "similar" in pwqcheck print_help() and man page. 133 | - Drop the CVS Id tags (stale ones would be confusing with our move to git). 134 | 135 | * Wed Dec 25 2019 Dmitry V. Levin 1.4.0-owl1 136 | - Implemented i18n support in pam_passwdqc, contributed by Oleg Solovyov, 137 | Andrey Cherepanov, and me. The i18n support is off by default, it can be 138 | enabled if Linux-PAM is built using --enable-nls configure option. 139 | - Implemented audit support in pam_passwdqc, contributed by Oleg Solovyov 140 | and me. The audit support is off by default, it can be enabled if Linux-PAM 141 | is built using --enable-audit configure option. 142 | 143 | * Mon Dec 09 2019 Solar Designer 1.3.2-owl1 144 | - Define _DEFAULT_SOURCE for our use of crypt(3) on newer glibc. 145 | The problem was identified and this change tested by Dmitry V. Levin. 146 | - Clarified in the man pages that /etc/passwdqc.conf is not read unless this 147 | suggested file location is specified with the config= option. 148 | - Clarified the OpenBSD configuration example. 149 | - Escape the minus sign in the OpenBSD configuration example to make the 150 | manpage linter happy, patch by Jackson Doak via Unit 193: 151 | https://www.openwall.com/lists/passwdqc-users/2019/04/16/1 152 | 153 | * Wed Jul 20 2016 Solar Designer 1.3.1-owl1 154 | - With "non-unix", initialize the pw_dir field in fake_pw now that (since 155 | passwdqc 1.1.3 in 2009) passwdqc_check.c uses that field. 156 | Bug reported by Jim Paris via Debian: https://bugs.debian.org/831356 157 | - Use size_t for variables holding strlen() return values. 158 | - Cap "max" at 10000 (in case a config set it higher; the default remains 40). 159 | - Check against the shortest allowed password length prior to checking against 160 | the old password (this affects reporting when the old password is empty). 161 | - For zeroization of sensitive data, use a wrapper around memset() called via 162 | a function pointer to reduce the likelihood of a compiler optimizing those 163 | calls out and to allow for overriding of this function with an OS-specific 164 | "secure" memory zeroization function. 165 | - In pwqgen, set stdout to non-buffered, and zeroize and free our own buffer 166 | holding the generated password. 167 | 168 | * Wed Apr 24 2013 Solar Designer 1.3.0-owl1 169 | - When checking is_simple() after discounting a common character sequence, 170 | apply the (negative) bias even for the passphrase length check. Previously, 171 | we were not doing this because passphrases are normally built from words, and 172 | the same code was being used for the check for dictionary words. 173 | - Expanded the list of common character sequences. Along with the change 174 | above, this reduces the number of passing passwords for RockYou top 100k from 175 | 35 to 18, and for RockYou top 1M from 2333 to 2273 (all of these are with 176 | passwdqc's default policy). 177 | - Moved the common character sequences check to be made after the dictionary 178 | words check, to avoid introducing more cases of misreporting. 179 | - Added pwqcheck.php, a PHP wrapper function around the pwqcheck program. 180 | 181 | * Tue Apr 23 2013 Solar Designer 1.2.4-owl1 182 | - In randomly generated passphrases: toggle case of the first character of each 183 | word only if we wouldn't achieve sufficient entropy otherwise, use a trailing 184 | separator if we achieve sufficient entropy even with the final word omitted 185 | (in fact, we now enable the use of different separators in more cases for this 186 | reason), use dashes rather than spaces to separate words when different 187 | separator characters are not in use. 188 | - Expanded the allowed size of randomly-generated passphrases in bits (now it's 189 | 24 to 85 in the tools, and 24 to 136 in the passwdqc_random() interface). 190 | 191 | * Wed Aug 15 2012 Solar Designer 1.2.3-owl1 192 | - Handle possible NULL returns from crypt(). 193 | - Declared all pre-initialized arrays and structs as const. 194 | - Added Darwin (Mac OS X) support to the Makefile, loosely based on a patch by 195 | Ronald Ip (thanks!) 196 | 197 | * Tue Jun 22 2010 Solar Designer 1.2.2-owl1 198 | - Introduced the GNU'ish "uninstall" make target name (a synonym for "remove"). 199 | - Makefile updates to make the "install" and "uninstall" targets with their 200 | default settings friendlier to Solaris systems. 201 | - Added a link to a wiki page with detailed Solaris-specific instructions to 202 | the PLATFORMS file. 203 | 204 | * Sat Mar 27 2010 Solar Designer 1.2.1-owl1 205 | - When matching against the reversed new password, always pass the original 206 | non-reversed new password (possibly with a substring removed) into is_simple(), 207 | but remove or check the correct substring in is_based() considering that the 208 | matching is possibly being done against the reversed password. 209 | 210 | * Tue Mar 16 2010 Solar Designer 1.2.0-owl1 211 | - New command-line options for pwqcheck: -1 and -2 for reading just 1 and 212 | just 2 lines from stdin, respectively (instead of reading 3 lines, which is 213 | the default), --multi for checking multiple passphrases at once (until EOF). 214 | - With randomly-generated passphrases, encode more entropy per separator 215 | character (by increasing the number of different separators from 8 to 16) and 216 | per word (by altering the case of the first letter of each word), which 217 | increases the default generated passphrase size from 42 to 47 bits. 218 | - Substring matching has been enhanced to partially discount rather than fully 219 | remove weak substrings, support leetspeak, and detect some common sequences of 220 | characters (sequential digits, letters in alphabetical order, adjacent keys on 221 | a QWERTY keyboard). 222 | - Detect and allow passphrases with non-ASCII characters in the words. 223 | - A number of optimizations have been made resulting in significant speedup 224 | of passwdqc_check() on real-world passwords. 225 | - Don't require %%optflags_lib such that the package can be built with 226 | "rpmbuild -tb" on the tarball on non-Owl. 227 | 228 | * Fri Oct 30 2009 Dmitry V. Levin 1.1.4-owl1 229 | - Added const qualifier to all arguments of passwdqc_check() and 230 | passwdqc_random(). 231 | - Implemented pwqcheck's stdin check for too long lines. 232 | - Applied markup corrections to passwdqc.conf(5) and pwqcheck(1) for better 233 | portability (by Kevin Steves and Jason McIntyre, with minor changes made 234 | by Solar Designer). 235 | - Changed use of mdoc's .Os macro to be consistent with other Openwall 236 | Project's software (by Solar Designer). 237 | 238 | * Wed Oct 21 2009 Dmitry V. Levin 1.1.3-owl1 239 | - Eliminated insufficiently portable EXIT_FAILURE and EXIT_SUCCESS macros. 240 | - In passwdqc_load.c, replaced redundant snprintf(3) with plain sprintf(3). 241 | - Added pw_dir checks to passwdqc_check(), similar to already existing 242 | pw_gecos checks. 243 | - Dropped undocumented support for multiple options per config file line. 244 | - Switched to a heavily cut-down BSD license. 245 | - Added ldconfig calls to %%post and %%postun scripts. 246 | 247 | * Sat Oct 17 2009 Solar Designer 1.1.2-owl1 248 | - In pwqcheck.c, replaced the uses of strsep(), which were insufficiently 249 | portable, with code based on strchr(). 250 | - Corrected the linker invocations for Solaris (tested on Solaris 10) and 251 | likely for HP-UX (untested). We broke this between 1.0.5 and 1.1.0. 252 | - Split the CFLAGS into two, separate for libraries (libpasswdqc, pam_passwdqc) 253 | and binaries (the pwq* programs). 254 | - In the Makefile, set umask 022 on mkdir's invoked by "make install". 255 | 256 | * Thu Oct 15 2009 Dmitry V. Levin 1.1.1-owl1 257 | - Relaxed license of pwqgen and pwqcheck manual pages. 258 | - Ensure that pwqgen's exit status is zero only if generated passphrase 259 | has been printed successfully. 260 | - Changed pwqcheck to print "OK" line on success. 261 | - Changed pwqcheck to print "Weak passphrase" diagnostics to stdout 262 | instead of stderr. 263 | 264 | * Sat Oct 10 2009 Solar Designer 1.1.0-owl1 265 | - Export passwdqc_params_load in libpasswdqc. 266 | - Minor English grammar corrections to messages produced by pam_passwdqc. 267 | - Minor documentation edits. 268 | - Added/adjusted copyright statements and attributions to reflect Dmitry's 269 | recent changes. 270 | 271 | * Mon Sep 28 2009 Dmitry V. Levin unreleased 272 | - Introduced libpasswdqc shared library. 273 | - Implemented pwqgen and pwqcheck utilities. 274 | - Implemented config= parameter support in libpasswdqc. 275 | - Packaged /etc/passwdqc.conf file with default configuration. 276 | - Added passwdqc.conf(5) manual page. 277 | 278 | * Tue Feb 12 2008 Solar Designer 1.0.5-owl1 279 | - Replaced the separator characters with some of those defined by RFC 3986 280 | as being safe within "userinfo" part of URLs without encoding. 281 | - Reduced the default value for the N2 parameter to min=... (the minimum 282 | length for passphrases) from 12 to 11. 283 | - Corrected the potentially misleading description of N2 (Debian bug #310595). 284 | - Applied minor grammar and style corrections to the documentation, a 285 | pam_passwdqc message, and source code comments. 286 | 287 | * Tue Apr 04 2006 Dmitry V. Levin 1.0.4-owl1 288 | - Changed Makefile to pass list of libraries to linker after regular 289 | object files, to fix build with -Wl,--as-needed. 290 | - Corrected specfile to make it build on x86_64. 291 | 292 | * Wed Aug 17 2005 Dmitry V. Levin 1.0.3-owl1 293 | - Fixed potential memory leak in conversation wrapper. 294 | - Restricted list of global symbols exported by the PAM module 295 | to standard set of six pam_sm_* functions. 296 | 297 | * Wed May 18 2005 Solar Designer 1.0.2-owl1 298 | - Fixed compiler warnings seen on FreeBSD 5.3. 299 | - Updated the Makefile to not require editing on FreeBSD. 300 | - Updated the FreeBSD-specific notes in PLATFORMS. 301 | 302 | * Sun Mar 27 2005 Solar Designer 1.0.1-owl1 303 | - Further compiler warning fixes on LP64 platforms. 304 | 305 | * Fri Mar 25 2005 Solar Designer 1.0-owl1 306 | - Corrected the source code to not break C strict aliasing rules. 307 | 308 | * Wed Jan 26 2005 Solar Designer 0.7.6-owl1 309 | - Disallow unreasonable random= settings. 310 | - Clarified the allowable bit sizes for randomly-generated passphrases and 311 | the lack of relationship between passphrase= and random= options. 312 | 313 | * Fri Oct 31 2003 Solar Designer 0.7.5-owl1 314 | - Assume invocation by root only if both the UID is 0 and the PAM service 315 | name is "passwd"; this should solve changing expired passwords on Solaris 316 | and HP-UX and make "enforce=users" safe. 317 | - Produce proper English explanations for a wider variety of settings. 318 | - Moved the "-c" out of CFLAGS, renamed FAKEROOT to DESTDIR. 319 | 320 | * Sat Jun 21 2003 Solar Designer 0.7.4-owl1 321 | - Documented that "enforce=users" may not always work for services other 322 | than the passwd command. 323 | - Applied a patch to PLATFORMS from Mike Gerdts of GE Medical Systems 324 | to reflect how Solaris 8 patch 108993-18 (or 108994-18 on x86) changes 325 | Solaris 8's PAM implementation to look like Solaris 9. 326 | 327 | * Mon Jun 02 2003 Solar Designer 0.7.3.1-owl1 328 | - Added URL. 329 | 330 | * Thu Oct 31 2002 Solar Designer 0.7.3-owl1 331 | - When compiling with gcc, also link with gcc. 332 | - Use $(MAKE) to invoke sub-makes. 333 | 334 | * Fri Oct 04 2002 Solar Designer 335 | - Solaris 9 notes in PLATFORMS. 336 | 337 | * Wed Sep 18 2002 Solar Designer 338 | - Build with Sun's C compiler cleanly, from Kevin Steves. 339 | - Use install -c as that actually makes a difference on at least HP-UX 340 | (otherwise install would possibly move files and not change the owner). 341 | 342 | * Fri Sep 13 2002 Solar Designer 343 | - Have the same pam_passwdqc binary work for both trusted and non-trusted 344 | HP-UX, from Kevin Steves. 345 | 346 | * Fri Sep 06 2002 Solar Designer 347 | - Use bigcrypt() on HP-UX whenever necessary, from Kevin Steves of Atomic 348 | Gears LLC. 349 | - Moved the old password checking into a separate function. 350 | 351 | * Wed Jul 31 2002 Solar Designer 352 | - Call it 0.6. 353 | 354 | * Sat Jul 27 2002 Solar Designer 355 | - Documented that the man page is under the 3-clause BSD-style license. 356 | - HP-UX 11 support. 357 | 358 | * Tue Jul 23 2002 Solar Designer 359 | - Applied minor corrections to the man page and at the same time eliminated 360 | unneeded/unimportant differences between it and the README. 361 | 362 | * Sun Jul 21 2002 Solar Designer 363 | - 0.5.1: imported the pam_passwdqc(8) manual page back from FreeBSD. 364 | 365 | * Tue Apr 16 2002 Solar Designer 366 | - 0.5: preliminary OpenPAM (FreeBSD-current) support in the code and related 367 | code cleanups (thanks to Dag-Erling Smorgrav). 368 | 369 | * Thu Feb 07 2002 Michail Litvak 370 | - Enforce our new spec file conventions. 371 | 372 | * Sun Nov 04 2001 Solar Designer 373 | - Updated to 0.4: 374 | - Added "ask_oldauthtok" and "check_oldauthtok" as needed for stacking with 375 | the Solaris pam_unix; 376 | - Permit for stacking of more than one instance of this module (no statics). 377 | 378 | * Tue Feb 13 2001 Solar Designer 379 | - Install the module as mode 755. 380 | 381 | * Tue Dec 19 2000 Solar Designer 382 | - Added "-Wall -fPIC" to the CFLAGS. 383 | 384 | * Mon Oct 30 2000 Solar Designer 385 | - 0.3: portability fixes (this might build on non-Linux-PAM now). 386 | 387 | * Fri Sep 22 2000 Solar Designer 388 | - 0.2: added "use_authtok", added README. 389 | 390 | * Fri Aug 18 2000 Solar Designer 391 | - 0.1, "retry_wanted" bugfix. 392 | 393 | * Sun Jul 02 2000 Solar Designer 394 | - Initial version (non-public). 395 | -------------------------------------------------------------------------------- /passwdqc_check.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_check.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2002,2010,2013,2016,2020 by Solar Designer. See LICENSE. 3 | */ 4 | 5 | #ifdef _MSC_VER 6 | #define _CRT_SECURE_NO_WARNINGS /* we use fopen(), sprintf(), strncat() */ 7 | #endif 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "passwdqc.h" /* also provides or equivalent "struct passwd" */ 15 | #include "passwdqc_filter.h" 16 | #include "wordset_4k.h" 17 | 18 | #include "passwdqc_i18n.h" 19 | 20 | #define REASON_ERROR \ 21 | _("check failed") 22 | 23 | #define REASON_SAME \ 24 | _("is the same as the old one") 25 | #define REASON_SIMILAR \ 26 | _("is based on the old one") 27 | 28 | #define REASON_SHORT \ 29 | _("too short") 30 | #define REASON_LONG \ 31 | _("too long") 32 | 33 | #define REASON_SIMPLESHORT \ 34 | _("not enough different characters or classes for this length") 35 | #define REASON_SIMPLE \ 36 | _("not enough different characters or classes") 37 | 38 | #define REASON_PERSONAL \ 39 | _("based on personal login information") 40 | 41 | #define REASON_WORD \ 42 | _("based on a dictionary word and not a passphrase") 43 | 44 | #define REASON_SEQ \ 45 | _("based on a common sequence of characters and not a passphrase") 46 | 47 | #define REASON_WORDLIST \ 48 | _("based on a word list entry") 49 | 50 | #define REASON_DENYLIST \ 51 | _("is in deny list") 52 | 53 | #define REASON_FILTER \ 54 | _("appears to be in a database") 55 | 56 | #define FIXED_BITS 15 57 | 58 | typedef unsigned long fixed; 59 | 60 | /* 61 | * Calculates the expected number of different characters for a random 62 | * password of a given length. The result is rounded down. We use this 63 | * with the _requested_ minimum length (so longer passwords don't have 64 | * to meet this strict requirement for their length). 65 | */ 66 | static int expected_different(int charset, int length) 67 | { 68 | fixed x, y, z; 69 | 70 | x = ((fixed)(charset - 1) << FIXED_BITS) / charset; 71 | y = x; 72 | while (--length > 0) 73 | y = (y * x) >> FIXED_BITS; 74 | z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y); 75 | 76 | return (int)(z >> FIXED_BITS); 77 | } 78 | 79 | /* 80 | * A password is too simple if it is too short for its class, or doesn't 81 | * contain enough different characters for its class, or doesn't contain 82 | * enough words for a passphrase. 83 | * 84 | * The biases are added to the length, and they may be positive or negative. 85 | * The passphrase length check uses passphrase_bias instead of bias so that 86 | * zero may be passed for this parameter when the (other) bias is non-zero 87 | * because of a dictionary word, which is perfectly normal for a passphrase. 88 | * The biases do not affect the number of different characters, character 89 | * classes, and word count. 90 | */ 91 | static int is_simple(const passwdqc_params_qc_t *params, const char *newpass, 92 | int bias, int passphrase_bias) 93 | { 94 | int length, classes, words, chars; 95 | int digits, lowers, uppers, others, unknowns; 96 | int c, p; 97 | 98 | length = classes = words = chars = 0; 99 | digits = lowers = uppers = others = unknowns = 0; 100 | p = ' '; 101 | while ((c = (unsigned char)newpass[length])) { 102 | length++; 103 | 104 | if (!isascii(c)) 105 | unknowns++; 106 | else if (isdigit(c)) 107 | digits++; 108 | else if (islower(c)) 109 | lowers++; 110 | else if (isupper(c)) 111 | uppers++; 112 | else 113 | others++; 114 | 115 | /* A word starts when a letter follows a non-letter or when a non-ASCII 116 | * character follows a space character. We treat all non-ASCII characters 117 | * as non-spaces, which is not entirely correct (there's the non-breaking 118 | * space character at 0xa0, 0x9a, or 0xff), but it should not hurt. */ 119 | if (isascii(p)) { 120 | if (isascii(c)) { 121 | if (isalpha(c) && !isalpha(p)) 122 | words++; 123 | } else if (isspace(p)) 124 | words++; 125 | } 126 | p = c; 127 | 128 | /* Count this character just once: when we're not going to see it anymore */ 129 | if (!strchr(&newpass[length], c)) 130 | chars++; 131 | } 132 | 133 | if (!length) 134 | return 1; 135 | 136 | /* Upper case characters and digits used in common ways don't increase the 137 | * strength of a password */ 138 | c = (unsigned char)newpass[0]; 139 | if (uppers && isascii(c) && isupper(c)) 140 | uppers--; 141 | c = (unsigned char)newpass[length - 1]; 142 | if (digits && isascii(c) && isdigit(c)) 143 | digits--; 144 | 145 | /* Count the number of different character classes we've seen. We assume 146 | * that there are no non-ASCII characters for digits. */ 147 | classes = 0; 148 | if (digits) 149 | classes++; 150 | if (lowers) 151 | classes++; 152 | if (uppers) 153 | classes++; 154 | if (others) 155 | classes++; 156 | if (unknowns && classes <= 1 && (!classes || digits || words >= 2)) 157 | classes++; 158 | 159 | for (; classes > 0; classes--) 160 | switch (classes) { 161 | case 1: 162 | if (length + bias >= params->min[0] && 163 | chars >= expected_different(10, params->min[0]) - 1) 164 | return 0; 165 | return 1; 166 | 167 | case 2: 168 | if (length + bias >= params->min[1] && 169 | chars >= expected_different(36, params->min[1]) - 1) 170 | return 0; 171 | if (!params->passphrase_words || 172 | words < params->passphrase_words) 173 | continue; 174 | if (length + passphrase_bias >= params->min[2] && 175 | chars >= expected_different(27, params->min[2]) - 1) 176 | return 0; 177 | continue; 178 | 179 | case 3: 180 | if (length + bias >= params->min[3] && 181 | chars >= expected_different(62, params->min[3]) - 1) 182 | return 0; 183 | continue; 184 | 185 | case 4: 186 | if (length + bias >= params->min[4] && 187 | chars >= expected_different(95, params->min[4]) - 1) 188 | return 0; 189 | continue; 190 | } 191 | 192 | return 1; 193 | } 194 | 195 | static char *unify(char *dst, const char *src) 196 | { 197 | const char *sptr; 198 | char *dptr; 199 | int c; 200 | 201 | if (!dst && !(dst = malloc(strlen(src) + 1))) 202 | return NULL; 203 | 204 | sptr = src; 205 | dptr = dst; 206 | do { 207 | c = (unsigned char)*sptr; 208 | if (isascii(c) && isupper(c)) 209 | c = tolower(c); 210 | switch (c) { 211 | case 'a': case '@': 212 | c = '4'; break; 213 | case 'e': 214 | c = '3'; break; 215 | /* Unfortunately, if we translate both 'i' and 'l' to '1', this would 216 | * associate these two letters with each other - e.g., "mile" would 217 | * match "MLLE", which is undesired. To solve this, we'd need to test 218 | * different translations separately, which is not implemented yet. */ 219 | case 'i': case '|': 220 | c = '!'; break; 221 | case 'l': 222 | c = '1'; break; 223 | case 'o': 224 | c = '0'; break; 225 | case 's': case '$': 226 | c = '5'; break; 227 | case 't': case '+': 228 | c = '7'; break; 229 | } 230 | *dptr++ = c; 231 | } while (*sptr++); 232 | 233 | return dst; 234 | } 235 | 236 | static char *reverse(const char *src) 237 | { 238 | const char *sptr; 239 | char *dst, *dptr; 240 | 241 | if (!(dst = malloc(strlen(src) + 1))) 242 | return NULL; 243 | 244 | sptr = &src[strlen(src)]; 245 | dptr = dst; 246 | while (sptr > src) 247 | *dptr++ = *--sptr; 248 | *dptr = '\0'; 249 | 250 | return dst; 251 | } 252 | 253 | static void clean(char *dst) 254 | { 255 | if (!dst) 256 | return; 257 | _passwdqc_memzero(dst, strlen(dst)); 258 | free(dst); 259 | } 260 | 261 | /* 262 | * Needle is based on haystack if both contain a long enough common 263 | * substring and needle would be too simple for a password with the 264 | * substring either removed with partial length credit for it added 265 | * or partially discounted for the purpose of the length check. 266 | */ 267 | static int is_based(const passwdqc_params_qc_t *params, 268 | const char *haystack, const char *needle, const char *original, 269 | int mode) 270 | { 271 | char *scratch; 272 | int length; 273 | int i, j; 274 | const char *p; 275 | int worst_bias; 276 | 277 | if (!params->match_length) /* disabled */ 278 | return 0; 279 | 280 | if (params->match_length < 0) /* misconfigured */ 281 | return 1; 282 | 283 | scratch = NULL; 284 | worst_bias = 0; 285 | 286 | length = (int)strlen(needle); 287 | for (i = 0; i <= length - params->match_length; i++) 288 | for (j = params->match_length; i + j <= length; j++) { 289 | int bias = 0, j1 = j - 1; 290 | const char q0 = needle[i], *q1 = &needle[i + 1]; 291 | for (p = haystack; *p; p++) 292 | if (*p == q0 && !strncmp(p + 1, q1, j1)) { /* or memcmp() */ 293 | if ((mode & 0xff) == 0) { /* remove & credit */ 294 | if (!scratch) { 295 | if (!(scratch = malloc(length + 1))) 296 | return 1; 297 | } 298 | /* remove j chars */ 299 | { 300 | int pos = length - (i + j); 301 | if (!(mode & 0x100)) /* not reversed */ 302 | pos = i; 303 | memcpy(scratch, original, pos); 304 | memcpy(&scratch[pos], 305 | &original[pos + j], 306 | length + 1 - (pos + j)); 307 | } 308 | /* add credit for match_length - 1 chars */ 309 | bias = params->match_length - 1; 310 | if (is_simple(params, scratch, bias, bias)) { 311 | clean(scratch); 312 | return 1; 313 | } 314 | } else { /* discount */ 315 | /* Require a 1 character longer match for substrings containing leetspeak 316 | * when matching against dictionary words */ 317 | bias = -1; 318 | if ((mode & 0xff) == 1) { /* words */ 319 | int pos = i, end = i + j; 320 | if (mode & 0x100) { /* reversed */ 321 | pos = length - end; 322 | end = length - i; 323 | } 324 | for (; pos < end; pos++) 325 | if (!isalpha((int)(unsigned char) 326 | original[pos])) { 327 | if (j == params->match_length) 328 | goto next_match_length; 329 | bias = 0; 330 | break; 331 | } 332 | } 333 | 334 | /* discount j - (match_length + bias) chars */ 335 | bias += (int)params->match_length - j; 336 | /* bias <= -1 */ 337 | if (bias < worst_bias) { 338 | if (is_simple(params, original, bias, 339 | (mode & 0xff) == 1 ? 0 : bias)) 340 | return 1; 341 | worst_bias = bias; 342 | } 343 | } 344 | } 345 | /* Zero bias implies that there were no matches for this length. If so, 346 | * there's no reason to try the next substring length (it would result in 347 | * no matches as well). We break out of the substring length loop and 348 | * proceed with all substring lengths for the next position in needle. */ 349 | if (!bias) 350 | break; 351 | next_match_length: 352 | ; 353 | } 354 | 355 | clean(scratch); 356 | 357 | return 0; 358 | } 359 | 360 | #define READ_LINE_MAX 8192 361 | #define READ_LINE_SIZE (READ_LINE_MAX + 2) 362 | 363 | static char *read_line(FILE *f, char *buf) 364 | { 365 | buf[READ_LINE_MAX] = '\n'; 366 | 367 | if (!fgets(buf, READ_LINE_SIZE, f)) 368 | return NULL; 369 | 370 | if (buf[READ_LINE_MAX] != '\n') { 371 | int c; 372 | do { 373 | c = getc(f); 374 | } while (c != EOF && c != '\n'); 375 | if (ferror(f)) 376 | return NULL; 377 | } 378 | 379 | char *p; 380 | if ((p = strpbrk(buf, "\r\n"))) 381 | *p = '\0'; 382 | 383 | return buf; 384 | } 385 | 386 | /* 387 | * Common sequences of characters. 388 | * We don't need to list any of the entire strings in reverse order because the 389 | * code checks the new password in both "unified" and "unified and reversed" 390 | * form against these strings (unifying them first indeed). We also don't have 391 | * to include common repeats of characters (e.g., "777", "!!!", "1000") because 392 | * these are often taken care of by the requirement on the number of different 393 | * characters. 394 | */ 395 | const char * const seq[] = { 396 | "0123456789", 397 | "`1234567890-=", 398 | "~!@#$%^&*()_+", 399 | "abcdefghijklmnopqrstuvwxyz", 400 | "a1b2c3d4e5f6g7h8i9j0", 401 | "1a2b3c4d5e6f7g8h9i0j", 402 | "abc123", 403 | "qwertyuiop[]\\asdfghjkl;'zxcvbnm,./", 404 | "qwertyuiop{}|asdfghjkl:\"zxcvbnm<>?", 405 | "qwertyuiopasdfghjklzxcvbnm", 406 | "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\", 407 | "!qaz@wsx#edc$rfv%tgb^yhn&ujm*ik<(ol>)p:?_{\"+}|", 408 | "qazwsxedcrfvtgbyhnujmikolp", 409 | "1q2w3e4r5t6y7u8i9o0p-[=]", 410 | "q1w2e3r4t5y6u7i8o9p0[-]=\\", 411 | "1qaz1qaz", 412 | "1qaz!qaz", /* can't unify '1' and '!' - see comment in unify() */ 413 | "1qazzaq1", 414 | "zaq!1qaz", 415 | "zaq!2wsx" 416 | }; 417 | 418 | /* 419 | * This wordlist check is now the least important given the checks above 420 | * and the support for passphrases (which are based on dictionary words, 421 | * and checked by other means). It is still useful to trap simple short 422 | * passwords (if short passwords are allowed) that are word-based, but 423 | * passed the other checks due to uncommon capitalization, digits, and 424 | * special characters. We (mis)use the same set of words that are used 425 | * to generate random passwords. This list is much smaller than those 426 | * used for password crackers, and it doesn't contain common passwords 427 | * that aren't short English words. We also support optional external 428 | * wordlist (for inexact matching) and deny list (for exact matching). 429 | */ 430 | static const char *is_word_based(const passwdqc_params_qc_t *params, 431 | const char *unified, const char *reversed, const char *original) 432 | { 433 | const char *reason = REASON_ERROR; 434 | char word[WORDSET_4K_LENGTH_MAX + 1], *buf = NULL; 435 | FILE *f = NULL; 436 | unsigned int i; 437 | 438 | word[WORDSET_4K_LENGTH_MAX] = '\0'; 439 | if (params->match_length) 440 | for (i = 0; _passwdqc_wordset_4k[i][0]; i++) { 441 | memcpy(word, _passwdqc_wordset_4k[i], WORDSET_4K_LENGTH_MAX); 442 | int length = (int)strlen(word); 443 | if (length < params->match_length) 444 | continue; 445 | if (!memcmp(word, _passwdqc_wordset_4k[i + 1], length)) 446 | continue; 447 | unify(word, word); 448 | if (is_based(params, word, unified, original, 1) || 449 | is_based(params, word, reversed, original, 0x101)) { 450 | reason = REASON_WORD; 451 | goto out; 452 | } 453 | } 454 | 455 | if (params->match_length) 456 | for (i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) { 457 | char *seq_i = unify(NULL, seq[i]); 458 | if (!seq_i) 459 | goto out; 460 | if (is_based(params, seq_i, unified, original, 2) || 461 | is_based(params, seq_i, reversed, original, 0x102)) { 462 | clean(seq_i); 463 | reason = REASON_SEQ; 464 | goto out; 465 | } 466 | clean(seq_i); 467 | } 468 | 469 | if (params->match_length && params->match_length <= 4) 470 | for (i = 1900; i <= 2039; i++) { 471 | sprintf(word, "%u", i); 472 | if (is_based(params, word, unified, original, 2) || 473 | is_based(params, word, reversed, original, 0x102)) { 474 | reason = REASON_SEQ; 475 | goto out; 476 | } 477 | } 478 | 479 | if (params->wordlist || params->denylist) 480 | if (!(buf = malloc(READ_LINE_SIZE))) 481 | goto out; 482 | 483 | if (params->wordlist) { 484 | if (!(f = fopen(params->wordlist, "r"))) 485 | goto out; 486 | while (read_line(f, buf)) { 487 | unify(buf, buf); 488 | if (!strcmp(buf, unified) || !strcmp(buf, reversed)) 489 | goto out_wordlist; 490 | if (!params->match_length || 491 | strlen(buf) < (size_t)params->match_length) 492 | continue; 493 | if (is_based(params, buf, unified, original, 1) || 494 | is_based(params, buf, reversed, original, 0x101)) { 495 | out_wordlist: 496 | reason = REASON_WORDLIST; 497 | goto out; 498 | } 499 | } 500 | if (ferror(f)) 501 | goto out; 502 | fclose(f); f = NULL; 503 | } 504 | 505 | if (params->denylist) { 506 | if (!(f = fopen(params->denylist, "r"))) 507 | goto out; 508 | while (read_line(f, buf)) { 509 | if (!strcmp(buf, original)) { 510 | reason = REASON_DENYLIST; 511 | goto out; 512 | } 513 | } 514 | if (ferror(f)) 515 | goto out; 516 | } 517 | 518 | reason = NULL; 519 | 520 | out: 521 | if (f) 522 | fclose(f); 523 | if (buf) { 524 | _passwdqc_memzero(buf, READ_LINE_SIZE); 525 | free(buf); 526 | } 527 | _passwdqc_memzero(word, sizeof(word)); 528 | return reason; 529 | } 530 | 531 | const char *passwdqc_check(const passwdqc_params_qc_t *params, 532 | const char *newpass, const char *oldpass, const struct passwd *pw) 533 | { 534 | char truncated[9]; 535 | char *u_newpass = NULL, *u_reversed = NULL; 536 | char *u_oldpass = NULL; 537 | char *u_name = NULL, *u_gecos = NULL, *u_dir = NULL; 538 | const char *reason = REASON_ERROR; 539 | 540 | size_t length = strlen(newpass); 541 | 542 | if (length < (size_t)params->min[4]) { 543 | reason = REASON_SHORT; 544 | goto out; 545 | } 546 | 547 | if (length > 10000) { 548 | reason = REASON_LONG; 549 | goto out; 550 | } 551 | 552 | if (length > (size_t)params->max) { 553 | if (params->max == 8) { 554 | truncated[0] = '\0'; 555 | strncat(truncated, newpass, 8); 556 | newpass = truncated; 557 | length = 8; 558 | if (oldpass && !strncmp(oldpass, newpass, 8)) { 559 | reason = REASON_SAME; 560 | goto out; 561 | } 562 | } else { 563 | reason = REASON_LONG; 564 | goto out; 565 | } 566 | } 567 | 568 | if (oldpass && !strcmp(oldpass, newpass)) { 569 | reason = REASON_SAME; 570 | goto out; 571 | } 572 | 573 | if (is_simple(params, newpass, 0, 0)) { 574 | reason = REASON_SIMPLE; 575 | if (length < (size_t)params->min[1] && 576 | params->min[1] <= params->max) 577 | reason = REASON_SIMPLESHORT; 578 | goto out; 579 | } 580 | 581 | if (!(u_newpass = unify(NULL, newpass))) 582 | goto out; /* REASON_ERROR */ 583 | if (!(u_reversed = reverse(u_newpass))) 584 | goto out; 585 | if (oldpass && !(u_oldpass = unify(NULL, oldpass))) 586 | goto out; 587 | if (pw) { 588 | if (!(u_name = unify(NULL, pw->pw_name)) || 589 | !(u_gecos = unify(NULL, pw->pw_gecos)) || 590 | !(u_dir = unify(NULL, pw->pw_dir))) 591 | goto out; 592 | } 593 | 594 | if (oldpass && params->similar_deny && 595 | (is_based(params, u_oldpass, u_newpass, newpass, 0) || 596 | is_based(params, u_oldpass, u_reversed, newpass, 0x100))) { 597 | reason = REASON_SIMILAR; 598 | goto out; 599 | } 600 | 601 | if (pw && 602 | (is_based(params, u_name, u_newpass, newpass, 0) || 603 | is_based(params, u_name, u_reversed, newpass, 0x100) || 604 | is_based(params, u_gecos, u_newpass, newpass, 0) || 605 | is_based(params, u_gecos, u_reversed, newpass, 0x100) || 606 | is_based(params, u_dir, u_newpass, newpass, 0) || 607 | is_based(params, u_dir, u_reversed, newpass, 0x100))) { 608 | reason = REASON_PERSONAL; 609 | goto out; 610 | } 611 | 612 | reason = is_word_based(params, u_newpass, u_reversed, newpass); 613 | 614 | if (!reason && params->filter) { 615 | passwdqc_filter_t flt; 616 | reason = REASON_ERROR; 617 | if (passwdqc_filter_open(&flt, params->filter)) 618 | goto out; 619 | int result = passwdqc_filter_lookup(&flt, newpass); 620 | passwdqc_filter_close(&flt); 621 | if (result < 0) 622 | goto out; 623 | reason = result ? REASON_FILTER : NULL; 624 | } 625 | 626 | out: 627 | _passwdqc_memzero(truncated, sizeof(truncated)); 628 | clean(u_newpass); 629 | clean(u_reversed); 630 | clean(u_oldpass); 631 | clean(u_name); 632 | clean(u_gecos); 633 | clean(u_dir); 634 | 635 | return reason; 636 | } 637 | -------------------------------------------------------------------------------- /passwdqc_filter.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020,2021 by Solar Designer 3 | * See LICENSE 4 | */ 5 | 6 | #define _FILE_OFFSET_BITS 64 7 | #define _LARGEFILE_SOURCE 1 8 | #define _LARGEFILE64_SOURCE 1 9 | #define _LARGE_FILES 1 10 | 11 | #ifdef _MSC_VER 12 | #define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ 13 | #define _CRT_SECURE_NO_WARNINGS /* we use open() */ 14 | #include /* for SEEK_SET and SEEK_END */ 15 | #include 16 | #define lseek _lseeki64 17 | #define ssize_t int /* MSVC's read() returns int and we don't need more here */ 18 | #define SSIZE_MAX INT_MAX 19 | #define OPEN_FLAGS (O_RDONLY | _O_BINARY | _O_RANDOM) 20 | #else 21 | #include 22 | #define OPEN_FLAGS O_RDONLY 23 | #endif 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | #include "passwdqc.h" 30 | #define PASSWDQC_FILTER_INTERNALS 31 | #include "passwdqc_filter.h" 32 | 33 | static ssize_t read_loop(int fd, void *buffer, size_t count) 34 | { 35 | ssize_t offset, block; 36 | 37 | offset = 0; 38 | while (count > 0 && count <= SSIZE_MAX) { 39 | block = read(fd, (char *)buffer + offset, count); 40 | 41 | if (block < 0) 42 | return block; 43 | if (!block) 44 | return offset; 45 | 46 | offset += block; 47 | count -= block; 48 | } 49 | 50 | return offset; 51 | } 52 | 53 | int passwdqc_filter_open(passwdqc_filter_t *flt, const char *filename) 54 | { 55 | if ((flt->fd = open(filename, OPEN_FLAGS)) < 0) 56 | return -1; 57 | 58 | if (read_loop(flt->fd, &flt->header, sizeof(flt->header)) != sizeof(flt->header) || 59 | passwdqc_filter_verify_header(&flt->header) || 60 | flt->header.hash_id < PASSWDQC_FILTER_HASH_MIN || flt->header.hash_id > PASSWDQC_FILTER_HASH_MAX || 61 | (size_t)lseek(flt->fd, 0, SEEK_END) != sizeof(flt->header) + (flt->header.capacity << 2)) { 62 | passwdqc_filter_close(flt); 63 | return -1; 64 | } 65 | 66 | return 0; 67 | } 68 | 69 | int passwdqc_filter_close(passwdqc_filter_t *flt) 70 | { 71 | int retval = close(flt->fd); 72 | flt->fd = -1; 73 | return retval; 74 | } 75 | 76 | static int check(const passwdqc_filter_t *flt, passwdqc_filter_i_t i, passwdqc_filter_f_t f) 77 | { 78 | int retval = -1; 79 | 80 | passwdqc_filter_packed_t p; 81 | if (lseek(flt->fd, sizeof(flt->header) + (uint64_t)i * sizeof(p), SEEK_SET) < 0 || 82 | read_loop(flt->fd, &p, sizeof(p)) != sizeof(p)) 83 | goto out; 84 | 85 | passwdqc_filter_unpacked_t u; 86 | unsigned int n = (unsigned int)passwdqc_filter_unpack(&u, &p, NULL); 87 | if (n > flt->header.bucket_size) 88 | goto out; 89 | 90 | unsigned int j; 91 | for (j = 0; j < n; j++) { 92 | if (passwdqc_filter_f_eq(u.slots[j], f, flt->header.bucket_size)) { 93 | retval = 1; 94 | goto out; 95 | } 96 | } 97 | 98 | retval = (n < flt->header.threshold) ? 0 : 2; 99 | 100 | out: 101 | _passwdqc_memzero(&p, sizeof(p)); 102 | _passwdqc_memzero(&u, sizeof(u)); 103 | return retval; 104 | } 105 | 106 | int passwdqc_filter_lookup(const passwdqc_filter_t *flt, const char *plaintext) 107 | { 108 | int retval = 3; 109 | passwdqc_filter_hash_t h; 110 | passwdqc_filter_f_t ftest; 111 | 112 | clean: 113 | switch (flt->header.hash_id) { 114 | case PASSWDQC_FILTER_HASH_MD4: 115 | passwdqc_filter_md4(&h, plaintext); 116 | ftest = 0x8c6420f439de2000ULL; 117 | break; 118 | case PASSWDQC_FILTER_HASH_NTLM_CP1252: 119 | passwdqc_filter_ntlm_cp1252(&h, plaintext); 120 | ftest = 0x26bd9256ff7e052eULL; 121 | break; 122 | default: 123 | return -1; 124 | } 125 | 126 | uint32_t nbuckets = (uint32_t)(flt->header.capacity >> 2); 127 | passwdqc_filter_i_t i = passwdqc_filter_h2i(&h, nbuckets); 128 | passwdqc_filter_f_t f = passwdqc_filter_h2f(&h); 129 | 130 | _passwdqc_memzero(&h, sizeof(h)); 131 | 132 | /* 133 | * The tests of i and f here not only self-test the code, but also prevent the 134 | * compiler from moving "return retval;" to before the computation of h, i, and 135 | * f, which would leave sensitive data from the real hash computation around. 136 | */ 137 | if (i >= nbuckets) 138 | return -1; 139 | 140 | if (retval <= 1) { 141 | /* Waste two syscalls on overwriting lseek()'s stack and current file offset */ 142 | i = passwdqc_filter_h2i(&h, nbuckets); /* 0 */ 143 | if (check(flt, i, f) < 0) 144 | return -1; 145 | if (f != ftest) 146 | return -1; 147 | return retval; 148 | } 149 | 150 | /* At least 1 character to overwrite passwdqc_filter_ntlm_cp1252()'s buffer */ 151 | plaintext = " 09AZaz\x7e\x7f\x80\x81\x9e\x9f\xa0\xff"; 152 | 153 | retval = check(flt, i, f); 154 | if (retval <= 1) 155 | goto clean; 156 | 157 | retval = check(flt, passwdqc_filter_alti(i, f, nbuckets), f); 158 | if (retval == 2) 159 | retval = 0; 160 | goto clean; 161 | } 162 | -------------------------------------------------------------------------------- /passwdqc_filter.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 by Solar Designer 3 | * See LICENSE 4 | */ 5 | 6 | #ifndef PASSWDQC_FILTER_H__ 7 | #define PASSWDQC_FILTER_H__ 8 | 9 | #include 10 | 11 | /* Higher-level API for use by passwdqc_check.c */ 12 | 13 | typedef struct { 14 | uint8_t version[4]; /* PASSWDQC_FILTER_VERSION */ 15 | uint8_t threshold; /* 0 to 4 */ 16 | uint8_t bucket_size; /* 2 to 4 */ 17 | uint8_t hash_id; /* one of PASSWDQC_FILTER_HASH_* */ 18 | uint8_t reserved1; 19 | uint64_t endianness; /* PASSWDQC_FILTER_ENDIANNESS */ 20 | uint64_t reserved2; /* e.g., for checksum */ 21 | uint64_t capacity, deletes, inserts, dupes, kicks; 22 | } passwdqc_filter_header_t; 23 | 24 | typedef struct { 25 | passwdqc_filter_header_t header; 26 | int fd; 27 | } passwdqc_filter_t; 28 | 29 | extern int passwdqc_filter_open(passwdqc_filter_t *flt, const char *filename); 30 | extern int passwdqc_filter_lookup(const passwdqc_filter_t *flt, const char *plaintext); 31 | extern int passwdqc_filter_close(passwdqc_filter_t *flt); 32 | 33 | #ifdef PASSWDQC_FILTER_INTERNALS 34 | /* Lower-level inlines for shared use by pwqfilter.c and passwdqc_filter.c */ 35 | 36 | #include /* for strcspn() */ 37 | 38 | /* We assume little-endian if this leaves __BYTE_ORDER undefined */ 39 | #if !defined(_MSC_VER) && !defined(__APPLE__) && !defined(__sun) && !defined(_AIX) 40 | #include 41 | #elif (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ 42 | defined(__sparc) || defined(_POWER) 43 | #define __LITTLE_ENDIAN 1234 44 | #define __BIG_ENDIAN 4321 45 | #define __BYTE_ORDER __BIG_ENDIAN 46 | #endif 47 | 48 | #include "md4.h" 49 | 50 | #define PASSWDQC_FILTER_VERSION "PWQ0" 51 | #define PASSWDQC_FILTER_ENDIANNESS 0x0807060504030201ULL 52 | 53 | static inline int passwdqc_filter_verify_header(const passwdqc_filter_header_t *header) 54 | { 55 | return (memcmp(header->version, PASSWDQC_FILTER_VERSION, sizeof(header->version)) || 56 | header->threshold > header->bucket_size || header->bucket_size < 2 || header->bucket_size > 4 || 57 | header->endianness != PASSWDQC_FILTER_ENDIANNESS || 58 | (header->capacity & 3) || header->capacity < 4 || header->capacity > ((1ULL << 32) - 1) * 4 || 59 | header->inserts - header->deletes > header->capacity) ? -1 : 0; 60 | } 61 | 62 | typedef enum { 63 | PASSWDQC_FILTER_HASH_OPAQUE = 0, 64 | PASSWDQC_FILTER_HASH_MIN = 1, 65 | PASSWDQC_FILTER_HASH_MD4 = 1, 66 | PASSWDQC_FILTER_HASH_NTLM_CP1252 = 2, 67 | PASSWDQC_FILTER_HASH_MAX = 2 68 | } passwdqc_filter_hash_id_t; 69 | 70 | typedef struct { 71 | uint64_t hi, lo; /* we access hi first, so let's also place it first */ 72 | } passwdqc_filter_packed_t; 73 | 74 | typedef uint32_t passwdqc_filter_i_t; 75 | typedef uint64_t passwdqc_filter_f_t; 76 | 77 | typedef struct { 78 | passwdqc_filter_f_t slots[4]; 79 | } passwdqc_filter_unpacked_t; 80 | 81 | typedef union { 82 | unsigned char uc[16]; 83 | uint32_t u32[4]; 84 | uint64_t u64[2]; 85 | } passwdqc_filter_hash_t; 86 | 87 | #ifdef __GNUC__ 88 | #define force_inline __attribute__ ((always_inline)) inline 89 | #define likely(x) __builtin_expect(!!(x), 1) 90 | #define unlikely(x) __builtin_expect(!!(x), 0) 91 | #else 92 | #define force_inline inline 93 | #define likely(x) (x) 94 | #define unlikely(x) (x) 95 | #endif 96 | 97 | static force_inline passwdqc_filter_i_t passwdqc_filter_wrap(uint32_t what, passwdqc_filter_i_t m) 98 | { 99 | return ((uint64_t)what * m) >> 32; 100 | } 101 | 102 | static force_inline passwdqc_filter_i_t passwdqc_filter_h2i(passwdqc_filter_hash_t *h, passwdqc_filter_i_t m) 103 | { 104 | uint32_t i; 105 | /* 106 | * Controversial optimization: when converting a hash to its hash table index 107 | * for the primary bucket, take its initial portion and swap the nibbles so 108 | * that we process most of the hash table semi-sequentially in case our input 109 | * is an ASCII-sorted list of hex-encoded hashes. A drawback is that we fail 110 | * to reach high load if our input is a biased fragment from such sorted list. 111 | */ 112 | #if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN 113 | i = h->u32[0]; 114 | #else 115 | i = (uint32_t)h->uc[0] << 24; 116 | i |= (uint32_t)h->uc[1] << 16; 117 | i |= (uint32_t)h->uc[2] << 8; 118 | i |= (uint32_t)h->uc[3]; 119 | #endif 120 | i = ((i & 0x0f0f0f0f) << 4) | ((i >> 4) & 0x0f0f0f0f); 121 | return passwdqc_filter_wrap(i, m); 122 | } 123 | 124 | static force_inline passwdqc_filter_f_t passwdqc_filter_h2f(passwdqc_filter_hash_t *h) 125 | { 126 | #if defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN 127 | return h->u64[1]; 128 | #else 129 | uint64_t f; 130 | f = (uint64_t)h->uc[8]; 131 | f |= (uint64_t)h->uc[9] << 8; 132 | f |= (uint64_t)h->uc[10] << 16; 133 | f |= (uint64_t)h->uc[11] << 24; 134 | f |= (uint64_t)h->uc[12] << 32; 135 | f |= (uint64_t)h->uc[13] << 40; 136 | f |= (uint64_t)h->uc[14] << 48; 137 | f |= (uint64_t)h->uc[15] << 56; 138 | return f; 139 | #endif 140 | } 141 | 142 | static force_inline passwdqc_filter_i_t passwdqc_filter_alti(passwdqc_filter_i_t i, passwdqc_filter_f_t f, passwdqc_filter_i_t m) 143 | { 144 | /* 145 | * We must not use more than 33 bits of the fingerprint here for consistent 146 | * behavior in case the fingerprint later gets truncated. 147 | */ 148 | int64_t alti = (int64_t)(m - 1 - i) - passwdqc_filter_wrap((uint32_t)f, m); 149 | #if 0 150 | /* 151 | * This is how we could have made use of the 33rd bit while staying in range, 152 | * but testing shows that this is unnecessary. 153 | */ 154 | alti -= ((f >> 32) & 1); 155 | #endif 156 | if (alti < 0) 157 | alti += m; 158 | #if 0 159 | assert((passwdqc_filter_i_t)alti < m); 160 | #endif 161 | return (passwdqc_filter_i_t)alti; 162 | } 163 | 164 | static inline unsigned int passwdqc_filter_ssdecode(unsigned int src) 165 | { 166 | /* First 16 tetrahedral numbers (n*(n+1)*(n+2)/3!) in reverse order */ 167 | static const uint16_t tab4[] = {816, 680, 560, 455, 364, 286, 220, 165, 120, 84, 56, 35, 20, 10, 4, 1}; 168 | /* First 16 triangular numbers (n*(n+1)/2!) in reverse order */ 169 | static const uint8_t tab3[] = {136, 120, 105, 91, 78, 66, 55, 45, 36, 28, 21, 15, 10, 6, 3, 1}; 170 | 171 | unsigned int dst, i = 0; 172 | 173 | while (src >= tab4[i]) 174 | src -= tab4[i++]; 175 | dst = i << 12; 176 | 177 | while (src >= tab3[i]) 178 | src -= tab3[i++]; 179 | dst |= i << 8; 180 | 181 | while (src >= 16 - i) 182 | src -= 16 - i++; 183 | dst |= i << 4; 184 | 185 | dst |= i + src; 186 | 187 | return dst; 188 | } 189 | 190 | static force_inline int passwdqc_filter_unpack(passwdqc_filter_unpacked_t *dst, const passwdqc_filter_packed_t *src, 191 | const uint16_t *ssdecode) 192 | { 193 | uint64_t hi = src->hi, f = src->lo; 194 | unsigned int ssi = hi >> (64 - 12); /* semi-sort index */ 195 | 196 | if (likely(ssi - 1 < 3876)) { 197 | passwdqc_filter_f_t ssd = ssdecode ? ssdecode[ssi - 1] : passwdqc_filter_ssdecode(ssi - 1); 198 | const unsigned int fbits = 33; 199 | const unsigned int lobits = fbits - 4; 200 | const passwdqc_filter_f_t lomask = ((passwdqc_filter_f_t)1 << lobits) - 1; 201 | dst->slots[0] = (f & lomask) | ((ssd & 0x000f) << lobits); 202 | f >>= lobits; 203 | dst->slots[1] = (f & lomask) | ((ssd & 0x00f0) << (lobits - 4)); 204 | f >>= lobits; 205 | f |= hi << (64 - 2 * lobits); 206 | dst->slots[2] = (f & lomask) | ((ssd & 0x0f00) << (lobits - 8)); 207 | f >>= lobits; 208 | dst->slots[3] = (f & lomask) | ((ssd & 0xf000) << (lobits - 12)); 209 | return 4; 210 | } 211 | 212 | if (likely(hi <= 1)) { 213 | if (!hi) 214 | return unlikely(f) ? -1 : 0; 215 | 216 | dst->slots[0] = f; 217 | return 1; 218 | } 219 | 220 | if (likely((ssi & 0xf80) == 0xf80)) { 221 | const unsigned int fbits = 41; 222 | const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; 223 | dst->slots[0] = f & fmask; 224 | f >>= fbits; 225 | f |= hi << (64 - fbits); 226 | dst->slots[1] = f & fmask; 227 | if (unlikely(dst->slots[0] < dst->slots[1])) 228 | return -1; 229 | f = hi >> (2 * fbits - 64); 230 | dst->slots[2] = f & fmask; 231 | if (unlikely(dst->slots[1] < dst->slots[2])) 232 | return -1; 233 | return 3; 234 | } 235 | 236 | if (likely((ssi & 0xfc0) == 0xf40)) { 237 | const unsigned int fbits = 61; 238 | const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; 239 | dst->slots[0] = f & fmask; 240 | f >>= fbits; 241 | f |= hi << (64 - fbits); 242 | dst->slots[1] = f & fmask; 243 | if (unlikely(dst->slots[0] < dst->slots[1])) 244 | return -1; 245 | return 2; 246 | } 247 | 248 | return -1; 249 | } 250 | 251 | static inline int passwdqc_filter_f_eq(passwdqc_filter_f_t stored, passwdqc_filter_f_t full, unsigned int largest_bucket_size) 252 | { 253 | if (likely((uint32_t)stored != (uint32_t)full)) 254 | return 0; 255 | /* 256 | * Ignore optional high bits of a stored fingerprint if they're all-zero, 257 | * regardless of whether the fingerprint possibly came from a large enough slot 258 | * for those zeroes to potentially be meaningful. We have to do this because 259 | * the fingerprint might have been previously stored in a larger (smaller-slot) 260 | * bucket and been kicked from there, in which case the zeroes are meaningless. 261 | * Exception: we don't have to do this if there were no larger buckets so far. 262 | */ 263 | if ((stored >> 33) || largest_bucket_size < 4) { 264 | if ((stored >> 41) || largest_bucket_size < 3) { 265 | if (stored >> 61) 266 | return likely(stored == full); 267 | else 268 | return likely(stored == (full & (((passwdqc_filter_f_t)1 << 61) - 1))); 269 | } else { 270 | return likely(stored == (full & (((passwdqc_filter_f_t)1 << 41) - 1))); 271 | } 272 | } else { 273 | return likely(stored == (full & (((passwdqc_filter_f_t)1 << 33) - 1))); 274 | } 275 | } 276 | 277 | static inline void passwdqc_filter_md4(passwdqc_filter_hash_t *dst, const char *src) 278 | { 279 | MD4_CTX ctx; 280 | MD4_Init(&ctx); 281 | MD4_Update(&ctx, src, strcspn(src, "\n\r")); 282 | MD4_Final(dst->uc, &ctx); 283 | } 284 | 285 | static inline void passwdqc_filter_ntlm_cp1252(passwdqc_filter_hash_t *dst, const char *src) 286 | { 287 | /* 288 | * 5 of these codes are undefined in CP1252. We let the original single-byte 289 | * values for them pass through, which appears to match how the HIBP v7 NTLM 290 | * hashes were generated. 291 | */ 292 | static const uint16_t c1[] = { 293 | 0x20ac, 0x81, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 294 | 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8d, 0x017d, 0x8f, 295 | 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 296 | 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x9d, 0x017e, 0x0178 297 | }; 298 | 299 | MD4_CTX ctx; 300 | MD4_Init(&ctx); 301 | while (*src != '\n' && *src != '\r' && *src) { 302 | unsigned int c = *(unsigned char *)src++; 303 | if (c - 128 < sizeof(c1) / sizeof(c1[0])) 304 | c = c1[c - 128]; 305 | #if defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN 306 | MD4_Update(&ctx, &c, 2); 307 | #else 308 | uint8_t ucs2[2] = {c, c >> 8}; 309 | MD4_Update(&ctx, ucs2, 2); 310 | #endif 311 | } 312 | MD4_Final(dst->uc, &ctx); 313 | } 314 | 315 | #endif /* PASSWDQC_FILTER_INTERNALS */ 316 | #endif /* PASSWDQC_FILTER_H__ */ 317 | -------------------------------------------------------------------------------- /passwdqc_i18n.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 by Dmitry V. Levin 3 | * Copyright (c) 2017 by Oleg Solovyov 4 | * See LICENSE 5 | */ 6 | 7 | #ifndef PASSWDQC_I18N_H__ 8 | #define PASSWDQC_I18N_H__ 9 | 10 | #ifdef ENABLE_NLS 11 | #ifndef PACKAGE 12 | #define PACKAGE "passwdqc" 13 | #endif 14 | #include 15 | #define _(msgid) dgettext(PACKAGE, msgid) 16 | #define P2_(msgid, count) (dngettext(PACKAGE, (msgid), (msgid), (count))) 17 | #define P3_(msgid, msgid_plural, count) (dngettext(PACKAGE, (msgid), (msgid_plural), (count))) 18 | #define N_(msgid) msgid 19 | #else 20 | #define _(msgid) (msgid) 21 | #define P2_(msgid, count) (msgid) 22 | #define P3_(msgid, msgid_plural, count) ((count) == 1 ? (msgid) : (msgid_plural)) 23 | #define N_(msgid) msgid 24 | #endif 25 | 26 | #endif /* PASSWDQC_I18N_H__ */ 27 | -------------------------------------------------------------------------------- /passwdqc_load.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008,2009 by Dmitry V. Levin 3 | * Copyright (c) 2021 by Solar Designer 4 | * See LICENSE 5 | */ 6 | 7 | #ifdef _MSC_VER 8 | #define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ 9 | #define _CRT_SECURE_NO_WARNINGS /* we use fopen(), strerror(), sprintf() */ 10 | #endif 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "passwdqc.h" 20 | #include "concat.h" 21 | 22 | static char *mkreason(const char *what, const char *pathname, 23 | unsigned int lineno, const char *why) 24 | { 25 | char buf[sizeof(unsigned int) * 3 + 1]; 26 | const char *at_line = (lineno ? " at line " : ""); 27 | const char *at_num = (lineno ? buf : ""); 28 | 29 | if (lineno) 30 | sprintf(buf, "%u", lineno); 31 | return concat(what, " \"", pathname, "\"", at_line, at_num, ": ", 32 | (why ? why : strerror(errno)), NULL); 33 | } 34 | 35 | static int 36 | parse_file(FILE *fp, passwdqc_params_t *params, char **reason, 37 | const char *pathname) 38 | { 39 | unsigned int lineno; 40 | char buf[8192]; 41 | 42 | for (lineno = 1; fgets(buf, sizeof(buf), fp); ++lineno) { 43 | char *str, *end, *rt; 44 | const char *cstr; 45 | int rc; 46 | 47 | if (strlen(buf) >= sizeof(buf) - 1) { 48 | *reason = mkreason("Error reading", pathname, 49 | lineno, "Line too long"); 50 | return -1; 51 | } 52 | 53 | str = buf + strspn(buf, " \t\r\n"); 54 | if (!*str || *str == '#') 55 | continue; 56 | 57 | if ((end = strpbrk(str, "\r\n"))) 58 | *end = '\0'; 59 | else 60 | end = str + strlen(str); 61 | 62 | while (end > str && (*--end == ' ' || *end == '\t')) 63 | *end = '\0'; 64 | 65 | cstr = str; 66 | if ((rc = passwdqc_params_parse(params, &rt, 1, &cstr))) { 67 | *reason = mkreason("Error loading", pathname, 68 | lineno, (rt ? rt : "Out of memory")); 69 | free(rt); 70 | return rc; 71 | } 72 | } 73 | 74 | if (!feof(fp) || ferror(fp)) { 75 | *reason = mkreason("Error reading", pathname, 0, NULL); 76 | return -1; 77 | } 78 | 79 | return 0; 80 | } 81 | 82 | struct dev_ino_t; 83 | struct dev_ino_t { 84 | struct dev_ino_t *next; 85 | dev_t dev; 86 | ino_t ino; 87 | }; 88 | 89 | static struct dev_ino_t *dev_ino_head; 90 | 91 | int 92 | passwdqc_params_load(passwdqc_params_t *params, char **reason, 93 | const char *pathname) 94 | { 95 | int rc; 96 | FILE *fp; 97 | struct dev_ino_t di, *di_p; 98 | struct stat st; 99 | 100 | if (!(fp = fopen(pathname, "r"))) { 101 | *reason = mkreason("Error opening", pathname, 0, NULL); 102 | return -1; 103 | } 104 | 105 | if (fstat(fileno(fp), &st)) { 106 | *reason = mkreason("Error stat", pathname, 0, NULL); 107 | fclose(fp); 108 | return -1; 109 | } 110 | 111 | di.dev = st.st_dev; 112 | di.ino = st.st_ino; 113 | for (di_p = dev_ino_head; di_p; di_p = di_p->next) 114 | if (di_p->dev == di.dev && di_p->ino == di.ino) 115 | break; 116 | if (di_p) { 117 | *reason = mkreason("Error opening", pathname, 0, 118 | "Loop detected"); 119 | fclose(fp); 120 | return -1; 121 | } 122 | 123 | di.next = dev_ino_head; 124 | dev_ino_head = &di; 125 | 126 | rc = parse_file(fp, params, reason, pathname); 127 | fclose(fp); 128 | 129 | dev_ino_head = dev_ino_head->next; 130 | return rc; 131 | } 132 | -------------------------------------------------------------------------------- /passwdqc_memzero.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 by Solar Designer. See LICENSE. 3 | */ 4 | 5 | #ifdef _MSC_VER 6 | #include 7 | #else 8 | #include 9 | #endif 10 | 11 | static void memzero(void *buf, size_t len) 12 | { 13 | #ifdef _MSC_VER 14 | SecureZeroMemory(buf, len); 15 | #else 16 | memset(buf, 0, len); 17 | #endif 18 | } 19 | 20 | void (*_passwdqc_memzero)(void *, size_t) = memzero; 21 | -------------------------------------------------------------------------------- /passwdqc_params_free.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_params_load.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_params_parse.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_params_reset.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_parse.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2003,2005,2016,2020,2021 by Solar Designer 3 | * Copyright (c) 2008,2009 by Dmitry V. Levin 4 | * See LICENSE 5 | */ 6 | 7 | #ifdef _MSC_VER 8 | #define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ 9 | #endif 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "passwdqc.h" 17 | #include "concat.h" 18 | 19 | static const char *skip_prefix(const char *sample, const char *prefix) 20 | { 21 | size_t len = strlen(prefix); 22 | 23 | if (strncmp(sample, prefix, len)) 24 | return NULL; 25 | return sample + len; 26 | } 27 | 28 | static int 29 | parse_option(passwdqc_params_t *params, char **reason, const char *option) 30 | { 31 | const char *err = "Invalid parameter value"; 32 | const char * const err_oom = "Out of memory"; 33 | const char *p; 34 | char *e; 35 | int i, rc = 0; 36 | unsigned long v; 37 | 38 | *reason = NULL; 39 | if ((p = skip_prefix(option, "min="))) { 40 | for (i = 0; i < 5; i++) { 41 | if (!strncmp(p, "disabled", 8)) { 42 | v = INT_MAX; 43 | p += 8; 44 | } else { 45 | v = strtoul(p, &e, 10); 46 | p = e; 47 | } 48 | if (i < 4 && *p++ != ',') 49 | goto parse_error; 50 | if (v > INT_MAX) 51 | goto parse_error; 52 | if (i && (int)v > params->qc.min[i - 1]) 53 | goto parse_error; 54 | params->qc.min[i] = v; 55 | } 56 | if (*p) 57 | goto parse_error; 58 | } else if ((p = skip_prefix(option, "max="))) { 59 | v = strtoul(p, &e, 10); 60 | if (*e || v < 8 || v > INT_MAX) 61 | goto parse_error; 62 | if (v > 10000) 63 | v = 10000; 64 | params->qc.max = v; 65 | } else if ((p = skip_prefix(option, "passphrase="))) { 66 | v = strtoul(p, &e, 10); 67 | if (*e || v > INT_MAX) 68 | goto parse_error; 69 | params->qc.passphrase_words = v; 70 | } else if ((p = skip_prefix(option, "match="))) { 71 | v = strtoul(p, &e, 10); 72 | if (*e || v > INT_MAX) 73 | goto parse_error; 74 | params->qc.match_length = v; 75 | } else if ((p = skip_prefix(option, "similar="))) { 76 | if (!strcmp(p, "permit")) 77 | params->qc.similar_deny = 0; 78 | else if (!strcmp(p, "deny")) 79 | params->qc.similar_deny = 1; 80 | else 81 | goto parse_error; 82 | } else if ((p = skip_prefix(option, "random="))) { 83 | v = strtoul(p, &e, 10); 84 | if (!strcmp(e, ",only")) { 85 | e += 5; 86 | params->qc.min[4] = INT_MAX; 87 | } 88 | if (*e || (v && v < 24) || v > 136) 89 | goto parse_error; 90 | params->qc.random_bits = v; 91 | } else if ((p = skip_prefix(option, "wordlist="))) { 92 | free(params->qc.wordlist); 93 | params->qc.wordlist = NULL; 94 | if (*p && !(params->qc.wordlist = strdup(p))) { 95 | err = err_oom; 96 | goto parse_error; 97 | } 98 | } else if ((p = skip_prefix(option, "denylist="))) { 99 | free(params->qc.denylist); 100 | params->qc.denylist = NULL; 101 | if (*p && !(params->qc.denylist = strdup(p))) { 102 | err = err_oom; 103 | goto parse_error; 104 | } 105 | } else if ((p = skip_prefix(option, "filter="))) { 106 | free(params->qc.filter); 107 | params->qc.filter = NULL; 108 | if (*p && !(params->qc.filter = strdup(p))) { 109 | err = err_oom; 110 | goto parse_error; 111 | } 112 | } else if ((p = skip_prefix(option, "enforce="))) { 113 | params->pam.flags &= ~F_ENFORCE_MASK; 114 | if (!strcmp(p, "users")) 115 | params->pam.flags |= F_ENFORCE_USERS; 116 | else if (!strcmp(p, "everyone")) 117 | params->pam.flags |= F_ENFORCE_EVERYONE; 118 | else if (strcmp(p, "none")) 119 | goto parse_error; 120 | } else if (!strcmp(option, "non-unix")) { 121 | if (params->pam.flags & F_CHECK_OLDAUTHTOK) 122 | goto parse_error; 123 | params->pam.flags |= F_NON_UNIX; 124 | } else if ((p = skip_prefix(option, "retry="))) { 125 | v = strtoul(p, &e, 10); 126 | if (*e || v > INT_MAX) 127 | goto parse_error; 128 | params->pam.retry = v; 129 | } else if ((p = skip_prefix(option, "ask_oldauthtok"))) { 130 | params->pam.flags &= ~F_ASK_OLDAUTHTOK_MASK; 131 | if (params->pam.flags & F_USE_FIRST_PASS) 132 | goto parse_error; 133 | if (!p[0]) 134 | params->pam.flags |= F_ASK_OLDAUTHTOK_PRELIM; 135 | else if (!strcmp(p, "=update")) 136 | params->pam.flags |= F_ASK_OLDAUTHTOK_UPDATE; 137 | else 138 | goto parse_error; 139 | } else if (!strcmp(option, "check_oldauthtok")) { 140 | if (params->pam.flags & F_NON_UNIX) 141 | goto parse_error; 142 | params->pam.flags |= F_CHECK_OLDAUTHTOK; 143 | } else if (!strcmp(option, "use_first_pass")) { 144 | if (params->pam.flags & F_ASK_OLDAUTHTOK_MASK) 145 | goto parse_error; 146 | params->pam.flags |= F_USE_FIRST_PASS | F_USE_AUTHTOK; 147 | } else if (!strcmp(option, "use_authtok")) { 148 | params->pam.flags |= F_USE_AUTHTOK; 149 | } else if (!strcmp(option, "noaudit")) { 150 | params->pam.flags |= F_NO_AUDIT; 151 | } else if ((p = skip_prefix(option, "config="))) { 152 | if ((rc = passwdqc_params_load(params, reason, p))) 153 | goto parse_error; 154 | } else { 155 | err = "Invalid parameter"; 156 | goto parse_error; 157 | } 158 | 159 | return 0; 160 | 161 | parse_error: 162 | passwdqc_params_free(params); 163 | e = concat("Error parsing parameter \"", option, "\": ", 164 | (rc ? (*reason ? *reason : err_oom) : err), NULL); 165 | free(*reason); 166 | *reason = e; 167 | return rc ? rc : -1; 168 | } 169 | 170 | int 171 | passwdqc_params_parse(passwdqc_params_t *params, char **reason, 172 | int argc, const char *const *argv) 173 | { 174 | int i; 175 | 176 | *reason = NULL; 177 | for (i = 0; i < argc; ++i) { 178 | int rc; 179 | 180 | if ((rc = parse_option(params, reason, argv[i]))) 181 | return rc; 182 | } 183 | return 0; 184 | } 185 | 186 | static const passwdqc_params_t defaults = { 187 | { 188 | {INT_MAX, 24, 11, 8, 7}, /* min */ 189 | 72, /* max */ 190 | 3, /* passphrase_words */ 191 | 4, /* match_length */ 192 | 1, /* similar_deny */ 193 | 47, /* random_bits */ 194 | NULL, /* wordlist */ 195 | NULL, /* denylist */ 196 | NULL /* filter */ 197 | }, 198 | { 199 | F_ENFORCE_EVERYONE, /* flags */ 200 | 3 /* retry */ 201 | } 202 | }; 203 | 204 | void passwdqc_params_reset(passwdqc_params_t *params) 205 | { 206 | *params = defaults; 207 | } 208 | 209 | void passwdqc_params_free(passwdqc_params_t *params) 210 | { 211 | free(params->qc.wordlist); 212 | free(params->qc.denylist); 213 | free(params->qc.filter); 214 | passwdqc_params_reset(params); 215 | } 216 | -------------------------------------------------------------------------------- /passwdqc_random.3: -------------------------------------------------------------------------------- 1 | .so man3/libpasswdqc.3 2 | -------------------------------------------------------------------------------- /passwdqc_random.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2000-2002,2005,2008,2010,2013,2016,2021 by Solar Designer 3 | * See LICENSE 4 | */ 5 | 6 | #ifdef _MSC_VER 7 | #define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ 8 | #include 9 | #include 10 | #else 11 | #include 12 | #include 13 | #include 14 | #include 15 | #endif 16 | 17 | #include 18 | 19 | #include "passwdqc.h" 20 | #include "wordset_4k.h" 21 | 22 | /* 23 | * We separate words in the generated "passphrases" with random special 24 | * characters out of a set of 16 (so we encode 4 bits per separator 25 | * character). To enable the use of our "passphrases" within FTP URLs 26 | * (and similar), we pick characters that are defined by RFC 3986 as 27 | * being safe within "userinfo" part of URLs without encoding and 28 | * without having a special meaning. Out of those, we avoid characters 29 | * that are visually ambiguous or difficult over the phone. This 30 | * happens to leave us with exactly 8 symbols, and we add 8 digits that 31 | * are not visually ambiguous. Unfortunately, the exclamation mark 32 | * might be confused for the digit 1 (which we don't use), though. 33 | */ 34 | #define SEPARATORS "-_!$&*+=23456789" 35 | 36 | /* 37 | * Number of bits encoded per separator character. 38 | */ 39 | #define SEPARATOR_BITS 4 40 | 41 | /* 42 | * Number of bits encoded per word. We use 4096 words, which gives 12 bits, 43 | * and we toggle the case of the first character, which gives one bit more. 44 | */ 45 | #define WORD_BITS 13 46 | 47 | /* 48 | * Number of bits encoded per separator and word. 49 | */ 50 | #define SWORD_BITS \ 51 | (SEPARATOR_BITS + WORD_BITS) 52 | 53 | /* 54 | * Maximum number of words to use. 55 | */ 56 | #define WORDS_MAX 8 57 | 58 | /* 59 | * Minimum and maximum number of bits to encode. With the settings above, 60 | * these are 24 and 136, respectively. 61 | */ 62 | #define BITS_MIN \ 63 | (2 * (WORD_BITS - 1)) 64 | #define BITS_MAX \ 65 | (WORDS_MAX * SWORD_BITS) 66 | 67 | #ifndef _MSC_VER 68 | static ssize_t read_loop(int fd, void *buffer, size_t count) 69 | { 70 | ssize_t offset, block; 71 | 72 | offset = 0; 73 | while (count > 0 && count <= SSIZE_MAX) { 74 | block = read(fd, (char *)buffer + offset, count); 75 | 76 | if (block < 0) { 77 | if (errno == EINTR) 78 | continue; 79 | return block; 80 | } 81 | 82 | if (!block) 83 | return offset; 84 | 85 | offset += block; 86 | count -= block; 87 | } 88 | 89 | return offset; 90 | } 91 | #endif 92 | 93 | char *passwdqc_random(const passwdqc_params_qc_t *params) 94 | { 95 | int bits = params->random_bits; /* further code assumes signed type */ 96 | if (bits < BITS_MIN || bits > BITS_MAX) 97 | return NULL; 98 | 99 | /* 100 | * Calculate the number of words to use. The first word is always present 101 | * (hence the "1 +" and the "- WORD_BITS"). Each one of the following words, 102 | * if any, is prefixed by a separator character, so we use SWORD_BITS when 103 | * calculating how many additional words to use. We divide "bits - WORD_BITS" 104 | * by SWORD_BITS with rounding up (hence the addition of "SWORD_BITS - 1"). 105 | */ 106 | int word_count = 1 + (bits + (SWORD_BITS - 1 - WORD_BITS)) / SWORD_BITS; 107 | 108 | /* 109 | * Special case: would we still encode enough bits if we omit the final word, 110 | * but keep the would-be-trailing separator? 111 | */ 112 | int trailing_separator = (SWORD_BITS * (word_count - 1) >= bits); 113 | word_count -= trailing_separator; 114 | 115 | /* 116 | * To determine whether we need to use different separator characters or maybe 117 | * not, calculate the number of words we'd need to use if we don't use 118 | * different separators. We calculate it by dividing "bits" by WORD_BITS with 119 | * rounding up (hence the addition of "WORD_BITS - 1"). The resulting number 120 | * is either the same as or greater than word_count. Use different separators 121 | * only if their use, in the word_count calculation above, has helped reduce 122 | * word_count. 123 | */ 124 | int use_separators = ((bits + (WORD_BITS - 1)) / WORD_BITS != word_count); 125 | trailing_separator &= use_separators; 126 | 127 | /* 128 | * Toggle case of the first character of each word only if we wouldn't achieve 129 | * sufficient entropy otherwise. 130 | */ 131 | int toggle_case = (bits > 132 | ((WORD_BITS - 1) * word_count) + 133 | (use_separators ? (SEPARATOR_BITS * (word_count - !trailing_separator)) : 0)); 134 | 135 | char output[0x100]; 136 | 137 | /* 138 | * Calculate and check the maximum possible length of a "passphrase" we may 139 | * generate for a given word_count. We add 1 to WORDSET_4K_LENGTH_MAX to 140 | * account for separators (whether different or not). When there's no 141 | * trailing separator, we subtract 1. The check against sizeof(output) uses 142 | * ">=" to account for NUL termination. 143 | */ 144 | unsigned int max_length = word_count * (WORDSET_4K_LENGTH_MAX + 1) - !trailing_separator; 145 | if (max_length >= sizeof(output) || (int)max_length > params->max) 146 | return NULL; 147 | 148 | unsigned char rnd[WORDS_MAX * 3]; 149 | 150 | #ifdef _MSC_VER 151 | HCRYPTPROV hprov; 152 | if (!CryptAcquireContextA(&hprov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) 153 | return NULL; 154 | memset(rnd, 0, sizeof(rnd)); /* old Windows would use previous content as extra seed */ 155 | if (!CryptGenRandom(hprov, sizeof(rnd), rnd)) { 156 | CryptReleaseContext(hprov, 0); 157 | return NULL; 158 | } 159 | CryptReleaseContext(hprov, 0); 160 | #else 161 | int fd; 162 | if ((fd = open("/dev/urandom", O_RDONLY)) < 0) 163 | return NULL; 164 | if (read_loop(fd, rnd, sizeof(rnd)) != sizeof(rnd)) { 165 | close(fd); 166 | return NULL; 167 | } 168 | close(fd); 169 | #endif 170 | 171 | size_t length = 0; 172 | const unsigned char *rndptr; 173 | 174 | for (rndptr = rnd; rndptr <= rnd + sizeof(rnd) - 3; rndptr += 3) { 175 | /* 176 | * Append a word. 177 | * 178 | * Treating *rndptr as little-endian, we use bits 0 to 11 for the word index 179 | * and bit 13 for toggling the case of the first character. Bits 12, 14, and 180 | * 15 are left unused. Bits 16 to 23 are left for the separator. 181 | */ 182 | unsigned int i = (((unsigned int)rndptr[1] & 0x0f) << 8) | rndptr[0]; 183 | const char *start = _passwdqc_wordset_4k[i]; 184 | const char *end = memchr(start, '\0', WORDSET_4K_LENGTH_MAX); 185 | if (!end) 186 | end = start + WORDSET_4K_LENGTH_MAX; 187 | size_t extra = end - start; 188 | /* The ">=" leaves room for either one more separator or NUL */ 189 | if (length + extra >= sizeof(output)) 190 | break; 191 | memcpy(&output[length], start, extra); 192 | if (toggle_case) { 193 | /* Toggle case if bit set (we assume ASCII) */ 194 | output[length] ^= rndptr[1] & 0x20; 195 | bits--; 196 | } 197 | length += extra; 198 | bits -= WORD_BITS - 1; 199 | 200 | if (bits <= 0) 201 | break; 202 | 203 | /* 204 | * Append a separator character. We use bits 16 to 19. Bits 20 to 23 are left 205 | * unused. 206 | * 207 | * Special case: we may happen to leave a trailing separator if it provides 208 | * enough bits on its own. With WORD_BITS 13 and SEPARATOR_BITS 4, this 209 | * happens e.g. for bits values from 31 to 34, 48 to 51, 65 to 68. 210 | */ 211 | if (use_separators) { 212 | i = rndptr[2] & 0x0f; 213 | output[length++] = SEPARATORS[i]; 214 | bits -= SEPARATOR_BITS; 215 | } else { 216 | output[length++] = SEPARATORS[0]; 217 | } 218 | 219 | if (bits <= 0) 220 | break; 221 | } 222 | 223 | char *retval = NULL; 224 | 225 | if (bits <= 0 && length < sizeof(output)) { 226 | output[length] = '\0'; 227 | retval = strdup(output); 228 | } 229 | 230 | _passwdqc_memzero(rnd, sizeof(rnd)); 231 | _passwdqc_memzero(output, length); 232 | 233 | return retval; 234 | } 235 | -------------------------------------------------------------------------------- /po/ru.po: -------------------------------------------------------------------------------- 1 | # A passphrase strength checking and policy enforcement toolset. 2 | # Copyright (c) 2000-2003,2005,2008,2010,2013,2016 by Solar Designer 3 | # Copyright (c) 2008,2009 by Dmitry V. Levin 4 | # This file is distributed under the same license as the passwdqc package. 5 | # 6 | # Oleg Solovyov , 2017. 7 | # Andrey Cherepanov , 2017. 8 | # Dmitry V. Levin , 2021. 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: passwdqc 2.0.3\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2021-03-11 20:00+0000\n" 14 | "PO-Revision-Date: 2021-03-11 20:00+0000\n" 15 | "Last-Translator: Dmitry V. Levin \n" 16 | "Language-Team: Russian\n" 17 | "Language: ru\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=2; plural=n%10==1 && n%100!=11 ? 0 : 1;\n" 22 | 23 | #: pam_passwdqc.c:68 24 | msgid "Enter current password: " 25 | msgstr "Введите старый пароль: " 26 | 27 | #: pam_passwdqc.c:70 28 | msgid "Enter new password: " 29 | msgstr "Введите новый пароль: " 30 | 31 | #: pam_passwdqc.c:72 32 | msgid "Re-type new password: " 33 | msgstr "Повторите новый пароль: " 34 | 35 | #: pam_passwdqc.c:75 36 | msgid "System configuration error. Please contact your administrator." 37 | msgstr "Ошибка настройки системы. Свяжитесь с вашим администратором." 38 | 39 | #: pam_passwdqc.c:79 40 | msgid "" 41 | "\n" 42 | "You can now choose the new password.\n" 43 | msgstr "" 44 | "\n" 45 | "Вы можете выбрать новый пароль.\n" 46 | 47 | #: pam_passwdqc.c:81 48 | msgid "" 49 | "\n" 50 | "You can now choose the new password or passphrase.\n" 51 | msgstr "" 52 | "\n" 53 | "Вы можете выбрать новый пароль или парольную фразу.\n" 54 | 55 | #: pam_passwdqc.c:85 56 | #, c-format 57 | msgid "" 58 | "A good password should be a mix of upper and lower case letters, digits, " 59 | "and\n" 60 | "other characters. You can use a password containing at least %d character.\n" 61 | msgid_plural "" 62 | "A good password should be a mix of upper and lower case letters, digits, " 63 | "and\n" 64 | "other characters. You can use a password containing at least %d " 65 | "characters.\n" 66 | msgstr[0] "" 67 | "В хорошем пароле приветствуется наличие заглавных и строчных букв, цифр\n" 68 | "и прочих символов. Пароль должен содержать не менее %d символа.\n" 69 | msgstr[1] "" 70 | "В хорошем пароле приветствуется наличие заглавных и строчных букв, цифр\n" 71 | "и прочих символов. Пароль должен содержать не менее %d символов.\n" 72 | 73 | #: pam_passwdqc.c:94 74 | #, c-format 75 | msgid "" 76 | "A valid password should be a mix of upper and lower case letters, digits, " 77 | "and\n" 78 | "other characters. You can use a password containing at least %d character\n" 79 | "from at least %d of these 4 classes.\n" 80 | "An upper case letter that begins the password and a digit that ends it do " 81 | "not\n" 82 | "count towards the number of character classes used.\n" 83 | msgid_plural "" 84 | "A valid password should be a mix of upper and lower case letters, digits, " 85 | "and\n" 86 | "other characters. You can use a password containing at least %d characters\n" 87 | "from at least %d of these 4 classes.\n" 88 | "An upper case letter that begins the password and a digit that ends it do " 89 | "not\n" 90 | "count towards the number of character classes used.\n" 91 | msgstr[0] "" 92 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 93 | "и может содержать от %d символа, принадлежащего минимум %d из этих 4 " 94 | "классов.\n" 95 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 96 | "конце.\n" 97 | msgstr[1] "" 98 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 99 | "и может содержать от %d символов, принадлежащих минимум %d из этих 4 " 100 | "классов.\n" 101 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 102 | "конце.\n" 103 | 104 | #: pam_passwdqc.c:109 105 | #, c-format 106 | msgid "" 107 | "A valid password should be a mix of upper and lower case letters, digits, " 108 | "and\n" 109 | "other characters. You can use a password containing at least %d character\n" 110 | "from all of these classes.\n" 111 | "An upper case letter that begins the password and a digit that ends it do " 112 | "not\n" 113 | "count towards the number of character classes used.\n" 114 | msgid_plural "" 115 | "A valid password should be a mix of upper and lower case letters, digits, " 116 | "and\n" 117 | "other characters. You can use a password containing at least %d characters\n" 118 | "from all of these classes.\n" 119 | "An upper case letter that begins the password and a digit that ends it do " 120 | "not\n" 121 | "count towards the number of character classes used.\n" 122 | msgstr[0] "" 123 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 124 | "и может содержать от %d символа, принадлежащего всем этим классам.\n" 125 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 126 | "конце.\n" 127 | msgstr[1] "" 128 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 129 | "и может содержать от %d символов, принадлежащих всем этим классам.\n" 130 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 131 | "конце.\n" 132 | 133 | #: pam_passwdqc.c:124 134 | #, c-format 135 | msgid "" 136 | "A valid password should be a mix of upper and lower case letters, digits, " 137 | "and\n" 138 | "other characters. You can use a password containing at least %d character\n" 139 | "from all of these classes, or a password containing at least %d characters\n" 140 | "from just 3 of these 4 classes.\n" 141 | "An upper case letter that begins the password and a digit that ends it do " 142 | "not\n" 143 | "count towards the number of character classes used.\n" 144 | msgid_plural "" 145 | "A valid password should be a mix of upper and lower case letters, digits, " 146 | "and\n" 147 | "other characters. You can use a password containing at least %d characters\n" 148 | "from all of these classes, or a password containing at least %d characters\n" 149 | "from just 3 of these 4 classes.\n" 150 | "An upper case letter that begins the password and a digit that ends it do " 151 | "not\n" 152 | "count towards the number of character classes used.\n" 153 | msgstr[0] "" 154 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 155 | "и может содержать либо не менее %d символа, принадлежащего всем этим " 156 | "классам,\n" 157 | "либо ещё больше символов (не менее %d), принадлежащих лишь 3 классам из 4.\n" 158 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 159 | "конце.\n" 160 | msgstr[1] "" 161 | "Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" 162 | "и может содержать либо не менее %d символов, принадлежащих всем этим " 163 | "классам,\n" 164 | "либо ещё больше символов (не менее %d), принадлежащих лишь 3 классам из 4.\n" 165 | "При подсчете классов не учитываются заглавная буква в начале и цифра в " 166 | "конце.\n" 167 | 168 | #: pam_passwdqc.c:141 169 | #, c-format 170 | msgid "" 171 | "A passphrase should be of at least %d word, %d to %d characters long, and\n" 172 | "contain enough different characters.\n" 173 | msgid_plural "" 174 | "A passphrase should be of at least %d words, %d to %d characters long, and\n" 175 | "contain enough different characters.\n" 176 | msgstr[0] "" 177 | "Парольная фраза должна состоять как минимум из %d слова, и содержать\n" 178 | "символы в количестве от %d до %d, среди которых достаточно различных.\n" 179 | msgstr[1] "" 180 | "Парольная фраза должна состоять как минимум из %d слов, и содержать\n" 181 | "символы в количестве от %d до %d, среди которых достаточно различных.\n" 182 | 183 | #: pam_passwdqc.c:149 184 | #, c-format 185 | msgid "" 186 | "Alternatively, if no one else can see your terminal now, you can pick this " 187 | "as\n" 188 | "your password: \"%s\".\n" 189 | msgstr "" 190 | "В качестве альтернативы, если ваш терминал никто сейчас не видит, можно\n" 191 | "набрать предлагаемый пароль: \"%s\".\n" 192 | 193 | #: pam_passwdqc.c:152 194 | #, c-format 195 | msgid "" 196 | "This system is configured to permit randomly generated passwords only.\n" 197 | "If no one else can see your terminal now, you can pick this as your\n" 198 | "password: \"%s\". Otherwise come back later.\n" 199 | msgstr "" 200 | "Система настроена на использование только таких паролей, которые были\n" 201 | "созданы с помощью генератора случайных чисел. Если ваш терминал никто\n" 202 | "сейчас не видит, можно набрать предлагаемый пароль: \"%s\".\n" 203 | "В противном случае попробуйте повторить попытку позднее.\n" 204 | 205 | #: pam_passwdqc.c:156 206 | msgid "" 207 | "This system is configured to use randomly generated passwords only,\n" 208 | "but the attempt to generate a password has failed. This could happen\n" 209 | "for a number of reasons: you could have requested an impossible password\n" 210 | "length, or the access to kernel random number pool could have failed." 211 | msgstr "" 212 | "Система настроена на использование только таких паролей, которые были\n" 213 | "созданы с помощью генератора случайных чисел, однако создать пароль\n" 214 | "не удалось. Это могло произойти по разным причинам, например,\n" 215 | "вы запросили слишком длинный пароль, либо было отказано в доступе\n" 216 | "к пулу случайных чисел ядра." 217 | 218 | #: pam_passwdqc.c:161 219 | msgid "This password may be too long for some services. Choose another." 220 | msgstr "" 221 | "Этот пароль может оказаться слишком длинным для некоторых служб. Выберите " 222 | "другой." 223 | 224 | #: pam_passwdqc.c:163 225 | msgid "Warning: your longer password will be truncated to 8 characters." 226 | msgstr "Внимание: ваш пароль будет усечён до 8 символов." 227 | 228 | #: pam_passwdqc.c:165 229 | #, c-format 230 | msgid "Weak password: %s." 231 | msgstr "Слабый пароль: %s." 232 | 233 | #: pam_passwdqc.c:167 234 | msgid "Sorry, you've mistyped the password that was generated for you." 235 | msgstr "Извините, вы ошиблись при вводе созданного для вас пароля." 236 | 237 | #: pam_passwdqc.c:169 238 | msgid "Sorry, passwords do not match." 239 | msgstr "Пароли не совпадают." 240 | 241 | #: pam_passwdqc.c:171 242 | msgid "Try again." 243 | msgstr "Попробуйте ещё раз." 244 | 245 | #: passwdqc_check.c:21 246 | msgid "check failed" 247 | msgstr "проверка не удалась" 248 | 249 | #: passwdqc_check.c:24 250 | msgid "is the same as the old one" 251 | msgstr "совпадает со старым" 252 | 253 | #: passwdqc_check.c:26 254 | msgid "is based on the old one" 255 | msgstr "основан на старом" 256 | 257 | #: passwdqc_check.c:29 258 | msgid "too short" 259 | msgstr "слишком короткий" 260 | 261 | #: passwdqc_check.c:31 262 | msgid "too long" 263 | msgstr "слишком длинный" 264 | 265 | #: passwdqc_check.c:34 266 | msgid "not enough different characters or classes for this length" 267 | msgstr "недостаточно символов или классов для заданной длины" 268 | 269 | #: passwdqc_check.c:36 270 | msgid "not enough different characters or classes" 271 | msgstr "недостаточно символов или классов" 272 | 273 | #: passwdqc_check.c:39 274 | msgid "based on personal login information" 275 | msgstr "основан на персональных данных" 276 | 277 | #: passwdqc_check.c:42 278 | msgid "based on a dictionary word and not a passphrase" 279 | msgstr "основан на слове из словаря и не является парольной фразой" 280 | 281 | #: passwdqc_check.c:45 282 | msgid "based on a common sequence of characters and not a passphrase" 283 | msgstr "" 284 | "основан на простой последовательности символов и не является парольной фразой" 285 | 286 | #: passwdqc_check.c:48 287 | msgid "based on a word list entry" 288 | msgstr "основан на слове из списка" 289 | 290 | #: passwdqc_check.c:51 291 | msgid "is in deny list" 292 | msgstr "в списке запрещённых" 293 | 294 | #: passwdqc_check.c:54 295 | msgid "appears to be in a database" 296 | msgstr "обнаружен в базе данных" 297 | -------------------------------------------------------------------------------- /pwqcheck.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2009 Dmitry V. Levin 2 | .\" All rights reserved. 3 | .\" Copyright (c) 2000-2003,2005,2008,2010,2019,2020 Solar Designer 4 | .\" All rights reserved. 5 | .\" 6 | .\" Redistribution and use in source and binary forms, with or without 7 | .\" modification, are permitted. 8 | .\" 9 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 10 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 11 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 12 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 13 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 14 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 15 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 16 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 17 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 18 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 19 | .\" SUCH DAMAGE. 20 | .\" 21 | .Dd December 30, 2020 22 | .Dt PWQCHECK 1 23 | .Os "Openwall Project" 24 | .Sh NAME 25 | .Nm pwqcheck 26 | .Nd Check passphrase quality 27 | .Sh SYNOPSIS 28 | .Nm Op Ar options 29 | .Sh DESCRIPTION 30 | The 31 | .Nm 32 | program checks passphrase quality using the libpasswdqc library. 33 | By default, it expects to read 3 lines from standard input: 34 | .Pp 35 | .Bl -item -compact -offset indent 36 | .It 37 | first line is a new password, 38 | .It 39 | second line is an old password, and 40 | .It 41 | third line is either an existing account name or a 42 | .Xr passwd 5 43 | entry. 44 | .El 45 | .Pp 46 | There are a number of supported options, which can be used to control the 47 | .Nm 48 | behavior. 49 | .Pp 50 | .Nm 51 | prints 52 | .Ar OK 53 | on success. Scripts invoking 54 | .Nm 55 | are suggested to check for both a zero exit status and the 56 | .Ar OK 57 | line. 58 | .Sh OPTIONS 59 | .Bl -tag -width Ds 60 | .Sm off 61 | .It Xo 62 | .Cm min No = 63 | .Ar N0 , N1 , N2 , N3 , N4 64 | .Xc 65 | .Sm on 66 | .Pq default: min=disabled,24,11,8,7 67 | The minimum allowed password lengths for different kinds of 68 | passwords/passphrases. 69 | The keyword 70 | .Cm disabled 71 | can be used to 72 | disallow passwords of a given kind regardless of their length. 73 | Each subsequent number is required to be no larger than the preceding 74 | one. 75 | .Pp 76 | .Ar N0 77 | is used for passwords consisting of characters from one character 78 | class only. 79 | The character classes are: digits, lower-case letters, upper-case 80 | letters, and other characters. 81 | There is also a special class for 82 | .No non- Ns Tn ASCII 83 | characters, which could not be classified, but are assumed to be non-digits. 84 | .Pp 85 | .Ar N1 86 | is used for passwords consisting of characters from two character 87 | classes that do not meet the requirements for a passphrase. 88 | .Pp 89 | .Ar N2 90 | is used for passphrases. 91 | Note that besides meeting this length requirement, 92 | a passphrase must also consist of a sufficient number of words (see the 93 | .Cm passphrase 94 | option below). 95 | .Pp 96 | .Ar N3 97 | and 98 | .Ar N4 99 | are used for passwords consisting of characters from three 100 | and four character classes, respectively. 101 | .Pp 102 | When calculating the number of character classes, upper-case letters 103 | used as the first character and digits used as the last character of a 104 | password are not counted. 105 | .Pp 106 | In addition to being sufficiently long, passwords are required to 107 | contain enough different characters for the character classes and 108 | the minimum length they have been checked against. 109 | .Pp 110 | .It Cm max Ns = Ns Ar N 111 | .Pq default: Cm max Ns = Ns 72 112 | The maximum allowed password length. 113 | This can be used to prevent users from setting passwords that may be 114 | too long for some system services. 115 | The value 8 is treated specially: if 116 | .Cm max 117 | is set to 8, passwords longer than 8 characters will not be rejected, 118 | but will be truncated to 8 characters for the strength checks and the 119 | user will be warned. 120 | This is to be used with the traditional DES-based password hashes, 121 | which truncate the password at 8 characters. 122 | .Pp 123 | It is important that you do set 124 | .Cm max Ns = Ns 8 125 | if you are using the traditional 126 | hashes, or some weak passwords will pass the checks. 127 | .It Cm passphrase Ns = Ns Ar N 128 | .Pq default: Cm passphrase Ns = Ns 3 129 | The number of words required for a passphrase. 130 | .It Cm match Ns = Ns Ar N 131 | .Pq default: Cm match Ns = Ns 4 132 | The length of common substring required to conclude that a password is 133 | at least partially based on information found in a character string, 134 | or 0 to disable the substring search. 135 | Note that the password will not be rejected once a weak substring is 136 | found; it will instead be subjected to the usual strength requirements 137 | with the weak substring partially discounted. 138 | .Pp 139 | The substring search is case-insensitive and is able to detect and 140 | remove a common substring spelled backwards. 141 | .It Xo 142 | .Sm off 143 | .Cm similar No = Cm permit | deny 144 | .Sm on 145 | .Xc 146 | .Pq default: Cm similar Ns = Ns Cm deny 147 | Whether a new password is allowed to be similar to the old one. 148 | The passwords are considered to be similar when there is a sufficiently 149 | long common substring and the new password with the substring partially 150 | discounted would be weak. 151 | .It Cm wordlist Ns = Ns Ar FILE 152 | Deny passwords that are based on lines of the tiny external text 153 | .Ar FILE , 154 | which can reasonably be e.g. a list of a few thousand common passwords. 155 | Common dictionary words may also reasonably be included, especially in a 156 | local language other than English, or longer yet common English words. 157 | (passwdqc includes a list of a few thousand common English words of 158 | lengths from 3 to 6 built in. Any word list possibly specified with 159 | this option is used in addition to the built-in word list.) 160 | .Pp 161 | Substring matching and discounting will be used if the 162 | .Cm match 163 | setting 164 | above is non-zero. Please note that this is very inefficient, and isn't 165 | to be used with large wordlists. 166 | .It Cm denylist Ns = Ns Ar FILE 167 | Deny passwords or passphrases directly appearing in the tiny external text 168 | .Ar FILE . 169 | That file can reasonably be e.g. a list of common passwords if 170 | only a relaxed policy is desired and stricter checks are thus disabled 171 | (using their separate options). Such policy would only be somewhat 172 | effective against online/remote attacks, but not against offline attacks 173 | on hashed passwords. 174 | .It Cm filter Ns = Ns Ar FILE 175 | Deny passwords or passphrases directly appearing in a maybe huge binary 176 | filter 177 | .Ar FILE 178 | created with pwqfilter. This is very efficient, needing at 179 | most two random disk reads per query. A filter created from millions of 180 | leaked passwords can reasonably be used on top of passwdqc's other 181 | checks to further reduce the number of passing yet weak passwords 182 | without causing unreasonable inconvenience (as e.g. higher minimum 183 | lengths and character set requirements could). 184 | .It Cm config Ns = Ns Ar FILE 185 | Load config 186 | .Ar FILE 187 | in the 188 | .Cm passwdqc.conf 189 | format. This file may define any options described in 190 | .Xr passwdqc.conf 5 , but only the 191 | .Cm min , 192 | .Cm max , 193 | .Cm passphrase , 194 | .Cm match Ns , 195 | and 196 | .Cm config 197 | options are honored by 198 | .Nm . 199 | .It Cm -1 200 | Read just 1 line (new passphrase). 201 | This is needed to use 202 | .Nm 203 | as the passwordcheck program on OpenBSD - e.g., with 204 | ":passwordcheck=/usr/bin/pwqcheck \-1:\\" 205 | (without the quotes, but with the trailing backslash) 206 | in the "default" section in 207 | .Cm /etc/login.conf . 208 | .It Cm -2 209 | Read just 2 lines (new and old passphrases). 210 | .It Cm --multi 211 | Check multiple passphrases (until EOF). 212 | This option may be used on its own or along with the 213 | .Cm -1 214 | or 215 | .Cm -2 216 | options. 217 | .Nm 218 | will read 1, 2, or 3 lines and will output one line per passphrase to check. 219 | The lines will start with either 220 | .Ar OK 221 | or a message explaining why the passphrase did not pass the checks, 222 | followed by a colon and a space, and finally followed by the passphrase. 223 | The explanatory message is guaranteed to not include a colon. 224 | With this option, the exit status of 225 | .Nm 226 | depends solely on whether there were any errors preventing the strength of 227 | passphrases from being fully checked or not. 228 | A primary use for this option is to test different policies and/or different 229 | versions of passwdqc on large passphrase lists. 230 | .It Cm --version 231 | Output 232 | .Nm 233 | program version and exit. 234 | .It Cm -h , --help 235 | Output 236 | .Nm 237 | help text and exit. 238 | .El 239 | .Sh EXIT STATUS 240 | .Nm 241 | exits with non-zero status when it encounters invalid config file, 242 | invalid option, invalid parameter value, invalid data in standard input, 243 | and in any case when it fails to check passphrase strength. 244 | Without the 245 | .Cm --multi 246 | option, 247 | .Nm 248 | also exits with non-zero status when it detects a weak passphrase. 249 | .Sh FILES 250 | .Pa /etc/passwdqc.conf 251 | (not read unless this suggested file location is specified with the 252 | .Cm config=/etc/passwdqc.conf 253 | option). 254 | .Sh SEE ALSO 255 | .Xr pwqgen 1 , 256 | .Xr libpasswdqc 3 , 257 | .Xr passwd 5 , 258 | .Xr passwdqc.conf 5 , 259 | .Xr pam_passwdqc 8 . 260 | .Pp 261 | https://www.openwall.com/passwdqc/ 262 | .Sh AUTHORS 263 | The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. 264 | The 265 | .Nm 266 | program was originally written for ALT GNU/*/Linux by Dmitry V. Levin, 267 | indirectly reusing code from pam_passwdqc (via libpasswdqc). 268 | This manual page (derived from the pam_passwdqc documentation) 269 | was written for Openwall GNU/*/Linux by Dmitry V. Levin. 270 | -------------------------------------------------------------------------------- /pwqcheck.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008,2009 by Dmitry V. Levin 3 | * Copyright (c) 2010,2016,2021 by Solar Designer 4 | * See LICENSE 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "passwdqc.h" 12 | 13 | static void clean(char *dst, size_t size) 14 | { 15 | if (!dst) 16 | return; 17 | _passwdqc_memzero(dst, size); 18 | free(dst); 19 | } 20 | 21 | static char *read_line(size_t size, int eof_ok) 22 | { 23 | char *p, *buf = malloc(size + 1); 24 | 25 | if (!buf) { 26 | fprintf(stderr, "pwqcheck: Memory allocation failed.\n"); 27 | return NULL; 28 | } 29 | 30 | if (!fgets(buf, size + 1, stdin)) { 31 | clean(buf, size + 1); 32 | if (!eof_ok || !feof(stdin) || ferror(stdin)) 33 | fprintf(stderr, 34 | "pwqcheck: Error reading standard input.\n"); 35 | return NULL; 36 | } 37 | 38 | if (strlen(buf) >= size) { 39 | clean(buf, size + 1); 40 | fprintf(stderr, "pwqcheck: Line too long.\n"); 41 | return NULL; 42 | } 43 | 44 | if ((p = strpbrk(buf, "\r\n"))) 45 | *p = '\0'; 46 | 47 | return buf; 48 | } 49 | 50 | static char *extract_string(char **stringp) 51 | { 52 | char *token = *stringp, *colon; 53 | 54 | if (!token) 55 | return ""; 56 | 57 | colon = strchr(token, ':'); 58 | if (colon) { 59 | *colon = '\0'; 60 | *stringp = colon + 1; 61 | } else 62 | *stringp = NULL; 63 | 64 | return token; 65 | } 66 | 67 | static struct passwd *parse_pwline(char *line, struct passwd *pw) 68 | { 69 | if (!strchr(line, ':')) { 70 | #ifdef _MSC_VER 71 | memset(pw, 0, sizeof(*pw)); 72 | pw->pw_name = line; 73 | #else 74 | struct passwd *p = getpwnam(line); 75 | endpwent(); 76 | if (!p) { 77 | fprintf(stderr, "pwqcheck: User not found.\n"); 78 | return NULL; 79 | } 80 | if (p->pw_passwd) 81 | _passwdqc_memzero(p->pw_passwd, strlen(p->pw_passwd)); 82 | memcpy(pw, p, sizeof(*pw)); 83 | #endif 84 | } else { 85 | memset(pw, 0, sizeof(*pw)); 86 | pw->pw_name = extract_string(&line); 87 | pw->pw_passwd = extract_string(&line); 88 | extract_string(&line); /* uid */ 89 | extract_string(&line); /* gid */ 90 | pw->pw_gecos = extract_string(&line); 91 | pw->pw_dir = extract_string(&line); 92 | pw->pw_shell = line ? line : ""; 93 | if (!*pw->pw_name || !*pw->pw_dir) { 94 | fprintf(stderr, "pwqcheck: Invalid passwd entry.\n"); 95 | return NULL; 96 | } 97 | } 98 | return pw; 99 | } 100 | 101 | static void 102 | print_help(void) 103 | { 104 | puts("Check passphrase quality.\n" 105 | "\nFor each passphrase to check, pwqcheck reads up to 3 lines from standard input:\n" 106 | " first line is a new passphrase,\n" 107 | " second line is an old passphrase, and\n" 108 | " third line is either an existing account name or a passwd entry.\n" 109 | "\nUsage: pwqcheck [options]\n" 110 | "\nValid options are:\n" 111 | " min=N0,N1,N2,N3,N4\n" 112 | " set minimum allowed lengths for different kinds of passphrases;\n" 113 | " max=N\n" 114 | " set maximum allowed passphrase length;\n" 115 | " passphrase=N\n" 116 | " set number of words required for a passphrase;\n" 117 | " match=N\n" 118 | " set length of common substring in substring check;\n" 119 | " similar=permit|deny\n" 120 | " whether a new passphrase is allowed to be similar to the old one;\n" 121 | " wordlist=FILE\n" 122 | " deny passwords that are based on lines of a tiny external text file;\n" 123 | " denylist=FILE\n" 124 | " deny passphrases directly appearing in a tiny external text file;\n" 125 | " filter=FILE\n" 126 | " deny passphrases directly appearing in a maybe huge binary filter file;\n" 127 | " config=FILE\n" 128 | " load config FILE in passwdqc.conf format;\n" 129 | " -1\n" 130 | " read just 1 line (new passphrase);\n" 131 | " -2\n" 132 | " read just 2 lines (new and old passphrases);\n" 133 | " --multi\n" 134 | " check multiple passphrases (until EOF);\n" 135 | " --version\n" 136 | " print program version and exit;\n" 137 | " -h or --help\n" 138 | " print this help text and exit."); 139 | } 140 | 141 | int main(int argc, const char **argv) 142 | { 143 | passwdqc_params_t params; 144 | const char *check_reason; 145 | char *parse_reason, *newpass, *oldpass, *pwline; 146 | struct passwd pwbuf, *pw; 147 | int lines_to_read = 3, multi = 0; 148 | size_t size = 8192; 149 | int rc = 1; 150 | 151 | while (argc > 1 && argv[1][0] == '-') { 152 | const char *arg = argv[1]; 153 | 154 | if (!strcmp("-h", arg) || !strcmp("--help", arg)) { 155 | print_help(); 156 | return 0; 157 | } 158 | 159 | if (!strcmp("--version", arg)) { 160 | printf("pwqcheck version %s\n", PASSWDQC_VERSION); 161 | return 0; 162 | } 163 | 164 | if ((arg[1] == '1' || arg[1] == '2') && !arg[2]) { 165 | lines_to_read = arg[1] - '0'; 166 | goto next_arg; 167 | } 168 | 169 | if (!strcmp("--multi", arg)) { 170 | multi = 1; 171 | goto next_arg; 172 | } 173 | 174 | break; 175 | 176 | next_arg: 177 | argc--; argv++; 178 | } 179 | 180 | passwdqc_params_reset(¶ms); 181 | if (argc > 1 && 182 | passwdqc_params_parse(¶ms, &parse_reason, argc - 1, 183 | argv + 1)) { 184 | fprintf(stderr, "pwqcheck: %s\n", 185 | (parse_reason ? parse_reason : "Out of memory")); 186 | free(parse_reason); 187 | return rc; 188 | } 189 | 190 | if ((size_t)params.qc.max + 1 > size) 191 | size = (size_t)params.qc.max + 1; 192 | 193 | next_pass: 194 | oldpass = pwline = NULL; pw = NULL; 195 | if (!(newpass = read_line(size, multi))) { 196 | if (multi && feof(stdin) && !ferror(stdin) && 197 | fflush(stdout) >= 0) 198 | rc = 0; 199 | goto done; 200 | } 201 | if (lines_to_read >= 2 && !(oldpass = read_line(size, 0))) 202 | goto done; 203 | if (lines_to_read >= 3 && (!(pwline = read_line(size, 0)) || 204 | !parse_pwline(pwline, pw = &pwbuf))) 205 | goto done; 206 | 207 | check_reason = passwdqc_check(¶ms.qc, newpass, oldpass, pw); 208 | if (!check_reason) { 209 | if (multi) 210 | printf("OK: %s\n", newpass); 211 | else if (puts("OK") >= 0 && fflush(stdout) >= 0) 212 | rc = 0; 213 | goto cleanup; 214 | } 215 | if (multi) 216 | printf("Bad passphrase (%s): %s\n", check_reason, newpass); 217 | else 218 | printf("Bad passphrase (%s)\n", check_reason); 219 | 220 | cleanup: 221 | _passwdqc_memzero(&pwbuf, sizeof(pwbuf)); 222 | clean(pwline, size); 223 | clean(oldpass, size); 224 | clean(newpass, size); 225 | 226 | if (multi) 227 | goto next_pass; 228 | 229 | passwdqc_params_free(¶ms); 230 | 231 | return rc; 232 | 233 | done: 234 | multi = 0; 235 | goto cleanup; 236 | } 237 | -------------------------------------------------------------------------------- /pwqcheck.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'r'), 40 | 1 => array('pipe', 'w')); 41 | // Leave stderr (fd 2) pointing to where it is, likely to error_log 42 | 43 | // Replace characters that would violate the protocol 44 | $newpass = strtr($newpass, "\n", '.'); 45 | $oldpass = strtr($oldpass, "\n", '.'); 46 | $user = strtr($user, "\n:", '..'); 47 | 48 | // Trigger a "too short" rather than "is the same" message in this special case 49 | if (!$newpass && !$oldpass) 50 | $oldpass = '.'; 51 | 52 | if ($args) 53 | $args = ' ' . $args; 54 | if (!$user) 55 | $args = ' -2' . $args; // passwdqc 1.2.0+ 56 | 57 | $command = 'exec '; // No need to keep the shell process around on Unix 58 | $command .= 'pwqcheck' . $args; 59 | if (!($process = @proc_open($command, $descriptorspec, $pipes))) 60 | return $retval; 61 | 62 | $err = 0; 63 | fwrite($pipes[0], "$newpass\n$oldpass\n") || $err = 1; 64 | if ($user) 65 | fwrite($pipes[0], "$user::::$aux:/:\n") || $err = 1; 66 | fclose($pipes[0]) || $err = 1; 67 | ($output = stream_get_contents($pipes[1])) || $err = 1; 68 | fclose($pipes[1]); 69 | 70 | $status = proc_close($process); 71 | 72 | // There must be a linefeed character at the end. Remove it. 73 | if (substr($output, -1) === "\n") 74 | $output = substr($output, 0, -1); 75 | else 76 | $err = 1; 77 | 78 | if ($err === 0 && ($status === 0 || $output !== 'OK')) 79 | $retval = $output; 80 | 81 | return $retval; 82 | } 83 | 84 | ?> 85 | -------------------------------------------------------------------------------- /pwqfilter.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2021 Solar Designer 2 | .\" All rights reserved. 3 | .\" 4 | .\" Redistribution and use in source and binary forms, with or without 5 | .\" modification, are permitted. 6 | .\" 7 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 8 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 9 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 10 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 11 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 12 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 13 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 14 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 15 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 16 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 17 | .\" SUCH DAMAGE. 18 | .\" 19 | .Dd January 25, 2021 20 | .Dt PWQFILTER 1 21 | .Os "Openwall Project" 22 | .Sh NAME 23 | .Nm pwqfilter 24 | .Nd Manage binary passphrase filter files 25 | .Sh SYNOPSIS 26 | .Nm Op Ar options 27 | .Sh DESCRIPTION 28 | The 29 | .Nm 30 | program searches, creates, or updates binary passphrase filter files, which can also be used with 31 | .Xr pwqcheck 1 and 32 | .Xr pam_passwdqc 8 . 33 | Input and/or output binary filter files are specified via their corresponding command-line options, 34 | whereas passphrases to look up or add, or their hashes, are read from standard input. 35 | .Pp 36 | .Nm 37 | works on arbitrary plain text strings or hex-encoded hashes, 38 | and thus can also be reused in lieu of 39 | .Xr grep 1 40 | for many purposes unrelated to passphrases and security. 41 | .Pp 42 | For the binary filters, 43 | .Nm 44 | and thus the rest of passwdqc currently use an improved cuckoo filter, which is a probabilistic data structure. 45 | Occasional false positives are possible (fewer than 1 in a billion), but false negatives are not. 46 | .Sh MODE OPTIONS 47 | .Bl -tag -width Ds 48 | .It Cm --lookup 49 | Look up plaintexts or hashes on standard input against an existing filter. 50 | This is the default mode. 51 | .It Cm --status 52 | Report usage statistics for an existing filter. 53 | .It Cm --create=CAPACITY 54 | Create a new filter with CAPACITY entries, reading the initial set of plaintexts or hashes from standard input. 55 | .Pp 56 | The currently implemented cuckoo filter has a typical maximum load of around 98% 57 | (as long as there are no duplicate inputs and the hashes are unbiased, or less otherwise). 58 | The specified CAPACITY should thus be higher than the maximum expected number of entries by at least 2.04%. 59 | .It Cm --insert 60 | Insert (add) entries into an existing filter, reading the plaintexts or hashes from standard input. 61 | .It Cm --test-fp-rate 62 | Estimate the false positive rate (FP rate) of a filter. 63 | This option can be used on its own or along with another mode, in which case the test is performed after that other mode's action. 64 | .El 65 | .Sh OPTIMIZATION OPTIONS 66 | These can be used with 67 | .Cm --create 68 | or 69 | .Cm --insert . 70 | .Bl -tag -width Ds 71 | .It Cm --optimize-fp-rate 72 | Better than default FP rate at a cost of briefly slower inserts after a load of 30% to 40% and then again after 60% to 70%. 73 | .It Cm --optimize-fp-rate-at-high-load 74 | Better than default FP rate at load ~95% to 98%, a lot worse below ~90%. 75 | .El 76 | .Sh INPUT AND OUTPUT OPTIONS 77 | .Bl -tag -width Ds 78 | .It Cm -f FILE , --filter=FILE 79 | Read an existing filter from FILE 80 | .It Cm -o FILE , --output=FILE 81 | Write a new or modified filter to FILE 82 | .It Cm --pre-hashed 83 | Look up or insert by hex-encoded hashes, not plaintexts. 84 | .Pp 85 | This option is later implied for further actions on filters created with it specified and no 86 | .Cm --hash-* , 87 | because 88 | .Nm 89 | has no way to know what hash type such filters use. 90 | .It Cm --hash-md4 91 | Hash plaintexts with MD4 prior to lookup or insert. 92 | This is the default for new filters. 93 | .Pp 94 | When used with 95 | .Cm --pre-hashed , 96 | specify that the pre-hashing was done with MD4. 97 | .Pp 98 | Cuckoo filters' use of a hash function is non-cryptographic, hence MD4's otherwise inadequate cryptographic security is irrelevant. 99 | .It Cm --hash-ntlm-cp1252 100 | Hash assumed CP1252 encoding plaintexts with NTLM prior to lookup or insert, or specify that the pre-hashing was done that way 101 | (e.g., like it was in a HIBP v7 download). 102 | .El 103 | .Sh LOOKUP OUTPUT MODIFIER OPTIONS 104 | These are similar to those of 105 | .Xr grep 1 . 106 | .Bl -tag -width Ds 107 | .It Cm -c , --count 108 | Output a count of (non-)matching lines instead of the lines themselves. 109 | .It Cm -n , --line-number 110 | Prefix each line with its number in the input stream. 111 | .It Cm -v , --invert-match 112 | Output or count non-matching lines. 113 | .El 114 | .Sh GENERAL OPTIONS 115 | .Bl -tag -width Ds 116 | .It Cm --verbose 117 | Output additional information. 118 | .It Cm --version 119 | Output 120 | .Nm 121 | program version and exit. 122 | .It Cm -h , --help 123 | Output 124 | .Nm 125 | help text and exit. 126 | .El 127 | .Sh EXIT STATUS 128 | When looking up against an existing filter, 129 | .Nm 130 | exits with 0 if selected plaintexts or hashes are found, 1 if not found, or 2 on error. 131 | These exit codes are compatible with those of 132 | .Xr grep 1 . 133 | In other modes, 134 | .Nm 135 | exits with 0 on success and 2 on error. 136 | .Sh SEE ALSO 137 | .Xr grep 1 , 138 | .Xr pwqcheck 1 , 139 | .Xr passwdqc.conf 5 , 140 | .Xr pam_passwdqc 8 . 141 | .Pp 142 | https://www.openwall.com/passwdqc/ 143 | .Sh AUTHORS 144 | .Nm 145 | and this manual page were written by Solar Designer. 146 | -------------------------------------------------------------------------------- /pwqgen.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2009 Dmitry V. Levin 2 | .\" All rights reserved. 3 | .\" Copyright (c) 2019 Solar Designer 4 | .\" All rights reserved. 5 | .\" 6 | .\" Redistribution and use in source and binary forms, with or without 7 | .\" modification, are permitted. 8 | .\" 9 | .\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 10 | .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 11 | .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 12 | .\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 13 | .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 14 | .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 15 | .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 16 | .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 17 | .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 18 | .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 19 | .\" SUCH DAMAGE. 20 | .\" 21 | .Dd March 10, 2021 22 | .Dt PWQGEN 1 23 | .Os "Openwall Project" 24 | .Sh NAME 25 | .Nm pwqgen 26 | .Nd Generate quality controllable random passphrase 27 | .Sh SYNOPSIS 28 | .Nm Op Ar options 29 | .Sh DESCRIPTION 30 | The 31 | .Nm 32 | program generates a random passphrase using the libpasswdqc library. 33 | Strength of the generated passphrase depends on the amount of randomness 34 | read from 35 | .Pa /dev/urandom . 36 | .Sh OPTIONS 37 | .Bl -tag -width indent 38 | .It Cm random Ns = Ns Ar N 39 | .Pq default: Cm random Ns = Ns 47 40 | The size of randomly-generated passphrase in bits (24 to 136). 41 | .It Cm config Ns = Ns Ar FILE 42 | Load config 43 | .Ar FILE 44 | in the 45 | .Cm passwdqc.conf 46 | format. This file may define any options described in 47 | .Xr passwdqc.conf 5 , but only the 48 | .Cm random 49 | and 50 | .Cm config 51 | options are honored by 52 | .Nm . 53 | .It Cm --version 54 | Output 55 | .Nm 56 | program version and exit. 57 | .It Cm -h , --help 58 | Output 59 | .Nm 60 | help text and exit. 61 | .El 62 | .Sh EXIT STATUS 63 | .Nm 64 | exits with non-zero status when it encounters invalid config file, 65 | invalid option, invalid parameter value, when it fails to obtain enough 66 | randomness, and in any case when it fails to generate a passphrase. 67 | .Sh FILES 68 | .Pa /etc/passwdqc.conf 69 | (not read unless this suggested file location is specified with the 70 | .Cm config=/etc/passwdqc.conf 71 | option). 72 | .Sh SEE ALSO 73 | .Xr pwqcheck 1 , 74 | .Xr libpasswdqc 3 , 75 | .Xr urandom 4 , 76 | .Xr passwdqc.conf 5 , 77 | .Xr pam_passwdqc 8 . 78 | .Pp 79 | https://www.openwall.com/passwdqc/ 80 | .Sh AUTHORS 81 | The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. 82 | The 83 | .Nm 84 | program was originally written for ALT GNU/*/Linux by Dmitry V. Levin, 85 | indirectly reusing code from pam_passwdqc (via libpasswdqc). 86 | This manual page was written for Openwall GNU/*/Linux by Dmitry V. Levin. 87 | -------------------------------------------------------------------------------- /pwqgen.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008,2009 by Dmitry V. Levin 3 | * Copyright (c) 2016,2021 by Solar Designer 4 | * See LICENSE 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "passwdqc.h" 12 | 13 | static void 14 | print_help(void) 15 | { 16 | puts("Generate quality controllable passphrase.\n" 17 | "\nUsage: pwqgen [options]\n" 18 | "\nValid options are:\n" 19 | " random=N\n" 20 | " set size of randomly-generated passphrase in bits;\n" 21 | " config=FILE\n" 22 | " load config FILE in passwdqc.conf format;\n" 23 | " --version\n" 24 | " print program version and exit;\n" 25 | " -h or --help\n" 26 | " print this help text and exit."); 27 | } 28 | 29 | int main(int argc, const char **argv) 30 | { 31 | passwdqc_params_t params; 32 | char *reason, *pass; 33 | int retval; 34 | 35 | if (argc > 1 && argv[1][0] == '-') { 36 | if (!strcmp("-h", argv[1]) || !strcmp("--help", argv[1])) { 37 | print_help(); 38 | return 0; 39 | } 40 | 41 | if (!strcmp("--version", argv[1])) { 42 | printf("pwqgen version %s\n", PASSWDQC_VERSION); 43 | return 0; 44 | } 45 | } 46 | 47 | passwdqc_params_reset(¶ms); 48 | if (argc > 1 && 49 | passwdqc_params_parse(¶ms, &reason, argc - 1, argv + 1)) { 50 | fprintf(stderr, "pwqgen: %s\n", 51 | (reason ? reason : "Out of memory")); 52 | free(reason); 53 | return 1; 54 | } 55 | 56 | pass = passwdqc_random(¶ms.qc); 57 | passwdqc_params_free(¶ms); 58 | if (!pass) { 59 | fprintf(stderr, "pwqgen: Failed to generate a passphrase.\n" 60 | "This could happen for a number of reasons: you could have requested\n" 61 | "an impossible passphrase length, or the access to kernel random number\n" 62 | "pool could have failed.\n"); 63 | return 1; 64 | } 65 | 66 | setvbuf(stdout, NULL, _IONBF, 0); 67 | 68 | retval = (puts(pass) >= 0 && fflush(stdout) == 0) ? 0 : 1; 69 | 70 | _passwdqc_memzero(pass, strlen(pass)); 71 | free(pass); 72 | 73 | return retval; 74 | } 75 | -------------------------------------------------------------------------------- /wordset_4k.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by Solar Designer and placed in the 3 | * public domain. 4 | */ 5 | 6 | #ifndef WORDSET_4K_H__ 7 | #define WORDSET_4K_H__ 8 | 9 | #define WORDSET_4K_LENGTH_MAX 6 10 | 11 | extern const char _passwdqc_wordset_4k[][WORDSET_4K_LENGTH_MAX]; 12 | 13 | #endif /* WORDSET_4K_H__ */ 14 | --------------------------------------------------------------------------------