diff --git a/contrib/pam_modules/pam_passwdqc/.gitattributes b/contrib/pam_modules/pam_passwdqc/.gitattributes new file mode 100644 index 000000000000..b9160d9011d1 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/.gitattributes @@ -0,0 +1,2 @@ +.git* export-ignore +/ci export-ignore diff --git a/contrib/pam_modules/pam_passwdqc/.github/workflows/ci.yml b/contrib/pam_modules/pam_passwdqc/.github/workflows/ci.yml new file mode 100644 index 000000000000..352d64db733c --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/.github/workflows/ci.yml @@ -0,0 +1,207 @@ +name: CI + +on: [push, pull_request] + +jobs: + whitespace-errors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: check + run: git diff-index --check --cached 4b825dc642cb6eb9a060e54bf8d69288fbee4904 + + gcc13-x86_64: + runs-on: ubuntu-latest + env: + CC: gcc-13 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + gcc12-x86_64: + runs-on: ubuntu-latest + env: + CC: gcc-12 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + gcc11-x86_64: + runs-on: ubuntu-20.04 + env: + CC: gcc-11 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + gcc10-x86_64: + runs-on: ubuntu-20.04 + env: + CC: gcc-10 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + gcc9-x86_64: + runs-on: ubuntu-20.04 + env: + CC: gcc + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + gcc8-x86_64: + runs-on: ubuntu-20.04 + env: + CC: gcc-8 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang15-x86_64: + runs-on: ubuntu-latest + env: + CC: clang-15 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang14-x86_64: + runs-on: ubuntu-latest + env: + CC: clang-14 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang13-x86_64: + runs-on: ubuntu-latest + env: + CC: clang-13 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang12-x86_64: + runs-on: ubuntu-20.04 + env: + CC: clang-12 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang11-x86_64: + runs-on: ubuntu-20.04 + env: + CC: clang-11 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang10-x86_64: + runs-on: ubuntu-20.04 + env: + CC: clang + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang9-x86_64: + runs-on: ubuntu-20.04 + env: + CC: clang-9 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh + + clang8-x86_64: + runs-on: ubuntu-20.04 + env: + CC: clang-8 + TARGET: x86_64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: install dependencies + run: ci/install-dependencies.sh + - name: build check + run: ci/run-build-and-tests.sh diff --git a/contrib/pam_modules/pam_passwdqc/.gitignore b/contrib/pam_modules/pam_passwdqc/.gitignore new file mode 100644 index 000000000000..0a833d7cebac --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/.gitignore @@ -0,0 +1,7 @@ +*.o +*.so +*.so.* +/passwdqc.pc +/pwqcheck +/pwqfilter +/pwqgen diff --git a/contrib/pam_modules/pam_passwdqc/CHANGES b/contrib/pam_modules/pam_passwdqc/CHANGES new file mode 100644 index 000000000000..75548a2d77bd --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/CHANGES @@ -0,0 +1,122 @@ + Significant changes between 2.0.2 and 2.0.3. + +Added pkg-config file. + +Changed enforce=users to support "chpasswd" PAM service in addition to +traditionally supported "passwd". + + + Significant changes between 2.0.1 and 2.0.2. + +Improved pam_passwdqc's auto-generated policy descriptions further, so +that lines are wrapped at a more consistent length. + +Added the libpasswdqc(3) manual page and links to it for all functions +documented in there. + +Added scripts to support Continuous Integration on GitHub (included in +the git repository, but excluded from release tarballs). + + + Significant changes between 2.0.0 and 2.0.1. + +Improved pam_passwdqc's auto-generated policy descriptions, which were +slightly misformatted since the introduction of i18n support in 1.4.0. +Now they not only look prettier, but also make it clearer that the +mentioned lengths are merely the minimums and not the recommended ones. + +Updated Russian translation for consistency with the above and to cover +messages added in 1.9.0+. + +Increased maximum size of randomly-generated passphrases to 136 bits. +This was already the limit in the underlying API, but the tools' limit +was set to 85. This increase is to allow for a wider variety of use +cases for the tools. + +In the Makefile, use CPPFLAGS and LDFLAGS consistently to be friendlier +to packaging by distros. + +Added this file CHANGES based on two latest release announcements, and +started to maintain it. + + + Significant changes between 1.9.0 and 2.0.0. + +Introduce and use passwdqc_params_free(). This is a minor addition to +the libpasswdqc API related to the addition of external files support. + + + Significant changes between 1.5.0 and 1.9.0. + +Added support for external wordlist, denylist, and binary filter. With +these, passwdqc can be configured to deny passwords and passphrases that +are based on lines of a tiny external text file (the "wordlist" option), +directly appear in a tiny external text file (the "denylist" option), +or/and directly appear in a maybe huge binary filter file (the "filter" +option). While usage of larger external text files is inefficient, the +binary filters are very efficient. + +The binary filters can be created and otherwise managed with the newly +added pwqfilter(1) program. It can create a binary filter from a list +of plaintexts or from MD4 or NTLM hashes. The latter are supported in a +way that enables importing of HIBP (Pwned Passwords) database revisions +into passwdqc binary filters. pwqfilter works on arbitrary plain text +strings or hex-encoded hashes, and it can also be reused in lieu of +grep(1) for many purposes, even unrelated to passphrases and security. + +Merged changes needed for building with Visual Studio on Windows. This +includes a refactoring of the random passphrase generator code to make +it shared between platforms. + + + Significant changes between 1.4.1 and 1.5.0. + +Updated the included wordlist to avoid some inappropriate words in +randomly generated passphrases while not removing any words from the +"word-based" check, and also to have plenty of extra words for +subsequent removal of more words that might be considered inappropriate +from the initial 4096 that are used for randomly generated passphrases. +Most of the added words came from EFF Diceware, BIP-0039, and our own +processing of Project Gutenberg Australia books. + + + Significant changes between 1.4.0 and 1.4.1. + +Set default for "max" to 72 (was 40). The previous setting was based on +a reading of RFC 1939, which in practice did not matter. The new one is +based on bcrypt's truncation at 72, which actually still matters. + +Documented "similar" in pwqcheck(1) help message and manual page. This +is a setting that was supported before and documented for other passwdqc +components before, but was apparently erroneously omitted from here. + + + Significant changes between 1.3.2 and 1.4.0. + +Implemented i18n support in pam_passwdqc, contributed by Oleg Solovyov, +Andrey Cherepanov, and Dmitry V. Levin. The i18n support is off by +default, it can be enabled if Linux-PAM is built using --enable-nls +configure option. + +Implemented audit support in pam_passwdqc, contributed by Oleg Solovyov +and Dmitry V. Levin. The audit support is off by default, it can be +enabled if Linux-PAM is built using --enable-audit configure option. + +Both of these optional new features had been introduced and are enabled +in ALT Linux distributions, so this version is effectively upstreaming +the changes from there. + + + Significant changes between 1.3.1 and 1.3.2. + +Compatibility for building with newer versions of glibc, where we now +have to define _DEFAULT_SOURCE for our use of crypt(3). The problem was +identified and this change tested by Dmitry V. Levin. + +Clarified in the man pages that /etc/passwdqc.conf is not read unless +this suggested file location is specified with the config= option. + +Clarified the OpenBSD configuration example. + +Escape the minus sign in the OpenBSD configuration example to make the +man page linter happy, patch by Jackson Doak via Unit 193. diff --git a/contrib/pam_modules/pam_passwdqc/INSTALL b/contrib/pam_modules/pam_passwdqc/INSTALL new file mode 100644 index 000000000000..2124c9879118 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/INSTALL @@ -0,0 +1,47 @@ +Many Linux distributions, FreeBSD 5.0+, and DragonFly BSD 2.2+ include +pam_passwdqc or the full-blown passwdqc package in the distribution or +provide it as a "native" package to be installed - so you may just use +that. The instructions below apply if your distribution lacks passwdqc +or if you prefer to build and install passwdqc on your own (such as to +get a newer version of it than one available in/for the distribution). + +On a system with the PAM (Pluggable Authentication Modules) framework, +you may build all components of passwdqc (the library, the PAM module, +and two command-line programs) by simply running "make". To install, +run "make install". To uninstall, run "make uninstall". + +On a system with the PAM framework built with i18n support enabled +you may also build pam_passwdqc with i18n support by adding +-DENABLE_NLS=1 to CPPFLAGS. To compile translation files, run +"make locales". To install them, run "make install_locales". + +On a system with the PAM framework built with Linux audit support +enabled you may also build pam_passwdqc with audit support by adding +-DHAVE_LIBAUDIT=1 to CPPFLAGS. + +On a system without PAM, you may build everything but the PAM module +with "make utils". To install, run "make install_lib install_utils". +To uninstall, run "make remove_lib remove_utils". + +Please note that currently passwdqc's default is to install right into +system directories such as /etc, /lib, /usr/lib, /usr/include, +/usr/share/man, /usr/bin. If desired, these pathnames may be overridden +on make's command-line (please see Makefile for the available macro +names and passwdqc.spec for some examples). + +Since passwdqc installs a new shared library, you may need to run the +ldconfig(8) program to update the dynamic linker cache. + +Alternatively, on a Red Hat'ish Linux system and under an account +configured to build RPM packages (perhaps with ~/.rpmmacros specifying +the proper pathnames for %_topdir, %_tmppath, and %buildroot), you may +build RPM packages by running "rpmbuild -tb passwdqc-2.0.3.tar.gz", then +install the two binary subpackages with "rpm -Uvh passwdqc*-2.0.3*.rpm". +This works due to the RPM spec file included in the tarball. + +Please refer to README and PLATFORMS for information on configuring your +system to use the PAM module. You may also refer to the pam_passwdqc(8) +and passwdqc.conf(5) manual pages. + +Please refer to the pwqcheck(1), pwqfilter(1), and pwqgen(1) manual +pages for information on using the command-line programs. diff --git a/contrib/pam_modules/pam_passwdqc/INTERNALS b/contrib/pam_modules/pam_passwdqc/INTERNALS index bce8f85ad54d..082f29a163f9 100644 --- a/contrib/pam_modules/pam_passwdqc/INTERNALS +++ b/contrib/pam_modules/pam_passwdqc/INTERNALS @@ -1,2 +1,2 @@ -The functions defined in passwdqc.h may be used without PAM at all. -They will eventually be moved into a libpasswdqc. +The functions defined in passwdqc.h may be used without PAM at all, and +all of them are in fact exported by libpasswdqc. diff --git a/contrib/pam_modules/pam_passwdqc/LICENSE b/contrib/pam_modules/pam_passwdqc/LICENSE index dc50971f05da..b6baa37f34a3 100644 --- a/contrib/pam_modules/pam_passwdqc/LICENSE +++ b/contrib/pam_modules/pam_passwdqc/LICENSE @@ -1,9 +1,28 @@ -You're allowed to do whatever you like with this software (including -re-distribution in source and/or binary form, with or without -modification), provided that credit is given where it is due and any -modified versions are marked as such. There's absolutely no warranty. - -Note that you don't have to re-distribute this software under these -same relaxed terms. In particular, you're free to place modified -versions under (L)GPL, thus disallowing further re-distribution in -binary-only form. +Two manual pages (pam_passwdqc.8 and passwdqc.conf.5) are under the +3-clause BSD-style license as specified within the files themselves. + +concat.c, wordset_4k.c, wordset_4k.h, pam_macros.h, and pwqcheck.php +are in the public domain, but at your option they may also be used under +this package's license below. + +Files in ci directory (install-dependencies.sh and run-build-and-tests.sh) +are provided under the terms of the GNU General Public License version 2 or +later. These files are not included in passwdqc release tarballs. + +The rest of the files in this package fall under the following terms +(heavily cut-down "BSD license"): + +Redistribution and use in source and binary forms, with or without +modification, are permitted. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/contrib/pam_modules/pam_passwdqc/Makefile b/contrib/pam_modules/pam_passwdqc/Makefile index 49678481456b..404f0364d39a 100644 --- a/contrib/pam_modules/pam_passwdqc/Makefile +++ b/contrib/pam_modules/pam_passwdqc/Makefile @@ -1,48 +1,321 @@ # -# Copyright (c) 2000,2001 by Solar Designer. See LICENSE. +# Copyright (c) 2000-2003,2005,2009,2010,2020 by Solar Designer +# Copyright (c) 2008,2009,2017 by Dmitry V. Levin +# Copyright (c) 2017 by Oleg Solovyov +# See LICENSE # -CC = gcc -LD = ld -RM = rm -f -MKDIR = mkdir -p -INSTALL = install -CFLAGS = -c -Wall -fPIC -DHAVE_SHADOW -O2 -LDFLAGS = -s -lpam -lcrypt --shared -LDFLAGS_SUN = -s -lpam -lcrypt -G - +PACKAGE = passwdqc +VERSION = 2.0.3 TITLE = pam_passwdqc -LIBSHARED = $(TITLE).so +SHARED_LIB = libpasswdqc.so.1 +DEVEL_LIB = libpasswdqc.so +SHARED_LIB_DARWIN = libpasswdqc.0.dylib +SHARED_LIB_CYGWIN = cygpasswdqc-0.dll +DEVEL_LIB_DARWIN = libpasswdqc.dylib +DEVEL_LIB_CYGWIN = libpasswdqc.dll.a +MAP_LIB = libpasswdqc.map +PAM_SO_SUFFIX = +SHARED_PAM = $(TITLE).so$(PAM_SO_SUFFIX) +MAP_PAM = pam_passwdqc.map SHLIBMODE = 755 +HEADER = passwdqc.h +INCMODE = 644 +PKGCONFIG = passwdqc.pc +PKGCONFMODE = 644 +MAN1 = pwqgen.1 pwqcheck.1 pwqfilter.1 +MAN3 = libpasswdqc.3 \ + passwdqc_params_reset.3 \ + passwdqc_params_load.3 \ + passwdqc_params_parse.3 \ + passwdqc_params_free.3 \ + passwdqc_check.3 \ + passwdqc_random.3 +MAN5 = passwdqc.conf.5 +MAN8 = $(TITLE).8 +MANMODE = 644 +BINDIR = /usr/bin +BINMODE = 755 +CONFDIR = /etc +CONFMODE = 644 +SHARED_LIBDIR = /lib +SHARED_LIBDIR_CYGWIN = /usr/bin +SHARED_LIBDIR_SUN = /usr/lib +SHARED_LIBDIR_REL = ../..$(SHARED_LIBDIR) +DEVEL_LIBDIR = /usr/lib SECUREDIR = /lib/security -FAKEROOT = +SECUREDIR_SUN = /usr/lib/security +SECUREDIR_DARWIN = /usr/lib/pam +INCLUDEDIR = /usr/include +PKGCONFIGDIR = $(DEVEL_LIBDIR)/pkgconfig +MANDIR = /usr/share/man +DESTDIR = +LOCALEDIR = /usr/share/locale +LOCALEMODE = 644 + +LANGUAGES = ru + +CC = gcc +LD = $(CC) +LD_lib = $(LD) +RM = rm -f +LN_s = ln -s -f +MKDIR = umask 022 && mkdir -p +INSTALL = install -c +# We support Sun's older /usr/ucb/install, but not the newer /usr/sbin/install. +INSTALL_SUN = /usr/ucb/install -c +CFLAGS = -Wall -W -O2 +CFLAGS_lib = $(CFLAGS) -fPIC +CFLAGS_bin = $(CFLAGS) -fomit-frame-pointer +CPPFLAGS = +CPPFLAGS_bin = $(CPPFLAGS) +CPPFLAGS_lib = $(CPPFLAGS) -DPACKAGE=\\\"$(PACKAGE)\\\" +MSGFMT = msgfmt +XGETTEXT = xgettext +XGETTEXT_OPTS = --keyword=_ --keyword=P2_:1,1 --keyword=P3_:1,2 --language=C --add-comments +MSGMERGE = msgmerge + +LDFLAGS = +LDFLAGS_shared = $(LDFLAGS) --shared +LDFLAGS_shared_LINUX = $(LDFLAGS) --shared +LDFLAGS_shared_SUN = $(LDFLAGS) -G +LDFLAGS_shared_HP = $(LDFLAGS) -b +LDFLAGS_lib = $(LDFLAGS_shared) +LDFLAGS_lib_LINUX = $(LDFLAGS_shared_LINUX) \ + -Wl,--soname,$(SHARED_LIB),--version-script,$(MAP_LIB) +LDFLAGS_lib_SUN = $(LDFLAGS_shared_SUN) +LDFLAGS_lib_HP = $(LDFLAGS_shared_HP) +LDFLAGS_lib_CYGWIN = $(LDFLAGS_shared) \ + -Wl,--out-implib=$(DEVEL_LIB_CYGWIN) \ + -Wl,--export-all-symbols \ + -Wl,--enable-auto-import +LDFLAGS_pam = $(LDFLAGS_shared) +LDFLAGS_pam_LINUX = $(LDFLAGS_shared_LINUX) \ + -Wl,--version-script,$(MAP_PAM) +LDFLAGS_pam_SUN = $(LDFLAGS_shared_SUN) +LDFLAGS_pam_HP = $(LDFLAGS_shared_HP) + +LDLIBS_lib = +LDLIBS_pam = -lpam -lcrypt +LDLIBS_pam_LINUX = -lpam -lcrypt +LDLIBS_pam_SUN = -lpam -lcrypt +LDLIBS_pam_HP = -lpam -lsec +LDLIBS_pam_DARWIN = -lpam -lSystem + +# Uncomment this to use cc instead of gcc +#CC = cc +# Uncomment this to use Sun's C compiler flags +#CFLAGS = -xO2 +#CFLAGS_lib = $(CFLAGS) -KPIC +#CFLAGS_bin = $(CFLAGS) +# Uncomment this to use HP's ANSI C compiler flags +#CFLAGS = -Ae +w1 +W 474,486,542 +O2 +#CFLAGS_lib = $(CFLAGS) +z +#CFLAGS_bin = $(CFLAGS) + +CONFIGS = passwdqc.conf +BINS = pwqgen pwqcheck pwqfilter +BINS_CYGWIN = $(BINS) $(SHARED_LIB_CYGWIN) +PROJ = $(SHARED_LIB) $(DEVEL_LIB) $(SHARED_PAM) $(BINS) $(PKGCONFIG) +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 +OBJS_PAM = pam_passwdqc.o passwdqc_memzero.o +OBJS_GEN = pwqgen.o passwdqc_memzero.o +OBJS_CHECK = pwqcheck.o passwdqc_memzero.o +OBJS_FILTER = pwqfilter.o md4.o + +default: all -PROJ = $(LIBSHARED) -OBJS = pam_passwdqc.o passwdqc_check.o passwdqc_random.o wordset_4k.o +all locales pam utils install install_lib install_locales install_pam install_utils uninstall remove remove_lib remove_locales remove_pam remove_utils: + case "`uname -s`" in \ + Linux) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ + LDFLAGS_lib="$(LDFLAGS_lib_LINUX)" \ + LDFLAGS_pam="$(LDFLAGS_pam_LINUX)" \ + LDLIBS_pam="$(LDLIBS_pam_LINUX)" \ + $@_wrapped;; \ + SunOS) $(MAKE) -e CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ + LD_lib=ld \ + LDFLAGS_lib="$(LDFLAGS_lib_SUN)" \ + LDFLAGS_pam="$(LDFLAGS_pam_SUN)" \ + LDLIBS_pam="$(LDLIBS_pam_SUN)" \ + INSTALL="$(INSTALL_SUN)" \ + SHARED_LIBDIR="$(SHARED_LIBDIR_SUN)" \ + SECUREDIR="$(SECUREDIR_SUN)" \ + $@_wrapped;; \ + HP-UX) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib) -DHAVE_SHADOW" \ + LD_lib=ld \ + LDFLAGS_lib="$(LDFLAGS_lib_HP)" \ + LDFLAGS_pam="$(LDFLAGS_pam_HP)" \ + LDLIBS_pam="$(LDLIBS_pam_HP)" \ + $@_wrapped;; \ + Darwin) $(MAKE) \ + SHARED_LIB="$(SHARED_LIB_DARWIN)" \ + DEVEL_LIB="$(DEVEL_LIB_DARWIN)" \ + SECUREDIR="$(SECUREDIR_DARWIN)" \ + LDLIBS_pam="$(LDLIBS_pam_DARWIN)" \ + $@_wrapped;; \ + CYGWIN_NT*) $(MAKE) CPPFLAGS_lib="$(CPPFLAGS_lib)" \ + SHARED_LIB="$(SHARED_LIB_CYGWIN)" \ + SHARED_LIBDIR="$(SHARED_LIBDIR_CYGWIN)" \ + DEVEL_LIB="$(DEVEL_LIB_CYGWIN)" \ + LDFLAGS_lib="$(LDFLAGS_lib_CYGWIN)" \ + BINS="$(BINS_CYGWIN)" \ + CYGWIN=true \ + $@_wrapped;; \ + *) $(MAKE) $@_wrapped;; \ + esac -all: - if [ "`uname -s`" = "SunOS" ]; then \ - make LDFLAGS="$(LDFLAGS_SUN)" $(PROJ); \ - else \ - make $(PROJ); \ - fi +all_wrapped: pam_wrapped utils_wrapped $(PKGCONFIG) -$(LIBSHARED): $(OBJS) - $(LD) $(LDFLAGS) $(OBJS) -o $(LIBSHARED) +pam_wrapped: $(SHARED_PAM) + +utils_wrapped: $(BINS) + +$(SHARED_LIB): $(OBJS_LIB) $(MAP_LIB) + $(LD_lib) $(LDFLAGS_lib) $(OBJS_LIB) $(LDLIBS_lib) -o $(SHARED_LIB) + +$(DEVEL_LIB): $(SHARED_LIB) +ifndef CYGWIN + $(LN_s) $(SHARED_LIB) $(DEVEL_LIB) +endif + +$(SHARED_PAM): $(OBJS_PAM) $(MAP_PAM) $(DEVEL_LIB) + $(LD_lib) $(LDFLAGS_pam) $(OBJS_PAM) $(LDLIBS_pam) -L. -lpasswdqc -o $(SHARED_PAM) + +pwqgen: $(OBJS_GEN) $(DEVEL_LIB) + $(LD) $(LDFLAGS) $(OBJS_GEN) -L. -lpasswdqc -o $@ + +pwqcheck: $(OBJS_CHECK) $(DEVEL_LIB) + $(LD) $(LDFLAGS) $(OBJS_CHECK) -L. -lpasswdqc -o $@ + +pwqfilter: $(OBJS_FILTER) + $(LD) $(LDFLAGS) $(OBJS_FILTER) -o $@ + +pwqgen.o: pwqgen.c passwdqc.h + $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c + +pwqcheck.o: pwqcheck.c passwdqc.h + $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c + +pwqfilter.o: pwqfilter.c passwdqc_filter.h passwdqc.h + $(CC) $(CPPFLAGS_bin) $(CFLAGS_bin) -c $*.c .c.o: - $(CC) $(CFLAGS) $*.c + $(CC) $(CPPFLAGS_lib) $(CFLAGS_lib) -c $*.c + +$(PKGCONFIG): $(PKGCONFIG).in + sed -e "s|@VERSION@|$(VERSION)|g" $< > $@ +concat.o: concat.h pam_passwdqc.o: passwdqc.h pam_macros.h -passwdqc_check.o: passwdqc.h -passwdqc_random.o: passwdqc.h +passwdqc_check.o: passwdqc.h passwdqc_filter.h wordset_4k.h +passwdqc_filter.o: passwdqc.h passwdqc_filter.h +passwdqc_load.o: passwdqc.h concat.h +passwdqc_parse.o: passwdqc.h concat.h +passwdqc_random.o: passwdqc.h wordset_4k.h +wordset_4k.o: wordset_4k.h + +install_wrapped: install_lib_wrapped install_utils_wrapped install_pam_wrapped + @echo 'Consider running ldconfig(8) to update the dynamic linker cache.' -install: - $(MKDIR) $(FAKEROOT)$(SECUREDIR) - $(INSTALL) -m $(SHLIBMODE) $(LIBSHARED) $(FAKEROOT)$(SECUREDIR) +install_lib_wrapped: + $(MKDIR) $(DESTDIR)$(CONFDIR) + $(INSTALL) -m $(CONFMODE) $(CONFIGS) $(DESTDIR)$(CONFDIR)/ -remove: - $(RM) $(FAKEROOT)$(SECUREDIR)/$(TITLE).so + $(MKDIR) $(DESTDIR)$(SHARED_LIBDIR) + $(INSTALL) -m $(SHLIBMODE) $(SHARED_LIB) $(DESTDIR)$(SHARED_LIBDIR)/ + + $(MKDIR) $(DESTDIR)$(DEVEL_LIBDIR) +ifndef CYGWIN + $(LN_s) $(SHARED_LIBDIR_REL)/$(SHARED_LIB) \ + $(DESTDIR)$(DEVEL_LIBDIR)/$(DEVEL_LIB) +else + $(INSTALL) -m $(SHLIBMODE) $(DEVEL_LIB) $(DESTDIR)$(DEVEL_LIBDIR)/ +endif + + $(MKDIR) $(DESTDIR)$(INCLUDEDIR) + $(INSTALL) -m $(INCMODE) $(HEADER) $(DESTDIR)$(INCLUDEDIR)/ + + $(MKDIR) $(DESTDIR)$(PKGCONFIGDIR) + $(INSTALL) -m $(PKGCONFMODE) $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/ + + $(MKDIR) $(DESTDIR)$(MANDIR)/man3 + $(INSTALL) -m $(MANMODE) $(MAN3) $(DESTDIR)$(MANDIR)/man3/ + + $(MKDIR) $(DESTDIR)$(MANDIR)/man5 + $(INSTALL) -m $(MANMODE) $(MAN5) $(DESTDIR)$(MANDIR)/man5/ + +install_utils_wrapped: + $(MKDIR) $(DESTDIR)$(BINDIR) + $(INSTALL) -m $(BINMODE) $(BINS) $(DESTDIR)$(BINDIR)/ + + $(MKDIR) $(DESTDIR)$(MANDIR)/man1 + $(INSTALL) -m $(MANMODE) $(MAN1) $(DESTDIR)$(MANDIR)/man1/ + +install_pam_wrapped: + $(MKDIR) $(DESTDIR)$(SECUREDIR) + $(INSTALL) -m $(SHLIBMODE) $(SHARED_PAM) $(DESTDIR)$(SECUREDIR)/ + + $(MKDIR) $(DESTDIR)$(MANDIR)/man8 + $(INSTALL) -m $(MANMODE) $(MAN8) $(DESTDIR)$(MANDIR)/man8/ + +POFILES = $(LANGUAGES:%=po/%.po) +MOFILES = $(LANGUAGES:%=po/%.mo) +POTFILE_DEPS = pam_passwdqc.c passwdqc_check.c +POTFILE = po/$(PACKAGE).pot + +$(POTFILE): $(POTFILE_DEPS) + $(XGETTEXT) $(XGETTEXT_OPTS) -o $@-t $^ && mv $@-t $@ + +.SUFFIXES: .po .mo + +.po.mo: + $(MSGFMT) -c -o $@-t $< && mv $@-t $@ + +update_po: $(POTFILE) $(POFILES) + for f in $(POFILES); do $(MSGMERGE) -U $$f $< || exit; done + +update_mo: $(MOFILES) + +locales_wrapped: update_mo + +install_locales_wrapped: + for lang in $(LANGUAGES); do \ + $(MKDIR) $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES && \ + $(INSTALL) -m $(LOCALEMODE) po/$$lang.mo \ + $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/$(PACKAGE).mo || exit; \ + done + +uninstall_wrapped remove_wrapped: remove_pam_wrapped remove_utils_wrapped remove_lib_wrapped remove_locales_wrapped + +remove_pam_wrapped: + $(RM) $(DESTDIR)$(MANDIR)/man8/$(MAN8) + $(RM) $(DESTDIR)$(SECUREDIR)/$(SHARED_PAM) + +remove_utils_wrapped: + for f in $(MAN1); do $(RM) $(DESTDIR)$(MANDIR)/man1/$$f; done + for f in $(BINS); do $(RM) $(DESTDIR)$(BINDIR)/$$f; done + +remove_lib_wrapped: + for f in $(MAN5); do $(RM) $(DESTDIR)$(MANDIR)/man5/$$f; done + for f in $(MAN3); do $(RM) $(DESTDIR)$(MANDIR)/man3/$$f; done + for f in $(HEADER); do $(RM) $(DESTDIR)$(INCLUDEDIR)/$$f; done + for f in $(PKGCONFIG); do $(RM) $(DESTDIR)$(PKGCONFIGDIR)/$$f; done + for f in $(DEVEL_LIB); do $(RM) $(DESTDIR)$(DEVEL_LIBDIR)/$$f; done + for f in $(SHARED_LIB); do $(RM) $(DESTDIR)$(SHARED_LIBDIR)/$$f; done + for f in $(CONFIGS); do $(RM) $(DESTDIR)$(CONFDIR)/$$f; done + +remove_locales_wrapped: + for f in $(LANGUAGES); do $(RM) $(DESTDIR)$(LOCALEDIR)/$$f/LC_MESSAGES/$(PACKAGE).mo; done clean: - $(RM) $(PROJ) *.o + $(RM) $(PROJ) $(POTFILE) $(MOFILES) *.o + +.PHONY: all all_wrapped clean install install_lib install_locales install_pam install_utils \ + pam pam_wrapped uninstall remove remove_lib remove_pam remove_utils \ + utils utils_wrapped \ + update_mo update_po \ + locales locales_wrapped \ + install_wrapped install_lib_wrapped install_locales_wrapped install_pam_wrapped \ + install_utils_wrapped \ + remove_wrapped remove_lib_wrapped remove_locales_wrapped remove_pam_wrapped \ + remove_utils_wrapped diff --git a/contrib/pam_modules/pam_passwdqc/PLATFORMS b/contrib/pam_modules/pam_passwdqc/PLATFORMS index 35176e67e749..f468f233a866 100644 --- a/contrib/pam_modules/pam_passwdqc/PLATFORMS +++ b/contrib/pam_modules/pam_passwdqc/PLATFORMS @@ -1,30 +1,55 @@ Please see the README for instructions common to all platforms and descriptions of the options mentioned here. Linux. Most modern Linux distributions use Linux-PAM with a password changing module which understands "use_authtok". Thus, you may choose which module prompts for the old password, things should work either way. - FreeBSD. + FreeBSD 5+, DragonFly BSD 2.2+. -As of this writing (April 2002), FreeBSD-current is moving to OpenPAM -which pam_passwdqc already includes support for. The next step would -be for FreeBSD to start actually using PAM from password changing. -Once that becomes a reality, you should be able to use pam_passwdqc -with FreeBSD. +FreeBSD 5 and newer, as well as DragonFly BSD 2.2 and newer, include +pam_passwdqc in the base system. You should be able to use either the +included or the distributed separately version of pam_passwdqc with +these systems. There's a commented out usage example in the default +/etc/pam.d/passwd. +FreeBSD 4 and older used a cut down version of Linux-PAM (not OpenPAM) +and didn't use PAM for password changing. - Solaris. -pam_passwdqc has to ask for the old password during the update phase. -Use "ask_oldauthtok=update check_oldauthtok" with pam_passwdqc and -"use_first_pass" with pam_unix. + OpenBSD. + +OpenBSD does not use PAM, however it is able to use passwdqc's pwqcheck +program. Insert the line ":passwordcheck=/usr/bin/pwqcheck -1:\" +(without the quotes, but with the trailing backslash) into the "default" +section in /etc/login.conf. + + + Solaris, HP-UX 11. + +On Solaris 2.6, 7, and 8 (without patch 108993-18/108994-18 or later) +and on HP-UX 11, pam_passwdqc has to ask for the old password during +the update phase. Use "ask_oldauthtok=update check_oldauthtok" with +pam_passwdqc and "use_first_pass" with pam_unix. + +On Solaris 8 (with patch 108993-18/108994-18 or later), 9, and 10, +use pam_passwdqc instead of both pam_authtok_get and pam_authtok_check, +and set "retry=1" with pam_passwdqc as the passwd command has its own +handling for that. You will likely also need to set "max=8" in order to actually enforce -not-so-weak passwords with the obsolete "traditional" crypt(3) hashes -that most Solaris systems use. Of course this way you only get about -one third of the functionality of pam_passwdqc. +not-so-weak passwords with the obsolete traditional DES-based hashes +that most Solaris systems use and the flawed approach HP-UX uses to +process characters past 8. Of course this way you only get about one +third of the functionality of pam_passwdqc. As a better alternative, +on modern Solaris systems you may edit the "CRYPT_DEFAULT=__unix__" line +in /etc/security/policy.conf to read "CRYPT_DEFAULT=2a" to enable the +OpenBSD-style bcrypt (Blowfish-based) password hashing. + +There's a wiki page with detailed instructions specific to Solaris: + +https://openwall.info/wiki/passwdqc/solaris diff --git a/contrib/pam_modules/pam_passwdqc/README b/contrib/pam_modules/pam_passwdqc/README index ca2af89955da..0561070e30e4 100644 --- a/contrib/pam_modules/pam_passwdqc/README +++ b/contrib/pam_modules/pam_passwdqc/README @@ -1,143 +1,191 @@ pam_passwdqc is a simple password strength checking module for PAM-aware password changing programs, such as passwd(1). In addition to checking regular passwords, it offers support for passphrases and -can provide randomly generated passwords. All features are optional -and can be (re-)configured without rebuilding. +can provide randomly generated ones. All features are optional and +can be (re-)configured without rebuilding. This module should be stacked before your usual password changing module (such as pam_unix or pam_pwdb) in the password management group (the "password" lines in /etc/pam.d/passwd or /etc/pam.conf). The password changing module should then be told to use the provided new authentication token (new password) rather than request it from the user. There's usually the "use_authtok" option to do that. If your password changing module lacks the "use_authtok" option or its prompts are inconsistent with pam_passwdqc's, you may tell pam_passwdqc to ask for the old password as well, with "ask_oldauthtok". In that case the option to use with the password changing module is "use_first_pass". -There's a number of supported options which can be used to modify the +There are a number of supported options, which can be used to modify the behavior of pam_passwdqc (defaults are given in square brackets): - min=N0,N1,N2,N3,N4 [min=disabled,24,12,8,7] + config=FILE [] -The minimum allowed password lengths, separately for different kinds -of passwords/passphrases. The special word "disabled" can be used to -disallow passwords of a given kind regardless of their length. Each -subsequent number is required to be no larger than the preceding one. +Load the specified configuration FILE, which must be in the +passwdqc.conf format (described in the passwdqc.conf(5) manual page). +This file may define any options described in here, including load of +yet another configuration file, but loops are not allowed. + + min=N0,N1,N2,N3,N4 [min=disabled,24,11,8,7] + +The minimum allowed password lengths for different kinds of passwords +and passphrases. The keyword "disabled" can be used to disallow +passwords of a given kind regardless of their length. Each subsequent +number is required to be no larger than the preceding one. N0 is used for passwords consisting of characters from one character -class only. (The character classes are: digits, lower-case letters, -upper-case letters, and other characters. There's also the special -class for non-ASCII characters which couldn't be classified, but are -assumed to be non-digits.) +class only. The character classes are: digits, lower-case letters, +upper-case letters, and other characters. There is also a special +class for non-ASCII characters, which could not be classified, but are +assumed to be non-digits. N1 is used for passwords consisting of characters from two character -classes, which don't meet the requirements for a passphrase. +classes that do not meet the requirements for a passphrase. -N2 is used for passphrases. A passphrase must consist of sufficient -words (see the "passphrase" option, below). +N2 is used for passphrases. Note that besides meeting this length +requirement, a passphrase must also consist of a sufficient number of +words (see the "passphrase" option below). N3 and N4 are used for passwords consisting of characters from three and four character classes, respectively. When calculating the number of character classes, upper-case letters used as the first character and digits used as the last character of a password are not counted. In addition to being sufficiently long, passwords are required to contain enough different characters for the character classes and -the minimum length they've been checked against. +the minimum length they have been checked against. - max=N [max=40] + max=N [max=72] The maximum allowed password length. This can be used to prevent -users from setting passwords which may be too long for some system +users from setting passwords that may be too long for some system services. -The value 8 is treated specially. Passwords longer than 8 characters -will not be rejected, but will be truncated to 8 characters for the -strength checks and the user will be warned. This is to be used with -the traditional crypt(3) password hashes. +The value 8 is treated specially: with max=8, passwords longer than 8 +characters will not be rejected, but will be truncated to 8 characters +for the strength checks and the user will be warned. This is to be +used with the traditional DES-based password hashes, which truncate +the password at 8 characters. -It is important that you do set max=8 if you're using the traditional +It is important that you do set max=8 if you are using the traditional hashes, or some weak passwords will pass the checks. passphrase=N [passphrase=3] The number of words required for a passphrase, or 0 to disable the -support for passphrases. +support for user-chosen passphrases. match=N [match=4] The length of common substring required to conclude that a password is at least partially based on information found in a character string, or 0 to disable the substring search. Note that the password will not -be rejected once a weak substring is found. Instead, the password -will be subjected to the usual strength requirements with the weak -substring removed. +be rejected once a weak substring is found; it will instead be +subjected to the usual strength requirements with the weak substring +partially discounted. The substring search is case-insensitive and is able to detect and remove a common substring spelled backwards. similar=permit|deny [similar=deny] Whether a new password is allowed to be similar to the old one. The -passwords are considered to be similar when there's a sufficiently -long common substring and the new password with the substring removed -would be weak. - - random=N[,only] [random=42] - -The size of randomly-generated passwords in bits, or 0 to disable this -feature. Passwords that contain the offered randomly-generated string -will be allowed regardless of other possible restrictions. +passwords are considered to be similar when there is a sufficiently +long common substring and the new password with the substring partially +discounted would be weak. + + wordlist=FILE [] + +Deny passwords that are based on lines of a tiny external text file, +which can reasonably be e.g. a list of a few thousand common passwords. +Common dictionary words may also reasonably be included, especially in a +local language other than English, or longer yet common English words. +(passwdqc includes a list of a few thousand common English words of +lengths from 3 to 6 built in. Any word list possibly specified with +this option is used in addition to the built-in word list.) + +Substring matching and discounting will be used if the "match" setting +above is non-zero. Please note that this is very inefficient, and isn't +to be used with large wordlists. + + denylist=FILE [] + +Deny passwords or passphrases directly appearing in a tiny external text +file. That file can reasonably be e.g. a list of common passwords if +only a relaxed policy is desired and stricter checks are thus disabled +(using their separate options). Such policy would only be somewhat +effective against online/remote attacks, but not against offline attacks +on hashed passwords. + + filter=FILE [] + +Deny passwords or passphrases directly appearing in a maybe huge binary +filter file created with pwqfilter. This is very efficient, needing at +most two random disk reads per query. A filter created from millions of +leaked passwords can reasonably be used on top of passwdqc's other +checks to further reduce the number of passing yet weak passwords +without causing unreasonable inconvenience (as e.g. higher minimum +lengths and character set requirements could). + + random=N[,only] [random=47] + +The size of randomly-generated passphrases in bits (24 to 136), or 0 to +disable this feature. Any passphrase that contains the offered +randomly-generated string will be allowed regardless of other possible +restrictions. The "only" modifier can be used to disallow user-chosen passwords. enforce=none|users|everyone [enforce=everyone] The module can be configured to warn of weak passwords only, but not -actually enforce strong passwords. The "users" setting will enforce -strong passwords for non-root users only. +actually enforce strong passwords. The "users" setting is like +"everyone" for all PAM services except "chpasswd" and "passwd". +For these two PAM services "users" will enforce strong passwords +for invocations by non-root users only. non-unix [] -By default, the module uses getpwnam(3) to obtain the user's personal +Normally, the module uses getpwnam(3) to obtain the user's personal login information and use that during the password strength checks. -This behavior can be disabled with "non-unix". +This behavior can be disabled with the "non-unix" option. retry=N [retry=3] The number of times the module will ask for a new password if the user fails to provide a sufficiently strong password and enter it twice the first time. ask_oldauthtok[=update] [] Ask for the old password as well. Normally, pam_passwdqc leaves this -task for the password changing module. A simple "ask_oldauthtok" will -cause pam_passwdqc to ask for the old password during the preliminary -check phase. With "ask_oldauthtok=update", pam_passwdqc will do that -during the update phase. +task for subsequent modules. With no argument, the "ask_oldauthtok" +option will cause pam_passwdqc to ask for the old password during the +preliminary check phase. With "ask_oldauthtok=update", pam_passwdqc +will do that during the update phase. check_oldauthtok [] This tells pam_passwdqc to validate the old password before giving a -new password prompt. Normally, this task is left for the password -changing module. +new password prompt. Normally, this task is left for subsequent +modules. -The primary use for this option is with "ask_oldauthtok=update" in -which case no other modules have a chance to run and validate the -password between the prompts. Of course, this will only work with +The primary use for this option is when "ask_oldauthtok=update" is +also specified, in which case no other module gets a chance to ask +for and validate the password. Of course, this will only work with Unix passwords. use_first_pass [] use_authtok [] Use the new password obtained by modules stacked before pam_passwdqc. This disables user interaction within pam_passwdqc. With this module, the only difference between "use_first_pass" and "use_authtok" is that the former is incompatible with "ask_oldauthtok". --- -Solar Designer + noaudit [] + +If audit is enabled at build time, the PAM module logs audit events once +user tries to change their credentials. This option disables that audit +logging. diff --git a/contrib/pam_modules/pam_passwdqc/ci/install-dependencies.sh b/contrib/pam_modules/pam_passwdqc/ci/install-dependencies.sh new file mode 100755 index 000000000000..17124d527dc6 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/ci/install-dependencies.sh @@ -0,0 +1,86 @@ +#!/bin/sh -ex +# +# Copyright (c) 2018-2020 The strace developers. +# All rights reserved. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +j=-j`nproc` || j= +type sudo >/dev/null 2>&1 && sudo=sudo || sudo= +common_packages='file gettext libaudit-dev libpam0g-dev make' + +retry_if_failed() +{ + for i in `seq 0 99`; do + "$@" && i= && break || sleep 1 + done + [ -z "$i" ] +} + +updated= +apt_get_install() +{ + [ -n "$updated" ] || { + retry_if_failed $sudo apt-get -qq update + updated=1 + } + retry_if_failed $sudo \ + apt-get -qq --no-install-suggests --no-install-recommends \ + install -y "$@" +} + +git_installed= +clone_repo() +{ + local src dst branch + src="$1"; shift + dst="$1"; shift + branch="${1-}" + + [ -n "$git_installed" ] || { + apt_get_install git ca-certificates + git_installed=1 + } + + case "$src" in + *://*) ;; + *) local url path + url="$(git config remote.origin.url)" + path="${url#*://*/}" + src="${url%$path}$src" + ;; + esac + + retry_if_failed \ + git clone --depth=1 ${branch:+--branch $branch} "$src" "$dst" +} + +case "$TARGET" in + x32|x86) + packages="$common_packages gcc-multilib" + ;; + *) + packages="$common_packages gcc" + ;; +esac + +case "$CC" in + gcc-*) + retry_if_failed \ + $sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + case "$TARGET" in + x32|x86) + apt_get_install $packages "$CC"-multilib "$CC" + ;; + *) + apt_get_install $packages "$CC" + ;; + esac + ;; + clang*) + apt_get_install $packages "$CC" + ;; + *) + apt_get_install $packages + ;; +esac diff --git a/contrib/pam_modules/pam_passwdqc/ci/run-build-and-tests.sh b/contrib/pam_modules/pam_passwdqc/ci/run-build-and-tests.sh new file mode 100755 index 000000000000..b7f078276ff1 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/ci/run-build-and-tests.sh @@ -0,0 +1,42 @@ +#!/bin/sh -ex +# +# Copyright (c) 2018-2020 The strace developers. +# All rights reserved. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +case "${TARGET-}" in + x32) + CC="$CC -mx32" + ;; + x86) + CC="$CC -m32" + ;; +esac + +echo 'BEGIN OF BUILD ENVIRONMENT INFORMATION' +uname -a |head -1 +libc="$(ldd /bin/sh |sed -n 's|^[^/]*\(/[^ ]*/libc\.so[^ ]*\).*|\1|p' |head -1)" +$libc |head -1 +file -L /bin/sh +$CC --version |head -1 +$CC -print-multi-lib ||: +make --version |head -1 +kver="$(printf '%s\n%s\n' '#include ' 'LINUX_VERSION_CODE' | $CC $CPPFLAGS -E -P -)" +printf 'kernel-headers %s.%s.%s\n' $(($kver/65536)) $(($kver/256%256)) $(($kver%256)) +echo 'END OF BUILD ENVIRONMENT INFORMATION' + +nproc="$(nproc)" || nproc=1 +j="-j$nproc" + +make -k $j \ + CC="$CC" \ + CPPFLAGS='-DENABLE_NLS=1 -DHAVE_LIBAUDIT=1 -DLINUX_PAM=1' \ + CFLAGS_bin='-Wall -W -Werror' \ + CFLAGS_lib='-Wall -W -Werror -fPIC' \ + all locales + +if git status --porcelain |grep ^.; then + echo >&2 'git status reported uncleanness' + exit 1 +fi diff --git a/contrib/pam_modules/pam_passwdqc/concat.c b/contrib/pam_modules/pam_passwdqc/concat.c new file mode 100644 index 000000000000..de1f75dcbb51 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/concat.c @@ -0,0 +1,70 @@ +/* + * concat() - allocate memory and safely concatenate strings in portable C + * (and C++ if you like). + * + * This code deals gracefully with potential integer overflows (perhaps when + * input strings are maliciously long), as well as with input strings changing + * from under it (perhaps because of misbehavior of another thread). It does + * not depend on non-portable functions such as snprintf() and asprintf(). + * + * Written by Solar Designer and placed in the + * public domain. + */ + +#include +#include +#include +#include +#include "concat.h" + +char *concat(const char *s1, ...) +{ + va_list args; + const char *s; + char *p, *result; + size_t l, m, n; + + m = n = strlen(s1); + va_start(args, s1); + while ((s = va_arg(args, char *))) { + l = strlen(s); + if ((m += l) < l) + break; + } + va_end(args); + if (s || m >= INT_MAX) + return NULL; + + result = (char *)malloc(m + 1); + if (!result) + return NULL; + + memcpy(p = result, s1, n); + p += n; + va_start(args, s1); + while ((s = va_arg(args, char *))) { + l = strlen(s); + if ((n += l) < l || n > m) + break; + memcpy(p, s, l); + p += l; + } + va_end(args); + if (s || m != n || p != result + n) { + free(result); + return NULL; + } + + *p = 0; + return result; +} + +#ifdef TEST +#include + +int main(int argc, char **argv) +{ + puts(concat(argv[0], argv[1], argv[2], argv[3], NULL)); + return 0; +} +#endif diff --git a/contrib/pam_modules/pam_passwdqc/concat.h b/contrib/pam_modules/pam_passwdqc/concat.h new file mode 100644 index 000000000000..a79b4bbc3b12 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/concat.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2009 by Dmitry V. Levin. See LICENSE. + */ + +#ifndef CONCAT_H__ +#define CONCAT_H__ + +extern char *concat(const char *, ...); + +#endif /* CONCAT_H__ */ diff --git a/contrib/pam_modules/pam_passwdqc/libpasswdqc.3 b/contrib/pam_modules/pam_passwdqc/libpasswdqc.3 new file mode 100644 index 000000000000..197ea4d49ff4 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/libpasswdqc.3 @@ -0,0 +1,211 @@ +.\" Copyright (c) 2021 Dmitry V. Levin +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 19, 2021 +.Dt LIBPASSWDQC 3 +.Os "Openwall Project" +.Sh NAME +.Nm passwdqc_params_reset , +.Nm passwdqc_params_load , +.Nm passwdqc_params_parse , +.Nm passwdqc_params_free , +.Nm passwdqc_check , +.Nm passwdqc_random +.Nd password strength checking functions +.Sh LIBRARY +Password strength checking library +.Pq libpasswdqc, -lpasswdqc +.Sh SYNOPSIS +.In passwdqc.h +.Bd -literal +typedef struct { + passwdqc_params_qc_t qc; + passwdqc_params_pam_t pam; +} passwdqc_params_t; +.Ed +.Ft void +.Fn passwdqc_params_reset "passwdqc_params_t *params" +.Ft int +.Fo passwdqc_params_load +.Fa "passwdqc_params_t *params" +.Fa "char **reason" +.Fa "const char *pathname" +.Fc +.Ft int +.Fo passwdqc_params_parse +.Fa "passwdqc_params_t *params" +.Fa "char **reason" +.Fa "int argc" +.Fa "const char *const *argv" +.Fc +.Ft void +.Fn passwdqc_params_free "passwdqc_params_t *params" +.Ft const char * +.Fo passwdqc_check +.Fa "const passwdqc_params_qc_t *params" +.Fa "const char *newpass" +.Fa "const char *oldpass" +.Fa "const struct passwd *pw" +.Fc +.Ft char * +.Fn passwdqc_random "const passwdqc_params_qc_t *params" +.Sh DESCRIPTION +The +.Fn passwdqc_params_reset +function initializes the passwdqc_params_t structure specified by +.Fa params +argument to compile-time defaults. +.Pp +The +.Fn passwdqc_params_load +function fills in the passwdqc_params_t structure specified by +.Fa params +argument according to the configuration options listed in the file specified by +.Fa pathname +argument. When the passwdqc_params_t structure is no longer needed, +the memory allocated by this function should be released using +.Fn passwdqc_params_free . +.Pp +The +.Fn passwdqc_params_parse +function fills in the passwdqc_params_t structure specified by +.Fa params +argument according to the configuration options specified by +.Fa argc +and +.Fa argv +arguments. When the passwdqc_params_t structure is no longer needed, +the memory allocated by this function should be released using +.Fn passwdqc_params_free . +.Pp +The +.Fn passwdqc_params_free +function frees the memory allocated by +.Fn passwdqc_params_load +and +.Fn passwdqc_params_parse +functions when filling in the passwdqc_params_t structure specified by +.Fa params +argument. +.Pp +The +.Fn passwdqc_check +function checks the quality of the passphrase specified by +.Fa newpass +argument according to the configuration specified by +.Fa params +argument. If an optional old passphrase is specified by +.Fa oldpass +argument, +.Fa newpass +is additionally checked against +.Fa oldpass +for similarity. If an optional passwd record is specified by +.Fa pw +argument, +.Fa newpass +is additionally checked whether it is based on the personal login information +in the passwd record. +.Pp +The +.Fn passwdqc_random +function generates a random passphrase according to the configuration +specified by +.Fa params +argument. +.Sh RETURN VALUES +The +.Fn passwdqc_params_reset +and +.Fn passwdqc_params_free +functions do not return a value. +.Pp +Upon successful completion the +.Fn passwdqc_params_load +and +.Fn passwdqc_params_parse +functions return 0. Otherwise, -1 is returned and a pointer to dynamically +allocated memory containing the error string is assigned to +.Fa *reason . +This memory should be released using free(3) when no longer needed. +.Pp +Upon successful completion the +.Fn passwdqc_check +function returns NULL. Otherwise, a string describing the error is returned. +The returned string is statically allocated and valid for the lifetime of the +program. +.Pp +Upon successful completion the +.Fn passwdqc_random +function returns a dynamically allocated string containing the generated +passphrase. Otherwise, NULL is returned. The string should be released using +free(3) when no longer needed. +.Sh FILES +.Pa /etc/passwdqc.conf +(not read unless this suggested file location is specified with the +.Ar pathname +argument or with +.Cm config Ns = Ns Ar /etc/passwdqc.conf +configuration option). +.Sh EXAMPLES +The following example shows how to use the libpasswdqc library with system +configuration options to check a passphrase. +.Bd -literal -offset 2n +#include +#include +#include +#include + +bool +check(const char *newpass, const char *oldpass, const struct passwd *pw) +{ + static const char config[] = "/etc/passwdqc.conf"; + char *parse_reason; + const char *check_result = ""; + passwdqc_params_t params; + passwdqc_params_reset(¶ms); + if (passwdqc_params_load(¶ms, &parse_reason, config)) { + fprintf(stderr, "passwdqc_params_load: %s\en", + parse_reason ? parse_reason : "Out of memory"); + free(parse_reason); + goto out; + } + check_result = passwdqc_check(¶ms.qc, newpass, oldpass, pw); + if (check_result) + fprintf(stderr, "passwdqc_check: %s\en", check_result); +out: + passwdqc_params_free(¶ms); + return !check_result; +} +.Ed +.Sh SEE ALSO +.Xr passwdqc.conf 5 , +.Xr pwqcheck 1 , +.Xr pwqgen 1 , +.Xr pam_passwdqc 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh HISTORY +The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. +The libpasswdqc library was originally written for ALT GNU/*/Linux +by Dmitry V. Levin, reusing code from pam_passwdqc. +The +.Fn passwdqc_params_free +function was added in version 2.0.0 by Solar Designer. +.Sh AUTHORS +This manual page was written by Dmitry V. Levin. diff --git a/contrib/pam_modules/pam_passwdqc/libpasswdqc.map b/contrib/pam_modules/pam_passwdqc/libpasswdqc.map new file mode 100644 index 000000000000..20465e4d58c1 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/libpasswdqc.map @@ -0,0 +1,11 @@ +{ + global: + passwdqc_check; + passwdqc_params_load; + passwdqc_params_parse; + passwdqc_params_reset; + passwdqc_params_free; + passwdqc_random; + local: + *; +}; diff --git a/contrib/pam_modules/pam_passwdqc/md4.c b/contrib/pam_modules/pam_passwdqc/md4.c new file mode 100644 index 000000000000..0daf1ebc6c17 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/md4.c @@ -0,0 +1,272 @@ +/* + * This is an OpenSSL API compatible (but not ABI compatible) implementation + * of the RSA Data Security, Inc. MD4 Message-Digest Algorithm (RFC 1320). + * + * Homepage: + * https://openwall.info/wiki/people/solar/software/public-domain-source-code/md4 + * + * Author: + * Alexander Peslyak, better known as Solar Designer + * + * This software was written by Alexander Peslyak in 2001. No copyright is + * claimed, and the software is hereby placed in the public domain. + * In case this attempt to disclaim copyright and place the software in the + * public domain is deemed null and void, then the software is + * Copyright (c) 2001 Alexander Peslyak and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * (This is a heavily cut-down "BSD license".) + * + * This differs from Colin Plumb's older public domain implementation in that + * no exactly 32-bit integer data type is required (any 32-bit or wider + * unsigned integer data type will do), there's no compile-time endianness + * configuration, and the function prototypes match OpenSSL's. No code from + * Colin Plumb's implementation has been reused; this comment merely compares + * the properties of the two independent implementations. + * + * The primary goals of this implementation are portability and ease of use. + * It is meant to be fast, but not as fast as possible. Some known + * optimizations are not included to reduce source code size and avoid + * compile-time configuration. + */ + +#ifndef HAVE_OPENSSL + +#include + +#include "md4.h" + +/* + * The basic MD4 functions. + * + * F and G are optimized compared to their RFC 1320 definitions, with the + * optimization for F borrowed from Colin Plumb's MD5 implementation. + */ +#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z)))) +#define G(x, y, z) (((x) & ((y) | (z))) | ((y) & (z))) +#define H(x, y, z) ((x) ^ (y) ^ (z)) + +/* + * The MD4 transformation for all three rounds. + */ +#define STEP(f, a, b, c, d, x, s) \ + (a) += f((b), (c), (d)) + (x); \ + (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); + +/* + * SET reads 4 input bytes in little-endian byte order and stores them in a + * properly aligned word in host byte order. + * + * The check for little-endian architectures that tolerate unaligned memory + * accesses is just an optimization. Nothing will break if it fails to detect + * a suitable architecture. + * + * Unfortunately, this optimization may be a C strict aliasing rules violation + * if the caller's data buffer has effective type that cannot be aliased by + * MD4_u32plus. In practice, this problem may occur if these MD4 routines are + * inlined into a calling function, or with future and dangerously advanced + * link-time optimizations. For the time being, keeping these MD4 routines in + * their own translation unit avoids the problem. + */ +#if defined(__i386__) || defined(__x86_64__) || defined(__vax__) +#define SET(n) \ + (*(MD4_u32plus *)&ptr[(n) * 4]) +#define GET(n) \ + SET(n) +#else +#define SET(n) \ + (ctx->block[(n)] = \ + (MD4_u32plus)ptr[(n) * 4] | \ + ((MD4_u32plus)ptr[(n) * 4 + 1] << 8) | \ + ((MD4_u32plus)ptr[(n) * 4 + 2] << 16) | \ + ((MD4_u32plus)ptr[(n) * 4 + 3] << 24)) +#define GET(n) \ + (ctx->block[(n)]) +#endif + +/* + * This processes one or more 64-byte data blocks, but does NOT update the bit + * counters. There are no alignment requirements. + */ +static const void *body(MD4_CTX *ctx, const void *data, size_t size) +{ + const unsigned char *ptr; + MD4_u32plus a, b, c, d; + MD4_u32plus saved_a, saved_b, saved_c, saved_d; + const MD4_u32plus ac1 = 0x5a827999, ac2 = 0x6ed9eba1; + + ptr = (const unsigned char *)data; + + a = ctx->a; + b = ctx->b; + c = ctx->c; + d = ctx->d; + + do { + saved_a = a; + saved_b = b; + saved_c = c; + saved_d = d; + +/* Round 1 */ + STEP(F, a, b, c, d, SET(0), 3) + STEP(F, d, a, b, c, SET(1), 7) + STEP(F, c, d, a, b, SET(2), 11) + STEP(F, b, c, d, a, SET(3), 19) + STEP(F, a, b, c, d, SET(4), 3) + STEP(F, d, a, b, c, SET(5), 7) + STEP(F, c, d, a, b, SET(6), 11) + STEP(F, b, c, d, a, SET(7), 19) + STEP(F, a, b, c, d, SET(8), 3) + STEP(F, d, a, b, c, SET(9), 7) + STEP(F, c, d, a, b, SET(10), 11) + STEP(F, b, c, d, a, SET(11), 19) + STEP(F, a, b, c, d, SET(12), 3) + STEP(F, d, a, b, c, SET(13), 7) + STEP(F, c, d, a, b, SET(14), 11) + STEP(F, b, c, d, a, SET(15), 19) + +/* Round 2 */ + STEP(G, a, b, c, d, GET(0) + ac1, 3) + STEP(G, d, a, b, c, GET(4) + ac1, 5) + STEP(G, c, d, a, b, GET(8) + ac1, 9) + STEP(G, b, c, d, a, GET(12) + ac1, 13) + STEP(G, a, b, c, d, GET(1) + ac1, 3) + STEP(G, d, a, b, c, GET(5) + ac1, 5) + STEP(G, c, d, a, b, GET(9) + ac1, 9) + STEP(G, b, c, d, a, GET(13) + ac1, 13) + STEP(G, a, b, c, d, GET(2) + ac1, 3) + STEP(G, d, a, b, c, GET(6) + ac1, 5) + STEP(G, c, d, a, b, GET(10) + ac1, 9) + STEP(G, b, c, d, a, GET(14) + ac1, 13) + STEP(G, a, b, c, d, GET(3) + ac1, 3) + STEP(G, d, a, b, c, GET(7) + ac1, 5) + STEP(G, c, d, a, b, GET(11) + ac1, 9) + STEP(G, b, c, d, a, GET(15) + ac1, 13) + +/* Round 3 */ + STEP(H, a, b, c, d, GET(0) + ac2, 3) + STEP(H, d, a, b, c, GET(8) + ac2, 9) + STEP(H, c, d, a, b, GET(4) + ac2, 11) + STEP(H, b, c, d, a, GET(12) + ac2, 15) + STEP(H, a, b, c, d, GET(2) + ac2, 3) + STEP(H, d, a, b, c, GET(10) + ac2, 9) + STEP(H, c, d, a, b, GET(6) + ac2, 11) + STEP(H, b, c, d, a, GET(14) + ac2, 15) + STEP(H, a, b, c, d, GET(1) + ac2, 3) + STEP(H, d, a, b, c, GET(9) + ac2, 9) + STEP(H, c, d, a, b, GET(5) + ac2, 11) + STEP(H, b, c, d, a, GET(13) + ac2, 15) + STEP(H, a, b, c, d, GET(3) + ac2, 3) + STEP(H, d, a, b, c, GET(11) + ac2, 9) + STEP(H, c, d, a, b, GET(7) + ac2, 11) + STEP(H, b, c, d, a, GET(15) + ac2, 15) + + a += saved_a; + b += saved_b; + c += saved_c; + d += saved_d; + + ptr += 64; + } while (size -= 64); + + ctx->a = a; + ctx->b = b; + ctx->c = c; + ctx->d = d; + + return ptr; +} + +void MD4_Init(MD4_CTX *ctx) +{ + ctx->a = 0x67452301; + ctx->b = 0xefcdab89; + ctx->c = 0x98badcfe; + ctx->d = 0x10325476; + + ctx->lo = 0; + ctx->hi = 0; +} + +void MD4_Update(MD4_CTX *ctx, const void *data, size_t size) +{ + MD4_u32plus saved_lo; + size_t used, available; + + saved_lo = ctx->lo; + if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) + ctx->hi++; + ctx->hi += (MD4_u32plus)(size >> 29); + + used = saved_lo & 0x3f; + + if (used) { + available = 64 - used; + + if (size < available) { + memcpy(&ctx->buffer[used], data, size); + return; + } + + memcpy(&ctx->buffer[used], data, available); + data = (const unsigned char *)data + available; + size -= available; + body(ctx, ctx->buffer, 64); + } + + if (size >= 64) { + data = body(ctx, data, size & ~(size_t)0x3f); + size &= 0x3f; + } + + memcpy(ctx->buffer, data, size); +} + +#define OUT(dst, src) \ + (dst)[0] = (unsigned char)(src); \ + (dst)[1] = (unsigned char)((src) >> 8); \ + (dst)[2] = (unsigned char)((src) >> 16); \ + (dst)[3] = (unsigned char)((src) >> 24); + +void MD4_Final(unsigned char *result, MD4_CTX *ctx) +{ + size_t used, available; + + used = ctx->lo & 0x3f; + + ctx->buffer[used++] = 0x80; + + available = 64 - used; + + if (available < 8) { + memset(&ctx->buffer[used], 0, available); + body(ctx, ctx->buffer, 64); + used = 0; + available = 64; + } + + memset(&ctx->buffer[used], 0, available - 8); + + ctx->lo <<= 3; + OUT(&ctx->buffer[56], ctx->lo) + OUT(&ctx->buffer[60], ctx->hi) + + body(ctx, ctx->buffer, 64); + + OUT(&result[0], ctx->a) + OUT(&result[4], ctx->b) + OUT(&result[8], ctx->c) + OUT(&result[12], ctx->d) + +#if 0 + memset(ctx, 0, sizeof(*ctx)); +#endif +} + +#endif diff --git a/contrib/pam_modules/pam_passwdqc/md4.h b/contrib/pam_modules/pam_passwdqc/md4.h new file mode 100644 index 000000000000..7d68651b2121 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/md4.h @@ -0,0 +1,49 @@ +/* + * This is an OpenSSL API compatible (but not ABI compatible) implementation + * of the RSA Data Security, Inc. MD4 Message-Digest Algorithm (RFC 1320). + * + * Homepage: + * https://openwall.info/wiki/people/solar/software/public-domain-source-code/md4 + * + * Author: + * Alexander Peslyak, better known as Solar Designer + * + * This software was written by Alexander Peslyak in 2001. No copyright is + * claimed, and the software is hereby placed in the public domain. + * In case this attempt to disclaim copyright and place the software in the + * public domain is deemed null and void, then the software is + * Copyright (c) 2001 Alexander Peslyak and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * See md4.c for more information. + */ + +#ifdef HAVE_OPENSSL +#include +#elif !defined(_MD4_H) +#define _MD4_H + +#include /* for size_t */ + +/* Any 32-bit or wider unsigned integer data type will do */ +typedef unsigned int MD4_u32plus; + +typedef struct { + MD4_u32plus lo, hi; + MD4_u32plus a, b, c, d; + unsigned char buffer[64]; +#if !(defined(__i386__) || defined(__x86_64__) || defined(__vax__)) + MD4_u32plus block[16]; +#endif +} MD4_CTX; + +extern void MD4_Init(MD4_CTX *ctx); +extern void MD4_Update(MD4_CTX *ctx, const void *data, size_t size); +extern void MD4_Final(unsigned char *result, MD4_CTX *ctx); + +#endif diff --git a/contrib/pam_modules/pam_passwdqc/pam_macros.h b/contrib/pam_modules/pam_passwdqc/pam_macros.h index adc04bcb1f77..e4741bc307cc 100644 --- a/contrib/pam_modules/pam_passwdqc/pam_macros.h +++ b/contrib/pam_modules/pam_passwdqc/pam_macros.h @@ -1,28 +1,42 @@ /* * These macros are partially based on Linux-PAM's , * which were organized by Cristian Gafton and I believe are in the public * domain. + * + * - Solar Designer */ -#if !defined(_PAM_MACROS_H) && !defined(_pam_overwrite) -#define _PAM_MACROS_H +#ifndef PAM_PASSWDQC_MACROS_H__ +#define PAM_PASSWDQC_MACROS_H__ #include #include -#define _pam_overwrite(x) \ - memset((x), 0, strlen((x))) +#define pwqc_overwrite_string(x) \ +do { \ + if (x) \ + memset((x), 0, strlen(x)); \ +} while (0) + +#define pwqc_drop_mem(x) \ +do { \ + if (x) { \ + free(x); \ + (x) = NULL; \ + } \ +} while (0) -#define _pam_drop_reply(/* struct pam_response * */ reply, /* int */ replies) \ +#define pwqc_drop_pam_reply(/* struct pam_response* */ reply, /* int */ replies) \ do { \ - int i; \ + if (reply) { \ + int reply_i; \ \ - for (i = 0; i < (replies); i++) \ - if ((reply)[i].resp) { \ - _pam_overwrite((reply)[i].resp); \ - free((reply)[i].resp); \ + for (reply_i = 0; reply_i < (replies); ++reply_i) { \ + pwqc_overwrite_string((reply)[reply_i].resp); \ + pwqc_drop_mem((reply)[reply_i].resp); \ + } \ + pwqc_drop_mem(reply); \ } \ - if ((reply)) free((reply)); \ } while (0) -#endif +#endif /* PAM_PASSWDQC_MACROS_H__ */ diff --git a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.8 b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.8 new file mode 100644 index 000000000000..cd070339afbe --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.8 @@ -0,0 +1,102 @@ +.\" Copyright (c) 2001 Networks Associates Technology, Inc. +.\" All rights reserved. +.\" Copyright (c) 2009 Dmitry V. Levin +.\" All rights reserved. +.\" Copyright (c) 2009,2019 Solar Designer +.\" All rights reserved. +.\" +.\" Portions of this software were developed for the FreeBSD Project by +.\" ThinkSec AS and NAI Labs, the Security Research Division of Network +.\" Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 +.\" ("CBOSS"), as part of the DARPA CHATS research program. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. The name of the author may not be used to endorse or promote +.\" products derived from this software without specific prior written +.\" permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd December 9, 2019 +.Dt PAM_PASSWDQC 8 +.Os "Openwall Project" +.Sh NAME +.Nm pam_passwdqc +.Nd Password quality-control PAM module +.Sh SYNOPSIS +.Op Ar service-name +.Ar module-type +.Ar control-flag +.Pa pam_passwdqc +.Op Ar options +.Sh DESCRIPTION +The +.Nm +module is a simple password strength checking module for +PAM. +In addition to checking regular passwords, it offers support for +passphrases and can provide randomly generated ones. +.Pp +The +.Nm +module provides functionality for only one PAM management group: +password changing. +In terms of the +.Ar module-type +parameter, this is the +.Dq Li password +feature. +.Pp +The +.Fn pam_chauthtok +service function may ask the user for a new password, and verify that +it meets certain minimum standards. +If the chosen password is unsatisfactory, the service function returns +.Dv PAM_AUTHTOK_ERR . +.Pp +The set of options that may be passed to the module is exactly the +same as the set of options that may be specified in the configuration +file (suggested location +.Pa /etc/passwdqc.conf , +to be specified in the +.Cm config=/etc/passwdqc.conf +option). These options are described in +.Xr passwdqc.conf 5 . +.Sh SEE ALSO +.Xr pam.conf 5 , +.Xr passwdqc.conf 5 , +.Xr pam 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh AUTHORS +The +.Nm +module was written for Openwall GNU/*/Linux by +.An Solar Designer Aq solar at openwall.com . +This manual page was written for the +.Fx +Project by +ThinkSec AS and NAI Labs, the Security Research Division of Network +Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 +.Pq Dq CBOSS , +as part of the DARPA CHATS research program. +It has since been revised, most importantly to refer to +.Xr passwdqc.conf 5 +instead of describing the options right on this page. diff --git a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.c b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.c index 7c3973105cd2..6be3d9e0499f 100644 --- a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.c +++ b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.c @@ -1,559 +1,564 @@ /* - * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + * Copyright (c) 2000-2003,2005,2012,2016,2019,2021 by Solar Designer + * Copyright (c) 2017,2018 by Dmitry V. Levin + * Copyright (c) 2017,2018 by Oleg Solovyov + * See LICENSE */ +#ifdef __FreeBSD__ +/* For vsnprintf(3) */ +#define _XOPEN_SOURCE 600 +#else #define _XOPEN_SOURCE 500 #define _XOPEN_SOURCE_EXTENDED #define _XOPEN_VERSION 500 +#define _DEFAULT_SOURCE +#endif #include #include #include #include #include #include #include #ifdef HAVE_SHADOW #include #endif +#ifdef HAVE_LIBAUDIT +#include +#include +#endif #define PAM_SM_PASSWORD #ifndef LINUX_PAM #include #endif #include #include "pam_macros.h" #if !defined(PAM_EXTERN) && !defined(PAM_STATIC) #define PAM_EXTERN extern #endif #if !defined(PAM_AUTHTOK_RECOVERY_ERR) && defined(PAM_AUTHTOK_RECOVER_ERR) #define PAM_AUTHTOK_RECOVERY_ERR PAM_AUTHTOK_RECOVER_ERR #endif -#if defined(__sun__) && !defined(LINUX_PAM) && !defined(_OPENPAM) -/* Sun's PAM doesn't use const here */ +#if (defined(__sun) || defined(__hpux)) && \ + !defined(LINUX_PAM) && !defined(_OPENPAM) +/* Sun's PAM doesn't use const here, while Linux-PAM and OpenPAM do */ #define lo_const #else #define lo_const const #endif +#ifdef _OPENPAM +/* OpenPAM doesn't use const here, while Linux-PAM does */ +#define l_const +#else +#define l_const lo_const +#endif typedef lo_const void *pam_item_t; #include "passwdqc.h" -#define F_ENFORCE_MASK 0x00000003 -#define F_ENFORCE_USERS 0x00000001 -#define F_ENFORCE_ROOT 0x00000002 -#define F_ENFORCE_EVERYONE F_ENFORCE_MASK -#define F_NON_UNIX 0x00000004 -#define F_ASK_OLDAUTHTOK_MASK 0x00000030 -#define F_ASK_OLDAUTHTOK_PRELIM 0x00000010 -#define F_ASK_OLDAUTHTOK_UPDATE 0x00000020 -#define F_CHECK_OLDAUTHTOK 0x00000040 -#define F_USE_FIRST_PASS 0x00000100 -#define F_USE_AUTHTOK 0x00000200 - -typedef struct { - passwdqc_params_t qc; - int flags; - int retry; -} params_t; - -static params_t defaults = { - { - {INT_MAX, 24, 12, 8, 7}, /* min */ - 40, /* max */ - 3, /* passphrase_words */ - 4, /* match_length */ - 1, /* similar_deny */ - 42 /* random_bits */ - }, - F_ENFORCE_EVERYONE, /* flags */ - 3 /* retry */ -}; +#include "passwdqc_i18n.h" #define PROMPT_OLDPASS \ - "Enter current password: " + _("Enter current password: ") #define PROMPT_NEWPASS1 \ - "Enter new password: " + _("Enter new password: ") #define PROMPT_NEWPASS2 \ - "Re-type new password: " + _("Re-type new password: ") #define MESSAGE_MISCONFIGURED \ - "System configuration error. Please contact your administrator." + _("System configuration error. Please contact your administrator.") #define MESSAGE_INVALID_OPTION \ - "pam_passwdqc: Invalid option: \"%s\"." + "pam_passwdqc: %s." #define MESSAGE_INTRO_PASSWORD \ - "\nYou can now choose the new password.\n" + _("\nYou can now choose the new password.\n") #define MESSAGE_INTRO_BOTH \ - "\nYou can now choose the new password or passphrase.\n" -#define MESSAGE_EXPLAIN_PASSWORD_1 \ - "A valid password should be a mix of upper and lower case letters,\n" \ - "digits and other characters. You can use a%s %d character long\n" \ - "password with characters from at least 3 of these 4 classes.\n" \ - "Characters that form a common pattern are discarded by the check.\n" -#define MESSAGE_EXPLAIN_PASSWORD_2 \ - "A valid password should be a mix of upper and lower case letters,\n" \ - "digits and other characters. You can use a%s %d character long\n" \ - "password with characters from at least 3 of these 4 classes, or\n" \ - "a%s %d character long password containing characters from all the\n" \ - "classes. Characters that form a common pattern are discarded by\n" \ - "the check.\n" -#define MESSAGE_EXPLAIN_PASSPHRASE \ - "A passphrase should be of at least %d words, %d to %d characters\n" \ - "long and contain enough different characters.\n" + _("\nYou can now choose the new password or passphrase.\n") + +#define MESSAGE_EXPLAIN_PASSWORD_1_CLASS(count) \ + P3_( \ + "A good password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d character.\n", \ + \ + "A good password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d characters.\n", \ + count), (count) + +#define MESSAGE_EXPLAIN_PASSWORD_N_CLASSES(count) \ + P3_( \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d character\n" \ + "from at least %d of these 4 classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d characters\n" \ + "from at least %d of these 4 classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + count), (count) + +#define MESSAGE_EXPLAIN_PASSWORD_ALL_CLASSES(count) \ + P3_( \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d character\n" \ + "from all of these classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d characters\n" \ + "from all of these classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + count), (count) + +#define MESSAGE_EXPLAIN_PASSWORD_ALL_OR_3_CLASSES(count) \ + P3_( \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d character\n" \ + "from all of these classes, or a password containing at least %d characters\n" \ + "from just 3 of these 4 classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + \ + "A valid password should be a mix of upper and lower case letters, digits, and\n" \ + "other characters. You can use a password containing at least %d characters\n" \ + "from all of these classes, or a password containing at least %d characters\n" \ + "from just 3 of these 4 classes.\n" \ + "An upper case letter that begins the password and a digit that ends it do not\n" \ + "count towards the number of character classes used.\n", \ + count), (count) + +#define MESSAGE_EXPLAIN_PASSPHRASE(count) \ + P3_(\ + "A passphrase should be of at least %d word, %d to %d characters long, and\n" \ + "contain enough different characters.\n", \ + \ + "A passphrase should be of at least %d words, %d to %d characters long, and\n" \ + "contain enough different characters.\n", \ + count), (count) + #define MESSAGE_RANDOM \ - "Alternatively, if noone else can see your terminal now, you can\n" \ - "pick this as your password: \"%s\".\n" + _("Alternatively, if no one else can see your terminal now, you can pick this as\n" \ + "your password: \"%s\".\n") #define MESSAGE_RANDOMONLY \ - "This system is configured to permit randomly generated passwords\n" \ - "only. If noone else can see your terminal now, you can pick this\n" \ - "as your password: \"%s\". Otherwise, come back later.\n" + _("This system is configured to permit randomly generated passwords only.\n" \ + "If no one else can see your terminal now, you can pick this as your\n" \ + "password: \"%s\". Otherwise come back later.\n") #define MESSAGE_RANDOMFAILED \ - "This system is configured to use randomly generated passwords\n" \ - "only, but the attempt to generate a password has failed. This\n" \ - "could happen for a number of reasons: you could have requested\n" \ - "an impossible password length, or the access to kernel random\n" \ - "number pool could have failed." + _("This system is configured to use randomly generated passwords only,\n" \ + "but the attempt to generate a password has failed. This could happen\n" \ + "for a number of reasons: you could have requested an impossible password\n" \ + "length, or the access to kernel random number pool could have failed.") #define MESSAGE_TOOLONG \ - "This password may be too long for some services. Choose another." + _("This password may be too long for some services. Choose another.") #define MESSAGE_TRUNCATED \ - "Warning: your longer password will be truncated to 8 characters." + _("Warning: your longer password will be truncated to 8 characters.") #define MESSAGE_WEAKPASS \ - "Weak password: %s." + _("Weak password: %s.") #define MESSAGE_NOTRANDOM \ - "Sorry, you've mistyped the password that was generated for you." + _("Sorry, you've mistyped the password that was generated for you.") #define MESSAGE_MISTYPED \ - "Sorry, passwords do not match." + _("Sorry, passwords do not match.") #define MESSAGE_RETRY \ - "Try again." + _("Try again.") -static int converse(pam_handle_t *pamh, int style, lo_const char *text, +static int logaudit(pam_handle_t *pamh, int status, passwdqc_params_t *params) +{ +#ifdef HAVE_LIBAUDIT + if (!(params->pam.flags & F_NO_AUDIT)) { + int rc = pam_modutil_audit_write(pamh, AUDIT_USER_CHAUTHTOK, "pam_passwdqc", status); + if (status == PAM_SUCCESS) + status = rc; + } +#else /* !HAVE_LIBAUDIT */ + (void) pamh; +#endif + passwdqc_params_free(params); + return status; +} + +static int converse(pam_handle_t *pamh, int style, l_const char *text, struct pam_response **resp) { - struct pam_conv *conv; + pam_item_t item; + const struct pam_conv *conv; struct pam_message msg, *pmsg; int status; - status = pam_get_item(pamh, PAM_CONV, (pam_item_t *)&conv); + *resp = NULL; + status = pam_get_item(pamh, PAM_CONV, &item); if (status != PAM_SUCCESS) return status; + conv = item; pmsg = &msg; msg.msg_style = style; msg.msg = text; - *resp = NULL; return conv->conv(1, (lo_const struct pam_message **)&pmsg, resp, conv->appdata_ptr); } #ifdef __GNUC__ __attribute__ ((format (printf, 3, 4))) #endif static int say(pam_handle_t *pamh, int style, const char *format, ...) { va_list args; char buffer[0x800]; int needed; struct pam_response *resp; int status; va_start(args, format); needed = vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); if ((unsigned int)needed < sizeof(buffer)) { status = converse(pamh, style, buffer, &resp); - _pam_overwrite(buffer); + pwqc_drop_pam_reply(resp, 1); } else { status = PAM_ABORT; - memset(buffer, 0, sizeof(buffer)); } + _passwdqc_memzero(buffer, sizeof(buffer)); return status; } -static int check_max(params_t *params, pam_handle_t *pamh, const char *newpass) +static int check_max(passwdqc_params_qc_t *qc, pam_handle_t *pamh, + const char *newpass) { - if ((int)strlen(newpass) > params->qc.max) { - if (params->qc.max != 8) { + if (strlen(newpass) > (size_t)qc->max) { + if (qc->max != 8) { say(pamh, PAM_ERROR_MSG, MESSAGE_TOOLONG); return -1; } say(pamh, PAM_TEXT_INFO, MESSAGE_TRUNCATED); } return 0; } -static int parse(params_t *params, pam_handle_t *pamh, - int argc, const char **argv) +static int check_pass(struct passwd *pw, const char *pass) { - const char *p; - char *e; - int i; - unsigned long v; - - while (argc) { - if (!strncmp(*argv, "min=", 4)) { - p = *argv + 4; - for (i = 0; i < 5; i++) { - if (!strncmp(p, "disabled", 8)) { - v = INT_MAX; - p += 8; - } else { - v = strtoul(p, &e, 10); - p = e; - } - if (i < 4 && *p++ != ',') break; - if (v > INT_MAX) break; - if (i && (int)v > params->qc.min[i - 1]) break; - params->qc.min[i] = v; - } - if (*p) break; - } else - if (!strncmp(*argv, "max=", 4)) { - v = strtoul(*argv + 4, &e, 10); - if (*e || v < 8 || v > INT_MAX) break; - params->qc.max = v; - } else - if (!strncmp(*argv, "passphrase=", 11)) { - v = strtoul(*argv + 11, &e, 10); - if (*e || v > INT_MAX) break; - params->qc.passphrase_words = v; - } else - if (!strncmp(*argv, "match=", 6)) { - v = strtoul(*argv + 6, &e, 10); - if (*e || v > INT_MAX) break; - params->qc.match_length = v; - } else - if (!strncmp(*argv, "similar=", 8)) { - if (!strcmp(*argv + 8, "permit")) - params->qc.similar_deny = 0; - else - if (!strcmp(*argv + 8, "deny")) - params->qc.similar_deny = 1; - else - break; - } else - if (!strncmp(*argv, "random=", 7)) { - v = strtoul(*argv + 7, &e, 10); - if (!strcmp(e, ",only")) { - e += 5; - params->qc.min[4] = INT_MAX; - } - if (*e || v > INT_MAX) break; - params->qc.random_bits = v; - } else - if (!strncmp(*argv, "enforce=", 8)) { - params->flags &= ~F_ENFORCE_MASK; - if (!strcmp(*argv + 8, "users")) - params->flags |= F_ENFORCE_USERS; - else - if (!strcmp(*argv + 8, "everyone")) - params->flags |= F_ENFORCE_EVERYONE; - else - if (strcmp(*argv + 8, "none")) - break; - } else - if (!strcmp(*argv, "non-unix")) { - if (params->flags & F_CHECK_OLDAUTHTOK) break; - params->flags |= F_NON_UNIX; - } else - if (!strncmp(*argv, "retry=", 6)) { - v = strtoul(*argv + 6, &e, 10); - if (*e || v > INT_MAX) break; - params->retry = v; - } else - if (!strncmp(*argv, "ask_oldauthtok", 14)) { - params->flags &= ~F_ASK_OLDAUTHTOK_MASK; - if (params->flags & F_USE_FIRST_PASS) break; - if (!strcmp(*argv + 14, "=update")) - params->flags |= F_ASK_OLDAUTHTOK_UPDATE; - else - if (!(*argv)[14]) - params->flags |= F_ASK_OLDAUTHTOK_PRELIM; - else - break; - } else - if (!strcmp(*argv, "check_oldauthtok")) { - if (params->flags & F_NON_UNIX) break; - params->flags |= F_CHECK_OLDAUTHTOK; - } else - if (!strcmp(*argv, "use_first_pass")) { - if (params->flags & F_ASK_OLDAUTHTOK_MASK) break; - params->flags |= F_USE_FIRST_PASS | F_USE_AUTHTOK; - } else - if (!strcmp(*argv, "use_authtok")) { - params->flags |= F_USE_AUTHTOK; - } else - break; - argc--; argv++; - } + const char *hash; + int retval; - if (argc) { - say(pamh, PAM_ERROR_MSG, getuid() != 0 ? - MESSAGE_MISCONFIGURED : MESSAGE_INVALID_OPTION, *argv); - return PAM_ABORT; +#ifdef HAVE_SHADOW +#ifdef __hpux + if (iscomsec()) { +#else + if (!strcmp(pw->pw_passwd, "x")) { +#endif + struct spwd *spw = getspnam(pw->pw_name); + endspent(); + if (!spw) + return -1; + hash = NULL; + if (strlen(spw->sp_pwdp) >= 13) { +#ifdef __hpux + hash = bigcrypt(pass, spw->sp_pwdp); +#else + hash = crypt(pass, spw->sp_pwdp); +#endif + } + retval = (hash && !strcmp(hash, spw->sp_pwdp)) ? 0 : -1; + _passwdqc_memzero(spw->sp_pwdp, strlen(spw->sp_pwdp)); + return retval; } +#endif - return PAM_SUCCESS; + hash = NULL; + if (strlen(pw->pw_passwd) >= 13) + hash = crypt(pass, pw->pw_passwd); + retval = (hash && !strcmp(hash, pw->pw_passwd)) ? 0 : -1; + _passwdqc_memzero(pw->pw_passwd, strlen(pw->pw_passwd)); + return retval; +} + +static int am_root(pam_handle_t *pamh) +{ + pam_item_t item; + const char *service; + + if (getuid() != 0) + return 0; + + if (pam_get_item(pamh, PAM_SERVICE, &item) != PAM_SUCCESS) + return 0; + service = item; + + return !strcmp(service, "passwd") || !strcmp(service, "chpasswd"); } PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { - params_t params; + passwdqc_params_t params; struct pam_response *resp; struct passwd *pw, fake_pw; -#ifdef HAVE_SHADOW - struct spwd *spw; -#endif - char *user, *oldpass, *newpass, *randompass; - const char *reason; + pam_item_t item; + const char *user, *oldpass, *newpass; + char *trypass, *randompass; + char *parse_reason; + const char *check_reason; int ask_oldauthtok; int randomonly, enforce, retries_left, retry_wanted; int status; - params = defaults; - status = parse(¶ms, pamh, argc, argv); - if (status != PAM_SUCCESS) - return status; + passwdqc_params_reset(¶ms); + if (passwdqc_params_parse(¶ms, &parse_reason, argc, argv)) { + say(pamh, PAM_ERROR_MSG, am_root(pamh) ? + MESSAGE_INVALID_OPTION : MESSAGE_MISCONFIGURED, + parse_reason); + free(parse_reason); + return PAM_ABORT; + } + status = PAM_SUCCESS; ask_oldauthtok = 0; if (flags & PAM_PRELIM_CHECK) { - if (params.flags & F_ASK_OLDAUTHTOK_PRELIM) + if (params.pam.flags & F_ASK_OLDAUTHTOK_PRELIM) ask_oldauthtok = 1; - } else - if (flags & PAM_UPDATE_AUTHTOK) { - if (params.flags & F_ASK_OLDAUTHTOK_UPDATE) + } else if (flags & PAM_UPDATE_AUTHTOK) { + if (params.pam.flags & F_ASK_OLDAUTHTOK_UPDATE) ask_oldauthtok = 1; - } else + } else { + passwdqc_params_free(¶ms); return PAM_SERVICE_ERR; + } - if (ask_oldauthtok && getuid() != 0) { + if (ask_oldauthtok && !am_root(pamh)) { status = converse(pamh, PAM_PROMPT_ECHO_OFF, PROMPT_OLDPASS, &resp); if (status == PAM_SUCCESS) { if (resp && resp->resp) { status = pam_set_item(pamh, PAM_OLDAUTHTOK, resp->resp); - _pam_drop_reply(resp, 1); + pwqc_drop_pam_reply(resp, 1); } else status = PAM_AUTHTOK_RECOVERY_ERR; } if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); } - if (flags & PAM_PRELIM_CHECK) + if (flags & PAM_PRELIM_CHECK) { + passwdqc_params_free(¶ms); return status; + } - status = pam_get_item(pamh, PAM_USER, (pam_item_t *)&user); + status = pam_get_item(pamh, PAM_USER, &item); if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); + user = item; - status = pam_get_item(pamh, PAM_OLDAUTHTOK, (pam_item_t *)&oldpass); + status = pam_get_item(pamh, PAM_OLDAUTHTOK, &item); if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); + oldpass = item; - if (params.flags & F_NON_UNIX) { + if (params.pam.flags & F_NON_UNIX) { pw = &fake_pw; - pw->pw_name = user; + memset(pw, 0, sizeof(*pw)); + pw->pw_name = (char *)user; pw->pw_gecos = ""; + pw->pw_dir = ""; } else { +/* As currently implemented, we don't avoid timing leaks for valid vs. not + * usernames and hashes. Normally, the username would have already been + * checked and determined valid, and the check_oldauthtok option is only needed + * on systems that happen to have similar timing leaks all over the place. */ pw = getpwnam(user); endpwent(); if (!pw) - return PAM_USER_UNKNOWN; - if ((params.flags & F_CHECK_OLDAUTHTOK) && getuid() != 0) { - if (!oldpass) - status = PAM_AUTH_ERR; - else -#ifdef HAVE_SHADOW - if (!strcmp(pw->pw_passwd, "x")) { - spw = getspnam(user); - endspent(); - if (spw) { - if (strcmp(crypt(oldpass, spw->sp_pwdp), - spw->sp_pwdp)) - status = PAM_AUTH_ERR; - memset(spw->sp_pwdp, 0, - strlen(spw->sp_pwdp)); - } else - status = PAM_AUTH_ERR; - } else -#endif - if (strcmp(crypt(oldpass, pw->pw_passwd), - pw->pw_passwd)) - status = PAM_AUTH_ERR; - } - memset(pw->pw_passwd, 0, strlen(pw->pw_passwd)); + return logaudit(pamh, PAM_USER_UNKNOWN, ¶ms); + if ((params.pam.flags & F_CHECK_OLDAUTHTOK) && !am_root(pamh) + && (!oldpass || check_pass(pw, oldpass))) + status = PAM_AUTH_ERR; + _passwdqc_memzero(pw->pw_passwd, strlen(pw->pw_passwd)); if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); } randomonly = params.qc.min[4] > params.qc.max; - if (getuid() != 0) - enforce = params.flags & F_ENFORCE_USERS; + if (am_root(pamh)) + enforce = params.pam.flags & F_ENFORCE_ROOT; else - enforce = params.flags & F_ENFORCE_ROOT; + enforce = params.pam.flags & F_ENFORCE_USERS; - if (params.flags & F_USE_AUTHTOK) { - status = pam_get_item(pamh, PAM_AUTHTOK, - (pam_item_t *)&newpass); + if (params.pam.flags & F_USE_AUTHTOK) { + status = pam_get_item(pamh, PAM_AUTHTOK, &item); if (status != PAM_SUCCESS) - return status; - if (!newpass || (check_max(¶ms, pamh, newpass) && enforce)) - return PAM_AUTHTOK_ERR; - reason = _passwdqc_check(¶ms.qc, newpass, oldpass, pw); - if (reason) { - say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, reason); + return logaudit(pamh, status, ¶ms); + newpass = item; + if (!newpass || + (check_max(¶ms.qc, pamh, newpass) && enforce)) + return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); + check_reason = + passwdqc_check(¶ms.qc, newpass, oldpass, pw); + if (check_reason) { + say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, + check_reason); if (enforce) status = PAM_AUTHTOK_ERR; } - return status; + return logaudit(pamh, status, ¶ms); } - retries_left = params.retry; + retries_left = params.pam.retry; retry: retry_wanted = 0; if (!randomonly && params.qc.passphrase_words && params.qc.min[2] <= params.qc.max) status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_BOTH); else status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_PASSWORD); if (status != PAM_SUCCESS) - return status; - - if (!randomonly && params.qc.min[3] <= params.qc.min[4]) - status = say(pamh, PAM_TEXT_INFO, MESSAGE_EXPLAIN_PASSWORD_1, - params.qc.min[3] == 8 || params.qc.min[3] == 11 ? "n" : "", + return logaudit(pamh, status, ¶ms); + + if (!randomonly && params.qc.min[0] == params.qc.min[4]) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_1_CLASS(params.qc.min[4])); + + else if (!randomonly && params.qc.min[3] == params.qc.min[4]) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_N_CLASSES(params.qc.min[4]), + params.qc.min[1] != params.qc.min[3] ? 3 : 2); + else if (!randomonly && params.qc.min[3] == INT_MAX) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_ALL_CLASSES(params.qc.min[4])); + else if (!randomonly) { + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_ALL_OR_3_CLASSES(params.qc.min[4]), params.qc.min[3]); - else - if (!randomonly) - status = say(pamh, PAM_TEXT_INFO, MESSAGE_EXPLAIN_PASSWORD_2, - params.qc.min[3] == 8 || params.qc.min[3] == 11 ? "n" : "", - params.qc.min[3], - params.qc.min[4] == 8 || params.qc.min[4] == 11 ? "n" : "", - params.qc.min[4]); + } if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); if (!randomonly && - params.qc.passphrase_words && - params.qc.min[2] <= params.qc.max) { - status = say(pamh, PAM_TEXT_INFO, MESSAGE_EXPLAIN_PASSPHRASE, - params.qc.passphrase_words, + params.qc.passphrase_words && params.qc.min[2] <= params.qc.max) { + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSPHRASE(params.qc.passphrase_words), params.qc.min[2], params.qc.max); if (status != PAM_SUCCESS) - return status; + return logaudit(pamh, status, ¶ms); } - randompass = _passwdqc_random(¶ms.qc); + randompass = passwdqc_random(¶ms.qc); if (randompass) { status = say(pamh, PAM_TEXT_INFO, randomonly ? MESSAGE_RANDOMONLY : MESSAGE_RANDOM, randompass); if (status != PAM_SUCCESS) { - _pam_overwrite(randompass); - randompass = NULL; + pwqc_overwrite_string(randompass); + pwqc_drop_mem(randompass); } - } else - if (randomonly) { - say(pamh, PAM_ERROR_MSG, getuid() != 0 ? - MESSAGE_MISCONFIGURED : MESSAGE_RANDOMFAILED); - return PAM_AUTHTOK_ERR; + } else if (randomonly) { + say(pamh, PAM_ERROR_MSG, am_root(pamh) ? + MESSAGE_RANDOMFAILED : MESSAGE_MISCONFIGURED); + return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); } status = converse(pamh, PAM_PROMPT_ECHO_OFF, PROMPT_NEWPASS1, &resp); if (status == PAM_SUCCESS && (!resp || !resp->resp)) status = PAM_AUTHTOK_ERR; if (status != PAM_SUCCESS) { - if (randompass) _pam_overwrite(randompass); - return status; + pwqc_overwrite_string(randompass); + pwqc_drop_mem(randompass); + return logaudit(pamh, status, ¶ms); } - newpass = strdup(resp->resp); + trypass = strdup(resp->resp); - _pam_drop_reply(resp, 1); + pwqc_drop_pam_reply(resp, 1); - if (!newpass) { - if (randompass) _pam_overwrite(randompass); - return PAM_AUTHTOK_ERR; + if (!trypass) { + pwqc_overwrite_string(randompass); + pwqc_drop_mem(randompass); + return logaudit(pamh, PAM_AUTHTOK_ERR, ¶ms); } - if (check_max(¶ms, pamh, newpass) && enforce) { + if (check_max(¶ms.qc, pamh, trypass) && enforce) { status = PAM_AUTHTOK_ERR; retry_wanted = 1; } - reason = NULL; + check_reason = NULL; /* unused */ if (status == PAM_SUCCESS && - (!randompass || !strstr(newpass, randompass)) && + (!randompass || !strstr(trypass, randompass)) && (randomonly || - (reason = _passwdqc_check(¶ms.qc, newpass, oldpass, pw)))) { + (check_reason = passwdqc_check(¶ms.qc, trypass, oldpass, pw)))) { if (randomonly) say(pamh, PAM_ERROR_MSG, MESSAGE_NOTRANDOM); else - say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, reason); + say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, + check_reason); if (enforce) { status = PAM_AUTHTOK_ERR; retry_wanted = 1; } } if (status == PAM_SUCCESS) status = converse(pamh, PAM_PROMPT_ECHO_OFF, PROMPT_NEWPASS2, &resp); if (status == PAM_SUCCESS) { if (resp && resp->resp) { - if (strcmp(newpass, resp->resp)) { + if (strcmp(trypass, resp->resp)) { status = say(pamh, PAM_ERROR_MSG, MESSAGE_MISTYPED); if (status == PAM_SUCCESS) { status = PAM_AUTHTOK_ERR; retry_wanted = 1; } } - _pam_drop_reply(resp, 1); + pwqc_drop_pam_reply(resp, 1); } else status = PAM_AUTHTOK_ERR; } if (status == PAM_SUCCESS) - status = pam_set_item(pamh, PAM_AUTHTOK, newpass); + status = pam_set_item(pamh, PAM_AUTHTOK, trypass); - if (randompass) _pam_overwrite(randompass); - _pam_overwrite(newpass); - free(newpass); + pwqc_overwrite_string(randompass); + pwqc_drop_mem(randompass); + + pwqc_overwrite_string(trypass); + pwqc_drop_mem(trypass); if (retry_wanted && --retries_left > 0) { status = say(pamh, PAM_TEXT_INFO, MESSAGE_RETRY); if (status == PAM_SUCCESS) goto retry; } - return status; + return logaudit(pamh, status, ¶ms); } #ifdef PAM_MODULE_ENTRY PAM_MODULE_ENTRY("pam_passwdqc"); #elif defined(PAM_STATIC) -struct pam_module _pam_passwdqc_modstruct = { +const struct pam_module _pam_passwdqc_modstruct = { "pam_passwdqc", NULL, NULL, NULL, NULL, NULL, pam_sm_chauthtok }; #endif diff --git a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.map b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.map new file mode 100644 index 000000000000..83d24d09099c --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.map @@ -0,0 +1,7 @@ +{ + global: + pam_sm_chauthtok; + + local: + *; +}; diff --git a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.spec b/contrib/pam_modules/pam_passwdqc/pam_passwdqc.spec deleted file mode 100644 index 437db6f0c644..000000000000 --- a/contrib/pam_modules/pam_passwdqc/pam_passwdqc.spec +++ /dev/null @@ -1,67 +0,0 @@ -# $Id: pam_passwdqc.spec,v 1.11 2002/04/16 16:56:52 solar Exp $ - -Summary: Pluggable password "quality check". -Name: pam_passwdqc -Version: 0.5 -Release: owl1 -License: relaxed BSD and (L)GPL-compatible -Group: System Environment/Base -Source: pam_passwdqc-%version.tar.gz -BuildRoot: /override/%name-%version - -%description -pam_passwdqc is a simple password strength checking module for -PAM-aware password changing programs, such as passwd(1). In addition -to checking regular passwords, it offers support for passphrases and -can provide randomly generated passwords. All features are optional -and can be (re-)configured without rebuilding. - -%prep -%setup -q - -%build -make CFLAGS="-c -Wall -fPIC -DHAVE_SHADOW -DLINUX_PAM $RPM_OPT_FLAGS" - -%install -rm -rf $RPM_BUILD_ROOT -make install FAKEROOT=$RPM_BUILD_ROOT - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root) -%doc LICENSE README -/lib/security/pam_passwdqc.so - -%changelog -* Tue Apr 16 2002 Solar Designer -- 0.5: preliminary OpenPAM (FreeBSD-current) support in the code and related -code cleanups (thanks to Dag-Erling Smorgrav). - -* Thu Feb 07 2002 Michail Litvak -- Enforce our new spec file conventions. - -* Sun Nov 04 2001 Solar Designer -- Updated to 0.4: -- Added "ask_oldauthtok" and "check_oldauthtok" as needed for stacking with -the Solaris pam_unix; -- Permit for stacking of more than one instance of this module (no statics). - -* Tue Feb 13 2001 Solar Designer -- Install the module as mode 755. - -* Tue Dec 19 2000 Solar Designer -- Added "-Wall -fPIC" to the CFLAGS. - -* Mon Oct 30 2000 Solar Designer -- 0.3: portability fixes (this might build on non-Linux-PAM now). - -* Fri Sep 22 2000 Solar Designer -- 0.2: added "use_authtok", added README. - -* Fri Aug 18 2000 Solar Designer -- 0.1, "retry_wanted" bugfix. - -* Sun Jul 02 2000 Solar Designer -- Initial version (non-public). diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc.conf b/contrib/pam_modules/pam_passwdqc/passwdqc.conf new file mode 100644 index 000000000000..be8eab00c52c --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc.conf @@ -0,0 +1,12 @@ +min=disabled,24,11,8,7 +max=72 +passphrase=3 +match=4 +similar=deny +random=47 +enforce=everyone +retry=3 +# The below are just examples, by default none of these are used +#wordlist=/usr/share/john/password.lst +#denylist=/etc/passwdqc.deny +#filter=/opt/passwdqc/hibp.pwq diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc.conf.5 b/contrib/pam_modules/pam_passwdqc/passwdqc.conf.5 new file mode 100644 index 000000000000..5f659317c4dc --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc.conf.5 @@ -0,0 +1,312 @@ +.\" Copyright (c) 2000-2003,2005,2008,2019,2020 Solar Designer +.\" All rights reserved. +.\" Copyright (c) 2001 Networks Associates Technology, Inc. +.\" All rights reserved. +.\" Copyright (c) 2009 Dmitry V. Levin +.\" All rights reserved. +.\" +.\" Portions of this software were developed for the FreeBSD Project by +.\" ThinkSec AS and NAI Labs, the Security Research Division of Network +.\" Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 +.\" ("CBOSS"), as part of the DARPA CHATS research program. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. The name of the author may not be used to endorse or promote +.\" products derived from this software without specific prior written +.\" permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 10, 2021 +.Dt PASSWDQC.CONF 5 +.Os "Openwall Project" +.Sh NAME +.Nm passwdqc.conf +.Nd libpasswdqc configuration file +.Sh DESCRIPTION +libpasswdqc is a simple password strength checking library. +In addition to checking regular passwords, it offers support for +passphrases and can provide randomly generated ones. +A +.Nm +configuration file may be used to override default libpasswdqc settings. +.Sh FORMAT +A +.Nm +file consists of 0 or more lines of the following format: +.Dl Ar option Ns = Ns Ar value +.Pp +Empty lines and lines beginning with +.Dq Li # +are ignored. +Whitespace characters between the +.Ar option , +.Dq Li = , +and +.Ar value +are not allowed. +.Sh DIRECTIVE OPTIONS +.Bl -tag -width indent +.It Cm config Ns = Ns Ar FILE +Load the specified configuration +.Ar FILE +in the +.Cm passwdqc.conf +format. +This file may define any options described in this manual, +including load of yet another configuration file, but loops are not allowed. +.El +.Sh PASSWORD QUALITY CONTROL OPTIONS +.Bl -tag -width Ds +.Sm off +.It Xo +.Cm min No = +.Ar N0 , N1 , N2 , N3 , N4 +.Xc +.Sm on +.Pq default: min=disabled,24,11,8,7 +The minimum allowed password lengths for different kinds of +passwords/passphrases. +The keyword +.Cm disabled +can be used to +disallow passwords of a given kind regardless of their length. +Each subsequent number is required to be no larger than the preceding +one. +.Pp +.Ar N0 +is used for passwords consisting of characters from one character +class only. +The character classes are: digits, lower-case letters, upper-case +letters, and other characters. +There is also a special class for +.No non- Ns Tn ASCII +characters, which could not be classified, but are assumed to be non-digits. +.Pp +.Ar N1 +is used for passwords consisting of characters from two character +classes that do not meet the requirements for a passphrase. +.Pp +.Ar N2 +is used for passphrases. +Note that besides meeting this length requirement, +a passphrase must also consist of a sufficient number of words (see the +.Cm passphrase +option below). +.Pp +.Ar N3 +and +.Ar N4 +are used for passwords consisting of characters from three +and four character classes, respectively. +.Pp +When calculating the number of character classes, upper-case letters +used as the first character and digits used as the last character of a +password are not counted. +.Pp +In addition to being sufficiently long, passwords are required to +contain enough different characters for the character classes and +the minimum length they have been checked against. +.Pp +.It Cm max Ns = Ns Ar N +.Pq default: Cm max Ns = Ns 72 +The maximum allowed password length. +This can be used to prevent users from setting passwords that may be +too long for some system services. +The value 8 is treated specially: if +.Cm max +is set to 8, passwords longer than 8 characters will not be rejected, +but will be truncated to 8 characters for the strength checks and the +user will be warned. +This is to be used with the traditional DES-based password hashes, +which truncate the password at 8 characters. +.Pp +It is important that you do set +.Cm max Ns = Ns 8 +if you are using the traditional +hashes, or some weak passwords will pass the checks. +.It Cm passphrase Ns = Ns Ar N +.Pq default: Cm passphrase Ns = Ns 3 +The number of words required for a passphrase, or 0 to disable the +support for user-chosen passphrases. +.It Cm match Ns = Ns Ar N +.Pq default: Cm match Ns = Ns 4 +The length of common substring required to conclude that a password is +at least partially based on information found in a character string, +or 0 to disable the substring search. +Note that the password will not be rejected once a weak substring is +found; it will instead be subjected to the usual strength requirements +with the weak substring partially discounted. +.Pp +The substring search is case-insensitive and is able to detect and +remove a common substring spelled backwards. +.It Xo +.Sm off +.Cm similar No = Cm permit | deny +.Sm on +.Xc +.Pq default: Cm similar Ns = Ns Cm deny +Whether a new password is allowed to be similar to the old one. +The passwords are considered to be similar when there is a sufficiently +long common substring and the new password with the substring partially +discounted would be weak. +.It Cm wordlist Ns = Ns Ar FILE +Deny passwords that are based on lines of the tiny external text +.Ar FILE , +which can reasonably be e.g. a list of a few thousand common passwords. +Common dictionary words may also reasonably be included, especially in a +local language other than English, or longer yet common English words. +(passwdqc includes a list of a few thousand common English words of +lengths from 3 to 6 built in. Any word list possibly specified with +this option is used in addition to the built-in word list.) +.Pp +Substring matching and discounting will be used if the +.Cm match +setting +above is non-zero. Please note that this is very inefficient, and isn't +to be used with large wordlists. +.It Cm denylist Ns = Ns Ar FILE +Deny passwords or passphrases directly appearing in the tiny external text +.Ar FILE . +That file can reasonably be e.g. a list of common passwords if +only a relaxed policy is desired and stricter checks are thus disabled +(using their separate options). Such policy would only be somewhat +effective against online/remote attacks, but not against offline attacks +on hashed passwords. +.It Cm filter Ns = Ns Ar FILE +Deny passwords or passphrases directly appearing in a maybe huge binary +filter +.Ar FILE +created with pwqfilter. This is very efficient, needing at +most two random disk reads per query. A filter created from millions of +leaked passwords can reasonably be used on top of passwdqc's other +checks to further reduce the number of passing yet weak passwords +without causing unreasonable inconvenience (as e.g. higher minimum +lengths and character set requirements could). +.It Xo +.Sm off +.Cm random No = Ar N +.Op , Cm only +.Sm on +.Xc +.Pq default: Cm random Ns = Ns 47 +The size of randomly-generated passphrases in bits (24 to 136), +or 0 to disable this feature. +Any passphrase that contains the offered randomly-generated string will be +allowed regardless of other possible restrictions. +.Pp +The +.Cm only +modifier can be used to disallow user-chosen passwords. +.El +.Sh PAM MODULE OPTIONS +.Bl -tag -width indent +.It Xo +.Sm off +.Cm enforce No = Cm none | users | everyone +.Sm on +.Xc +.Pq default: Cm enforce Ns = Ns Cm everyone +The PAM module can be configured to warn of weak passwords only, but not +actually enforce strong passwords. +The +.Cm users +setting is like +.Cm everyone +for all PAM services except +.Cm chpasswd +and +.Cm passwd . +For these two PAM services +.Cm users +will enforce strong passwords for invocations by non-root users only. +.It Cm non-unix +Normally, the PAM module uses +.Xr getpwnam 3 +to obtain the user's personal login information and use that during +the password strength checks. +This behavior can be disabled with the +.Cm non-unix +option. +.It Cm retry Ns = Ns Ar N +.Pq default: Cm retry Ns = Ns 3 +The number of times the PAM module will ask for a new password if the +user fails to provide a sufficiently strong password and enter it twice +the first time. +.It Cm ask_oldauthtok Ns Op = Ns Cm update +Ask for the old password as well. +Normally, the PAM module leaves this task for subsequent modules. +With no argument, the +.Cm ask_oldauthtok +option will cause the PAM module to ask for the old password during the +preliminary check phase. If the +.Cm ask_oldauthtok +option is specified with the +.Cm update +argument, the PAM module will do that during the update phase. +.It Cm check_oldauthtok +This tells the PAM module to validate the old password before giving a +new password prompt. +Normally, this task is left for subsequent modules. +.Pp +The primary use for this option is when +.Cm ask_oldauthtok Ns = Ns Cm update +is also specified, in which case no other module gets a chance to ask +for and validate the password. +Of course, this will only work with +.Ux +passwords. +.It Cm use_first_pass , use_authtok +Use the new password obtained by other modules stacked before the PAM +module. This disables user interaction within the PAM module. +The only difference between +.Cm use_first_pass +and +.Cm use_authtok +is that the former is incompatible with +.Cm ask_oldauthtok . +.It Cm noaudit +If audit is enabled at build time, the PAM module logs audit events once +user tries to change their credentials. This option disables that audit +logging. +.El +.Sh FILES +.Pa /etc/passwdqc.conf +(not read unless this suggested file location is specified with the +.Cm config=/etc/passwdqc.conf +option). +.Sh SEE ALSO +.Xr getpwnam 3 , +.Xr libpasswdqc 3 , +.Xr pam_passwdqc 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh AUTHORS +The pam_passwdqc module was written for Openwall GNU/*/Linux by +.An Solar Designer Aq solar at openwall.com . +This manual page was derived from +.Xr pam_passwdqc 8 . The latter, derived from the author's +documentation, was written for the +.Fx +Project by +ThinkSec AS and NAI Labs, the Security Research Division of Network +Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 +.Pq Dq CBOSS , +as part of the DARPA CHATS research program. diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc.h b/contrib/pam_modules/pam_passwdqc/passwdqc.h index 4767d6bc4220..d31f46231ff1 100644 --- a/contrib/pam_modules/pam_passwdqc/passwdqc.h +++ b/contrib/pam_modules/pam_passwdqc/passwdqc.h @@ -1,24 +1,82 @@ /* - * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + * Copyright (c) 2000-2002,2016,2019,2020,2021 by Solar Designer + * Copyright (c) 2008,2009 by Dmitry V. Levin + * See LICENSE */ -#ifndef _PASSWDQC_H -#define _PASSWDQC_H +#ifndef PASSWDQC_H__ +#define PASSWDQC_H__ +#ifndef _MSC_VER #include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _MSC_VER +/* Partial struct passwd just to accommodate passwdqc code's expectations */ +struct passwd { + char *pw_name; + char *pw_passwd; + char *pw_gecos; + char *pw_dir; + char *pw_shell; +}; +#endif typedef struct { int min[5], max; int passphrase_words; int match_length; int similar_deny; int random_bits; + char *wordlist; + char *denylist; + char *filter; +} passwdqc_params_qc_t; + +typedef struct { + int flags; + int retry; +} passwdqc_params_pam_t; + +typedef struct { + passwdqc_params_qc_t qc; + passwdqc_params_pam_t pam; } passwdqc_params_t; -extern char _passwdqc_wordset_4k[0x1000][6]; +extern const char *passwdqc_check(const passwdqc_params_qc_t *params, + const char *newpass, const char *oldpass, const struct passwd *pw); +extern char *passwdqc_random(const passwdqc_params_qc_t *params); + +extern int passwdqc_params_parse(passwdqc_params_t *params, + char **reason, int argc, const char *const *argv); +extern int passwdqc_params_load(passwdqc_params_t *params, + char **reason, const char *pathname); +extern void passwdqc_params_reset(passwdqc_params_t *params); +extern void passwdqc_params_free(passwdqc_params_t *params); -extern const char *_passwdqc_check(passwdqc_params_t *params, - const char *newpass, const char *oldpass, struct passwd *pw); -extern char *_passwdqc_random(passwdqc_params_t *params); +#define F_ENFORCE_MASK 0x00000003 +#define F_ENFORCE_USERS 0x00000001 +#define F_ENFORCE_ROOT 0x00000002 +#define F_ENFORCE_EVERYONE F_ENFORCE_MASK +#define F_NON_UNIX 0x00000004 +#define F_ASK_OLDAUTHTOK_MASK 0x00000030 +#define F_ASK_OLDAUTHTOK_PRELIM 0x00000010 +#define F_ASK_OLDAUTHTOK_UPDATE 0x00000020 +#define F_CHECK_OLDAUTHTOK 0x00000040 +#define F_USE_FIRST_PASS 0x00000100 +#define F_USE_AUTHTOK 0x00000200 +#define F_NO_AUDIT 0x00000400 +#define PASSWDQC_VERSION "2.0.3" + +extern void (*_passwdqc_memzero)(void *, size_t); + +#ifdef __cplusplus +} #endif + +#endif /* PASSWDQC_H__ */ diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc.pc.in b/contrib/pam_modules/pam_passwdqc/passwdqc.pc.in new file mode 100644 index 000000000000..675d05929598 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc.pc.in @@ -0,0 +1,5 @@ +Name: passwdqc +Description: Password/passphrase strength checking and policy enforcement +Version: @VERSION@ +Libs: -lpasswdqc +Cflags: diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc.spec b/contrib/pam_modules/pam_passwdqc/passwdqc.spec new file mode 100644 index 000000000000..219efcccc292 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc.spec @@ -0,0 +1,394 @@ +Summary: A password/passphrase strength checking and policy enforcement toolset. +Name: passwdqc +Version: 2.0.3 +Release: owl1 +License: BSD-compatible +Group: System Environment/Base +URL: https://www.openwall.com/passwdqc/ +Source: https://www.openwall.com/passwdqc/%name-%version.tar.gz +Provides: pam_passwdqc = %version-%release +Obsoletes: pam_passwdqc < %version-%release +BuildRequires: pam-devel +BuildRoot: /override/%name-%version + +%description +passwdqc is a password/passphrase strength checking and policy +enforcement toolset, including a PAM module (pam_passwdqc), command-line +programs (pwqcheck, pwqfilter, and pwqgen), and a library (libpasswdqc). + +pam_passwdqc is normally invoked on password changes by programs such as +passwd(1). It is capable of checking password or passphrase strength, +enforcing a policy, and offering randomly-generated passphrases, with +all of these features being optional and easily (re-)configurable. + +pwqcheck and pwqgen are standalone password/passphrase strength checking +and random passphrase generator programs, respectively, which are usable +from scripts. + +The pwqfilter program searches, creates, or updates binary passphrase +filter files, which can also be used with pwqcheck and pam_passwdqc. + +libpasswdqc is the underlying library, which may also be used from +third-party programs. + +%package devel +Summary: Libraries and header files for building passwdqc-aware applications. +Group: Development/Libraries +Requires: %name = %version-%release + +%description devel +This package contains development libraries and header files needed for +building passwdqc-aware applications. + +%prep +%setup -q + +%{expand:%%define optflags_lib %{?optflags_lib:%optflags_lib}%{!?optflags_lib:%optflags}} + +%build +%__make \ + CPPFLAGS='-DLINUX_PAM' \ + CFLAGS_bin='-Wall -W %optflags' \ + CFLAGS_lib='-Wall -W -fPIC %optflags_lib' + +%install +rm -rf %buildroot +%__make install DESTDIR=%buildroot MANDIR=%_mandir \ + SHARED_LIBDIR=/%_lib DEVEL_LIBDIR=%_libdir \ + SECUREDIR=/%_lib/security + +%post -p /sbin/ldconfig +%postun -p /sbin/ldconfig + +%files +%defattr(-,root,root) +%doc CHANGES LICENSE README pwqcheck.php +%config(noreplace) /etc/passwdqc.conf +/%_lib/lib*.so* +%_bindir/* +/%_lib/security/pam_passwdqc.so +%_mandir/man[158]/* + +%files devel +%defattr(-,root,root) +%_includedir/*.h +%_libdir/pkgconfig/passwdqc.pc +%_libdir/lib*.so +%_mandir/man3/* + +%changelog +* Fri Jun 23 2023 Dmitry V. Levin 2.0.3-owl1 +- wordset_4k: Move "enroll" to the multiple spellings list (by Solar Designer) +- Don't #include on macOS (by Solar Designer) +- pwqfilter: Allow --pre-hashed after --hash* (by Solar Designer) +- Add pkg-config file (by Egor Ignatov) +- Makefile: add Cygwin support (by Chad Dougherty) +- Remove non-existent symbols from the linker version script +to fix -Wl,--no-undefined-version (by Fangrui Song) +- pam_passwdqc: extend enforce=users to support chpasswd PAM service +in addition to traditionally supported passwd + +* Sun Apr 04 2021 Solar Designer 2.0.2-owl1 +- Changes by Dmitry V. Levin: + - pam_passwdqc: enhance formatting of auto-generated policy descriptions + - Add libpasswdqc(3) manual page + - Add manual page links for all functions documented in libpasswdqc(3) + - Package section 3 manual pages into devel subpackage + - LICENSE: mention the license of CI scripts (which are not packaged) +- Update CHANGES + +* Wed Mar 10 2021 Solar Designer 2.0.1-owl1 +- Changes by Dmitry V. Levin: + - pam_passwdqc: enhance auto-generated policy descriptions + - Makefile: use CPPFLAGS and LDFLAGS consistently + - Makefile: remove *.po dependence on passwdqc.pot + - Remove generated passwdqc.pot from the repository + - po/ru.po: regenerate using "make update_po" + - po/ru.po: translate new messages added in 1.9.0+ +- wordset_4k: Move "whisky" to the multiple spellings list +- Increase maximum size of randomly-generated passphrases to 136 bits +- Add CHANGES based on two latest release announcements, start to maintain it + +* Wed Feb 17 2021 Solar Designer 2.0.0-owl2 +- Update the package description to include pwqfilter. + +* Tue Feb 16 2021 Solar Designer 2.0.0-owl1 +- Introduce and use passwdqc_params_free(). + +* Fri Jan 29 2021 Solar Designer 1.9.0-owl1 +- Add support for external wordlist, denylist, and binary filter. +- passwdqc_random(): Obtain all of the random bytes before the loop. +- Merge changes needed for building with Visual Studio on Windows. + +* Mon Jan 25 2021 Solar Designer 1.5.0-owl1 +- Updated the included wordlist to avoid some inappropriate words in randomly +generated passphrases while not removing any words from the "word-based" check, +and also to have plenty of extra words for subsequent removal of more words +that might be considered inappropriate from the initial 4096 that are used for +randomly generated passphrases. + +* Mon Jan 25 2021 Solar Designer 1.4.1-owl1 +- Set default for "max" to 72 (was 40). +- Document "similar" in pwqcheck print_help() and man page. +- Drop the CVS Id tags (stale ones would be confusing with our move to git). + +* Wed Dec 25 2019 Dmitry V. Levin 1.4.0-owl1 +- Implemented i18n support in pam_passwdqc, contributed by Oleg Solovyov, +Andrey Cherepanov, and me. The i18n support is off by default, it can be +enabled if Linux-PAM is built using --enable-nls configure option. +- Implemented audit support in pam_passwdqc, contributed by Oleg Solovyov +and me. The audit support is off by default, it can be enabled if Linux-PAM +is built using --enable-audit configure option. + +* Mon Dec 09 2019 Solar Designer 1.3.2-owl1 +- Define _DEFAULT_SOURCE for our use of crypt(3) on newer glibc. +The problem was identified and this change tested by Dmitry V. Levin. +- Clarified in the man pages that /etc/passwdqc.conf is not read unless this +suggested file location is specified with the config= option. +- Clarified the OpenBSD configuration example. +- Escape the minus sign in the OpenBSD configuration example to make the +manpage linter happy, patch by Jackson Doak via Unit 193: +https://www.openwall.com/lists/passwdqc-users/2019/04/16/1 + +* Wed Jul 20 2016 Solar Designer 1.3.1-owl1 +- With "non-unix", initialize the pw_dir field in fake_pw now that (since +passwdqc 1.1.3 in 2009) passwdqc_check.c uses that field. +Bug reported by Jim Paris via Debian: https://bugs.debian.org/831356 +- Use size_t for variables holding strlen() return values. +- Cap "max" at 10000 (in case a config set it higher; the default remains 40). +- Check against the shortest allowed password length prior to checking against +the old password (this affects reporting when the old password is empty). +- For zeroization of sensitive data, use a wrapper around memset() called via +a function pointer to reduce the likelihood of a compiler optimizing those +calls out and to allow for overriding of this function with an OS-specific +"secure" memory zeroization function. +- In pwqgen, set stdout to non-buffered, and zeroize and free our own buffer +holding the generated password. + +* Wed Apr 24 2013 Solar Designer 1.3.0-owl1 +- When checking is_simple() after discounting a common character sequence, +apply the (negative) bias even for the passphrase length check. Previously, +we were not doing this because passphrases are normally built from words, and +the same code was being used for the check for dictionary words. +- Expanded the list of common character sequences. Along with the change +above, this reduces the number of passing passwords for RockYou top 100k from +35 to 18, and for RockYou top 1M from 2333 to 2273 (all of these are with +passwdqc's default policy). +- Moved the common character sequences check to be made after the dictionary +words check, to avoid introducing more cases of misreporting. +- Added pwqcheck.php, a PHP wrapper function around the pwqcheck program. + +* Tue Apr 23 2013 Solar Designer 1.2.4-owl1 +- In randomly generated passphrases: toggle case of the first character of each +word only if we wouldn't achieve sufficient entropy otherwise, use a trailing +separator if we achieve sufficient entropy even with the final word omitted +(in fact, we now enable the use of different separators in more cases for this +reason), use dashes rather than spaces to separate words when different +separator characters are not in use. +- Expanded the allowed size of randomly-generated passphrases in bits (now it's +24 to 85 in the tools, and 24 to 136 in the passwdqc_random() interface). + +* Wed Aug 15 2012 Solar Designer 1.2.3-owl1 +- Handle possible NULL returns from crypt(). +- Declared all pre-initialized arrays and structs as const. +- Added Darwin (Mac OS X) support to the Makefile, loosely based on a patch by +Ronald Ip (thanks!) + +* Tue Jun 22 2010 Solar Designer 1.2.2-owl1 +- Introduced the GNU'ish "uninstall" make target name (a synonym for "remove"). +- Makefile updates to make the "install" and "uninstall" targets with their +default settings friendlier to Solaris systems. +- Added a link to a wiki page with detailed Solaris-specific instructions to +the PLATFORMS file. + +* Sat Mar 27 2010 Solar Designer 1.2.1-owl1 +- When matching against the reversed new password, always pass the original +non-reversed new password (possibly with a substring removed) into is_simple(), +but remove or check the correct substring in is_based() considering that the +matching is possibly being done against the reversed password. + +* Tue Mar 16 2010 Solar Designer 1.2.0-owl1 +- New command-line options for pwqcheck: -1 and -2 for reading just 1 and +just 2 lines from stdin, respectively (instead of reading 3 lines, which is +the default), --multi for checking multiple passphrases at once (until EOF). +- With randomly-generated passphrases, encode more entropy per separator +character (by increasing the number of different separators from 8 to 16) and +per word (by altering the case of the first letter of each word), which +increases the default generated passphrase size from 42 to 47 bits. +- Substring matching has been enhanced to partially discount rather than fully +remove weak substrings, support leetspeak, and detect some common sequences of +characters (sequential digits, letters in alphabetical order, adjacent keys on +a QWERTY keyboard). +- Detect and allow passphrases with non-ASCII characters in the words. +- A number of optimizations have been made resulting in significant speedup +of passwdqc_check() on real-world passwords. +- Don't require %%optflags_lib such that the package can be built with +"rpmbuild -tb" on the tarball on non-Owl. + +* Fri Oct 30 2009 Dmitry V. Levin 1.1.4-owl1 +- Added const qualifier to all arguments of passwdqc_check() and +passwdqc_random(). +- Implemented pwqcheck's stdin check for too long lines. +- Applied markup corrections to passwdqc.conf(5) and pwqcheck(1) for better +portability (by Kevin Steves and Jason McIntyre, with minor changes made +by Solar Designer). +- Changed use of mdoc's .Os macro to be consistent with other Openwall +Project's software (by Solar Designer). + +* Wed Oct 21 2009 Dmitry V. Levin 1.1.3-owl1 +- Eliminated insufficiently portable EXIT_FAILURE and EXIT_SUCCESS macros. +- In passwdqc_load.c, replaced redundant snprintf(3) with plain sprintf(3). +- Added pw_dir checks to passwdqc_check(), similar to already existing +pw_gecos checks. +- Dropped undocumented support for multiple options per config file line. +- Switched to a heavily cut-down BSD license. +- Added ldconfig calls to %%post and %%postun scripts. + +* Sat Oct 17 2009 Solar Designer 1.1.2-owl1 +- In pwqcheck.c, replaced the uses of strsep(), which were insufficiently +portable, with code based on strchr(). +- Corrected the linker invocations for Solaris (tested on Solaris 10) and +likely for HP-UX (untested). We broke this between 1.0.5 and 1.1.0. +- Split the CFLAGS into two, separate for libraries (libpasswdqc, pam_passwdqc) +and binaries (the pwq* programs). +- In the Makefile, set umask 022 on mkdir's invoked by "make install". + +* Thu Oct 15 2009 Dmitry V. Levin 1.1.1-owl1 +- Relaxed license of pwqgen and pwqcheck manual pages. +- Ensure that pwqgen's exit status is zero only if generated passphrase +has been printed successfully. +- Changed pwqcheck to print "OK" line on success. +- Changed pwqcheck to print "Weak passphrase" diagnostics to stdout +instead of stderr. + +* Sat Oct 10 2009 Solar Designer 1.1.0-owl1 +- Export passwdqc_params_load in libpasswdqc. +- Minor English grammar corrections to messages produced by pam_passwdqc. +- Minor documentation edits. +- Added/adjusted copyright statements and attributions to reflect Dmitry's +recent changes. + +* Mon Sep 28 2009 Dmitry V. Levin unreleased +- Introduced libpasswdqc shared library. +- Implemented pwqgen and pwqcheck utilities. +- Implemented config= parameter support in libpasswdqc. +- Packaged /etc/passwdqc.conf file with default configuration. +- Added passwdqc.conf(5) manual page. + +* Tue Feb 12 2008 Solar Designer 1.0.5-owl1 +- Replaced the separator characters with some of those defined by RFC 3986 +as being safe within "userinfo" part of URLs without encoding. +- Reduced the default value for the N2 parameter to min=... (the minimum +length for passphrases) from 12 to 11. +- Corrected the potentially misleading description of N2 (Debian bug #310595). +- Applied minor grammar and style corrections to the documentation, a +pam_passwdqc message, and source code comments. + +* Tue Apr 04 2006 Dmitry V. Levin 1.0.4-owl1 +- Changed Makefile to pass list of libraries to linker after regular +object files, to fix build with -Wl,--as-needed. +- Corrected specfile to make it build on x86_64. + +* Wed Aug 17 2005 Dmitry V. Levin 1.0.3-owl1 +- Fixed potential memory leak in conversation wrapper. +- Restricted list of global symbols exported by the PAM module +to standard set of six pam_sm_* functions. + +* Wed May 18 2005 Solar Designer 1.0.2-owl1 +- Fixed compiler warnings seen on FreeBSD 5.3. +- Updated the Makefile to not require editing on FreeBSD. +- Updated the FreeBSD-specific notes in PLATFORMS. + +* Sun Mar 27 2005 Solar Designer 1.0.1-owl1 +- Further compiler warning fixes on LP64 platforms. + +* Fri Mar 25 2005 Solar Designer 1.0-owl1 +- Corrected the source code to not break C strict aliasing rules. + +* Wed Jan 26 2005 Solar Designer 0.7.6-owl1 +- Disallow unreasonable random= settings. +- Clarified the allowable bit sizes for randomly-generated passphrases and +the lack of relationship between passphrase= and random= options. + +* Fri Oct 31 2003 Solar Designer 0.7.5-owl1 +- Assume invocation by root only if both the UID is 0 and the PAM service +name is "passwd"; this should solve changing expired passwords on Solaris +and HP-UX and make "enforce=users" safe. +- Produce proper English explanations for a wider variety of settings. +- Moved the "-c" out of CFLAGS, renamed FAKEROOT to DESTDIR. + +* Sat Jun 21 2003 Solar Designer 0.7.4-owl1 +- Documented that "enforce=users" may not always work for services other +than the passwd command. +- Applied a patch to PLATFORMS from Mike Gerdts of GE Medical Systems +to reflect how Solaris 8 patch 108993-18 (or 108994-18 on x86) changes +Solaris 8's PAM implementation to look like Solaris 9. + +* Mon Jun 02 2003 Solar Designer 0.7.3.1-owl1 +- Added URL. + +* Thu Oct 31 2002 Solar Designer 0.7.3-owl1 +- When compiling with gcc, also link with gcc. +- Use $(MAKE) to invoke sub-makes. + +* Fri Oct 04 2002 Solar Designer +- Solaris 9 notes in PLATFORMS. + +* Wed Sep 18 2002 Solar Designer +- Build with Sun's C compiler cleanly, from Kevin Steves. +- Use install -c as that actually makes a difference on at least HP-UX +(otherwise install would possibly move files and not change the owner). + +* Fri Sep 13 2002 Solar Designer +- Have the same pam_passwdqc binary work for both trusted and non-trusted +HP-UX, from Kevin Steves. + +* Fri Sep 06 2002 Solar Designer +- Use bigcrypt() on HP-UX whenever necessary, from Kevin Steves of Atomic +Gears LLC. +- Moved the old password checking into a separate function. + +* Wed Jul 31 2002 Solar Designer +- Call it 0.6. + +* Sat Jul 27 2002 Solar Designer +- Documented that the man page is under the 3-clause BSD-style license. +- HP-UX 11 support. + +* Tue Jul 23 2002 Solar Designer +- Applied minor corrections to the man page and at the same time eliminated +unneeded/unimportant differences between it and the README. + +* Sun Jul 21 2002 Solar Designer +- 0.5.1: imported the pam_passwdqc(8) manual page back from FreeBSD. + +* Tue Apr 16 2002 Solar Designer +- 0.5: preliminary OpenPAM (FreeBSD-current) support in the code and related +code cleanups (thanks to Dag-Erling Smorgrav). + +* Thu Feb 07 2002 Michail Litvak +- Enforce our new spec file conventions. + +* Sun Nov 04 2001 Solar Designer +- Updated to 0.4: +- Added "ask_oldauthtok" and "check_oldauthtok" as needed for stacking with +the Solaris pam_unix; +- Permit for stacking of more than one instance of this module (no statics). + +* Tue Feb 13 2001 Solar Designer +- Install the module as mode 755. + +* Tue Dec 19 2000 Solar Designer +- Added "-Wall -fPIC" to the CFLAGS. + +* Mon Oct 30 2000 Solar Designer +- 0.3: portability fixes (this might build on non-Linux-PAM now). + +* Fri Sep 22 2000 Solar Designer +- 0.2: added "use_authtok", added README. + +* Fri Aug 18 2000 Solar Designer +- 0.1, "retry_wanted" bugfix. + +* Sun Jul 02 2000 Solar Designer +- Initial version (non-public). diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_check.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_check.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_check.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_check.c b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c index 01265ff495e5..bb405af8cc48 100644 --- a/contrib/pam_modules/pam_passwdqc/passwdqc_check.c +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c @@ -1,361 +1,636 @@ /* - * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + * Copyright (c) 2000-2002,2010,2013,2016,2020 by Solar Designer. See LICENSE. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS /* we use fopen(), sprintf(), strncat() */ +#endif + +#include #include #include #include -#include -#include "passwdqc.h" +#include "passwdqc.h" /* also provides or equivalent "struct passwd" */ +#include "passwdqc_filter.h" +#include "wordset_4k.h" + +#include "passwdqc_i18n.h" #define REASON_ERROR \ - "check failed" + _("check failed") #define REASON_SAME \ - "is the same as the old one" + _("is the same as the old one") #define REASON_SIMILAR \ - "is based on the old one" + _("is based on the old one") #define REASON_SHORT \ - "too short" + _("too short") #define REASON_LONG \ - "too long" + _("too long") #define REASON_SIMPLESHORT \ - "not enough different characters or classes for this length" + _("not enough different characters or classes for this length") #define REASON_SIMPLE \ - "not enough different characters or classes" + _("not enough different characters or classes") #define REASON_PERSONAL \ - "based on personal login information" + _("based on personal login information") #define REASON_WORD \ - "based on a dictionary word and not a passphrase" + _("based on a dictionary word and not a passphrase") + +#define REASON_SEQ \ + _("based on a common sequence of characters and not a passphrase") + +#define REASON_WORDLIST \ + _("based on a word list entry") + +#define REASON_DENYLIST \ + _("is in deny list") + +#define REASON_FILTER \ + _("appears to be in a database") #define FIXED_BITS 15 typedef unsigned long fixed; /* * Calculates the expected number of different characters for a random - * password of a given length. The result is rounded down. We use this + * password of a given length. The result is rounded down. We use this * with the _requested_ minimum length (so longer passwords don't have * to meet this strict requirement for their length). */ static int expected_different(int charset, int length) { fixed x, y, z; x = ((fixed)(charset - 1) << FIXED_BITS) / charset; y = x; - while (--length > 0) y = (y * x) >> FIXED_BITS; + while (--length > 0) + y = (y * x) >> FIXED_BITS; z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y); return (int)(z >> FIXED_BITS); } /* * A password is too simple if it is too short for its class, or doesn't * contain enough different characters for its class, or doesn't contain * enough words for a passphrase. + * + * The biases are added to the length, and they may be positive or negative. + * The passphrase length check uses passphrase_bias instead of bias so that + * zero may be passed for this parameter when the (other) bias is non-zero + * because of a dictionary word, which is perfectly normal for a passphrase. + * The biases do not affect the number of different characters, character + * classes, and word count. */ -static int is_simple(passwdqc_params_t *params, const char *newpass) +static int is_simple(const passwdqc_params_qc_t *params, const char *newpass, + int bias, int passphrase_bias) { int length, classes, words, chars; int digits, lowers, uppers, others, unknowns; int c, p; length = classes = words = chars = 0; digits = lowers = uppers = others = unknowns = 0; p = ' '; while ((c = (unsigned char)newpass[length])) { length++; - if (!isascii(c)) unknowns++; else - if (isdigit(c)) digits++; else - if (islower(c)) lowers++; else - if (isupper(c)) uppers++; else + if (!isascii(c)) + unknowns++; + else if (isdigit(c)) + digits++; + else if (islower(c)) + lowers++; + else if (isupper(c)) + uppers++; + else others++; - if (isascii(c) && isalpha(c) && isascii(p) && !isalpha(p)) - words++; +/* A word starts when a letter follows a non-letter or when a non-ASCII + * character follows a space character. We treat all non-ASCII characters + * as non-spaces, which is not entirely correct (there's the non-breaking + * space character at 0xa0, 0x9a, or 0xff), but it should not hurt. */ + if (isascii(p)) { + if (isascii(c)) { + if (isalpha(c) && !isalpha(p)) + words++; + } else if (isspace(p)) + words++; + } p = c; +/* Count this character just once: when we're not going to see it anymore */ if (!strchr(&newpass[length], c)) chars++; } - if (!length) return 1; + if (!length) + return 1; /* Upper case characters and digits used in common ways don't increase the * strength of a password */ c = (unsigned char)newpass[0]; - if (uppers && isascii(c) && isupper(c)) uppers--; + if (uppers && isascii(c) && isupper(c)) + uppers--; c = (unsigned char)newpass[length - 1]; - if (digits && isascii(c) && isdigit(c)) digits--; + if (digits && isascii(c) && isdigit(c)) + digits--; -/* Count the number of different character classes we've seen. We assume - * that there're no non-ASCII characters for digits. */ +/* Count the number of different character classes we've seen. We assume + * that there are no non-ASCII characters for digits. */ classes = 0; - if (digits) classes++; - if (lowers) classes++; - if (uppers) classes++; - if (others) classes++; - if (unknowns && (!classes || (digits && classes == 1))) classes++; + if (digits) + classes++; + if (lowers) + classes++; + if (uppers) + classes++; + if (others) + classes++; + if (unknowns && classes <= 1 && (!classes || digits || words >= 2)) + classes++; for (; classes > 0; classes--) switch (classes) { case 1: - if (length >= params->min[0] && + if (length + bias >= params->min[0] && chars >= expected_different(10, params->min[0]) - 1) return 0; return 1; case 2: - if (length >= params->min[1] && + if (length + bias >= params->min[1] && chars >= expected_different(36, params->min[1]) - 1) return 0; if (!params->passphrase_words || words < params->passphrase_words) continue; - if (length >= params->min[2] && + if (length + passphrase_bias >= params->min[2] && chars >= expected_different(27, params->min[2]) - 1) return 0; continue; case 3: - if (length >= params->min[3] && + if (length + bias >= params->min[3] && chars >= expected_different(62, params->min[3]) - 1) return 0; continue; case 4: - if (length >= params->min[4] && + if (length + bias >= params->min[4] && chars >= expected_different(95, params->min[4]) - 1) return 0; continue; } return 1; } -static char *unify(const char *src) +static char *unify(char *dst, const char *src) { const char *sptr; - char *dst, *dptr; + char *dptr; int c; - if (!(dst = malloc(strlen(src) + 1))) + if (!dst && !(dst = malloc(strlen(src) + 1))) return NULL; sptr = src; dptr = dst; do { c = (unsigned char)*sptr; if (isascii(c) && isupper(c)) - *dptr++ = tolower(c); - else - *dptr++ = *sptr; + c = tolower(c); + switch (c) { + case 'a': case '@': + c = '4'; break; + case 'e': + c = '3'; break; +/* Unfortunately, if we translate both 'i' and 'l' to '1', this would + * associate these two letters with each other - e.g., "mile" would + * match "MLLE", which is undesired. To solve this, we'd need to test + * different translations separately, which is not implemented yet. */ + case 'i': case '|': + c = '!'; break; + case 'l': + c = '1'; break; + case 'o': + c = '0'; break; + case 's': case '$': + c = '5'; break; + case 't': case '+': + c = '7'; break; + } + *dptr++ = c; } while (*sptr++); return dst; } static char *reverse(const char *src) { const char *sptr; char *dst, *dptr; if (!(dst = malloc(strlen(src) + 1))) return NULL; sptr = &src[strlen(src)]; dptr = dst; while (sptr > src) *dptr++ = *--sptr; *dptr = '\0'; return dst; } static void clean(char *dst) { - if (dst) { - memset(dst, 0, strlen(dst)); - free(dst); - } + if (!dst) + return; + _passwdqc_memzero(dst, strlen(dst)); + free(dst); } /* * Needle is based on haystack if both contain a long enough common * substring and needle would be too simple for a password with the - * substring removed. + * substring either removed with partial length credit for it added + * or partially discounted for the purpose of the length check. */ -static int is_based(passwdqc_params_t *params, - const char *haystack, const char *needle, const char *original) +static int is_based(const passwdqc_params_qc_t *params, + const char *haystack, const char *needle, const char *original, + int mode) { char *scratch; int length; int i, j; const char *p; - int match; + int worst_bias; if (!params->match_length) /* disabled */ return 0; if (params->match_length < 0) /* misconfigured */ return 1; - if (strstr(haystack, needle)) /* based on haystack entirely */ - return 1; - scratch = NULL; + worst_bias = 0; - length = strlen(needle); + length = (int)strlen(needle); for (i = 0; i <= length - params->match_length; i++) for (j = params->match_length; i + j <= length; j++) { - match = 0; + int bias = 0, j1 = j - 1; + const char q0 = needle[i], *q1 = &needle[i + 1]; for (p = haystack; *p; p++) - if (*p == needle[i] && !strncmp(p, &needle[i], j)) { - match = 1; - if (!scratch) { - if (!(scratch = malloc(length + 1))) + if (*p == q0 && !strncmp(p + 1, q1, j1)) { /* or memcmp() */ + if ((mode & 0xff) == 0) { /* remove & credit */ + if (!scratch) { + if (!(scratch = malloc(length + 1))) + return 1; + } + /* remove j chars */ + { + int pos = length - (i + j); + if (!(mode & 0x100)) /* not reversed */ + pos = i; + memcpy(scratch, original, pos); + memcpy(&scratch[pos], + &original[pos + j], + length + 1 - (pos + j)); + } + /* add credit for match_length - 1 chars */ + bias = params->match_length - 1; + if (is_simple(params, scratch, bias, bias)) { + clean(scratch); return 1; - } - memcpy(scratch, original, i); - memcpy(&scratch[i], &original[i + j], - length + 1 - (i + j)); - if (is_simple(params, scratch)) { - clean(scratch); - return 1; + } + } else { /* discount */ +/* Require a 1 character longer match for substrings containing leetspeak + * when matching against dictionary words */ + bias = -1; + if ((mode & 0xff) == 1) { /* words */ + int pos = i, end = i + j; + if (mode & 0x100) { /* reversed */ + pos = length - end; + end = length - i; + } + for (; pos < end; pos++) + if (!isalpha((int)(unsigned char) + original[pos])) { + if (j == params->match_length) + goto next_match_length; + bias = 0; + break; + } + } + + /* discount j - (match_length + bias) chars */ + bias += (int)params->match_length - j; + /* bias <= -1 */ + if (bias < worst_bias) { + if (is_simple(params, original, bias, + (mode & 0xff) == 1 ? 0 : bias)) + return 1; + worst_bias = bias; + } } } - if (!match) break; +/* Zero bias implies that there were no matches for this length. If so, + * there's no reason to try the next substring length (it would result in + * no matches as well). We break out of the substring length loop and + * proceed with all substring lengths for the next position in needle. */ + if (!bias) + break; +next_match_length: + ; } clean(scratch); return 0; } +#define READ_LINE_MAX 8192 +#define READ_LINE_SIZE (READ_LINE_MAX + 2) + +static char *read_line(FILE *f, char *buf) +{ + buf[READ_LINE_MAX] = '\n'; + + if (!fgets(buf, READ_LINE_SIZE, f)) + return NULL; + + if (buf[READ_LINE_MAX] != '\n') { + int c; + do { + c = getc(f); + } while (c != EOF && c != '\n'); + if (ferror(f)) + return NULL; + } + + char *p; + if ((p = strpbrk(buf, "\r\n"))) + *p = '\0'; + + return buf; +} + +/* + * Common sequences of characters. + * We don't need to list any of the entire strings in reverse order because the + * code checks the new password in both "unified" and "unified and reversed" + * form against these strings (unifying them first indeed). We also don't have + * to include common repeats of characters (e.g., "777", "!!!", "1000") because + * these are often taken care of by the requirement on the number of different + * characters. + */ +const char * const seq[] = { + "0123456789", + "`1234567890-=", + "~!@#$%^&*()_+", + "abcdefghijklmnopqrstuvwxyz", + "a1b2c3d4e5f6g7h8i9j0", + "1a2b3c4d5e6f7g8h9i0j", + "abc123", + "qwertyuiop[]\\asdfghjkl;'zxcvbnm,./", + "qwertyuiop{}|asdfghjkl:\"zxcvbnm<>?", + "qwertyuiopasdfghjklzxcvbnm", + "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\", + "!qaz@wsx#edc$rfv%tgb^yhn&ujm*ik<(ol>)p:?_{\"+}|", + "qazwsxedcrfvtgbyhnujmikolp", + "1q2w3e4r5t6y7u8i9o0p-[=]", + "q1w2e3r4t5y6u7i8o9p0[-]=\\", + "1qaz1qaz", + "1qaz!qaz", /* can't unify '1' and '!' - see comment in unify() */ + "1qazzaq1", + "zaq!1qaz", + "zaq!2wsx" +}; + /* * This wordlist check is now the least important given the checks above * and the support for passphrases (which are based on dictionary words, - * and checked by other means). It is still useful to trap simple short + * and checked by other means). It is still useful to trap simple short * passwords (if short passwords are allowed) that are word-based, but * passed the other checks due to uncommon capitalization, digits, and - * special characters. We (mis)use the same set of words that are used - * to generate random passwords. This list is much smaller than those + * special characters. We (mis)use the same set of words that are used + * to generate random passwords. This list is much smaller than those * used for password crackers, and it doesn't contain common passwords - * that aren't short English words. Perhaps support for large wordlists - * should still be added, even though this is now of little importance. + * that aren't short English words. We also support optional external + * wordlist (for inexact matching) and deny list (for exact matching). */ -static int is_word_based(passwdqc_params_t *params, - const char *needle, const char *original) +static const char *is_word_based(const passwdqc_params_qc_t *params, + const char *unified, const char *reversed, const char *original) { - char word[7]; - char *unified; - int i; - - word[6] = '\0'; - for (i = 0; i < 0x1000; i++) { - memcpy(word, _passwdqc_wordset_4k[i], 6); - if ((int)strlen(word) < params->match_length) continue; - unified = unify(word); - if (is_based(params, unified, needle, original)) { - clean(unified); - return 1; + const char *reason = REASON_ERROR; + char word[WORDSET_4K_LENGTH_MAX + 1], *buf = NULL; + FILE *f = NULL; + unsigned int i; + + word[WORDSET_4K_LENGTH_MAX] = '\0'; + if (params->match_length) + for (i = 0; _passwdqc_wordset_4k[i][0]; i++) { + memcpy(word, _passwdqc_wordset_4k[i], WORDSET_4K_LENGTH_MAX); + int length = (int)strlen(word); + if (length < params->match_length) + continue; + if (!memcmp(word, _passwdqc_wordset_4k[i + 1], length)) + continue; + unify(word, word); + if (is_based(params, word, unified, original, 1) || + is_based(params, word, reversed, original, 0x101)) { + reason = REASON_WORD; + goto out; } - clean(unified); } - return 0; -} + if (params->match_length) + for (i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) { + char *seq_i = unify(NULL, seq[i]); + if (!seq_i) + goto out; + if (is_based(params, seq_i, unified, original, 2) || + is_based(params, seq_i, reversed, original, 0x102)) { + clean(seq_i); + reason = REASON_SEQ; + goto out; + } + clean(seq_i); + } -const char *_passwdqc_check(passwdqc_params_t *params, - const char *newpass, const char *oldpass, struct passwd *pw) -{ - char truncated[9], *reversed; - char *u_newpass, *u_reversed; - char *u_oldpass; - char *u_name, *u_gecos; - const char *reason; - int length; + if (params->match_length && params->match_length <= 4) + for (i = 1900; i <= 2039; i++) { + sprintf(word, "%u", i); + if (is_based(params, word, unified, original, 2) || + is_based(params, word, reversed, original, 0x102)) { + reason = REASON_SEQ; + goto out; + } + } - reversed = NULL; - u_newpass = u_reversed = NULL; - u_oldpass = NULL; - u_name = u_gecos = NULL; + if (params->wordlist || params->denylist) + if (!(buf = malloc(READ_LINE_SIZE))) + goto out; + + if (params->wordlist) { + if (!(f = fopen(params->wordlist, "r"))) + goto out; + while (read_line(f, buf)) { + unify(buf, buf); + if (!strcmp(buf, unified) || !strcmp(buf, reversed)) + goto out_wordlist; + if (!params->match_length || + strlen(buf) < (size_t)params->match_length) + continue; + if (is_based(params, buf, unified, original, 1) || + is_based(params, buf, reversed, original, 0x101)) { +out_wordlist: + reason = REASON_WORDLIST; + goto out; + } + } + if (ferror(f)) + goto out; + fclose(f); f = NULL; + } + + if (params->denylist) { + if (!(f = fopen(params->denylist, "r"))) + goto out; + while (read_line(f, buf)) { + if (!strcmp(buf, original)) { + reason = REASON_DENYLIST; + goto out; + } + } + if (ferror(f)) + goto out; + } reason = NULL; - if (oldpass && !strcmp(oldpass, newpass)) - reason = REASON_SAME; +out: + if (f) + fclose(f); + if (buf) { + _passwdqc_memzero(buf, READ_LINE_SIZE); + free(buf); + } + _passwdqc_memzero(word, sizeof(word)); + return reason; +} - length = strlen(newpass); +const char *passwdqc_check(const passwdqc_params_qc_t *params, + const char *newpass, const char *oldpass, const struct passwd *pw) +{ + char truncated[9]; + char *u_newpass = NULL, *u_reversed = NULL; + char *u_oldpass = NULL; + char *u_name = NULL, *u_gecos = NULL, *u_dir = NULL; + const char *reason = REASON_ERROR; + + size_t length = strlen(newpass); - if (!reason && length < params->min[4]) + if (length < (size_t)params->min[4]) { reason = REASON_SHORT; + goto out; + } + + if (length > 10000) { + reason = REASON_LONG; + goto out; + } - if (!reason && length > params->max) { + if (length > (size_t)params->max) { if (params->max == 8) { truncated[0] = '\0'; strncat(truncated, newpass, 8); newpass = truncated; - if (oldpass && !strncmp(oldpass, newpass, 8)) + length = 8; + if (oldpass && !strncmp(oldpass, newpass, 8)) { reason = REASON_SAME; - } else + goto out; + } + } else { reason = REASON_LONG; + goto out; + } + } + + if (oldpass && !strcmp(oldpass, newpass)) { + reason = REASON_SAME; + goto out; } - if (!reason && is_simple(params, newpass)) { - if (length < params->min[1] && params->min[1] <= params->max) + if (is_simple(params, newpass, 0, 0)) { + reason = REASON_SIMPLE; + if (length < (size_t)params->min[1] && + params->min[1] <= params->max) reason = REASON_SIMPLESHORT; - else - reason = REASON_SIMPLE; + goto out; } - if (!reason) { - if ((reversed = reverse(newpass))) { - u_newpass = unify(newpass); - u_reversed = unify(reversed); - if (oldpass) - u_oldpass = unify(oldpass); - if (pw) { - u_name = unify(pw->pw_name); - u_gecos = unify(pw->pw_gecos); - } - } - if (!reversed || - !u_newpass || !u_reversed || - (oldpass && !u_oldpass) || - (pw && (!u_name || !u_gecos))) - reason = REASON_ERROR; + if (!(u_newpass = unify(NULL, newpass))) + goto out; /* REASON_ERROR */ + if (!(u_reversed = reverse(u_newpass))) + goto out; + if (oldpass && !(u_oldpass = unify(NULL, oldpass))) + goto out; + if (pw) { + if (!(u_name = unify(NULL, pw->pw_name)) || + !(u_gecos = unify(NULL, pw->pw_gecos)) || + !(u_dir = unify(NULL, pw->pw_dir))) + goto out; } - if (!reason && oldpass && params->similar_deny && - (is_based(params, u_oldpass, u_newpass, newpass) || - is_based(params, u_oldpass, u_reversed, reversed))) + if (oldpass && params->similar_deny && + (is_based(params, u_oldpass, u_newpass, newpass, 0) || + is_based(params, u_oldpass, u_reversed, newpass, 0x100))) { reason = REASON_SIMILAR; + goto out; + } - if (!reason && pw && - (is_based(params, u_name, u_newpass, newpass) || - is_based(params, u_name, u_reversed, reversed) || - is_based(params, u_gecos, u_newpass, newpass) || - is_based(params, u_gecos, u_reversed, reversed))) + if (pw && + (is_based(params, u_name, u_newpass, newpass, 0) || + is_based(params, u_name, u_reversed, newpass, 0x100) || + is_based(params, u_gecos, u_newpass, newpass, 0) || + is_based(params, u_gecos, u_reversed, newpass, 0x100) || + is_based(params, u_dir, u_newpass, newpass, 0) || + is_based(params, u_dir, u_reversed, newpass, 0x100))) { reason = REASON_PERSONAL; + goto out; + } - if (!reason && (int)strlen(newpass) < params->min[2] && - (is_word_based(params, u_newpass, newpass) || - is_word_based(params, u_reversed, reversed))) - reason = REASON_WORD; + reason = is_word_based(params, u_newpass, u_reversed, newpass); + + if (!reason && params->filter) { + passwdqc_filter_t flt; + reason = REASON_ERROR; + if (passwdqc_filter_open(&flt, params->filter)) + goto out; + int result = passwdqc_filter_lookup(&flt, newpass); + passwdqc_filter_close(&flt); + if (result < 0) + goto out; + reason = result ? REASON_FILTER : NULL; + } - memset(truncated, 0, sizeof(truncated)); - clean(reversed); - clean(u_newpass); clean(u_reversed); +out: + _passwdqc_memzero(truncated, sizeof(truncated)); + clean(u_newpass); + clean(u_reversed); clean(u_oldpass); - clean(u_name); clean(u_gecos); + clean(u_name); + clean(u_gecos); + clean(u_dir); return reason; } diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_filter.c b/contrib/pam_modules/pam_passwdqc/passwdqc_filter.c new file mode 100644 index 000000000000..2459e7c02536 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_filter.c @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020,2021 by Solar Designer + * See LICENSE + */ + +#define _FILE_OFFSET_BITS 64 +#define _LARGEFILE_SOURCE 1 +#define _LARGEFILE64_SOURCE 1 +#define _LARGE_FILES 1 + +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ +#define _CRT_SECURE_NO_WARNINGS /* we use open() */ +#include /* for SEEK_SET and SEEK_END */ +#include +#define lseek _lseeki64 +#define ssize_t int /* MSVC's read() returns int and we don't need more here */ +#define SSIZE_MAX INT_MAX +#define OPEN_FLAGS (O_RDONLY | _O_BINARY | _O_RANDOM) +#else +#include +#define OPEN_FLAGS O_RDONLY +#endif + +#include +#include +#include + +#include "passwdqc.h" +#define PASSWDQC_FILTER_INTERNALS +#include "passwdqc_filter.h" + +static ssize_t read_loop(int fd, void *buffer, size_t count) +{ + ssize_t offset, block; + + offset = 0; + while (count > 0 && count <= SSIZE_MAX) { + block = read(fd, (char *)buffer + offset, count); + + if (block < 0) + return block; + if (!block) + return offset; + + offset += block; + count -= block; + } + + return offset; +} + +int passwdqc_filter_open(passwdqc_filter_t *flt, const char *filename) +{ + if ((flt->fd = open(filename, OPEN_FLAGS)) < 0) + return -1; + + if (read_loop(flt->fd, &flt->header, sizeof(flt->header)) != sizeof(flt->header) || + passwdqc_filter_verify_header(&flt->header) || + flt->header.hash_id < PASSWDQC_FILTER_HASH_MIN || flt->header.hash_id > PASSWDQC_FILTER_HASH_MAX || + (size_t)lseek(flt->fd, 0, SEEK_END) != sizeof(flt->header) + (flt->header.capacity << 2)) { + passwdqc_filter_close(flt); + return -1; + } + + return 0; +} + +int passwdqc_filter_close(passwdqc_filter_t *flt) +{ + int retval = close(flt->fd); + flt->fd = -1; + return retval; +} + +static int check(const passwdqc_filter_t *flt, passwdqc_filter_i_t i, passwdqc_filter_f_t f) +{ + int retval = -1; + + passwdqc_filter_packed_t p; + if (lseek(flt->fd, sizeof(flt->header) + (uint64_t)i * sizeof(p), SEEK_SET) < 0 || + read_loop(flt->fd, &p, sizeof(p)) != sizeof(p)) + goto out; + + passwdqc_filter_unpacked_t u; + unsigned int n = (unsigned int)passwdqc_filter_unpack(&u, &p, NULL); + if (n > flt->header.bucket_size) + goto out; + + unsigned int j; + for (j = 0; j < n; j++) { + if (passwdqc_filter_f_eq(u.slots[j], f, flt->header.bucket_size)) { + retval = 1; + goto out; + } + } + + retval = (n < flt->header.threshold) ? 0 : 2; + +out: + _passwdqc_memzero(&p, sizeof(p)); + _passwdqc_memzero(&u, sizeof(u)); + return retval; +} + +int passwdqc_filter_lookup(const passwdqc_filter_t *flt, const char *plaintext) +{ + int retval = 3; + passwdqc_filter_hash_t h; + passwdqc_filter_f_t ftest; + +clean: + switch (flt->header.hash_id) { + case PASSWDQC_FILTER_HASH_MD4: + passwdqc_filter_md4(&h, plaintext); + ftest = 0x8c6420f439de2000ULL; + break; + case PASSWDQC_FILTER_HASH_NTLM_CP1252: + passwdqc_filter_ntlm_cp1252(&h, plaintext); + ftest = 0x26bd9256ff7e052eULL; + break; + default: + return -1; + } + + uint32_t nbuckets = (uint32_t)(flt->header.capacity >> 2); + passwdqc_filter_i_t i = passwdqc_filter_h2i(&h, nbuckets); + passwdqc_filter_f_t f = passwdqc_filter_h2f(&h); + + _passwdqc_memzero(&h, sizeof(h)); + +/* + * The tests of i and f here not only self-test the code, but also prevent the + * compiler from moving "return retval;" to before the computation of h, i, and + * f, which would leave sensitive data from the real hash computation around. + */ + if (i >= nbuckets) + return -1; + + if (retval <= 1) { +/* Waste two syscalls on overwriting lseek()'s stack and current file offset */ + i = passwdqc_filter_h2i(&h, nbuckets); /* 0 */ + if (check(flt, i, f) < 0) + return -1; + if (f != ftest) + return -1; + return retval; + } + +/* At least 1 character to overwrite passwdqc_filter_ntlm_cp1252()'s buffer */ + plaintext = " 09AZaz\x7e\x7f\x80\x81\x9e\x9f\xa0\xff"; + + retval = check(flt, i, f); + if (retval <= 1) + goto clean; + + retval = check(flt, passwdqc_filter_alti(i, f, nbuckets), f); + if (retval == 2) + retval = 0; + goto clean; +} diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_filter.h b/contrib/pam_modules/pam_passwdqc/passwdqc_filter.h new file mode 100644 index 000000000000..f8315fca24e3 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_filter.h @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2020 by Solar Designer + * See LICENSE + */ + +#ifndef PASSWDQC_FILTER_H__ +#define PASSWDQC_FILTER_H__ + +#include + +/* Higher-level API for use by passwdqc_check.c */ + +typedef struct { + uint8_t version[4]; /* PASSWDQC_FILTER_VERSION */ + uint8_t threshold; /* 0 to 4 */ + uint8_t bucket_size; /* 2 to 4 */ + uint8_t hash_id; /* one of PASSWDQC_FILTER_HASH_* */ + uint8_t reserved1; + uint64_t endianness; /* PASSWDQC_FILTER_ENDIANNESS */ + uint64_t reserved2; /* e.g., for checksum */ + uint64_t capacity, deletes, inserts, dupes, kicks; +} passwdqc_filter_header_t; + +typedef struct { + passwdqc_filter_header_t header; + int fd; +} passwdqc_filter_t; + +extern int passwdqc_filter_open(passwdqc_filter_t *flt, const char *filename); +extern int passwdqc_filter_lookup(const passwdqc_filter_t *flt, const char *plaintext); +extern int passwdqc_filter_close(passwdqc_filter_t *flt); + +#ifdef PASSWDQC_FILTER_INTERNALS +/* Lower-level inlines for shared use by pwqfilter.c and passwdqc_filter.c */ + +#include /* for strcspn() */ +#if !defined(_MSC_VER) && !defined(__APPLE__) +#include +#endif + +#include "md4.h" + +#define PASSWDQC_FILTER_VERSION "PWQ0" +#define PASSWDQC_FILTER_ENDIANNESS 0x0807060504030201ULL + +static inline int passwdqc_filter_verify_header(const passwdqc_filter_header_t *header) +{ + return (memcmp(header->version, PASSWDQC_FILTER_VERSION, sizeof(header->version)) || + header->threshold > header->bucket_size || header->bucket_size < 2 || header->bucket_size > 4 || + header->endianness != PASSWDQC_FILTER_ENDIANNESS || + (header->capacity & 3) || header->capacity < 4 || header->capacity > ((1ULL << 32) - 1) * 4 || + header->inserts - header->deletes > header->capacity) ? -1 : 0; +} + +typedef enum { + PASSWDQC_FILTER_HASH_OPAQUE = 0, + PASSWDQC_FILTER_HASH_MIN = 1, + PASSWDQC_FILTER_HASH_MD4 = 1, + PASSWDQC_FILTER_HASH_NTLM_CP1252 = 2, + PASSWDQC_FILTER_HASH_MAX = 2 +} passwdqc_filter_hash_id_t; + +typedef struct { + uint64_t hi, lo; /* we access hi first, so let's also place it first */ +} passwdqc_filter_packed_t; + +typedef uint32_t passwdqc_filter_i_t; +typedef uint64_t passwdqc_filter_f_t; + +typedef struct { + passwdqc_filter_f_t slots[4]; +} passwdqc_filter_unpacked_t; + +typedef union { + unsigned char uc[16]; + uint32_t u32[4]; + uint64_t u64[2]; +} passwdqc_filter_hash_t; + +#ifdef __GNUC__ +#define force_inline __attribute__ ((always_inline)) inline +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#else +#define force_inline inline +#define likely(x) (x) +#define unlikely(x) (x) +#endif + +static force_inline passwdqc_filter_i_t passwdqc_filter_wrap(uint32_t what, passwdqc_filter_i_t m) +{ + return ((uint64_t)what * m) >> 32; +} + +static force_inline passwdqc_filter_i_t passwdqc_filter_h2i(passwdqc_filter_hash_t *h, passwdqc_filter_i_t m) +{ + uint32_t i; +/* + * Controversial optimization: when converting a hash to its hash table index + * for the primary bucket, take its initial portion and swap the nibbles so + * that we process most of the hash table semi-sequentially in case our input + * is an ASCII-sorted list of hex-encoded hashes. A drawback is that we fail + * to reach high load if our input is a biased fragment from such sorted list. + */ +#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN + i = h->u32[0]; +#else + i = (uint32_t)h->uc[0] << 24; + i |= (uint32_t)h->uc[1] << 16; + i |= (uint32_t)h->uc[2] << 8; + i |= (uint32_t)h->uc[3]; +#endif + i = ((i & 0x0f0f0f0f) << 4) | ((i >> 4) & 0x0f0f0f0f); + return passwdqc_filter_wrap(i, m); +} + +static force_inline passwdqc_filter_f_t passwdqc_filter_h2f(passwdqc_filter_hash_t *h) +{ +#if defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN + return h->u64[1]; +#else + uint64_t f; + f = (uint64_t)h->uc[8]; + f |= (uint64_t)h->uc[9] << 8; + f |= (uint64_t)h->uc[10] << 16; + f |= (uint64_t)h->uc[11] << 24; + f |= (uint64_t)h->uc[12] << 32; + f |= (uint64_t)h->uc[13] << 40; + f |= (uint64_t)h->uc[14] << 48; + f |= (uint64_t)h->uc[15] << 56; + return f; +#endif +} + +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) +{ +/* + * We must not use more than 33 bits of the fingerprint here for consistent + * behavior in case the fingerprint later gets truncated. + */ + int64_t alti = (int64_t)(m - 1 - i) - passwdqc_filter_wrap((uint32_t)f, m); +#if 0 +/* + * This is how we could have made use of the 33rd bit while staying in range, + * but testing shows that this is unnecessary. + */ + alti -= ((f >> 32) & 1); +#endif + if (alti < 0) + alti += m; +#if 0 + assert((passwdqc_filter_i_t)alti < m); +#endif + return (passwdqc_filter_i_t)alti; +} + +static inline unsigned int passwdqc_filter_ssdecode(unsigned int src) +{ + /* First 16 tetrahedral numbers (n*(n+1)*(n+2)/3!) in reverse order */ + static const uint16_t tab4[] = {816, 680, 560, 455, 364, 286, 220, 165, 120, 84, 56, 35, 20, 10, 4, 1}; + /* First 16 triangular numbers (n*(n+1)/2!) in reverse order */ + static const uint8_t tab3[] = {136, 120, 105, 91, 78, 66, 55, 45, 36, 28, 21, 15, 10, 6, 3, 1}; + + unsigned int dst, i = 0; + + while (src >= tab4[i]) + src -= tab4[i++]; + dst = i << 12; + + while (src >= tab3[i]) + src -= tab3[i++]; + dst |= i << 8; + + while (src >= 16 - i) + src -= 16 - i++; + dst |= i << 4; + + dst |= i + src; + + return dst; +} + +static force_inline int passwdqc_filter_unpack(passwdqc_filter_unpacked_t *dst, const passwdqc_filter_packed_t *src, + const uint16_t *ssdecode) +{ + uint64_t hi = src->hi, f = src->lo; + unsigned int ssi = hi >> (64 - 12); /* semi-sort index */ + + if (likely(ssi - 1 < 3876)) { + passwdqc_filter_f_t ssd = ssdecode ? ssdecode[ssi - 1] : passwdqc_filter_ssdecode(ssi - 1); + const unsigned int fbits = 33; + const unsigned int lobits = fbits - 4; + const passwdqc_filter_f_t lomask = ((passwdqc_filter_f_t)1 << lobits) - 1; + dst->slots[0] = (f & lomask) | ((ssd & 0x000f) << lobits); + f >>= lobits; + dst->slots[1] = (f & lomask) | ((ssd & 0x00f0) << (lobits - 4)); + f >>= lobits; + f |= hi << (64 - 2 * lobits); + dst->slots[2] = (f & lomask) | ((ssd & 0x0f00) << (lobits - 8)); + f >>= lobits; + dst->slots[3] = (f & lomask) | ((ssd & 0xf000) << (lobits - 12)); + return 4; + } + + if (likely(hi <= 1)) { + if (!hi) + return unlikely(f) ? -1 : 0; + + dst->slots[0] = f; + return 1; + } + + if (likely((ssi & 0xf80) == 0xf80)) { + const unsigned int fbits = 41; + const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; + dst->slots[0] = f & fmask; + f >>= fbits; + f |= hi << (64 - fbits); + dst->slots[1] = f & fmask; + if (unlikely(dst->slots[0] < dst->slots[1])) + return -1; + f = hi >> (2 * fbits - 64); + dst->slots[2] = f & fmask; + if (unlikely(dst->slots[1] < dst->slots[2])) + return -1; + return 3; + } + + if (likely((ssi & 0xfc0) == 0xf40)) { + const unsigned int fbits = 61; + const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; + dst->slots[0] = f & fmask; + f >>= fbits; + f |= hi << (64 - fbits); + dst->slots[1] = f & fmask; + if (unlikely(dst->slots[0] < dst->slots[1])) + return -1; + return 2; + } + + return -1; +} + +static inline int passwdqc_filter_f_eq(passwdqc_filter_f_t stored, passwdqc_filter_f_t full, unsigned int largest_bucket_size) +{ + if (likely((uint32_t)stored != (uint32_t)full)) + return 0; +/* + * Ignore optional high bits of a stored fingerprint if they're all-zero, + * regardless of whether the fingerprint possibly came from a large enough slot + * for those zeroes to potentially be meaningful. We have to do this because + * the fingerprint might have been previously stored in a larger (smaller-slot) + * bucket and been kicked from there, in which case the zeroes are meaningless. + * Exception: we don't have to do this if there were no larger buckets so far. + */ + if ((stored >> 33) || largest_bucket_size < 4) { + if ((stored >> 41) || largest_bucket_size < 3) { + if (stored >> 61) + return likely(stored == full); + else + return likely(stored == (full & (((passwdqc_filter_f_t)1 << 61) - 1))); + } else { + return likely(stored == (full & (((passwdqc_filter_f_t)1 << 41) - 1))); + } + } else { + return likely(stored == (full & (((passwdqc_filter_f_t)1 << 33) - 1))); + } +} + +static inline void passwdqc_filter_md4(passwdqc_filter_hash_t *dst, const char *src) +{ + MD4_CTX ctx; + MD4_Init(&ctx); + MD4_Update(&ctx, src, strcspn(src, "\n\r")); + MD4_Final(dst->uc, &ctx); +} + +static inline void passwdqc_filter_ntlm_cp1252(passwdqc_filter_hash_t *dst, const char *src) +{ +/* + * 5 of these codes are undefined in CP1252. We let the original single-byte + * values for them pass through, which appears to match how the HIBP v7 NTLM + * hashes were generated. + */ + static const uint16_t c1[] = { + 0x20ac, 0x81, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, + 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8d, 0x017d, 0x8f, + 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, + 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x9d, 0x017e, 0x0178 + }; + + MD4_CTX ctx; + MD4_Init(&ctx); + while (*src != '\n' && *src != '\r' && *src) { + unsigned int c = *(unsigned char *)src++; + if (c - 128 < sizeof(c1) / sizeof(c1[0])) + c = c1[c - 128]; +#if defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN + MD4_Update(&ctx, &c, 2); +#else + uint8_t ucs2[2] = {c, c >> 8}; + MD4_Update(&ctx, ucs2, 2); +#endif + } + MD4_Final(dst->uc, &ctx); +} + +#endif /* PASSWDQC_FILTER_INTERNALS */ +#endif /* PASSWDQC_FILTER_H__ */ diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_i18n.h b/contrib/pam_modules/pam_passwdqc/passwdqc_i18n.h new file mode 100644 index 000000000000..dbdad453cf5e --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_i18n.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 by Dmitry V. Levin + * Copyright (c) 2017 by Oleg Solovyov + * See LICENSE + */ + +#ifndef PASSWDQC_I18N_H__ +#define PASSWDQC_I18N_H__ + +#ifdef ENABLE_NLS +#ifndef PACKAGE +#define PACKAGE "passwdqc" +#endif +#include +#define _(msgid) dgettext(PACKAGE, msgid) +#define P2_(msgid, count) (dngettext(PACKAGE, (msgid), (msgid), (count))) +#define P3_(msgid, msgid_plural, count) (dngettext(PACKAGE, (msgid), (msgid_plural), (count))) +#define N_(msgid) msgid +#else +#define _(msgid) (msgid) +#define P2_(msgid, count) (msgid) +#define P3_(msgid, msgid_plural, count) ((count) == 1 ? (msgid) : (msgid_plural)) +#define N_(msgid) msgid +#endif + +#endif /* PASSWDQC_I18N_H__ */ diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_load.c b/contrib/pam_modules/pam_passwdqc/passwdqc_load.c new file mode 100644 index 000000000000..bc3e50877ad7 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_load.c @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2008,2009 by Dmitry V. Levin + * Copyright (c) 2021 by Solar Designer + * See LICENSE + */ + +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ +#define _CRT_SECURE_NO_WARNINGS /* we use fopen(), strerror(), sprintf() */ +#endif + +#include +#include +#include +#include +#include +#include + +#include "passwdqc.h" +#include "concat.h" + +static char *mkreason(const char *what, const char *pathname, + unsigned int lineno, const char *why) +{ + char buf[sizeof(unsigned int) * 3 + 1]; + const char *at_line = (lineno ? " at line " : ""); + const char *at_num = (lineno ? buf : ""); + + if (lineno) + sprintf(buf, "%u", lineno); + return concat(what, " \"", pathname, "\"", at_line, at_num, ": ", + (why ? why : strerror(errno)), NULL); +} + +static int +parse_file(FILE *fp, passwdqc_params_t *params, char **reason, + const char *pathname) +{ + unsigned int lineno; + char buf[8192]; + + for (lineno = 1; fgets(buf, sizeof(buf), fp); ++lineno) { + char *str, *end, *rt; + const char *cstr; + int rc; + + if (strlen(buf) >= sizeof(buf) - 1) { + *reason = mkreason("Error reading", pathname, + lineno, "Line too long"); + return -1; + } + + str = buf + strspn(buf, " \t\r\n"); + if (!*str || *str == '#') + continue; + + if ((end = strpbrk(str, "\r\n"))) + *end = '\0'; + else + end = str + strlen(str); + + while (end > str && (*--end == ' ' || *end == '\t')) + *end = '\0'; + + cstr = str; + if ((rc = passwdqc_params_parse(params, &rt, 1, &cstr))) { + *reason = mkreason("Error loading", pathname, + lineno, (rt ? rt : "Out of memory")); + free(rt); + return rc; + } + } + + if (!feof(fp) || ferror(fp)) { + *reason = mkreason("Error reading", pathname, 0, NULL); + return -1; + } + + return 0; +} + +struct dev_ino_t; +struct dev_ino_t { + struct dev_ino_t *next; + dev_t dev; + ino_t ino; +}; + +static struct dev_ino_t *dev_ino_head; + +int +passwdqc_params_load(passwdqc_params_t *params, char **reason, + const char *pathname) +{ + int rc; + FILE *fp; + struct dev_ino_t di, *di_p; + struct stat st; + + if (!(fp = fopen(pathname, "r"))) { + *reason = mkreason("Error opening", pathname, 0, NULL); + return -1; + } + + if (fstat(fileno(fp), &st)) { + *reason = mkreason("Error stat", pathname, 0, NULL); + fclose(fp); + return -1; + } + + di.dev = st.st_dev; + di.ino = st.st_ino; + for (di_p = dev_ino_head; di_p; di_p = di_p->next) + if (di_p->dev == di.dev && di_p->ino == di.ino) + break; + if (di_p) { + *reason = mkreason("Error opening", pathname, 0, + "Loop detected"); + fclose(fp); + return -1; + } + + di.next = dev_ino_head; + dev_ino_head = &di; + + rc = parse_file(fp, params, reason, pathname); + fclose(fp); + + dev_ino_head = dev_ino_head->next; + return rc; +} diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_memzero.c b/contrib/pam_modules/pam_passwdqc/passwdqc_memzero.c new file mode 100644 index 000000000000..3a2505e76217 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_memzero.c @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2016 by Solar Designer. See LICENSE. + */ + +#ifdef _MSC_VER +#include +#else +#include +#endif + +static void memzero(void *buf, size_t len) +{ +#ifdef _MSC_VER + SecureZeroMemory(buf, len); +#else + memset(buf, 0, len); +#endif +} + +void (*_passwdqc_memzero)(void *, size_t) = memzero; diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_params_free.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_params_free.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_params_free.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_params_load.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_params_load.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_params_load.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_params_parse.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_params_parse.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_params_parse.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_params_reset.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_params_reset.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_params_reset.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_parse.c b/contrib/pam_modules/pam_passwdqc/passwdqc_parse.c new file mode 100644 index 000000000000..1f8165e01df9 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_parse.c @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2000-2003,2005,2016,2020,2021 by Solar Designer + * Copyright (c) 2008,2009 by Dmitry V. Levin + * See LICENSE + */ + +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ +#endif + +#include +#include +#include +#include + +#include "passwdqc.h" +#include "concat.h" + +static const char *skip_prefix(const char *sample, const char *prefix) +{ + size_t len = strlen(prefix); + + if (strncmp(sample, prefix, len)) + return NULL; + return sample + len; +} + +static int +parse_option(passwdqc_params_t *params, char **reason, const char *option) +{ + const char *err = "Invalid parameter value"; + const char * const err_oom = "Out of memory"; + const char *p; + char *e; + int i, rc = 0; + unsigned long v; + + *reason = NULL; + if ((p = skip_prefix(option, "min="))) { + for (i = 0; i < 5; i++) { + if (!strncmp(p, "disabled", 8)) { + v = INT_MAX; + p += 8; + } else { + v = strtoul(p, &e, 10); + p = e; + } + if (i < 4 && *p++ != ',') + goto parse_error; + if (v > INT_MAX) + goto parse_error; + if (i && (int)v > params->qc.min[i - 1]) + goto parse_error; + params->qc.min[i] = v; + } + if (*p) + goto parse_error; + } else if ((p = skip_prefix(option, "max="))) { + v = strtoul(p, &e, 10); + if (*e || v < 8 || v > INT_MAX) + goto parse_error; + if (v > 10000) + v = 10000; + params->qc.max = v; + } else if ((p = skip_prefix(option, "passphrase="))) { + v = strtoul(p, &e, 10); + if (*e || v > INT_MAX) + goto parse_error; + params->qc.passphrase_words = v; + } else if ((p = skip_prefix(option, "match="))) { + v = strtoul(p, &e, 10); + if (*e || v > INT_MAX) + goto parse_error; + params->qc.match_length = v; + } else if ((p = skip_prefix(option, "similar="))) { + if (!strcmp(p, "permit")) + params->qc.similar_deny = 0; + else if (!strcmp(p, "deny")) + params->qc.similar_deny = 1; + else + goto parse_error; + } else if ((p = skip_prefix(option, "random="))) { + v = strtoul(p, &e, 10); + if (!strcmp(e, ",only")) { + e += 5; + params->qc.min[4] = INT_MAX; + } + if (*e || (v && v < 24) || v > 136) + goto parse_error; + params->qc.random_bits = v; + } else if ((p = skip_prefix(option, "wordlist="))) { + free(params->qc.wordlist); + params->qc.wordlist = NULL; + if (*p && !(params->qc.wordlist = strdup(p))) { + err = err_oom; + goto parse_error; + } + } else if ((p = skip_prefix(option, "denylist="))) { + free(params->qc.denylist); + params->qc.denylist = NULL; + if (*p && !(params->qc.denylist = strdup(p))) { + err = err_oom; + goto parse_error; + } + } else if ((p = skip_prefix(option, "filter="))) { + free(params->qc.filter); + params->qc.filter = NULL; + if (*p && !(params->qc.filter = strdup(p))) { + err = err_oom; + goto parse_error; + } + } else if ((p = skip_prefix(option, "enforce="))) { + params->pam.flags &= ~F_ENFORCE_MASK; + if (!strcmp(p, "users")) + params->pam.flags |= F_ENFORCE_USERS; + else if (!strcmp(p, "everyone")) + params->pam.flags |= F_ENFORCE_EVERYONE; + else if (strcmp(p, "none")) + goto parse_error; + } else if (!strcmp(option, "non-unix")) { + if (params->pam.flags & F_CHECK_OLDAUTHTOK) + goto parse_error; + params->pam.flags |= F_NON_UNIX; + } else if ((p = skip_prefix(option, "retry="))) { + v = strtoul(p, &e, 10); + if (*e || v > INT_MAX) + goto parse_error; + params->pam.retry = v; + } else if ((p = skip_prefix(option, "ask_oldauthtok"))) { + params->pam.flags &= ~F_ASK_OLDAUTHTOK_MASK; + if (params->pam.flags & F_USE_FIRST_PASS) + goto parse_error; + if (!p[0]) + params->pam.flags |= F_ASK_OLDAUTHTOK_PRELIM; + else if (!strcmp(p, "=update")) + params->pam.flags |= F_ASK_OLDAUTHTOK_UPDATE; + else + goto parse_error; + } else if (!strcmp(option, "check_oldauthtok")) { + if (params->pam.flags & F_NON_UNIX) + goto parse_error; + params->pam.flags |= F_CHECK_OLDAUTHTOK; + } else if (!strcmp(option, "use_first_pass")) { + if (params->pam.flags & F_ASK_OLDAUTHTOK_MASK) + goto parse_error; + params->pam.flags |= F_USE_FIRST_PASS | F_USE_AUTHTOK; + } else if (!strcmp(option, "use_authtok")) { + params->pam.flags |= F_USE_AUTHTOK; + } else if (!strcmp(option, "noaudit")) { + params->pam.flags |= F_NO_AUDIT; + } else if ((p = skip_prefix(option, "config="))) { + if ((rc = passwdqc_params_load(params, reason, p))) + goto parse_error; + } else { + err = "Invalid parameter"; + goto parse_error; + } + + return 0; + +parse_error: + passwdqc_params_free(params); + e = concat("Error parsing parameter \"", option, "\": ", + (rc ? (*reason ? *reason : err_oom) : err), NULL); + free(*reason); + *reason = e; + return rc ? rc : -1; +} + +int +passwdqc_params_parse(passwdqc_params_t *params, char **reason, + int argc, const char *const *argv) +{ + int i; + + *reason = NULL; + for (i = 0; i < argc; ++i) { + int rc; + + if ((rc = parse_option(params, reason, argv[i]))) + return rc; + } + return 0; +} + +static const passwdqc_params_t defaults = { + { + {INT_MAX, 24, 11, 8, 7}, /* min */ + 72, /* max */ + 3, /* passphrase_words */ + 4, /* match_length */ + 1, /* similar_deny */ + 47, /* random_bits */ + NULL, /* wordlist */ + NULL, /* denylist */ + NULL /* filter */ + }, + { + F_ENFORCE_EVERYONE, /* flags */ + 3 /* retry */ + } +}; + +void passwdqc_params_reset(passwdqc_params_t *params) +{ + *params = defaults; +} + +void passwdqc_params_free(passwdqc_params_t *params) +{ + free(params->qc.wordlist); + free(params->qc.denylist); + free(params->qc.filter); + passwdqc_params_reset(params); +} diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_random.3 b/contrib/pam_modules/pam_passwdqc/passwdqc_random.3 new file mode 100644 index 000000000000..60293214dd73 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_random.3 @@ -0,0 +1 @@ +.so man3/libpasswdqc.3 diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_random.c b/contrib/pam_modules/pam_passwdqc/passwdqc_random.c index 0d9a04bc1c20..5d7c575ae43f 100644 --- a/contrib/pam_modules/pam_passwdqc/passwdqc_random.c +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_random.c @@ -1,92 +1,234 @@ /* - * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + * Copyright (c) 2000-2002,2005,2008,2010,2013,2016,2021 by Solar Designer + * See LICENSE */ -#include -#include +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ +#include +#include +#else +#include #include #include #include +#endif + +#include #include "passwdqc.h" +#include "wordset_4k.h" + +/* + * We separate words in the generated "passphrases" with random special + * characters out of a set of 16 (so we encode 4 bits per separator + * character). To enable the use of our "passphrases" within FTP URLs + * (and similar), we pick characters that are defined by RFC 3986 as + * being safe within "userinfo" part of URLs without encoding and + * without having a special meaning. Out of those, we avoid characters + * that are visually ambiguous or difficult over the phone. This + * happens to leave us with exactly 8 symbols, and we add 8 digits that + * are not visually ambiguous. Unfortunately, the exclamation mark + * might be confused for the digit 1 (which we don't use), though. + */ +#define SEPARATORS "-_!$&*+=23456789" + +/* + * Number of bits encoded per separator character. + */ +#define SEPARATOR_BITS 4 + +/* + * Number of bits encoded per word. We use 4096 words, which gives 12 bits, + * and we toggle the case of the first character, which gives one bit more. + */ +#define WORD_BITS 13 + +/* + * Number of bits encoded per separator and word. + */ +#define SWORD_BITS \ + (SEPARATOR_BITS + WORD_BITS) -#define SEPARATORS "_,.;:-!&" +/* + * Maximum number of words to use. + */ +#define WORDS_MAX 8 -static int read_loop(int fd, char *buffer, int count) +/* + * Minimum and maximum number of bits to encode. With the settings above, + * these are 24 and 136, respectively. + */ +#define BITS_MIN \ + (2 * (WORD_BITS - 1)) +#define BITS_MAX \ + (WORDS_MAX * SWORD_BITS) + +#ifndef _MSC_VER +static ssize_t read_loop(int fd, void *buffer, size_t count) { - int offset, block; + ssize_t offset, block; offset = 0; - while (count > 0) { - block = read(fd, &buffer[offset], count); + while (count > 0 && count <= SSIZE_MAX) { + block = read(fd, (char *)buffer + offset, count); if (block < 0) { - if (errno == EINTR) continue; + if (errno == EINTR) + continue; return block; } - if (!block) return offset; + + if (!block) + return offset; offset += block; count -= block; } return offset; } +#endif -char *_passwdqc_random(passwdqc_params_t *params) +char *passwdqc_random(const passwdqc_params_qc_t *params) { - static char output[0x100]; - int bits; - int use_separators, count, i; - unsigned int length; - char *start, *end; - int fd; - unsigned char bytes[2]; + int bits = params->random_bits; /* further code assumes signed type */ + if (bits < BITS_MIN || bits > BITS_MAX) + return NULL; + +/* + * Calculate the number of words to use. The first word is always present + * (hence the "1 +" and the "- WORD_BITS"). Each one of the following words, + * if any, is prefixed by a separator character, so we use SWORD_BITS when + * calculating how many additional words to use. We divide "bits - WORD_BITS" + * by SWORD_BITS with rounding up (hence the addition of "SWORD_BITS - 1"). + */ + int word_count = 1 + (bits + (SWORD_BITS - 1 - WORD_BITS)) / SWORD_BITS; + +/* + * Special case: would we still encode enough bits if we omit the final word, + * but keep the would-be-trailing separator? + */ + int trailing_separator = (SWORD_BITS * (word_count - 1) >= bits); + word_count -= trailing_separator; + +/* + * To determine whether we need to use different separator characters or maybe + * not, calculate the number of words we'd need to use if we don't use + * different separators. We calculate it by dividing "bits" by WORD_BITS with + * rounding up (hence the addition of "WORD_BITS - 1"). The resulting number + * is either the same as or greater than word_count. Use different separators + * only if their use, in the word_count calculation above, has helped reduce + * word_count. + */ + int use_separators = ((bits + (WORD_BITS - 1)) / WORD_BITS != word_count); + trailing_separator &= use_separators; + +/* + * Toggle case of the first character of each word only if we wouldn't achieve + * sufficient entropy otherwise. + */ + int toggle_case = (bits > + ((WORD_BITS - 1) * word_count) + + (use_separators ? (SEPARATOR_BITS * (word_count - !trailing_separator)) : 0)); - if (!(bits = params->random_bits)) + char output[0x100]; + +/* + * Calculate and check the maximum possible length of a "passphrase" we may + * generate for a given word_count. We add 1 to WORDSET_4K_LENGTH_MAX to + * account for separators (whether different or not). When there's no + * trailing separator, we subtract 1. The check against sizeof(output) uses + * ">=" to account for NUL termination. + */ + unsigned int max_length = word_count * (WORDSET_4K_LENGTH_MAX + 1) - !trailing_separator; + if (max_length >= sizeof(output) || (int)max_length > params->max) return NULL; - count = 1 + ((bits - 12) + 14) / 15; - use_separators = ((bits + 11) / 12 != count); + unsigned char rnd[WORDS_MAX * 3]; - length = count * 7 - 1; - if (length >= sizeof(output) || (int)length > params->max) +#ifdef _MSC_VER + HCRYPTPROV hprov; + if (!CryptAcquireContextA(&hprov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) + return NULL; + memset(rnd, 0, sizeof(rnd)); /* old Windows would use previous content as extra seed */ + if (!CryptGenRandom(hprov, sizeof(rnd), rnd)) { + CryptReleaseContext(hprov, 0); + return NULL; + } + CryptReleaseContext(hprov, 0); +#else + int fd; + if ((fd = open("/dev/urandom", O_RDONLY)) < 0) return NULL; + if (read_loop(fd, rnd, sizeof(rnd)) != sizeof(rnd)) { + close(fd); + return NULL; + } + close(fd); +#endif - if ((fd = open("/dev/urandom", O_RDONLY)) < 0) return NULL; + size_t length = 0; + const unsigned char *rndptr; - length = 0; - do { - if (read_loop(fd, bytes, sizeof(bytes)) != sizeof(bytes)) { - close(fd); - return NULL; + for (rndptr = rnd; rndptr <= rnd + sizeof(rnd) - 3; rndptr += 3) { +/* + * Append a word. + * + * Treating *rndptr as little-endian, we use bits 0 to 11 for the word index + * and bit 13 for toggling the case of the first character. Bits 12, 14, and + * 15 are left unused. Bits 16 to 23 are left for the separator. + */ + unsigned int i = (((unsigned int)rndptr[1] & 0x0f) << 8) | rndptr[0]; + const char *start = _passwdqc_wordset_4k[i]; + const char *end = memchr(start, '\0', WORDSET_4K_LENGTH_MAX); + if (!end) + end = start + WORDSET_4K_LENGTH_MAX; + size_t extra = end - start; +/* The ">=" leaves room for either one more separator or NUL */ + if (length + extra >= sizeof(output)) + break; + memcpy(&output[length], start, extra); + if (toggle_case) { +/* Toggle case if bit set (we assume ASCII) */ + output[length] ^= rndptr[1] & 0x20; + bits--; } + length += extra; + bits -= WORD_BITS - 1; - i = (((int)bytes[1] & 0x0f) << 8) | (int)bytes[0]; - start = _passwdqc_wordset_4k[i]; - end = memchr(start, '\0', 6); - if (!end) end = start + 6; - if (length + (end - start) >= sizeof(output) - 1) { - close(fd); - return NULL; - } - memcpy(&output[length], start, end - start); - length += end - start; - bits -= 12; + if (bits <= 0) + break; - if (use_separators && bits > 3) { - i = ((int)bytes[1] & 0x70) >> 4; +/* + * Append a separator character. We use bits 16 to 19. Bits 20 to 23 are left + * unused. + * + * Special case: we may happen to leave a trailing separator if it provides + * enough bits on its own. With WORD_BITS 13 and SEPARATOR_BITS 4, this + * happens e.g. for bits values from 31 to 34, 48 to 51, 65 to 68. + */ + if (use_separators) { + i = rndptr[2] & 0x0f; output[length++] = SEPARATORS[i]; - bits -= 3; - } else - if (bits > 0) - output[length++] = ' '; - } while (bits > 0); + bits -= SEPARATOR_BITS; + } else { + output[length++] = SEPARATORS[0]; + } - memset(bytes, 0, sizeof(bytes)); - output[length] = '\0'; + if (bits <= 0) + break; + } - close(fd); + char *retval = NULL; + + if (bits <= 0 && length < sizeof(output)) { + output[length] = '\0'; + retval = strdup(output); + } + + _passwdqc_memzero(rnd, sizeof(rnd)); + _passwdqc_memzero(output, length); - return output; + return retval; } diff --git a/contrib/pam_modules/pam_passwdqc/po/.gitignore b/contrib/pam_modules/pam_passwdqc/po/.gitignore new file mode 100644 index 000000000000..6e85d371b7c9 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/po/.gitignore @@ -0,0 +1,2 @@ +*.mo +*.pot diff --git a/contrib/pam_modules/pam_passwdqc/po/ru.po b/contrib/pam_modules/pam_passwdqc/po/ru.po new file mode 100644 index 000000000000..e52aa53dd0a0 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/po/ru.po @@ -0,0 +1,296 @@ +# A passphrase strength checking and policy enforcement toolset. +# Copyright (c) 2000-2003,2005,2008,2010,2013,2016 by Solar Designer +# Copyright (c) 2008,2009 by Dmitry V. Levin +# This file is distributed under the same license as the passwdqc package. +# +# Oleg Solovyov , 2017. +# Andrey Cherepanov , 2017. +# Dmitry V. Levin , 2021. +msgid "" +msgstr "" +"Project-Id-Version: passwdqc 2.0.3\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-03-11 20:00+0000\n" +"PO-Revision-Date: 2021-03-11 20:00+0000\n" +"Last-Translator: Dmitry V. Levin \n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n%10==1 && n%100!=11 ? 0 : 1;\n" + +#: pam_passwdqc.c:68 +msgid "Enter current password: " +msgstr "Введите старый пароль: " + +#: pam_passwdqc.c:70 +msgid "Enter new password: " +msgstr "Введите новый пароль: " + +#: pam_passwdqc.c:72 +msgid "Re-type new password: " +msgstr "Повторите новый пароль: " + +#: pam_passwdqc.c:75 +msgid "System configuration error. Please contact your administrator." +msgstr "Ошибка настройки системы. Свяжитесь с вашим администратором." + +#: pam_passwdqc.c:79 +msgid "" +"\n" +"You can now choose the new password.\n" +msgstr "" +"\n" +"Вы можете выбрать новый пароль.\n" + +#: pam_passwdqc.c:81 +msgid "" +"\n" +"You can now choose the new password or passphrase.\n" +msgstr "" +"\n" +"Вы можете выбрать новый пароль или парольную фразу.\n" + +#: pam_passwdqc.c:85 +#, c-format +msgid "" +"A good password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d character.\n" +msgid_plural "" +"A good password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d " +"characters.\n" +msgstr[0] "" +"В хорошем пароле приветствуется наличие заглавных и строчных букв, цифр\n" +"и прочих символов. Пароль должен содержать не менее %d символа.\n" +msgstr[1] "" +"В хорошем пароле приветствуется наличие заглавных и строчных букв, цифр\n" +"и прочих символов. Пароль должен содержать не менее %d символов.\n" + +#: pam_passwdqc.c:94 +#, c-format +msgid "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d character\n" +"from at least %d of these 4 classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgid_plural "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d characters\n" +"from at least %d of these 4 classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgstr[0] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать от %d символа, принадлежащего минимум %d из этих 4 " +"классов.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" +msgstr[1] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать от %d символов, принадлежащих минимум %d из этих 4 " +"классов.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" + +#: pam_passwdqc.c:109 +#, c-format +msgid "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d character\n" +"from all of these classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgid_plural "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d characters\n" +"from all of these classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgstr[0] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать от %d символа, принадлежащего всем этим классам.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" +msgstr[1] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать от %d символов, принадлежащих всем этим классам.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" + +#: pam_passwdqc.c:124 +#, c-format +msgid "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d character\n" +"from all of these classes, or a password containing at least %d characters\n" +"from just 3 of these 4 classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgid_plural "" +"A valid password should be a mix of upper and lower case letters, digits, " +"and\n" +"other characters. You can use a password containing at least %d characters\n" +"from all of these classes, or a password containing at least %d characters\n" +"from just 3 of these 4 classes.\n" +"An upper case letter that begins the password and a digit that ends it do " +"not\n" +"count towards the number of character classes used.\n" +msgstr[0] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать либо не менее %d символа, принадлежащего всем этим " +"классам,\n" +"либо ещё больше символов (не менее %d), принадлежащих лишь 3 классам из 4.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" +msgstr[1] "" +"Пароль должен состоять из заглавных, строчных букв, цифр и других символов,\n" +"и может содержать либо не менее %d символов, принадлежащих всем этим " +"классам,\n" +"либо ещё больше символов (не менее %d), принадлежащих лишь 3 классам из 4.\n" +"При подсчете классов не учитываются заглавная буква в начале и цифра в " +"конце.\n" + +#: pam_passwdqc.c:141 +#, c-format +msgid "" +"A passphrase should be of at least %d word, %d to %d characters long, and\n" +"contain enough different characters.\n" +msgid_plural "" +"A passphrase should be of at least %d words, %d to %d characters long, and\n" +"contain enough different characters.\n" +msgstr[0] "" +"Парольная фраза должна состоять как минимум из %d слова, и содержать\n" +"символы в количестве от %d до %d, среди которых достаточно различных.\n" +msgstr[1] "" +"Парольная фраза должна состоять как минимум из %d слов, и содержать\n" +"символы в количестве от %d до %d, среди которых достаточно различных.\n" + +#: pam_passwdqc.c:149 +#, c-format +msgid "" +"Alternatively, if no one else can see your terminal now, you can pick this " +"as\n" +"your password: \"%s\".\n" +msgstr "" +"В качестве альтернативы, если ваш терминал никто сейчас не видит, можно\n" +"набрать предлагаемый пароль: \"%s\".\n" + +#: pam_passwdqc.c:152 +#, c-format +msgid "" +"This system is configured to permit randomly generated passwords only.\n" +"If no one else can see your terminal now, you can pick this as your\n" +"password: \"%s\". Otherwise come back later.\n" +msgstr "" +"Система настроена на использование только таких паролей, которые были\n" +"созданы с помощью генератора случайных чисел. Если ваш терминал никто\n" +"сейчас не видит, можно набрать предлагаемый пароль: \"%s\".\n" +"В противном случае попробуйте повторить попытку позднее.\n" + +#: pam_passwdqc.c:156 +msgid "" +"This system is configured to use randomly generated passwords only,\n" +"but the attempt to generate a password has failed. This could happen\n" +"for a number of reasons: you could have requested an impossible password\n" +"length, or the access to kernel random number pool could have failed." +msgstr "" +"Система настроена на использование только таких паролей, которые были\n" +"созданы с помощью генератора случайных чисел, однако создать пароль\n" +"не удалось. Это могло произойти по разным причинам, например,\n" +"вы запросили слишком длинный пароль, либо было отказано в доступе\n" +"к пулу случайных чисел ядра." + +#: pam_passwdqc.c:161 +msgid "This password may be too long for some services. Choose another." +msgstr "" +"Этот пароль может оказаться слишком длинным для некоторых служб. Выберите " +"другой." + +#: pam_passwdqc.c:163 +msgid "Warning: your longer password will be truncated to 8 characters." +msgstr "Внимание: ваш пароль будет усечён до 8 символов." + +#: pam_passwdqc.c:165 +#, c-format +msgid "Weak password: %s." +msgstr "Слабый пароль: %s." + +#: pam_passwdqc.c:167 +msgid "Sorry, you've mistyped the password that was generated for you." +msgstr "Извините, вы ошиблись при вводе созданного для вас пароля." + +#: pam_passwdqc.c:169 +msgid "Sorry, passwords do not match." +msgstr "Пароли не совпадают." + +#: pam_passwdqc.c:171 +msgid "Try again." +msgstr "Попробуйте ещё раз." + +#: passwdqc_check.c:21 +msgid "check failed" +msgstr "проверка не удалась" + +#: passwdqc_check.c:24 +msgid "is the same as the old one" +msgstr "совпадает со старым" + +#: passwdqc_check.c:26 +msgid "is based on the old one" +msgstr "основан на старом" + +#: passwdqc_check.c:29 +msgid "too short" +msgstr "слишком короткий" + +#: passwdqc_check.c:31 +msgid "too long" +msgstr "слишком длинный" + +#: passwdqc_check.c:34 +msgid "not enough different characters or classes for this length" +msgstr "недостаточно символов или классов для заданной длины" + +#: passwdqc_check.c:36 +msgid "not enough different characters or classes" +msgstr "недостаточно символов или классов" + +#: passwdqc_check.c:39 +msgid "based on personal login information" +msgstr "основан на персональных данных" + +#: passwdqc_check.c:42 +msgid "based on a dictionary word and not a passphrase" +msgstr "основан на слове из словаря и не является парольной фразой" + +#: passwdqc_check.c:45 +msgid "based on a common sequence of characters and not a passphrase" +msgstr "" +"основан на простой последовательности символов и не является парольной фразой" + +#: passwdqc_check.c:48 +msgid "based on a word list entry" +msgstr "основан на слове из списка" + +#: passwdqc_check.c:51 +msgid "is in deny list" +msgstr "в списке запрещённых" + +#: passwdqc_check.c:54 +msgid "appears to be in a database" +msgstr "обнаружен в базе данных" diff --git a/contrib/pam_modules/pam_passwdqc/pwqcheck.1 b/contrib/pam_modules/pam_passwdqc/pwqcheck.1 new file mode 100644 index 000000000000..fb13b3649363 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqcheck.1 @@ -0,0 +1,269 @@ +.\" Copyright (c) 2009 Dmitry V. Levin +.\" All rights reserved. +.\" Copyright (c) 2000-2003,2005,2008,2010,2019,2020 Solar Designer +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd December 30, 2020 +.Dt PWQCHECK 1 +.Os "Openwall Project" +.Sh NAME +.Nm pwqcheck +.Nd Check passphrase quality +.Sh SYNOPSIS +.Nm Op Ar options +.Sh DESCRIPTION +The +.Nm +program checks passphrase quality using the libpasswdqc library. +By default, it expects to read 3 lines from standard input: +.Pp +.Bl -item -compact -offset indent +.It +first line is a new password, +.It +second line is an old password, and +.It +third line is either an existing account name or a +.Xr passwd 5 +entry. +.El +.Pp +There are a number of supported options, which can be used to control the +.Nm +behavior. +.Pp +.Nm +prints +.Ar OK +on success. Scripts invoking +.Nm +are suggested to check for both a zero exit status and the +.Ar OK +line. +.Sh OPTIONS +.Bl -tag -width Ds +.Sm off +.It Xo +.Cm min No = +.Ar N0 , N1 , N2 , N3 , N4 +.Xc +.Sm on +.Pq default: min=disabled,24,11,8,7 +The minimum allowed password lengths for different kinds of +passwords/passphrases. +The keyword +.Cm disabled +can be used to +disallow passwords of a given kind regardless of their length. +Each subsequent number is required to be no larger than the preceding +one. +.Pp +.Ar N0 +is used for passwords consisting of characters from one character +class only. +The character classes are: digits, lower-case letters, upper-case +letters, and other characters. +There is also a special class for +.No non- Ns Tn ASCII +characters, which could not be classified, but are assumed to be non-digits. +.Pp +.Ar N1 +is used for passwords consisting of characters from two character +classes that do not meet the requirements for a passphrase. +.Pp +.Ar N2 +is used for passphrases. +Note that besides meeting this length requirement, +a passphrase must also consist of a sufficient number of words (see the +.Cm passphrase +option below). +.Pp +.Ar N3 +and +.Ar N4 +are used for passwords consisting of characters from three +and four character classes, respectively. +.Pp +When calculating the number of character classes, upper-case letters +used as the first character and digits used as the last character of a +password are not counted. +.Pp +In addition to being sufficiently long, passwords are required to +contain enough different characters for the character classes and +the minimum length they have been checked against. +.Pp +.It Cm max Ns = Ns Ar N +.Pq default: Cm max Ns = Ns 72 +The maximum allowed password length. +This can be used to prevent users from setting passwords that may be +too long for some system services. +The value 8 is treated specially: if +.Cm max +is set to 8, passwords longer than 8 characters will not be rejected, +but will be truncated to 8 characters for the strength checks and the +user will be warned. +This is to be used with the traditional DES-based password hashes, +which truncate the password at 8 characters. +.Pp +It is important that you do set +.Cm max Ns = Ns 8 +if you are using the traditional +hashes, or some weak passwords will pass the checks. +.It Cm passphrase Ns = Ns Ar N +.Pq default: Cm passphrase Ns = Ns 3 +The number of words required for a passphrase. +.It Cm match Ns = Ns Ar N +.Pq default: Cm match Ns = Ns 4 +The length of common substring required to conclude that a password is +at least partially based on information found in a character string, +or 0 to disable the substring search. +Note that the password will not be rejected once a weak substring is +found; it will instead be subjected to the usual strength requirements +with the weak substring partially discounted. +.Pp +The substring search is case-insensitive and is able to detect and +remove a common substring spelled backwards. +.It Xo +.Sm off +.Cm similar No = Cm permit | deny +.Sm on +.Xc +.Pq default: Cm similar Ns = Ns Cm deny +Whether a new password is allowed to be similar to the old one. +The passwords are considered to be similar when there is a sufficiently +long common substring and the new password with the substring partially +discounted would be weak. +.It Cm wordlist Ns = Ns Ar FILE +Deny passwords that are based on lines of the tiny external text +.Ar FILE , +which can reasonably be e.g. a list of a few thousand common passwords. +Common dictionary words may also reasonably be included, especially in a +local language other than English, or longer yet common English words. +(passwdqc includes a list of a few thousand common English words of +lengths from 3 to 6 built in. Any word list possibly specified with +this option is used in addition to the built-in word list.) +.Pp +Substring matching and discounting will be used if the +.Cm match +setting +above is non-zero. Please note that this is very inefficient, and isn't +to be used with large wordlists. +.It Cm denylist Ns = Ns Ar FILE +Deny passwords or passphrases directly appearing in the tiny external text +.Ar FILE . +That file can reasonably be e.g. a list of common passwords if +only a relaxed policy is desired and stricter checks are thus disabled +(using their separate options). Such policy would only be somewhat +effective against online/remote attacks, but not against offline attacks +on hashed passwords. +.It Cm filter Ns = Ns Ar FILE +Deny passwords or passphrases directly appearing in a maybe huge binary +filter +.Ar FILE +created with pwqfilter. This is very efficient, needing at +most two random disk reads per query. A filter created from millions of +leaked passwords can reasonably be used on top of passwdqc's other +checks to further reduce the number of passing yet weak passwords +without causing unreasonable inconvenience (as e.g. higher minimum +lengths and character set requirements could). +.It Cm config Ns = Ns Ar FILE +Load config +.Ar FILE +in the +.Cm passwdqc.conf +format. This file may define any options described in +.Xr passwdqc.conf 5 , but only the +.Cm min , +.Cm max , +.Cm passphrase , +.Cm match Ns , +and +.Cm config +options are honored by +.Nm . +.It Cm -1 +Read just 1 line (new passphrase). +This is needed to use +.Nm +as the passwordcheck program on OpenBSD - e.g., with +":passwordcheck=/usr/bin/pwqcheck \-1:\\" +(without the quotes, but with the trailing backslash) +in the "default" section in +.Cm /etc/login.conf . +.It Cm -2 +Read just 2 lines (new and old passphrases). +.It Cm --multi +Check multiple passphrases (until EOF). +This option may be used on its own or along with the +.Cm -1 +or +.Cm -2 +options. +.Nm +will read 1, 2, or 3 lines and will output one line per passphrase to check. +The lines will start with either +.Ar OK +or a message explaining why the passphrase did not pass the checks, +followed by a colon and a space, and finally followed by the passphrase. +The explanatory message is guaranteed to not include a colon. +With this option, the exit status of +.Nm +depends solely on whether there were any errors preventing the strength of +passphrases from being fully checked or not. +A primary use for this option is to test different policies and/or different +versions of passwdqc on large passphrase lists. +.It Cm --version +Output +.Nm +program version and exit. +.It Cm -h , --help +Output +.Nm +help text and exit. +.El +.Sh EXIT STATUS +.Nm +exits with non-zero status when it encounters invalid config file, +invalid option, invalid parameter value, invalid data in standard input, +and in any case when it fails to check passphrase strength. +Without the +.Cm --multi +option, +.Nm +also exits with non-zero status when it detects a weak passphrase. +.Sh FILES +.Pa /etc/passwdqc.conf +(not read unless this suggested file location is specified with the +.Cm config=/etc/passwdqc.conf +option). +.Sh SEE ALSO +.Xr pwqgen 1 , +.Xr libpasswdqc 3 , +.Xr passwd 5 , +.Xr passwdqc.conf 5 , +.Xr pam_passwdqc 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh AUTHORS +The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. +The +.Nm +program was originally written for ALT GNU/*/Linux by Dmitry V. Levin, +indirectly reusing code from pam_passwdqc (via libpasswdqc). +This manual page (derived from the pam_passwdqc documentation) +was written for Openwall GNU/*/Linux by Dmitry V. Levin. diff --git a/contrib/pam_modules/pam_passwdqc/pwqcheck.c b/contrib/pam_modules/pam_passwdqc/pwqcheck.c new file mode 100644 index 000000000000..7c029944c20d --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqcheck.c @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2008,2009 by Dmitry V. Levin + * Copyright (c) 2010,2016,2021 by Solar Designer + * See LICENSE + */ + +#include +#include +#include + +#include "passwdqc.h" + +static void clean(char *dst, size_t size) +{ + if (!dst) + return; + _passwdqc_memzero(dst, size); + free(dst); +} + +static char *read_line(size_t size, int eof_ok) +{ + char *p, *buf = malloc(size + 1); + + if (!buf) { + fprintf(stderr, "pwqcheck: Memory allocation failed.\n"); + return NULL; + } + + if (!fgets(buf, size + 1, stdin)) { + clean(buf, size + 1); + if (!eof_ok || !feof(stdin) || ferror(stdin)) + fprintf(stderr, + "pwqcheck: Error reading standard input.\n"); + return NULL; + } + + if (strlen(buf) >= size) { + clean(buf, size + 1); + fprintf(stderr, "pwqcheck: Line too long.\n"); + return NULL; + } + + if ((p = strpbrk(buf, "\r\n"))) + *p = '\0'; + + return buf; +} + +static char *extract_string(char **stringp) +{ + char *token = *stringp, *colon; + + if (!token) + return ""; + + colon = strchr(token, ':'); + if (colon) { + *colon = '\0'; + *stringp = colon + 1; + } else + *stringp = NULL; + + return token; +} + +static struct passwd *parse_pwline(char *line, struct passwd *pw) +{ + if (!strchr(line, ':')) { +#ifdef _MSC_VER + memset(pw, 0, sizeof(*pw)); + pw->pw_name = line; +#else + struct passwd *p = getpwnam(line); + endpwent(); + if (!p) { + fprintf(stderr, "pwqcheck: User not found.\n"); + return NULL; + } + if (p->pw_passwd) + _passwdqc_memzero(p->pw_passwd, strlen(p->pw_passwd)); + memcpy(pw, p, sizeof(*pw)); +#endif + } else { + memset(pw, 0, sizeof(*pw)); + pw->pw_name = extract_string(&line); + pw->pw_passwd = extract_string(&line); + extract_string(&line); /* uid */ + extract_string(&line); /* gid */ + pw->pw_gecos = extract_string(&line); + pw->pw_dir = extract_string(&line); + pw->pw_shell = line ? line : ""; + if (!*pw->pw_name || !*pw->pw_dir) { + fprintf(stderr, "pwqcheck: Invalid passwd entry.\n"); + return NULL; + } + } + return pw; +} + +static void +print_help(void) +{ + puts("Check passphrase quality.\n" + "\nFor each passphrase to check, pwqcheck reads up to 3 lines from standard input:\n" + " first line is a new passphrase,\n" + " second line is an old passphrase, and\n" + " third line is either an existing account name or a passwd entry.\n" + "\nUsage: pwqcheck [options]\n" + "\nValid options are:\n" + " min=N0,N1,N2,N3,N4\n" + " set minimum allowed lengths for different kinds of passphrases;\n" + " max=N\n" + " set maximum allowed passphrase length;\n" + " passphrase=N\n" + " set number of words required for a passphrase;\n" + " match=N\n" + " set length of common substring in substring check;\n" + " similar=permit|deny\n" + " whether a new passphrase is allowed to be similar to the old one;\n" + " wordlist=FILE\n" + " deny passwords that are based on lines of a tiny external text file;\n" + " denylist=FILE\n" + " deny passphrases directly appearing in a tiny external text file;\n" + " filter=FILE\n" + " deny passphrases directly appearing in a maybe huge binary filter file;\n" + " config=FILE\n" + " load config FILE in passwdqc.conf format;\n" + " -1\n" + " read just 1 line (new passphrase);\n" + " -2\n" + " read just 2 lines (new and old passphrases);\n" + " --multi\n" + " check multiple passphrases (until EOF);\n" + " --version\n" + " print program version and exit;\n" + " -h or --help\n" + " print this help text and exit."); +} + +int main(int argc, const char **argv) +{ + passwdqc_params_t params; + const char *check_reason; + char *parse_reason, *newpass, *oldpass, *pwline; + struct passwd pwbuf, *pw; + int lines_to_read = 3, multi = 0; + size_t size = 8192; + int rc = 1; + + while (argc > 1 && argv[1][0] == '-') { + const char *arg = argv[1]; + + if (!strcmp("-h", arg) || !strcmp("--help", arg)) { + print_help(); + return 0; + } + + if (!strcmp("--version", arg)) { + printf("pwqcheck version %s\n", PASSWDQC_VERSION); + return 0; + } + + if ((arg[1] == '1' || arg[1] == '2') && !arg[2]) { + lines_to_read = arg[1] - '0'; + goto next_arg; + } + + if (!strcmp("--multi", arg)) { + multi = 1; + goto next_arg; + } + + break; + +next_arg: + argc--; argv++; + } + + passwdqc_params_reset(¶ms); + if (argc > 1 && + passwdqc_params_parse(¶ms, &parse_reason, argc - 1, + argv + 1)) { + fprintf(stderr, "pwqcheck: %s\n", + (parse_reason ? parse_reason : "Out of memory")); + free(parse_reason); + return rc; + } + + if ((size_t)params.qc.max + 1 > size) + size = (size_t)params.qc.max + 1; + +next_pass: + oldpass = pwline = NULL; pw = NULL; + if (!(newpass = read_line(size, multi))) { + if (multi && feof(stdin) && !ferror(stdin) && + fflush(stdout) >= 0) + rc = 0; + goto done; + } + if (lines_to_read >= 2 && !(oldpass = read_line(size, 0))) + goto done; + if (lines_to_read >= 3 && (!(pwline = read_line(size, 0)) || + !parse_pwline(pwline, pw = &pwbuf))) + goto done; + + check_reason = passwdqc_check(¶ms.qc, newpass, oldpass, pw); + if (!check_reason) { + if (multi) + printf("OK: %s\n", newpass); + else if (puts("OK") >= 0 && fflush(stdout) >= 0) + rc = 0; + goto cleanup; + } + if (multi) + printf("Bad passphrase (%s): %s\n", check_reason, newpass); + else + printf("Bad passphrase (%s)\n", check_reason); + +cleanup: + _passwdqc_memzero(&pwbuf, sizeof(pwbuf)); + clean(pwline, size); + clean(oldpass, size); + clean(newpass, size); + + if (multi) + goto next_pass; + + passwdqc_params_free(¶ms); + + return rc; + +done: + multi = 0; + goto cleanup; +} diff --git a/contrib/pam_modules/pam_passwdqc/pwqcheck.php b/contrib/pam_modules/pam_passwdqc/pwqcheck.php new file mode 100644 index 000000000000..03f953d25bb4 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqcheck.php @@ -0,0 +1,84 @@ + array('pipe', 'r'), + 1 => array('pipe', 'w')); +// Leave stderr (fd 2) pointing to where it is, likely to error_log + +// Replace characters that would violate the protocol + $newpass = strtr($newpass, "\n", '.'); + $oldpass = strtr($oldpass, "\n", '.'); + $user = strtr($user, "\n:", '..'); + +// Trigger a "too short" rather than "is the same" message in this special case + if (!$newpass && !$oldpass) + $oldpass = '.'; + + if ($args) + $args = ' ' . $args; + if (!$user) + $args = ' -2' . $args; // passwdqc 1.2.0+ + + $command = 'exec '; // No need to keep the shell process around on Unix + $command .= 'pwqcheck' . $args; + if (!($process = @proc_open($command, $descriptorspec, $pipes))) + return $retval; + + $err = 0; + fwrite($pipes[0], "$newpass\n$oldpass\n") || $err = 1; + if ($user) + fwrite($pipes[0], "$user::::$aux:/:\n") || $err = 1; + fclose($pipes[0]) || $err = 1; + ($output = stream_get_contents($pipes[1])) || $err = 1; + fclose($pipes[1]); + + $status = proc_close($process); + +// There must be a linefeed character at the end. Remove it. + if (substr($output, -1) === "\n") + $output = substr($output, 0, -1); + else + $err = 1; + + if ($err === 0 && ($status === 0 || $output !== 'OK')) + $retval = $output; + + return $retval; +} + +?> diff --git a/contrib/pam_modules/pam_passwdqc/pwqfilter.1 b/contrib/pam_modules/pam_passwdqc/pwqfilter.1 new file mode 100644 index 000000000000..fa07aab3e3b2 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqfilter.1 @@ -0,0 +1,145 @@ +.\" Copyright (c) 2021 Solar Designer +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd January 25, 2021 +.Dt PWQFILTER 1 +.Os "Openwall Project" +.Sh NAME +.Nm pwqfilter +.Nd Manage binary passphrase filter files +.Sh SYNOPSIS +.Nm Op Ar options +.Sh DESCRIPTION +The +.Nm +program searches, creates, or updates binary passphrase filter files, which can also be used with +.Xr pwqcheck 1 and +.Xr pam_passwdqc 8 . +Input and/or output binary filter files are specified via their corresponding command-line options, +whereas passphrases to look up or add, or their hashes, are read from standard input. +.Pp +.Nm +works on arbitrary plain text strings or hex-encoded hashes, +and thus can also be reused in lieu of +.Xr grep 1 +for many purposes unrelated to passphrases and security. +.Pp +For the binary filters, +.Nm +and thus the rest of passwdqc currently use an improved cuckoo filter, which is a probabilistic data structure. +Occasional false positives are possible (fewer than 1 in a billion), but false negatives are not. +.Sh MODE OPTIONS +.Bl -tag -width Ds +.It Cm --lookup +Look up plaintexts or hashes on standard input against an existing filter. +This is the default mode. +.It Cm --status +Report usage statistics for an existing filter. +.It Cm --create=CAPACITY +Create a new filter with CAPACITY entries, reading the initial set of plaintexts or hashes from standard input. +.Pp +The currently implemented cuckoo filter has a typical maximum load of around 98% +(as long as there are no duplicate inputs and the hashes are unbiased, or less otherwise). +The specified CAPACITY should thus be higher than the maximum expected number of entries by at least 2.04%. +.It Cm --insert +Insert (add) entries into an existing filter, reading the plaintexts or hashes from standard input. +.It Cm --test-fp-rate +Estimate the false positive rate (FP rate) of a filter. +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. +.El +.Sh OPTIMIZATION OPTIONS +These can be used with +.Cm --create +or +.Cm --insert . +.Bl -tag -width Ds +.It Cm --optimize-fp-rate +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%. +.It Cm --optimize-fp-rate-at-high-load +Better than default FP rate at load ~95% to 98%, a lot worse below ~90%. +.El +.Sh INPUT AND OUTPUT OPTIONS +.Bl -tag -width Ds +.It Cm -f FILE , --filter=FILE +Read an existing filter from FILE +.It Cm -o FILE , --output=FILE +Write a new or modified filter to FILE +.It Cm --pre-hashed +Look up or insert by hex-encoded hashes, not plaintexts. +.Pp +This option is later implied for further actions on filters created with it specified and no +.Cm --hash-* , +because +.Nm +has no way to know what hash type such filters use. +.It Cm --hash-md4 +Hash plaintexts with MD4 prior to lookup or insert. +This is the default for new filters. +.Pp +When used with +.Cm --pre-hashed , +specify that the pre-hashing was done with MD4. +.Pp +Cuckoo filters' use of a hash function is non-cryptographic, hence MD4's otherwise inadequate cryptographic security is irrelevant. +.It Cm --hash-ntlm-cp1252 +Hash assumed CP1252 encoding plaintexts with NTLM prior to lookup or insert, or specify that the pre-hashing was done that way +(e.g., like it was in a HIBP v7 download). +.El +.Sh LOOKUP OUTPUT MODIFIER OPTIONS +These are similar to those of +.Xr grep 1 . +.Bl -tag -width Ds +.It Cm -c , --count +Output a count of (non-)matching lines instead of the lines themselves. +.It Cm -n , --line-number +Prefix each line with its number in the input stream. +.It Cm -v , --invert-match +Output or count non-matching lines. +.El +.Sh GENERAL OPTIONS +.Bl -tag -width Ds +.It Cm --verbose +Output additional information. +.It Cm --version +Output +.Nm +program version and exit. +.It Cm -h , --help +Output +.Nm +help text and exit. +.El +.Sh EXIT STATUS +When looking up against an existing filter, +.Nm +exits with 0 if selected plaintexts or hashes are found, 1 if not found, or 2 on error. +These exit codes are compatible with those of +.Xr grep 1 . +In other modes, +.Nm +exits with 0 on success and 2 on error. +.Sh SEE ALSO +.Xr grep 1 , +.Xr pwqcheck 1 , +.Xr passwdqc.conf 5 , +.Xr pam_passwdqc 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh AUTHORS +.Nm +and this manual page were written by Solar Designer. diff --git a/contrib/pam_modules/pam_passwdqc/pwqfilter.c b/contrib/pam_modules/pam_passwdqc/pwqfilter.c new file mode 100644 index 000000000000..226217778cbb --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqfilter.c @@ -0,0 +1,1218 @@ +/* + * Copyright (c) 2020 by Solar Designer + * See LICENSE + */ + +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use unlink() */ +#define _CRT_SECURE_NO_WARNINGS /* we use fopen() */ +#include +#else +#include /* for unlink() */ +#endif + +#include +#include +#include +#include +#include + +#include "md4.h" +#include "passwdqc.h" +#define PASSWDQC_FILTER_INTERNALS +#include "passwdqc_filter.h" + +/* Flags corresponding to command-line options, can use bits 3 to 23 */ +#define OPT_VERBOSE 0x08 +#define OPT_COUNT 0x10 +#define OPT_LINE_NUMBER 0x20 +#define OPT_INVERT_MATCH 0x40 +#define OPT_PRE_HASHED 0x80 + +#define OPT_HASH_ID_SHIFT 8 +#define OPT_HASH_MD4 (PASSWDQC_FILTER_HASH_MD4 << OPT_HASH_ID_SHIFT) +#define OPT_HASH_NTLM_CP1252 (PASSWDQC_FILTER_HASH_NTLM_CP1252 << OPT_HASH_ID_SHIFT) +#define OPT_HASH_ID_MASK (OPT_HASH_MD4 | OPT_HASH_NTLM_CP1252) + +#define OPT_FP_RATE 0x1000 +#define OPT_FP_RATE_AT_HIGH_LOAD 0x2000 +#define OPT_TEST_FP_RATE 0x4000 + +/* Bitmask of all supported hash types */ +#define OPT_HASH_ALL (OPT_HASH_MD4 | OPT_HASH_NTLM_CP1252) + +/* Bitmask of options only valid in lookup mode */ +#define OPT_LOOKUP (OPT_COUNT | OPT_LINE_NUMBER | OPT_INVERT_MATCH) + +/* Bitmask of options only valid in create and insert modes */ +#define OPT_INSERT (OPT_FP_RATE | OPT_FP_RATE_AT_HIGH_LOAD) + +/* + * Cache line alignment is very important here because of the pattern in which + * elements of ssencode[] are used. With 64-byte cache lines, we use 444 of + * them with proper alignment, but at worst 563 otherwise. With 128-byte cache + * lines, we use 260 with proper alignment, 319 with alignment to 64 but not + * 128 bytes, and at worst 374 otherwise. (These numbers do not include the + * additional uses by variables that we insert into the largest gap.) + */ +#ifdef __GNUC__ +__attribute__ ((aligned (128))) +#endif +static union { + uint16_t ssencode[0x10000]; + struct { +/* + * Skip until the largest gap in ssencode[], which is from 0xf000 to 0xfffe. + * We skip an additional 0x30 elements (96 bytes) so that the hot part of the + * header (its second 32 bytes) starts at the beginning of a cache line and + * further hot fields that we have in here fall into the same cache line. + * Moreover, with the current fields this lets us have the first 8 bytes of + * ssdecode[] in the same cache line as well, which makes the rest of it fit + * into 121 64-byte cache lines (otherwise, with poor luck it'd need 122). + * This brings our total cache usage for these globals to (444+1+121)*64 = + * 36224 bytes. + */ + uint16_t skip[0xf030]; + passwdqc_filter_header_t header; + uint64_t maxkicks; + passwdqc_filter_packed_t *packed; + passwdqc_filter_i_t nbuckets; + uint32_t options; /* bitmask of OPT_* flags */ + uint16_t ssdecode[3876]; + } s; +} globals; + +#define ssencode globals.ssencode +#define header globals.s.header +#define maxkicks globals.s.maxkicks +#define packed globals.s.packed +#define nbuckets globals.s.nbuckets +#define options globals.s.options +#define ssdecode globals.s.ssdecode + +/* Store a copy of (updated) header.threshold in the hottest cache line */ +#define SET_THRESHOLD(x) options = (options & 0xffffff) | ((uint32_t)(x) << 24); +#define GET_THRESHOLD (options >> 24) + +/* For inserts only, also store (updated) header.bucket_size */ +#define SET_BUCKET_SIZE(x) options = (options & ~7U) | (x); +#define GET_BUCKET_SIZE (options & 7) + +static void ssinit(void) +{ + unsigned int a, b, c, d, n = 0; + for (d = 0; d < 16; d++) + for (c = d; c < 16; c++) + for (b = c; b < 16; b++) + for (a = b; a < 16; a++) { + uint16_t ssd = (d << 12) | (c << 8) | (b << 4) | a; + assert(ssd == passwdqc_filter_ssdecode(n)); + assert(n < sizeof(ssdecode) / sizeof(ssdecode[0])); + ssdecode[n++] = ssd; + ssencode[ssd] = n; + } + assert(n == sizeof(ssdecode) / sizeof(ssdecode[0])); + assert(&ssdecode[n] <= &ssencode[0xffff]); +} + +static inline unsigned int unpack(passwdqc_filter_unpacked_t *dst, const passwdqc_filter_packed_t *src) +{ + /* -1 cast to unsigned becomes greater than bucket size */ + return (unsigned int)passwdqc_filter_unpack(dst, src, ssdecode); +} + +static inline int lookup(passwdqc_filter_hash_t *h, passwdqc_filter_f_t fmask) +{ + passwdqc_filter_i_t i = passwdqc_filter_h2i(h, nbuckets); + passwdqc_filter_f_t f = passwdqc_filter_h2f(h); + + passwdqc_filter_unpacked_t u; + unsigned int n = unpack(&u, &packed[i]); + if (unlikely(n > GET_BUCKET_SIZE)) + return -1; + + unsigned int j; + for (j = 0; j < n; j++) + if (passwdqc_filter_f_eq(u.slots[j] & fmask, f & fmask, GET_BUCKET_SIZE)) + return 1; + +/* + * We can skip checking the secondary bucket on lookup when the primary one + * is below the fill threshold, but only as long as there are no deletes yet. + * Whenever a delete brings a bucket from at to below header.threshold, it + * must update header.threshold, and then we must use that in here (we do). + */ + if (n < GET_THRESHOLD) + return 0; + + n = unpack(&u, &packed[passwdqc_filter_alti(i, f, nbuckets)]); + if (unlikely(n > GET_BUCKET_SIZE)) + return -1; + + for (j = 0; j < n; j++) + if (passwdqc_filter_f_eq(u.slots[j] & fmask, f & fmask, GET_BUCKET_SIZE)) + return 1; + + return 0; +} + +/* + * Code specialization flags assuming pack() will be inlined (the corresponding + * checks would best be omitted if not inlining). + */ +#define PACK_MASK_OLD 1 +#define PACK_MASK_NEW 2 +#define PACK_MASK_ALL (PACK_MASK_OLD | PACK_MASK_NEW) + +static force_inline void pack(passwdqc_filter_packed_t *dst, const passwdqc_filter_unpacked_t *src, unsigned int n, int flags) +{ + if (n == 4) { /* 4x 33-bit as 12-bit semi-sort index, 4x 29-bit */ +/* + * Encode 4x 33-bit fingerprints as 12-bit semi-sort index of 4x 4-bit values + * corresponding to most significant bits of each fingerprint, followed by 4x + * 29-bit values holding the rest of the fingerprint data in original form. + */ + const unsigned int fbits = 33; + const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; + passwdqc_filter_f_t a = src->slots[0]; + passwdqc_filter_f_t b = src->slots[1]; + passwdqc_filter_f_t c = src->slots[2]; + passwdqc_filter_f_t d = src->slots[3]; + if (flags & PACK_MASK_OLD) { + a &= fmask; b &= fmask; c &= fmask; + if (flags & PACK_MASK_NEW) + d &= fmask; + } +#define SORT(x, y) if (x < y) { passwdqc_filter_f_t z = x; x = y; y = z; } + SORT(a, b) + SORT(c, d) +/* + * The check for "b < c" can be skipped and further 3 SORT() steps performed + * unconditionally. This check is a controversial optimization for the case of + * updating previously sorted lists. Unfortunately, it increases the average + * number of comparisons (but not swaps) for random lists. + */ + if (b < c) { + SORT(b, d) + SORT(a, c) + SORT(b, c) + } + const unsigned int lobits = fbits - 4; + uint16_t ssd = (uint16_t)(a >> lobits); + ssd |= (b >> (lobits - 4)) & 0x00f0; + ssd |= (c >> (lobits - 8)) & 0x0f00; + ssd |= (d >> (lobits - 12)) & 0xf000; + const passwdqc_filter_f_t lomask = ((passwdqc_filter_f_t)1 << lobits) - 1; + a &= lomask; + b &= lomask; + c &= lomask; + d &= lomask; + dst->lo = a | (b << lobits) | (c << (2 * lobits)); + dst->hi = (c >> (64 - 2 * lobits)) | (d << (3 * lobits - 64)) | ((uint64_t)ssencode[ssd] << (64 - 12)); + return; + } + + if (n == 3) { /* 11111, 3x 41-bit */ + const unsigned int fbits = 41; + const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; + passwdqc_filter_f_t a = src->slots[0]; + passwdqc_filter_f_t b = src->slots[1]; + passwdqc_filter_f_t c = src->slots[2]; + if (flags & PACK_MASK_OLD) { + a &= fmask; b &= fmask; + if (flags & PACK_MASK_NEW) + c &= fmask; + } +/* + * Sorting of fewer than 4 entries is unnecessary, but we use it to detect some + * kinds of data corruption. It also very slightly improves compressibility of + * the resulting filter files. + */ + SORT(b, c) + SORT(a, c) + SORT(a, b) + dst->lo = a | (b << fbits); + dst->hi = (b >> (64 - fbits)) | (c << (2 * fbits - 64)) | ((uint64_t)0xf80 << (64 - 12)); + return; + } + + if (n == 2) { /* 111101, 2x 61-bit */ + const unsigned int fbits = 61; + const passwdqc_filter_f_t fmask = ((passwdqc_filter_f_t)1 << fbits) - 1; + passwdqc_filter_f_t a = src->slots[0]; + passwdqc_filter_f_t b = src->slots[1]; + if (flags & PACK_MASK_OLD) { + a &= fmask; + if (flags & PACK_MASK_NEW) + b &= fmask; + } + SORT(a, b) +#undef SORT + dst->lo = a | (b << fbits); + dst->hi = (b >> (64 - fbits)) | ((uint64_t)0xf40 << (64 - 12)); + return; + } + + assert(n == 1); + + dst->lo = src->slots[0]; + dst->hi = 1; +} + +static force_inline unsigned int peek(const passwdqc_filter_packed_t *src) +{ + uint64_t hi = src->hi; + + if (hi <= 1) + return (unsigned int)hi; /* 0 or 1 */ + + unsigned int ssi = hi >> (64 - 12); /* semi-sort index */ + + if (ssi <= 3876) + return 4; + + return (ssi >> 7) & 3; /* 2 or 3 */ +} + +static force_inline int kick(passwdqc_filter_unpacked_t *u, passwdqc_filter_i_t i, passwdqc_filter_f_t f, unsigned int size) +{ + uint32_t rnd = i; + + do { +/* + * Peek at alternate buckets for each of the fingerprints stored in the bucket + * that we have to kick an entry from. If one of those buckets isn't full, + * plan to kick that fingerprint. Moreover, if a bucket has 2 or more empty + * slots, don't look further and kick that fingerprint right away. There are + * two aspects here: (1) never missing a non-full bucket that is just one step + * away greatly reduces the number of kicks needed to reach high load factors + * (approximately from 16x to 6x of capacity for 98% as compared to pure random + * walk, and twice quicker in terms of real time on a certain machine), and (2) + * favoring buckets with 2+ empty slots tends to slightly lower the FP rate. + */ + passwdqc_filter_i_t ia; + passwdqc_filter_f_t fkick, fdiff = 0; + unsigned int n, j = size - 1, bestj = 0; + do { + fkick = u->slots[j]; + ia = passwdqc_filter_alti(i, fkick, nbuckets); + if ((n = peek(&packed[ia])) < size) { + bestj = j; + if (!j || n < size - 1) + goto kick; + } + fdiff |= f ^ fkick; + } while (j--); + +/* If there are no non-full buckets one step away, resort to random walk */ + if (!bestj) { +/* + * If our fingerprint to be (re-)inserted is the same as all of those we could + * have kicked, then we're at or close to the maximum number of duplicates for + * this fingerprint that we can hold. Don't (re-)insert this duplicate so that + * we don't waste many further kicks on a likely failure. Note that this isn't + * necessarily the fingerprint insert() was called on now. We might have + * already inserted the new fingerprint and if so are now deleting an excessive + * duplicate of something previously inserted. + */ + if (unlikely(!fdiff)) { + header.dupes++; + return 1; + } +/* + * Good randomness is crucial for the random walk. This simple formula works + * surprisingly well by mostly reusing variables that we maintain anyway. + */ + rnd = (rnd + (uint32_t)fdiff) * (uint32_t)header.kicks; + if (likely(size != 2)) { /* hopefully, compile-time */ + bestj = rnd >> 30; + while (bestj >= size) /* only if size == 3 */ + bestj = (rnd <<= 2) >> 30; + } else { + bestj = rnd >> 31; + } + } + + if (likely(bestj)) { /* recompute unless still have */ + fkick = u->slots[bestj]; + ia = passwdqc_filter_alti(i, fkick, nbuckets); + } + +kick: + u->slots[bestj] = f; + pack(&packed[i], u, size, 0); + + n = unpack(u, &packed[ia]); + if (unlikely(n > size)) + return -1; + + if (n < size) { + u->slots[n++] = fkick; + pack(&packed[ia], u, n, PACK_MASK_OLD); + header.inserts++; + header.kicks++; + return 0; + } + + f = fkick; + i = ia; + } while (likely(++header.kicks < maxkicks)); + + return -2; +} + +static inline int insert(passwdqc_filter_hash_t *h) +{ + passwdqc_filter_i_t i = passwdqc_filter_h2i(h, nbuckets); + passwdqc_filter_f_t f = passwdqc_filter_h2f(h); + +/* + * Plan to put this entry into the primary bucket if it's below the threshold. + * Otherwise see if the secondary bucket is less full and use it if so. This + * logic balances between two conflicting goals: letting us skip the secondary + * bucket on lookup when primary isn't full (or is below threshold), and + * filling different buckets across the entire table evenly. Each of these + * goals has two (luckily non-conflicting) sub-goals. The former reduces FP + * rate through comparing against fewer stored fingerprints, and speeds up + * lookups. The latter helps reach high load factors in fewer kicks and + * preserves more of the larger fingerprints by not putting unnecessarily many + * entries in one bucket while we could still avoid that, which also reduces + * FP rate. In terms of FP rate, different thresholds turn out to be optimal + * depending on target load factor: a threshold of 4 is more optimal for the + * highest load factors (near the maximum of 98%), lower thresholds like 2 are + * more optimal at lower load factors. Our gradual increase of effective + * bucket size plays a further role (even more important at low load factors). + */ + unsigned int n = peek(&packed[i]); + if (n >= GET_THRESHOLD) { + passwdqc_filter_i_t ia = passwdqc_filter_alti(i, f, nbuckets); + if (peek(&packed[ia]) < n) + i = ia; + } + + passwdqc_filter_unpacked_t u; + n = unpack(&u, &packed[i]); + if (unlikely(n > GET_BUCKET_SIZE)) + return -1; + + if (n < GET_BUCKET_SIZE) { + u.slots[n++] = f; + pack(&packed[i], &u, n, PACK_MASK_ALL); + header.inserts++; + return 0; + } + +/* + * At this point, we have one unpacked bucket that is at exactly the current + * bucket size. We could have chosen either primary or secondary at random, + * as the classic cuckoo filter insertion algorithm does, but testing shows + * that this is unnecessary and a fixed implementation-specific choice works + * just as well. + */ + + if (likely(n == 4)) { /* specialized code as an optimization */ +/* + * We only kick fingerprints from full buckets, which implies that they're + * already masked to the worst extent possible at the current bucket size. + * This lets us use optimized non-masking pack() in kick()'s loop, but only as + * long as we don't need the masking for the new fingerprint as well. Let's + * pre-mask it here to make this so. We already know we'll have to insert it + * into a full bucket (kicking another fingerprint from it), so we couldn't + * have preserved those bits anyway. + */ + f &= ((passwdqc_filter_f_t)1 << 33) - 1; + return kick(&u, i, f, 4); + } else if (likely(n == 2)) { /* and no bucket is larger yet */ + f &= ((passwdqc_filter_f_t)1 << 61) - 1; + return kick(&u, i, f, 2); + } else { /* n == 3 and no bucket is larger yet */ + f &= ((passwdqc_filter_f_t)1 << 41) - 1; + return kick(&u, i, f, 3); + } +} + +static const uint8_t fingerprint_sizes_234[] = {61, 41, 33}; +static const char * const hash_names[] = {"opaque", "MD4", "NTLM CP1252"}; + +static void print_status(void) +{ + printf("Capacity %llu, usage %llu (inserts %llu, deletes %llu), load %.2f%%\n" + "Hash type %s, buckets of %u at least %u-bit fingerprints, threshold %u\n" + "Effective duplicates omitted %llu, kicks %llu (%.2f of capacity)\n", + (unsigned long long)header.capacity, (unsigned long long)(header.inserts - header.deletes), + (unsigned long long)header.inserts, (unsigned long long)header.deletes, + 100. * (header.inserts - header.deletes) / header.capacity, + header.hash_id < sizeof(hash_names) / sizeof(hash_names[0]) ? hash_names[header.hash_id] : "unsupported", + (unsigned int)header.bucket_size, (unsigned int)fingerprint_sizes_234[header.bucket_size - 2], + (unsigned int)header.threshold, + (unsigned long long)header.dupes, (unsigned long long)header.kicks, + 1. * header.kicks / header.capacity); +} + +static int new_filter(void) +{ + header.capacity = (header.capacity + 3) & ~3ULL; + nbuckets = (uint32_t)(header.capacity >> 2); + packed = calloc(nbuckets, sizeof(*packed)); + if (!packed) { + perror("pwqfilter: calloc"); + return -1; + } + + memcpy(header.version, PASSWDQC_FILTER_VERSION, sizeof(header.version)); + if (options & OPT_FP_RATE_AT_HIGH_LOAD) + SET_THRESHOLD(header.threshold = 4) + else + SET_THRESHOLD(header.threshold = 2) + SET_BUCKET_SIZE(header.bucket_size = header.threshold) + header.hash_id = (options & OPT_HASH_ID_MASK) >> OPT_HASH_ID_SHIFT; + header.endianness = PASSWDQC_FILTER_ENDIANNESS; + + return 0; +} + +static int read_filter(const char *filename, int print_status_only) +{ + FILE *f = fopen(filename, "rb"); + if (!f) { + perror("pwqfilter: fopen"); + return -1; + } + + int retval = 0; + if (fread(&header, sizeof(header), 1, f) != 1) + goto fail_fread; + + if (passwdqc_filter_verify_header(&header)) { + fprintf(stderr, "pwqfilter: Invalid or unsupported input filter.\n"); + goto fail; + } + + if ((options & OPT_VERBOSE) || print_status_only) { + print_status(); + if (print_status_only) + goto out; + } + + SET_THRESHOLD(header.threshold) + SET_BUCKET_SIZE(header.bucket_size) + + if ((options & OPT_FP_RATE_AT_HIGH_LOAD) && header.threshold < 4) + fprintf(stderr, "pwqfilter: Warning: --optimize-fp-rate-at-high-load is too late for this filter.\n"); + + nbuckets = (uint32_t)(header.capacity >> 2); + if (SIZE_MAX <= 0xffffffffU && nbuckets > SIZE_MAX / sizeof(*packed)) { + fprintf(stderr, "pwqfilter: Input filter claims to be too large for this system.\n"); + goto fail; + } + + packed = malloc(nbuckets * sizeof(*packed)); + if (!packed) { + perror("pwqfilter: malloc"); + goto fail; + } + + if (fread(packed, sizeof(*packed), nbuckets, f) != nbuckets) { +fail_fread: + if (ferror(f)) + perror("pwqfilter: fread"); + else + fprintf(stderr, "pwqfilter: fread: Unexpected EOF\n"); +fail: + retval = -1; + } + +out: + fclose(f); + + return retval; +} + +static int write_filter(const char *filename) +{ + FILE *f = fopen(filename, "wb"); + if (!f) { + perror("pwqfilter: fopen"); + return -1; + } + + int retval = 0; + if (fwrite(&header, sizeof(header), 1, f) != 1 || + fwrite(packed, sizeof(*packed), nbuckets, f) != nbuckets) { + perror("pwqfilter: fwrite"); + retval = -1; + } + + if (fclose(f) || retval) { + if (!retval) + perror("pwqfilter: fclose"); + retval = -1; + if (unlink(filename)) + perror("pwqfilter: unlink"); + } + + return retval; +} + +#define READ_LINE_MAX 8192 + +static inline char *read_line(void) +{ +#ifdef __GNUC__ +__attribute__ ((aligned (128))) +#endif + static char buf[READ_LINE_MAX + 2]; + + buf[READ_LINE_MAX] = '\n'; + + if (unlikely(!fgets(buf, sizeof(buf), stdin))) { + if (ferror(stdin)) + perror("pwqfilter: fgets"); + return NULL; + } + + if (unlikely(buf[READ_LINE_MAX] != '\n')) { + int c; + do { + c = getc(stdin); + } while (c != EOF && c != '\n'); + if (ferror(stdin)) { + perror("pwqfilter: getc"); + return NULL; + } + } + + return buf; +} + +static inline int unhex(passwdqc_filter_hash_t *dst, const char *src) +{ +#ifdef __GNUC__ +__attribute__ ((aligned (64))) +#endif + static const uint8_t a2i[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 16, 16, 16, 16, 16, 16, 16, + 10, 11, 12, 13, 14, 15, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 10, 11, 12, 13, 14, 15 + }; + unsigned char *dp = dst->uc; + const unsigned char *dend = dst->uc + sizeof(dst->uc); + const unsigned char *sp = (const unsigned char *)src; + + do { + unsigned int c, hi, lo; + c = *sp++ - '0'; + if (c >= sizeof(a2i) || (hi = a2i[c]) > 15) + break; + c = *sp++ - '0'; + if (c >= sizeof(a2i) || (lo = a2i[c]) > 15) + break; + *dp++ = (hi << 4) | lo; + } while (likely(dp < dend)); + + return likely(dp == dend) ? 0 : -1; +} + +static inline int line_to_hash(passwdqc_filter_hash_t *dst, const char *line, unsigned long long lineno) +{ + if (options & OPT_HASH_ALL) { + if (unlikely(line[READ_LINE_MAX] != '\n')) { + fprintf(stderr, "\rpwqfilter: Line %llu too long.\n", lineno); + return -1; + } + if (options & OPT_HASH_MD4) + passwdqc_filter_md4(dst, line); + else + passwdqc_filter_ntlm_cp1252(dst, line); + } else if (unlikely(unhex(dst, line))) { + fprintf(stderr, "\rpwqfilter: Not a supported hex-encoded hash on standard input line %llu.\n", lineno); + return -1; + } + + return 0; +} + +static int lookup_loop(void) +{ + char *line; + unsigned long long lineno = 0, lookups = 0, positive = 0, negative = 0, errors = 0; + + while ((line = read_line())) { + lineno++; + + passwdqc_filter_hash_t h; + if (unlikely(line_to_hash(&h, line, lineno))) { + errors++; + continue; + } + + lookups++; + int status = lookup(&h, ~(passwdqc_filter_f_t)0); + if (unlikely(status < 0)) + break; + if (status) { + positive++; + if (!(options & (OPT_COUNT | OPT_INVERT_MATCH))) { +print: + if (options & OPT_LINE_NUMBER) + printf("%llu:", lineno); + fputs(line, stdout); + } + } else { + negative++; + if ((options & (OPT_COUNT | OPT_INVERT_MATCH)) == OPT_INVERT_MATCH) + goto print; + } + } + + if (line) + fprintf(stderr, "Data corruption detected, abandoning further search\n"); + else if (options & OPT_COUNT) + printf("%llu\n", (options & OPT_INVERT_MATCH) ? negative : positive); + if (options & OPT_VERBOSE) + fprintf(stderr, "Lines %llu, lookups %llu, positive %llu, negative %llu, errors %llu\n", + lineno, lookups, positive, negative, errors); + + if (line || ferror(stdin)) + return -1; + + return !!((options & OPT_INVERT_MATCH) ? negative : positive); +} + +static void set_bucket_size(void) +{ + uint64_t usage = header.inserts - header.deletes; + uint64_t max_kicks_until_size_3 = (header.capacity >> ((options & OPT_FP_RATE) ? 2 : 5)) * 3; + unsigned int size = 4; + if (usage < header.capacity * 44 / 100 && header.kicks <= max_kicks_until_size_3) + size = 2; + else if (usage < header.capacity * 71 / 100 && header.kicks <= (max_kicks_until_size_3 << 1)) + size = 3; + + if (size < GET_THRESHOLD) + size = GET_THRESHOLD; + + if (size > GET_BUCKET_SIZE || !header.inserts) { + if (size > GET_BUCKET_SIZE) + SET_BUCKET_SIZE((header.bucket_size = size)) + if (options & OPT_VERBOSE) { + putc('\r', stderr); + printf("Storing at least %u-bit fingerprints since load %.2f%%, kicks %.2f of capacity\n", + (unsigned int)fingerprint_sizes_234[GET_BUCKET_SIZE - 2], + 100. * (header.inserts - header.deletes) / header.capacity, + 1. * header.kicks / header.capacity); + } + } +} + +static void print_progress(unsigned long long lineno) +{ + fprintf(stderr, "\rLines %.*f%s, load %.2f%%, kicks %.2f of capacity", + lineno < 1000000 ? 0 : 3, + lineno < 1000000 ? (double)lineno : 1e-6 * lineno, + lineno < 1000000 ? "" : "M", + 100. * (header.inserts - header.deletes) / header.capacity, + 1. * header.kicks / header.capacity); +} + +static int insert_loop(void) +{ + uint64_t inserts_start = header.inserts; + uint64_t dupes_start = header.dupes; + + uint64_t checkpoint = 0, previous = 0; + uint64_t effort_step = (header.capacity + 199) / 200; + uint64_t inserts_step = effort_step; + uint64_t inserts_goal = header.capacity / 10; + if (inserts_goal < header.inserts) + inserts_goal = header.inserts; + maxkicks = header.capacity; + + int status = 0; + char *line; + unsigned long long lineno = 0, errors = 0; + +/* + * A threshold of 0 is different for lookup, but we can optimize its handling + * for insert. + */ + if (GET_THRESHOLD == 0) + SET_THRESHOLD(1) + + while ((line = read_line())) { + uint64_t effort = header.inserts + header.kicks; + if (unlikely(effort >= checkpoint)) { + set_bucket_size(); + if (!checkpoint || effort - previous >= 1000000) { + previous = effort; + print_progress(lineno); + } + checkpoint = effort + effort_step; + if (header.inserts >= inserts_goal) { + uint64_t usage = header.inserts - header.deletes; + if (usage > header.capacity) + break; + if (usage >= header.capacity * 97 / 100) + inserts_step = (header.capacity + 999 - usage) / 1000; + else + inserts_step = (header.capacity + 199 - usage) / 200; + inserts_goal = header.inserts + inserts_step; + maxkicks = header.kicks + header.capacity; + } + } + + lineno++; + + passwdqc_filter_hash_t h; + if (unlikely(line_to_hash(&h, line, lineno))) { + errors++; + continue; + } + + if (unlikely((status = insert(&h)) < 0)) + break; + } + + SET_THRESHOLD(header.threshold) + + if (line) { + print_progress(lineno); + if (status == -2) { +/* + * We have to abandon the filter here because when we bump into maxkicks we've + * kicked out and not re-inserted an entry likely other than the one we were + * trying to insert originally. To avoid this, we'd need a separate soft limit + * that we'd most likely bump into between insert() calls (not inside a call). + */ + fprintf(stderr, "\nProgress almost stalled, abandoning incomplete filter\n"); +/* + * For filters of medium size (some million entries), we expect to be able to + * achieve a little over 98% (e.g., 98.03%) with unbiased non-repeating inputs. + * For small filters, there's significant variability of maximum achievable + * load (e.g., 97.7% to 98.3%). For filters approaching the maximum supported + * capacity of almost 2^34, the biases caused by our use of only 32 bits in + * h2i() become significant and in simulation limit the achievable load e.g. to + * 97% for a capacity of a little over half the maximum. To be on the safe + * side, we only print a likely explanation for below 97% and only for filters + * that are way below the maximum capacity. + */ + if (header.capacity <= (1ULL << 32) && + header.inserts - header.deletes < header.capacity * 97 / 100) + fprintf(stderr, "Likely too many repeating%s inputs%s\n", + (options & OPT_HASH_ALL) ? "" : " or biased", + header.capacity < 1000000 ? " or filter is too small" : ""); + } else { /* -1 return from insert() or usage > capacity */ + fprintf(stderr, "\nData corruption detected, abandoning incomplete filter\n"); + } + } + fprintf(stderr, "\rLines %llu, inserts %llu, excessive effective duplicates %llu, errors %llu\n", + lineno, (unsigned long long)(header.inserts - inserts_start), (unsigned long long)(header.dupes - dupes_start), errors); + + return (line || ferror(stdin)) ? -1 : 0; +} + +static int test_fp_rate(void) +{ + unsigned int fps = 0, tests = 0, errors = 0; + + if (header.inserts != header.deletes) + do { + int i, n = tests + (1 << 22); /* signed int for OpenMP 2.5 */ +#ifdef _OPENMP +#pragma omp parallel for default(none) private(i) shared(n, fps, tests, errors) +#endif + for (i = tests; i < n; i++) { + passwdqc_filter_hash_t h; + MD4_CTX ctx; + MD4_Init(&ctx); + ctx.a += i; + MD4_Update(&ctx, "notNTLM", 8); + MD4_Final(h.uc, &ctx); + +/* + * Process the hash table semi-sequentially for some speedup. As long as we + * ensure we test all possible values of the first 3 bytes, this does not bias + * the final estimate, but the verbose output shown during testing might show + * biased numbers until eventually converging to the global average. See also + * the comment in passwdqc_filter_h2i(). + */ + h.uc[0] = i >> 22; + h.uc[1] = i >> 14; + h.uc[2] = i >> 6; + h.u32[0] = ((h.u32[0] & 0x0f0f0f0f) << 4) | ((h.u32[0] >> 4) & 0x0f0f0f0f); + + switch (lookup(&h, ~(passwdqc_filter_f_t)0xfffff)) { + case 0: + break; + case 1: +#ifdef _OPENMP +#pragma omp atomic +#endif + fps++; + break; + default: /* -1 */ +#ifdef _OPENMP +#pragma omp atomic +#endif + errors++; + } +#ifndef _OPENMP + if (unlikely(errors)) + break; +#endif + } + tests = n; + + double progress = 100. * tests / (1 << 30); + if (options & OPT_VERBOSE) + fprintf(stderr, "\rTests %u (%.2f%%), FPs %u (rate %.3e) for fingerprints cut by 20 bits", + tests, progress, fps, (double)fps / tests); + else + fprintf(stderr, "\rTests %u (%.2f%%)", tests, progress); + } while (tests < (1 << 30) && !errors); + + if (tests) + putc('\n', stderr); + if (errors) { + fprintf(stderr, "Data corruption detected, abandoning further testing\n"); + return -1; + } + if (fps) { + double bperfp = 1e-9 * ((unsigned long long)tests << 20) / fps; + printf("Estimated FP rate 1 in %.*f billion\n", (bperfp < 10) + (bperfp < 100) + (bperfp < 1000), bperfp); + } else { + printf("Estimated FP rate 0 (%s)\n", tests ? "no FPs seen in testing" : "empty filter"); + } + + return 0; +} + +static int opt_eq(const char *ref, const char *opt, const char **arg) +{ + size_t n = strlen(ref); + int retval = !strncmp(ref, opt, n) && (!opt[n] || opt[n] == '='); + if (retval && opt[n] && opt[n + 1]) + *arg = &opt[n + 1]; + return retval; +} + +static void print_help(void) +{ + puts("Manage binary passphrase filter files.\n" + "\nUsage: pwqfilter [options]\n" + "\nValid options are:\n" + "Modes\n" + " --lookup (default)\n" + " lookup plaintexts or hashes against an existing filter;\n" + " --status\n" + " print usage statistics for an existing filter;\n" + " --create=CAPACITY\n" + " create a new filter for up to ~98% of CAPACITY entries;\n" + " --insert\n" + " insert entries into an existing filter;\n" + " --test-fp-rate (can be used on its own or along with another mode)\n" + " estimate the false positive rate (FP rate) of a filter;\n" + "Optimizations (with --create or --insert)\n" + " --optimize-fp-rate\n" + " better than default FP rate, briefly slower inserts after ~30% and ~60%;\n" + " --optimize-fp-rate-at-high-load\n" + " better than default FP rate at load ~95% to 98%, a lot worse below ~90%;\n" + "Input and output\n" + " -f FILE or --filter=FILE\n" + " read an existing filter from FILE;\n" + " -o FILE or --output=FILE\n" + " write a new or modified filter to FILE;\n" + " --pre-hashed (default for filters created with this option and no --hash-*)\n" + " lookup or insert by hex-encoded hashes, not plaintexts;\n" + " --hash-md4 (default for new filters)\n" + " hash plaintexts with MD4 prior to lookup or insert;\n" + " --hash-ntlm-cp1252\n" + " hash assumed CP1252 plaintexts with NTLM prior to lookup or insert;\n" + "Lookup output modifiers\n" + " -c or --count\n" + " print a count of (non-)matching lines instead of the lines themselves;\n" + " -n or --line-number\n" + " prefix each line with its number in the input stream;\n" + " -v or --invert-match\n" + " print or count non-matching lines;\n" + "General\n" + " --verbose\n" + " print additional information;\n" + " --version\n" + " print program version and exit;\n" + " -h or --help\n" + " print this help text and exit."); +} + +int main(int argc, char **argv) +{ + enum {MODE_NONE = 0, MODE_LOOKUP = 1, MODE_STATUS = 2, MODE_CREATE = 3, MODE_INSERT} mode = MODE_NONE; + const char *input = NULL, *output = NULL; + + options = 0; + + if (unlikely(argc <= 1)) { + fprintf(stderr, "pwqfilter: No action requested, try --help.\n"); + return 2; + } + + while (argc > 1) { + const char *opt = argv[1], *arg = NULL; + if (opt[0] == '-' && opt[1] != '-' && opt[1] && opt[2]) { + static char optbuf[3] = {'-', 0, 0}; + optbuf[1] = opt[1]; + opt = optbuf; + memmove(&argv[1][1], &argv[1][2], strlen(&argv[1][1])); + } else { + argc--; argv++; + } + + if (!strcmp("-h", opt) || !strcmp("--help", opt)) { + print_help(); + return 0; + } + + if (!strcmp("--version", opt)) { + printf("pwqfilter version %s\n", PASSWDQC_VERSION); + return 0; + } + + if (!strcmp("--lookup", opt)) { + if (mode || output) + goto fail_conflict; + mode = MODE_LOOKUP; + continue; + } + + if (!strcmp("--status", opt)) { + if (mode || (options & (OPT_HASH_ALL | OPT_PRE_HASHED))) + goto fail_conflict; + mode = MODE_STATUS; + continue; + } + + if (opt_eq("--create", opt, &arg)) { + if (mode || input || (options & OPT_LOOKUP)) + goto fail_conflict; + mode = MODE_CREATE; + if (!arg) + goto fail_no_arg; + char *e; + header.capacity = strtoul(arg, &e, 0); + if (*e || !header.capacity || header.capacity > ((1ULL << 32) - 1) * 4) { + fprintf(stderr, "pwqfilter: Requested capacity is invalid or unsupported.\n"); + return 2; + } + continue; + } + + if (!strcmp("--insert", opt)) { + if (mode || (options & OPT_LOOKUP)) + goto fail_conflict; + mode = MODE_INSERT; + continue; + } + + if (!strcmp("--test-fp-rate", opt)) { + options |= OPT_TEST_FP_RATE; + continue; + } + + if (!strcmp("--optimize-fp-rate", opt)) { + if (options & OPT_FP_RATE_AT_HIGH_LOAD) + goto fail_conflict; + options |= OPT_FP_RATE; + continue; + } + + if (!strcmp("--optimize-fp-rate-at-high-load", opt)) { + if (options & OPT_FP_RATE) + goto fail_conflict; + options |= OPT_FP_RATE_AT_HIGH_LOAD; + continue; + } + + if (!strcmp("-f", opt) || opt_eq("--filter", opt, &arg)) { + if (mode == MODE_CREATE || input) + goto fail_conflict; + if (!opt[2]) { + argc--; + arg = *++argv; + } + if (!arg) + goto fail_no_arg; + input = arg; + continue; + } + + if (!strcmp("-o", opt) || opt_eq("--output", opt, &arg)) { + if (mode == MODE_LOOKUP || mode == MODE_STATUS || output) + goto fail_conflict; + if (!opt[2]) { + argc--; + arg = *++argv; + } + if (!arg) + goto fail_no_arg; + output = arg; + continue; + } + + if (!strcmp("--pre-hashed", opt)) { + if (mode == MODE_STATUS) + goto fail_conflict; + options |= OPT_PRE_HASHED; + continue; + } + + if (!strcmp("--hash-md4", opt)) { + if ((options & OPT_HASH_ALL) || mode == MODE_STATUS) + goto fail_conflict; + options |= OPT_HASH_MD4; + continue; + } + + if (!strcmp("--hash-ntlm-cp1252", opt)) { + if ((options & OPT_HASH_ALL) || mode == MODE_STATUS) + goto fail_conflict; + options |= OPT_HASH_NTLM_CP1252; + continue; + } + + if (!strcmp("-c", opt) || !strcmp("--count", opt)) { + if (mode > MODE_LOOKUP || (options & OPT_LINE_NUMBER)) + goto fail_conflict; + options |= OPT_COUNT; + continue; + } + + if (!strcmp("-n", opt) || !strcmp("--line-number", opt)) { + if (mode > MODE_LOOKUP || (options & OPT_COUNT)) + goto fail_conflict; + options |= OPT_LINE_NUMBER; + continue; + } + + if (!strcmp("-v", opt) || !strcmp("--invert-match", opt)) { + if (mode > MODE_LOOKUP) + goto fail_conflict; + options |= OPT_INVERT_MATCH; + continue; + } + + if (!strcmp("--verbose", opt)) { + options |= OPT_VERBOSE; + continue; + } + + fprintf(stderr, "pwqfilter: Option %s unrecognized.\n", opt); + return 2; + +fail_no_arg: + fprintf(stderr, "pwqfilter: Option %s requires an argument.\n", opt); + return 2; + +fail_conflict: + fprintf(stderr, "pwqfilter: Option %s conflicts with previously specified options.\n", opt); + return 2; + } + + if (!mode) { + if (options & OPT_TEST_FP_RATE) { + if (options & (OPT_HASH_ALL | OPT_PRE_HASHED)) + goto fail_unused; + } else { + mode = MODE_LOOKUP; /* default mode */ + } + } + + if (!input && !(options & (OPT_HASH_ALL | OPT_PRE_HASHED))) + options |= OPT_HASH_MD4; /* default hash type */ + + if (mode <= MODE_STATUS && output) { + fprintf(stderr, "pwqfilter: No filter modifications requested yet an output filename specified.\n"); + return 2; + } + + if ((mode != MODE_LOOKUP && (options & OPT_LOOKUP)) || + (mode < MODE_CREATE && (options & OPT_INSERT)) || + (mode != MODE_CREATE && (options & OPT_HASH_ALL) && (options & OPT_PRE_HASHED))) { +fail_unused: + fprintf(stderr, "pwqfilter: The requested mode doesn't use other specified options.\n"); + return 2; + } + + if (mode != MODE_CREATE && !input) { + fprintf(stderr, "pwqfilter: Neither requested to create a new filter nor to use an existing one.\n"); + return 2; + } + + if (mode > MODE_STATUS && !output) + fprintf(stderr, "pwqfilter: No output filename specified - doing a dry run.\n"); + + if ((input && read_filter(input, mode == MODE_STATUS)) || (!input && new_filter())) + return 2; + +/* + * The uses of (un)likely() here optimize for --create --pre-hashed. Somehow + * omitting them results in very different code (smaller and slower) in inner + * loops at least on a certain RHEL7'ish test system. + */ + if (unlikely(mode == MODE_STATUS)) { + if ((options & OPT_TEST_FP_RATE) && test_fp_rate()) + return 2; + return 0; + } + + if (!likely(options & OPT_PRE_HASHED)) { + if (header.hash_id > PASSWDQC_FILTER_HASH_MAX) { + fprintf(stderr, "pwqfilter: Input filter claims unsupported hash type.\n"); + return 2; + } + + if (header.hash_id != PASSWDQC_FILTER_HASH_OPAQUE) { + uint32_t new_options = (options & ~OPT_HASH_ID_MASK) | ((uint32_t)header.hash_id << OPT_HASH_ID_SHIFT); + if ((options & OPT_HASH_ALL) && new_options != options) { + fprintf(stderr, "pwqfilter: Input filter's hash type is different than requested.\n"); + return 2; + } + options = new_options; + } + } + + ssinit(); + + if (mode == MODE_LOOKUP) { + int status = 1 - lookup_loop(); + if ((options & OPT_TEST_FP_RATE) && test_fp_rate()) + return 2; + return status; + } + + if (likely(mode >= MODE_CREATE)) { +/* + * The weird combination of --pre-hashed and --hash* is allowed with --create + * for writing the claimed hash type into the filter, but shouldn't result in + * us hashing the hashes. + */ + if (options & OPT_PRE_HASHED) + options &= ~OPT_HASH_ALL; + + if (insert_loop()) + return 2; + + if (options & OPT_VERBOSE) + print_status(); + + if (output && write_filter(output)) + return 2; + } + + if ((options & OPT_TEST_FP_RATE) && test_fp_rate()) + return 2; + + return 0; +} diff --git a/contrib/pam_modules/pam_passwdqc/pwqgen.1 b/contrib/pam_modules/pam_passwdqc/pwqgen.1 new file mode 100644 index 000000000000..50efe469a6cc --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqgen.1 @@ -0,0 +1,86 @@ +.\" Copyright (c) 2009 Dmitry V. Levin +.\" All rights reserved. +.\" Copyright (c) 2019 Solar Designer +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 10, 2021 +.Dt PWQGEN 1 +.Os "Openwall Project" +.Sh NAME +.Nm pwqgen +.Nd Generate quality controllable random passphrase +.Sh SYNOPSIS +.Nm Op Ar options +.Sh DESCRIPTION +The +.Nm +program generates a random passphrase using the libpasswdqc library. +Strength of the generated passphrase depends on the amount of randomness +read from +.Pa /dev/urandom . +.Sh OPTIONS +.Bl -tag -width indent +.It Cm random Ns = Ns Ar N +.Pq default: Cm random Ns = Ns 47 +The size of randomly-generated passphrase in bits (24 to 136). +.It Cm config Ns = Ns Ar FILE +Load config +.Ar FILE +in the +.Cm passwdqc.conf +format. This file may define any options described in +.Xr passwdqc.conf 5 , but only the +.Cm random +and +.Cm config +options are honored by +.Nm . +.It Cm --version +Output +.Nm +program version and exit. +.It Cm -h , --help +Output +.Nm +help text and exit. +.El +.Sh EXIT STATUS +.Nm +exits with non-zero status when it encounters invalid config file, +invalid option, invalid parameter value, when it fails to obtain enough +randomness, and in any case when it fails to generate a passphrase. +.Sh FILES +.Pa /etc/passwdqc.conf +(not read unless this suggested file location is specified with the +.Cm config=/etc/passwdqc.conf +option). +.Sh SEE ALSO +.Xr pwqcheck 1 , +.Xr libpasswdqc 3 , +.Xr urandom 4 , +.Xr passwdqc.conf 5 , +.Xr pam_passwdqc 8 . +.Pp +https://www.openwall.com/passwdqc/ +.Sh AUTHORS +The pam_passwdqc module was written for Openwall GNU/*/Linux by Solar Designer. +The +.Nm +program was originally written for ALT GNU/*/Linux by Dmitry V. Levin, +indirectly reusing code from pam_passwdqc (via libpasswdqc). +This manual page was written for Openwall GNU/*/Linux by Dmitry V. Levin. diff --git a/contrib/pam_modules/pam_passwdqc/pwqgen.c b/contrib/pam_modules/pam_passwdqc/pwqgen.c new file mode 100644 index 000000000000..b8747115505b --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/pwqgen.c @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2008,2009 by Dmitry V. Levin + * Copyright (c) 2016,2021 by Solar Designer + * See LICENSE + */ + +#include +#include +#include + +#include "passwdqc.h" + +static void +print_help(void) +{ + puts("Generate quality controllable passphrase.\n" + "\nUsage: pwqgen [options]\n" + "\nValid options are:\n" + " random=N\n" + " set size of randomly-generated passphrase in bits;\n" + " config=FILE\n" + " load config FILE in passwdqc.conf format;\n" + " --version\n" + " print program version and exit;\n" + " -h or --help\n" + " print this help text and exit."); +} + +int main(int argc, const char **argv) +{ + passwdqc_params_t params; + char *reason, *pass; + int retval; + + if (argc > 1 && argv[1][0] == '-') { + if (!strcmp("-h", argv[1]) || !strcmp("--help", argv[1])) { + print_help(); + return 0; + } + + if (!strcmp("--version", argv[1])) { + printf("pwqgen version %s\n", PASSWDQC_VERSION); + return 0; + } + } + + passwdqc_params_reset(¶ms); + if (argc > 1 && + passwdqc_params_parse(¶ms, &reason, argc - 1, argv + 1)) { + fprintf(stderr, "pwqgen: %s\n", + (reason ? reason : "Out of memory")); + free(reason); + return 1; + } + + pass = passwdqc_random(¶ms.qc); + passwdqc_params_free(¶ms); + if (!pass) { + fprintf(stderr, "pwqgen: Failed to generate a passphrase.\n" + "This could happen for a number of reasons: you could have requested\n" + "an impossible passphrase length, or the access to kernel random number\n" + "pool could have failed.\n"); + return 1; + } + + setvbuf(stdout, NULL, _IONBF, 0); + + retval = (puts(pass) >= 0 && fflush(stdout) == 0) ? 0 : 1; + + _passwdqc_memzero(pass, strlen(pass)); + free(pass); + + return retval; +} diff --git a/contrib/pam_modules/pam_passwdqc/wordset_4k.c b/contrib/pam_modules/pam_passwdqc/wordset_4k.c index 1a604d49b00b..d8c62499a9fb 100644 --- a/contrib/pam_modules/pam_passwdqc/wordset_4k.c +++ b/contrib/pam_modules/pam_passwdqc/wordset_4k.c @@ -1,4108 +1,6297 @@ /* - * 4096 English words for generation of easy to memorize random passphrases. - * This list comes from a passphrase generator mentioned on sci.crypt, and I - * believe is in the public domain. + * English words for generation of easy to memorize random passphrases. + * This list comes from the MakePass passphrase generator developed by + * Dianelos Georgoudis , which was announced on + * sci.crypt on 1997/10/24. Here's a relevant excerpt from that posting: * - * I've replaced two 7-character words to save space. + * > The 4096 words in the word list were chosen according to the following + * > criteria: + * > - each word must contain between 3 and 6 characters + * > - each word must be a common English word + * > - each word should be clearly different from each other + * > word, orthographically or semantically + * > + * > The MakePass word list has been placed in the public domain + * + * At least two other sci.crypt postings by Dianelos Georgoudis also state + * that the word list is in the public domain, and so did the web page at: + * + * https://web.archive.org/web/%2a/http://www.tecapro.com/makepass.html + * + * which existed until 2006 and is available from the Wayback Machine as of + * this writing (March 2010). Specifically, the web page said: + * + * > The MakePass word list has been placed in the public domain. To download + * > a copy click here. You can use the MakePass word list for many other + * > purposes. + * + * "To download a copy click here" was a link to free/makepass.lst, which is + * currently available via the Wayback Machine: + * + * https://web.archive.org/web/%2a/http://www.tecapro.com/free/makepass.lst + * + * Further lists of common English words were appended to the end to allow for + * subsequent removal of "inappropriate" words from the initial list. + * + * Even though the original description of the list stated that "each word + * must contain between 3 and 6 characters", there were two 7-character words. + * These have been removed. + * + * Many "inappropriate" words have then been moved to near the end of list, so + * that they're not used for generated passphrases. + * + * The code in passwdqc_check.c and passwdqc_random.c makes the following + * assumptions about this list: + * + * - the first 4096 words are for random passphrase generation, and there are + * at least this many words present; + * - the words are of up to 6 characters long; + * - although some words may contain capital letters, no two words differ by + * the case of characters alone (e.g., converting the list to all-lowercase + * would yield a list of as many unique words); + * - the words contain alphabetical characters only; + * - if an entire word on this list matches the initial substring of other + * word(s) on the list, it is likely placed immediately before those words + * (e.g., "bake", "baker", "bakery"), which speeds up the "word-based" check. + * + * Additionally, the default minimum passphrase length of 11 characters + * specified in passwdqc_parse.c has been chosen such that a passphrase + * consisting of any three words from this list with two separator + * characters will pass the minimum length check. In other words, this + * default assumes that no word is shorter than 3 characters. */ -#include "passwdqc.h" +#include "wordset_4k.h" -char _passwdqc_wordset_4k[0x1000][6] = { +const char _passwdqc_wordset_4k[][WORDSET_4K_LENGTH_MAX] = { "Adam", - "Afghan", "Alaska", "Alice", - "Allah", "Amazon", "Andrew", - "Anglo", "Angola", "Antony", "April", - "Arab", "Arctic", "Athens", "Austin", - "Bach", "Baltic", - "Basque", "Berlin", - "Bible", "Bombay", "Bonn", "Boston", "Brazil", - "Briton", - "Buddha", "Burma", - "Caesar", "Cairo", "Canada", "Carl", "Carol", - "Celtic", "Chile", "China", - "Christ", "Congo", "Cuba", "Cyprus", "Czech", "Dallas", - "Danish", - "Darwin", "David", "Delhi", "Derby", "Diana", "Dublin", - "Dutch", "East", "Eden", "Edward", "Eric", "Essex", "Europe", "Eve", - "Exodus", "France", - "French", "Friday", - "Gandhi", - "Gaul", "Gemini", "Geneva", "George", - "German", "Gloria", - "God", "Gothic", "Greece", - "Greek", "Hague", "Haiti", "Hanoi", "Harry", "Havana", "Hawaii", - "Hebrew", "Henry", "Hermes", - "Hindu", - "Hitler", "Idaho", "Inca", "India", - "Indian", "Iowa", "Iran", "Iraq", - "Irish", "Isaac", "Isabel", - "Islam", "Israel", "Italy", "Ivan", "Jack", "Jacob", "James", "Japan", "Java", "Jersey", - "Jesus", - "Jewish", "Jim", "John", "Jordan", "Joseph", - "Judas", "Judy", "July", "June", "Kansas", "Karl", "Kenya", - "Koran", "Korea", "Kuwait", "Laos", "Latin", "Leo", "Libya", "Lima", "Lisbon", "Liz", "London", "Louvre", "Lucy", "Luther", - "Madame", "Madrid", "Malta", "Maria", "Mars", "Mary", "Maya", "Mecca", "Mexico", "Miami", "Mickey", "Milan", "Monaco", "Monday", "Moscow", - "Moses", - "Moslem", - "Mrs", "Munich", - "Muslim", "Naples", - "Nazi", "Nepal", "Newark", "Nile", - "Nobel", "North", "Norway", "Ohio", "Oscar", "Oslo", "Oxford", "Panama", "Paris", - "Pascal", "Paul", "Peking", "Peru", "Peter", "Philip", "Poland", - "Polish", "Prague", "Quebec", "Rex", "Rhine", - "Ritz", "Robert", "Roman", "Rome", "Rosa", "Russia", "Sahara", "Sam", "Saturn", - "Saudi", - "Saxon", - "Scot", "Seoul", "Somali", - "Sony", - "Soviet", "Spain", - "Stalin", "Sudan", "Suez", "Sunday", "Sweden", - "Swiss", "Sydney", "Syria", "Taiwan", "Tarzan", "Taurus", "Tehran", "Teresa", "Texas", "Thomas", "Tibet", "Tokyo", "Tom", - "Turk", "Turkey", "Uganda", "Venice", "Venus", "Vienna", "Viking", "Virgo", "Warsaw", "West", "Yale", "Yemen", "York", "Zaire", "Zurich", "aback", "abbey", "abbot", "abide", "ablaze", "able", "aboard", "abode", - "abort", "abound", "about", "above", "abroad", "abrupt", "absent", "absorb", "absurd", "abuse", "accent", "accept", "access", "accord", "accuse", "ace", "ache", "aching", "acid", "acidic", "acorn", "acre", "across", "act", "action", "active", "actor", "actual", "acute", "adapt", "add", "added", "addict", "adept", "adhere", "adjust", "admire", "admit", "adobe", "adopt", "adrift", "adult", "adverb", "advert", "aerial", "afar", "affair", "affect", "afford", "afield", "afloat", "afraid", "afresh", "after", "again", "age", "agency", "agenda", "agent", "aghast", "agile", "ago", "agony", "agree", "agreed", "ahead", "aid", "aide", "aim", "air", "airman", "airy", "akin", "alarm", "albeit", "album", "alert", "alibi", "alien", "alight", "align", "alike", "alive", "alkali", "all", "alley", "allied", "allow", "alloy", "ally", "almond", "almost", "aloft", "alone", "along", "aloof", "aloud", "alpha", "alpine", "also", "altar", "alter", "always", "amaze", "amber", "ambush", "amen", "amend", "amid", "amidst", "amiss", "among", "amount", "ample", "amuse", "anchor", "and", "anew", "angel", "anger", "angle", "angry", "animal", "ankle", "annoy", "annual", "answer", "anthem", "anti", "any", "anyhow", "anyway", "apart", "apathy", "apex", "apiece", "appeal", "appear", "apple", "apply", "apron", "arcade", "arcane", "arch", "ardent", "are", "area", "argue", "arid", "arise", "arm", "armful", "armpit", "army", "aroma", "around", - "arouse", "array", "arrest", "arrive", "arrow", "arson", "art", "artery", "artful", "artist", "ascent", "ashen", "ashore", "aside", "ask", "asleep", "aspect", "assay", "assent", "assert", "assess", "asset", "assign", "assist", "assume", "assure", "asthma", "astute", "asylum", "ate", "atlas", "atom", "atomic", "attach", "attack", "attain", "attend", "attic", "auburn", "audio", "audit", "august", "aunt", "auntie", "aura", "author", "auto", "autumn", "avail", "avenge", "avenue", "avert", "avid", "avoid", "await", "awake", "awaken", "award", "aware", "awash", "away", "awful", "awhile", "axes", "axiom", "axis", "axle", "aye", - "babe", "baby", "back", "backup", "bacon", "bad", "badge", "badly", "bag", "baggy", "bail", "bait", "bake", "baker", "bakery", "bald", "ball", "ballad", "ballet", "ballot", "bamboo", "ban", "banal", "banana", "band", - "bang", "bank", "bar", "barber", "bare", "barely", "barge", "bark", "barley", "barn", "baron", "barrel", "barren", "basalt", "base", "basic", "basil", "basin", "basis", "basket", "bass", "bat", "batch", "bath", "baton", "battle", "bay", "beach", "beacon", "beak", "beam", "bean", "bear", "beard", "beast", "beat", "beauty", "become", "bed", "beech", "beef", "beefy", "beep", "beer", "beet", "beetle", "before", "beggar", "begin", "behalf", "behave", "behind", "beige", "being", "belief", "bell", "belly", "belong", "below", "belt", "bench", "bend", "benign", "bent", "berry", "berth", "beset", "beside", "best", "bestow", "bet", "beta", "betray", "better", "beware", "beyond", "bias", "biceps", "bicker", "bid", "big", "bigger", "bike", "bile", "bill", "binary", "bind", "biopsy", "birch", "bird", "birdie", "birth", "bishop", "bit", - "bitch", "bite", "bitter", - "black", "blade", "blame", "bland", "blast", "blaze", "bleak", "blend", "bless", - "blew", "blind", "blink", "blip", "bliss", "blitz", "block", "blond", "blood", "bloody", "bloom", "blot", "blouse", - "blow", "blue", "bluff", "blunt", "blur", "blush", "boar", "board", "boast", "boat", "bodily", "body", "bogus", "boil", "bold", "bolt", "bomb", "bond", "bone", "bonnet", "bonus", "bony", "book", "boom", "boost", "boot", "booth", - "booze", "border", "bore", "borrow", "bosom", "boss", "both", "bother", "bottle", "bottom", "bought", "bounce", "bound", "bounty", "bout", "bovine", "bow", "bowel", "bowl", "box", "boy", "boyish", "brace", "brain", "brainy", "brake", "bran", "branch", "brand", "brandy", "brass", "brave", "bravo", "breach", "bread", "break", - "breast", "breath", "bred", "breed", "breeze", "brew", "brick", "bride", "bridge", "brief", "bright", "brim", "brine", "bring", "brink", "brisk", "broad", "broke", "broken", "bronze", "brook", "broom", - "brown", "bruise", "brush", "brutal", "brute", "bubble", "buck", "bucket", "buckle", "budget", "buffet", "buggy", "build", "bulb", "bulge", "bulk", "bulky", "bull", "bullet", "bully", "bump", "bumpy", "bunch", "bundle", "bunk", "bunny", "burden", "bureau", "burial", "buried", "burly", "burn", - "burnt", "burrow", "burst", "bury", "bus", - "bush", "bust", "bustle", "busy", "but", "butler", - "butt", "butter", "button", "buy", "buyer", "buzz", "bye", "byte", "cab", "cabin", "cable", "cache", "cactus", "cage", "cake", "calf", "call", "caller", "calm", "calmly", "came", "camel", "camera", "camp", "campus", "can", "canal", "canary", "cancel", "cancer", "candid", "candle", "candy", "cane", "canine", "canoe", "canopy", "canvas", "canyon", "cap", "cape", "car", "carbon", "card", "care", "career", "caress", "cargo", - "carnal", "carp", "carpet", "carrot", "carry", "cart", "cartel", "case", "cash", "cask", "cast", "castle", "casual", "cat", "catch", "cater", "cattle", "caught", "causal", "cause", "cave", "cease", "celery", "cell", "cellar", "cement", "censor", "census", "cereal", - "cervix", "chain", "chair", "chalk", "chalky", "champ", "chance", "change", "chant", "chaos", "chap", "chapel", "charge", "charm", "chart", "chase", "chat", "cheap", "cheat", "check", "cheek", "cheeky", "cheer", "cheery", "cheese", "chef", "cherry", "chess", "chest", "chew", "chic", - "chick", "chief", "child", "chill", "chilly", "chin", "chip", "choice", "choir", "choose", "chop", "choppy", "chord", "chorus", "chose", "chosen", "chrome", "chunk", "chunky", - "church", "cider", "cigar", "cinema", "circa", "circle", "circus", "cite", "city", "civic", "civil", "clad", "claim", "clammy", "clan", "clap", "clash", "clasp", "class", "clause", "claw", "clay", "clean", "clear", "clergy", "clerk", "clever", "click", "client", "cliff", - "climax", "climb", "clinch", "cling", "clinic", "clip", "cloak", "clock", "clone", "close", "closer", "closet", "cloth", "cloud", "cloudy", "clout", "clown", "club", "clue", "clumsy", "clung", "clutch", "coach", "coal", "coarse", "coast", "coat", "coax", "cobalt", "cobra", "coca", - "cock", "cocoa", "code", "coffee", "coffin", "cohort", "coil", "coin", "coke", "cold", "collar", "colon", "colony", "colt", "column", "comb", "combat", "come", "comedy", "comic", "commit", "common", "compel", "comply", "concur", "cone", "confer", "consul", "convex", "convey", "convoy", "cook", "cool", "cope", "copper", "copy", "coral", "cord", "core", "cork", "corn", "corner", "corps", "corpse", "corpus", "cortex", "cosmic", "cosmos", "cost", "costly", "cosy", "cotton", "couch", "cough", "could", "count", "county", "coup", "couple", "coupon", "course", "court", "cousin", "cove", "cover", "covert", "cow", "coward", "cowboy", "crab", - "crack", "cradle", "craft", "crafty", "crag", "crane", "crap", "crash", "crate", "crater", "crawl", "crazy", "creak", "cream", "creamy", "create", "credit", "creed", "creek", "creep", "creepy", "crept", "crest", "crew", "cried", "crime", "crisis", "crisp", "critic", "croft", "crook", "crop", "cross", "crow", "crowd", "crown", "crude", "cruel", "cruise", "crunch", "crush", "crust", "crux", "cry", "crypt", "cube", "cubic", "cuckoo", "cuff", "cult", "cup", "curb", "cure", "curfew", "curl", "curry", "curse", "cursor", "curve", "custom", "cut", "cute", "cycle", "cyclic", "cynic", "dad", - "daddy", "dagger", "daily", "dairy", "daisy", "dale", "damage", "damn", "damp", "dampen", "dance", "danger", "dare", "dark", "darken", "dash", "data", "date", "dawn", "day", "dead", "deadly", "deaf", "deal", "dealer", "dean", "dear", "death", "debate", "debit", "debris", "debt", "debtor", "decade", "decay", "decent", "decide", "deck", "decor", "decree", "deduce", "deed", "deep", "deeply", "deer", "defeat", "defect", "defend", "defer", "define", "defy", "degree", "deity", "delay", "delete", "delta", "demand", "demise", "demo", "demon", "demure", "denial", "denote", "dense", "dental", "deny", "depart", "depend", "depict", "deploy", "depot", "depth", "deputy", "derive", "desert", "design", "desire", "desist", "desk", "detail", "detect", "deter", "detest", "detour", "device", - "devil", "devise", "devoid", "devote", "devour", "dial", "diary", "dice", "dictum", "did", "die", "diesel", "diet", "differ", "digest", "digit", "dine", "dinghy", "dinner", "diode", "dire", "direct", "dirt", "dirty", "disc", "disco", "dish", "disk", "dismal", "dispel", "ditch", "dive", "divert", "divide", "divine", "dizzy", "docile", "dock", "doctor", "dog", "dogma", "dole", "doll", "dollar", "dolly", "domain", "dome", "domino", "donate", "done", "donkey", "donor", "doom", "door", "dorsal", "dose", "double", "doubt", "dough", "dour", "dove", "down", "dozen", "draft", "drag", "dragon", "drain", "drama", "drank", "draw", "drawer", "dread", "dream", "dreary", "dress", "drew", "dried", "drift", "drill", "drink", "drip", "drive", "driver", "drop", "drove", "drown", "drug", "drum", "drunk", "dry", "dual", "duck", "duct", "due", "duel", "duet", "duke", "dull", "duly", "dumb", "dummy", "dump", "dune", "dung", "duress", "during", "dusk", "dust", "dusty", "duty", "dwarf", "dwell", "dyer", "dying", "dynamo", "each", "eager", "eagle", "ear", "earl", "early", "earn", "earth", "ease", "easel", "easily", - "easter", "easy", "eat", "eaten", "eater", "echo", "eddy", "edge", "edible", "edict", "edit", "editor", "eerie", "eerily", "effect", "effort", "egg", "ego", "eight", "eighth", "eighty", "either", "elbow", "elder", "eldest", "elect", "eleven", "elicit", "elite", "else", "elude", "elves", "embark", "emblem", "embryo", "emerge", "emit", "empire", "employ", "empty", "enable", "enamel", "end", "endure", "enemy", "energy", "engage", "engine", "enjoy", "enlist", "enough", "ensure", "entail", "enter", "entire", "entry", "envoy", "envy", "enzyme", "epic", "epoch", "equal", "equate", "equip", "equity", "era", - "erase", - "erect", "erode", - "erotic", "errant", "error", "escape", - "escort", "essay", "estate", "esteem", "ethic", "ethnic", "evade", "even", "event", "ever", "every", "evict", "evil", "evoke", "evolve", "exact", "exam", "exceed", "excel", "except", "excess", "excise", "excite", "excuse", "exempt", "exert", "exile", "exist", "exit", "exotic", "expand", "expect", "expert", "expire", "export", "expose", "extend", "extra", "eye", "eyed", "fabric", "face", - "facial", "fact", "factor", "fade", "fail", "faint", "fair", "fairly", "fairy", "faith", "fake", "falcon", "fall", "false", "falter", "fame", "family", "famine", "famous", "fan", "fancy", "far", "farce", "fare", "farm", "farmer", "fast", "fasten", "faster", "fat", "fatal", "fate", "father", "fatty", "fault", "faulty", "fauna", "fear", "feast", "feat", "fed", "fee", "feeble", "feed", "feel", "feet", "fell", "fellow", "felt", "female", "fence", "fend", "ferry", "fetal", "fetch", "feudal", "fever", "few", "fewer", "fiance", "fiasco", "fiddle", "field", "fiend", "fierce", "fiery", "fifth", "fifty", "fig", "fight", "figure", "file", "fill", "filled", "filler", "film", "filter", "filth", "filthy", "final", "finale", "find", "fine", "finger", "finish", "finite", "fire", "firm", "firmly", "first", "fiscal", "fish", "fisher", "fist", "fit", "fitful", "five", "fix", "flag", "flair", "flak", "flame", "flank", "flap", "flare", "flash", "flask", "flat", "flaw", "fled", "flee", "fleece", "fleet", "flesh", "fleshy", "flew", "flick", "flight", "flimsy", "flint", - "flirt", "float", "flock", "flood", "floor", "floppy", "flora", "floral", "flour", "flow", "flower", "fluent", "fluffy", "fluid", "flung", "flurry", "flush", "flute", "flux", "fly", "flyer", "foal", "foam", "focal", "focus", "fog", "foil", "fold", "folk", "follow", "folly", "fond", "fondly", "font", "food", "fool", "foot", "for", "forbid", "force", "ford", "forest", "forge", "forget", "fork", "form", "formal", "format", "former", "fort", "forth", "forty", "forum", "fossil", "foster", "foul", "found", "four", "fourth", "fox", "foyer", "frail", "frame", "franc", "frank", "fraud", "free", "freed", "freely", "freer", "freeze", "frenzy", "fresh", "friar", "fridge", "fried", "friend", "fright", "fringe", "frock", "frog", "from", "front", "frost", "frosty", "frown", "frozen", "frugal", "fruit", "fudge", "fuel", "fulfil", "full", "fully", "fun", "fund", "funny", "fur", "furry", "fury", "fuse", "fusion", "fuss", "fussy", "futile", "future", "fuzzy", "gadget", - "gag", "gain", "gala", "galaxy", "gale", "gall", "galley", "gallon", "gallop", "gamble", "game", "gamma", "gang", "gap", "garage", "garden", "garlic", "gas", "gasp", "gate", "gather", "gauge", "gaunt", "gave", - "gay", "gaze", "gear", "geese", "gender", "gene", "genial", "genius", "genre", "gentle", "gently", "gentry", "genus", "get", "ghetto", "ghost", "giant", "gift", "giggle", "gill", "gilt", "ginger", "girl", "give", "given", "glad", "glade", "glance", "gland", "glare", "glass", "glassy", "gleam", "glee", "glide", "global", "globe", "gloom", "gloomy", "glory", "gloss", "glossy", "glove", "glow", "glue", "goal", "goat", "gold", "golden", "golf", "gone", "gong", "good", "goose", "gorge", "gory", "gosh", - "gospel", "gossip", "got", "govern", "gown", "grab", "grace", "grade", "grain", "grand", "grant", "grape", "graph", "grasp", "grass", "grassy", "grate", "grave", "gravel", "gravy", - "gray", "grease", "greasy", "great", "greed", "greedy", "green", "greet", "grew", - "grey", "grid", "grief", "grill", "grim", "grin", "grind", "grip", "grit", "gritty", "groan", "groin", "groom", "groove", "gross", "ground", "group", "grove", "grow", "grown", "growth", "grudge", "grunt", "guard", "guess", "guest", "guide", "guild", "guilt", "guilty", "guise", "guitar", "gulf", "gully", "gun", "gunman", "guru", "gut", "guy", - "gypsy", "habit", "hack", "had", "hail", "hair", "hairy", "hale", "half", "hall", "halt", "hamlet", "hammer", "hand", "handle", "handy", "hang", "hangar", "happen", "happy", "harass", "hard", "harder", "hardly", "hare", "harem", "harm", "harp", "harsh", "has", "hash", "hassle", "haste", "hasten", "hasty", "hat", "hatch", "hate", "haul", "haunt", "have", "haven", "havoc", "hawk", "hazard", "haze", "hazel", "hazy", "head", "heal", "health", "heap", "hear", "heard", "heart", "hearth", "hearty", "heat", "heater", "heaven", "heavy", "heck", "hectic", "hedge", "heel", "hefty", "height", "heir", "held", "helium", "helix", "hell", "hello", "helm", "helmet", "help", "hemp", "hence", "her", "herald", "herb", "herd", "here", "hereby", "hernia", "hero", "heroic", - "heroin", "hey", "heyday", "hick", "hidden", "hide", "high", "higher", "highly", "hill", "him", "hind", "hint", - "hippy", "hire", "his", "hiss", "hit", "hive", "hoard", "hoarse", "hobby", "hockey", "hold", "holder", "hole", "hollow", "holly", "holy", "home", "honest", "honey", "hood", "hook", "hope", "horn", - "horny", "horrid", "horror", "horse", "hose", "host", "hot", "hotel", "hound", "hour", "house", "hover", "how", "huge", "hull", "human", "humane", "humble", "humid", "hung", "hunger", "hungry", "hunt", "hurdle", "hurl", "hurry", "hurt", "hush", "hut", "hybrid", "hymn", "hyphen", "ice", "icing", "icon", "idea", "ideal", "idiom", "idiot", "idle", "idly", "idol", "ignite", "ignore", "ill", "image", "immune", "impact", "imply", "import", "impose", - "incest", "inch", "income", "incur", "indeed", "index", "indoor", "induce", "inept", "inert", "infant", "infect", "infer", "influx", "inform", "inject", "injure", "injury", "inlaid", "inland", "inlet", "inmate", "inn", "innate", "inner", "input", "insane", "insect", "insert", "inset", "inside", "insist", "insult", "insure", "intact", "intake", "intend", "inter", "into", "invade", "invent", "invest", "invite", "invoke", "inward", "iron", "ironic", "irony", "island", "isle", "issue", "itch", "item", "itself", "ivory", "jacket", "jade", "jaguar", "jail", "jargon", "jaw", "jazz", "jeep", "jelly", "jerky", "jest", "jet", "jewel", "job", "jock", "jockey", "join", - "joint", "joke", "jolly", "jolt", "joy", "joyful", "joyous", "judge", "juice", "juicy", "jumble", "jumbo", "jump", "jungle", "junior", "junk", "junta", "jury", "just", "karate", "keel", "keen", "keep", "keeper", "kept", "kernel", "kettle", "key", "khaki", "kick", "kid", "kidnap", "kidney", - "kill", - "killer", "kin", "kind", "kindly", "king", "kiss", "kite", "kitten", "knack", "knee", "knew", "knife", "knight", "knit", "knob", "knock", "knot", "know", "known", "label", "lace", "lack", "lad", "ladder", "laden", "lady", "lagoon", "laity", "lake", "lamb", "lame", "lamp", "lance", "land", "lane", "lap", "lapse", "large", "larval", "laser", "last", "latch", "late", "lately", "latent", "later", "latest", "latter", "laugh", "launch", "lava", "lavish", "law", "lawful", "lawn", "lawyer", "lay", "layer", "layman", "lazy", "lead", "leader", "leaf", "leafy", "league", "leak", "leaky", "lean", "leap", "learn", "lease", "leash", "least", "leave", "led", "ledge", "left", "leg", "legacy", "legal", "legend", "legion", "lemon", "lend", "length", "lens", "lent", "leper", "lesion", "less", "lessen", "lesser", "lesson", "lest", "let", "lethal", "letter", "level", "lever", "levy", "lewis", "liable", "liar", "libel", "lice", - "lick", "lid", "lie", "lied", "life", "lift", "light", "like", "likely", "limb", "lime", "limit", "limp", "line", "linear", "linen", "linger", "link", "lion", "lip", "liquid", "liquor", "list", "listen", "lit", "live", "lively", "liver", "lizard", "load", "loaf", "loan", "lobby", "lobe", "local", "locate", "lock", "locus", "lodge", "loft", "lofty", "log", "logic", "logo", "lone", "lonely", "long", "longer", "look", "loop", "loose", "loosen", "loot", "lord", "lorry", "lose", "loss", "lost", "lot", "lotion", "lotus", "loud", "loudly", "lounge", "lousy", "love", "lovely", - "lover", "low", "lower", "lowest", "loyal", "lucid", "luck", "lucky", "lull", "lump", "lumpy", "lunacy", "lunar", "lunch", "lung", "lure", "lurid", "lush", "lust", "lute", "luxury", "lying", "lymph", "lynch", "lyric", "macho", "macro", "mad", "madam", "made", "mafia", "magic", "magma", "magnet", "magnum", "maid", "maiden", "mail", "main", "mainly", "major", "make", "maker", "male", "malice", "mall", "malt", "mammal", "manage", "mane", "mania", "manic", "manner", "manor", "mantle", "manual", "manure", "many", "map", "maple", "marble", "march", "mare", "margin", "marina", "mark", "market", "marry", "marsh", "martin", "martyr", "mask", "mason", "mass", "mast", - "master", "match", "mate", "matrix", "matter", "mature", "maxim", "may", "maybe", "mayor", "maze", "mead", "meadow", "meal", "mean", "meant", "meat", "medal", "media", "median", "medic", "medium", "meet", "mellow", "melody", "melon", "melt", "member", "memo", "memory", "menace", "mend", "mental", "mentor", "menu", "mercy", "mere", "merely", "merge", "merger", "merit", "merry", "mesh", "mess", "messy", "met", "metal", "meter", "method", "methyl", "metric", "metro", "mid", "midday", "middle", "midst", "midway", "might", "mighty", "mild", "mildew", "mile", "milk", "milky", "mill", "mimic", "mince", "mind", "mine", "mini", "mink", "minor", "mint", "minus", "minute", "mirror", "mirth", "misery", "miss", "mist", "misty", "mite", "mix", "moan", "moat", "mobile", "mock", "mode", "model", "modem", "modern", "modest", "modify", "module", "moist", "molar", "mole", "molten", "moment", "money", "monies", "monk", "monkey", "month", "mood", "moody", "moon", "moor", "moral", "morale", "morbid", "more", "morgue", "mortal", "mortar", "mosaic", "mosque", "moss", "most", "mostly", "moth", "mother", "motion", "motive", "motor", "mould", "mount", "mourn", "mouse", "mouth", "move", "movie", "much", "muck", "mucus", "mud", "muddle", "muddy", "mule", "mummy", - "murder", "murky", "murmur", "muscle", "museum", "music", "mussel", "must", "mutant", "mute", "mutiny", "mutter", "mutton", "mutual", "muzzle", "myopic", "myriad", "myself", "mystic", "myth", "nadir", "nail", - "naked", "name", "namely", "nape", "napkin", "narrow", "nasal", "nasty", "nation", "native", "nature", "nausea", "naval", "nave", "navy", "near", "nearer", "nearly", "neat", "neatly", "neck", "need", "needle", "needy", "negate", "neon", "nephew", "nerve", "nest", "neural", "never", "newly", "next", "nice", "nicely", "niche", "nickel", "niece", "night", "nimble", "nine", "ninety", "ninth", "noble", "nobody", "node", "noise", "noisy", "non", "none", "noon", "nor", "norm", "normal", "nose", "nosy", "not", "note", "notice", "notify", "notion", "nought", "noun", "novel", "novice", "now", "nozzle", - "nude", "null", "numb", "number", "nurse", "nylon", "nymph", "oak", "oasis", "oath", "obese", "obey", "object", "oblige", "oboe", "obtain", "occult", "occupy", "occur", "ocean", "octave", "odd", "off", "offend", "offer", "office", "offset", "often", "oil", "oily", "okay", "old", "older", "oldest", "olive", "omega", "omen", "omit", "once", "one", "onion", "only", "onset", "onto", "onus", "onward", "opaque", "open", "openly", "opera", - "opium", "oppose", "optic", "option", "oracle", - "oral", "orange", "orbit", "orchid", "ordeal", "order", "organ", - "orgasm", "orient", "origin", "ornate", "orphan", "other", "otter", "ought", "ounce", "our", "out", "outer", "output", "outset", "oval", "oven", "over", "overt", "owe", "owing", "owl", "own", "owner", "oxide", "oxygen", "oyster", "ozone", "pace", "pack", "packet", "pact", "paddle", "paddy", "pagan", "page", "paid", "pain", "paint", "pair", "palace", "pale", "palm", "panel", "panic", "papa", "papal", "paper", "parade", "parcel", "pardon", "parent", "parish", "park", "parody", "parrot", "part", "partly", "party", "pass", "past", "paste", "pastel", "pastor", "pastry", "pat", "patch", "patent", "path", "patio", "patrol", "patron", "pause", "pave", "pawn", "pay", "peace", "peach", "peak", "pear", "pearl", "pedal", "peel", "peer", "pelvic", "pelvis", "pen", "penal", "pence", "pencil", - "penis", "penny", "people", "pepper", "per", "perch", "peril", "period", "perish", "permit", "person", "pest", "petite", "petrol", "petty", "phase", "phone", "photo", "phrase", "piano", "pick", "picket", "picnic", "pie", "piece", "pier", "pierce", "piety", "pig", "pigeon", "piggy", "pike", "pile", "pill", "pillar", "pillow", "pilot", "pin", "pinch", "pine", "pink", "pint", "pious", "pipe", "pirate", - "piss", "pistol", "piston", "pit", "pitch", "pity", "pivot", "pixel", "pizza", "place", "placid", "plague", "plain", "plan", "plane", "planet", "plank", "plant", "plasma", "plate", "play", "player", "plea", "plead", "please", "pledge", "plenty", "plenum", "plight", "plot", "ploy", "plug", "plum", "plump", "plunge", "plural", "plus", "plush", "pocket", "poem", "poet", "poetic", "poetry", "point", "poison", "polar", "pole", "police", "policy", "polite", "poll", "pollen", "polo", "pond", "ponder", "pony", "pool", "poor", "poorly", "pop", "pope", "poppy", "pore", "pork", "port", "portal", "pose", "posh", "post", "postal", - "pot", "potato", "potent", "pouch", "pound", "pour", "powder", "power", "praise", "pray", "prayer", "preach", "prefer", "prefix", "press", "pretty", "price", "pride", "priest", "primal", "prime", "prince", "print", "prior", "prism", "prison", "privy", "prize", "probe", "profit", "prompt", "prone", "proof", "propel", "proper", "prose", "proton", "proud", "prove", "proven", "proxy", "prune", "psalm", "pseudo", "psyche", "pub", "public", "puff", "pull", "pulp", "pulpit", "pulsar", "pulse", "pump", "punch", "punish", "punk", "pupil", "puppet", "puppy", "pure", "purely", "purge", "purify", "purple", "purse", "pursue", "push", "pushy", - "pussy", "put", "putt", "puzzle", "quaint", "quake", "quarry", "quartz", "quay", "queen", - "queer", "query", "quest", "queue", "quick", "quid", "quiet", "quilt", "quirk", "quit", "quite", "quiver", "quiz", "quota", "quote", "rabbit", "race", "racial", "racism", "rack", "racket", "radar", "radio", "radish", "radius", "raffle", "raft", "rage", "raid", "rail", "rain", "rainy", "raise", "rally", "ramp", "random", "range", "rank", "ransom", - "rape", "rapid", "rare", "rarely", "rarity", "rash", "rat", "rate", "rather", "ratify", "ratio", "rattle", "rave", "raven", "raw", "ray", "razor", "reach", "react", "read", "reader", "ready", "real", "really", "realm", "reap", "rear", "reason", "rebel", "recall", "recent", "recess", "recipe", "reckon", "record", "recoup", "rector", "red", "redeem", "reduce", "reed", "reef", "refer", "reform", "refuge", "refuse", "regal", "regard", "regent", "regime", "region", "regret", "reign", "reject", "relate", "relax", "relay", "relic", "relief", "relish", "rely", "remain", "remark", "remedy", "remind", "remit", "remote", "remove", "renal", "render", "rent", "rental", "repair", "repeal", "repeat", "repent", "reply", "report", "rescue", "resent", "reside", "resign", "resin", "resist", "resort", "rest", "result", "resume", "retail", "retain", "retina", "retire", "return", "reveal", "review", "revise", "revive", "revolt", "reward", "rhino", "rhyme", "rhythm", "ribbon", "rice", "rich", "rick", "rid", "ride", "rider", "ridge", "rife", "rifle", "rift", "right", "rigid", "ring", "rinse", "riot", "ripe", "ripen", "ripple", "rise", "risk", "risky", "rite", "ritual", "rival", "river", "road", "roar", "roast", "rob", "robe", "robin", "robot", "robust", "rock", "rocket", "rocky", "rod", "rode", "rodent", "rogue", "role", "roll", "roof", "room", "root", "rope", "rose", "rosy", "rotate", "rotor", "rotten", "rouge", "rough", "round", "route", "rover", "row", "royal", "rubble", "ruby", "rudder", "rude", "rugby", "ruin", "rule", "ruler", "rumble", "rump", "run", "rune", "rung", "runway", "rural", "rush", "rust", "rustic", "rusty", "sack", "sacred", "sad", "saddle", - "sadism", "sadly", "safari", "safe", "safely", "safer", "safety", "saga", "sage", "said", "sail", "sailor", "saint", "sake", "salad", "salary", "sale", "saline", "saliva", "salmon", "saloon", "salt", "salty", "salute", "same", "sample", "sand", "sandy", "sane", "sash", - "satan", "satin", "satire", "sauce", "sauna", "savage", "save", "say", "scale", "scalp", "scan", "scant", "scar", "scarce", "scare", "scarf", "scary", "scene", "scenic", "scent", "school", "scope", "score", "scorn", "scotch", "scout", "scrap", "scream", "screen", - "screw", "script", "scroll", "scrub", "scum", "sea", "seal", "seam", "seaman", "search", "season", "seat", "second", "secret", "sect", "sector", "secure", "see", "seed", "seeing", "seek", "seem", "seize", "seldom", "select", "self", "sell", "seller", "semi", "senate", "send", "senile", "senior", "sense", "sensor", "sent", "sentry", "sequel", "serene", "serial", "series", "sermon", "serum", "serve", "server", "set", "settle", "seven", "severe", "sewage", - "sex", - "sexual", - "sexy", "shabby", "shade", "shadow", "shady", "shaft", "shaggy", "shah", "shake", "shaky", "shall", "sham", "shame", "shape", "share", "shark", "sharp", "shawl", "she", "shear", "sheen", "sheep", "sheer", "sheet", "shelf", "shell", "sherry", "shield", "shift", "shine", "shiny", "ship", "shire", "shirt", - "shit", "shiver", "shock", "shoe", "shook", "shoot", "shop", "shore", "short", "shot", "should", "shout", "show", "shower", "shrank", "shrewd", "shrill", "shrimp", "shrine", "shrink", "shrub", "shrug", "shut", "shy", "shyly", "sick", "side", "siege", "sigh", "sight", "sigma", "sign", "signal", "silent", "silk", "silken", "silky", "sill", "silly", "silver", "simple", "simply", "since", "sinful", "sing", "singer", "single", "sink", "sir", "siren", "sister", "sit", "site", "six", "sixth", "sixty", "size", "sketch", "skill", "skin", "skinny", "skip", "skirt", "skull", "sky", "slab", "slack", "slain", "slam", "slang", "slap", "slate", "slater", - "slave", "sleek", "sleep", "sleepy", "sleeve", "slice", "slick", "slid", "slide", "slight", "slim", "slimy", "sling", "slip", "slit", "slogan", "slope", "sloppy", "slot", "slow", "slowly", "slug", "slum", "slump", "smack", "small", "smart", "smash", "smear", "smell", "smelly", "smelt", "smile", "smoke", "smoky", "smooth", "smug", "snack", "snail", "snake", "snap", "snatch", "sneak", "snow", "snowy", "snug", "soak", "soap", "sober", "soccer", "social", "sock", "socket", "soda", "sodden", "sodium", "sofa", "soft", "soften", "softly", "soggy", "soil", "solar", "sold", "sole", "solely", "solemn", "solid", "solo", "solve", "some", "son", "sonar", "sonata", "song", "sonic", "soon", "sooner", "soot", "soothe", "sordid", "sore", "sorrow", "sorry", "sort", "soul", "sound", "soup", "sour", "source", "space", "spade", "span", "spare", "spark", "sparse", "spasm", "spat", "spate", "speak", "spear", "speech", "speed", "speedy", "spell", "spend", - "sperm", "sphere", "spice", "spicy", "spider", "spiky", "spill", "spin", "spinal", "spine", "spiral", "spirit", "spit", "spite", "splash", "split", "spoil", "spoke", "sponge", "spoon", "sport", "spot", "spouse", "spray", "spread", "spree", "spring", "sprint", "spur", "squad", "square", "squash", "squat", "squid", "stab", "stable", "stack", "staff", "stage", "stain", "stair", "stake", "stale", "stall", "stamp", "stance", "stand", "staple", "star", "starch", "stare", "stark", "start", "starve", "state", "static", "statue", "status", "stay", "stead", "steady", "steak", "steal", "steam", "steel", "steep", "steer", "stem", "stench", "step", "stereo", "stern", "stew", "stick", "sticky", "stiff", "stifle", "stigma", "still", "sting", "stint", "stir", "stitch", "stock", "stocky", "stone", "stony", "stool", "stop", "store", "storm", "stormy", "story", "stout", "stove", "strain", "strait", "strand", "strap", "strata", "straw", "stray", "streak", "stream", "street", "stress", "strict", "stride", "strife", "strike", "string", - "strip", "strive", "stroke", "stroll", "strong", "stud", "studio", "study", "stuff", "stuffy", "stunt", "stupid", "sturdy", "style", "submit", "subtle", "subtly", "suburb", "such", - "suck", "sudden", "sue", "suffer", "sugar", "suit", "suite", "suitor", "sullen", "sultan", "sum", "summer", "summit", "summon", "sun", "sunny", "sunset", "super", "superb", "supper", "supple", "supply", "sure", "surely", "surf", "surge", "survey", "suture", "swamp", "swan", "swap", "swarm", "sway", "swear", "sweat", "sweaty", "sweep", "sweet", "swell", "swift", "swim", "swine", "swing", "swirl", "switch", "sword", "swore", "symbol", "synod", "syntax", "syrup", "system", "table", "tablet", "taboo", "tacit", "tackle", "tact", "tactic", "tail", "tailor", "take", "tale", "talent", "talk", "tall", "tally", "tame", "tandem", "tangle", "tank", "tap", "tape", "target", "tariff", "tart", "task", "taste", "tasty", "tattoo", "taut", "tavern", "tax", "taxi", "tea", "teach", "teak", "team", "tear", "tease", "tech", "teeth", "tell", "temper", "temple", "tempo", "tempt", "ten", "tenant", "tend", "tender", "tendon", "tennis", "tenor", "tense", "tensor", "tent", "tenth", "tenure", "term", "terror", "test", "text", "than", "thank", "that", "the", "their", "them", "theme", "then", "thence", "theory", "there", "these", "thesis", "they", "thick", "thief", "thigh", "thin", "thing", "think", "third", "thirst", "thirty", "this", "thorn", "those", "though", "thread", "threat", "three", "thrill", "thrive", "throat", "throne", "throng", "throw", - "thrust", "thud", "thug", "thumb", "thus", "thyme", "tick", "ticket", "tidal", "tide", "tidy", "tie", "tier", "tiger", "tight", "tile", "till", "tilt", "timber", "time", "timid", "tin", "tiny", "tip", "tissue", "title", "toad", "toast", "today", "toilet", "token", "told", "toll", "tomato", "tomb", "tonal", "tone", "tongue", "tonic", "too", "took", "tool", "tooth", "top", "topaz", "topic", "torch", "torque", "torso", "tort", "toss", "total", "touch", "tough", "tour", "toward", "towel", "tower", "town", "toxic", "toxin", "trace", "track", "tract", "trade", "tragic", "trail", "train", "trait", "tram", "trance", "trap", "trauma", "travel", "tray", "tread", "treat", "treaty", "treble", "tree", "trek", "tremor", "trench", "trend", "trendy", "trial", "tribal", "tribe", "trick", "tricky", "tried", "trifle", "trim", "trio", "trip", "triple", "troop", "trophy", "trot", "trough", "trout", "truce", "truck", "true", "truly", "trunk", "trust", "truth", "try", "tsar", "tube", "tumble", "tuna", "tundra", "tune", "tung", "tunic", "tunnel", "turban", "turf", "turn", "turtle", "tutor", "tweed", "twelve", "twenty", "twice", "twin", "twist", "two", "tycoon", "tying", "type", "tyrant", "ugly", "ulcer", "ultra", "umpire", "unable", "uncle", "under", "uneasy", "unfair", "unify", "union", "unique", "unit", "unite", "unity", "unlike", "unrest", "unruly", "until", "update", "upheld", "uphill", "uphold", "upon", "uproar", "upset", "upshot", "uptake", "upturn", "upward", "urban", "urge", "urgent", "urging", - "urine", "usable", "usage", "use", "useful", "user", "usual", - "uterus", "utmost", "utter", "vacant", "vacuum", - "vagina", "vague", "vain", "valet", "valid", "valley", "value", "valve", "van", "vanish", "vanity", "vary", "vase", "vast", "vat", "vault", "vector", "veil", "vein", "velvet", "vendor", "veneer", "venom", "vent", "venue", "verb", "verbal", "verge", "verify", "verity", "verse", "versus", "very", "vessel", "vest", "veto", "via", "viable", "vicar", "vice", "victim", "victor", "video", "view", "vigil", "vile", "villa", "vine", "vinyl", "viola", "violet", "violin", "viral", - "virgin", "virtue", "virus", "visa", "vision", "visit", "visual", "vital", "vivid", "vocal", "vodka", "vogue", "voice", "void", "volley", "volume", "vomit", "vote", "vowel", "voyage", "vulgar", "wade", "wage", "waist", "wait", "waiter", "wake", "walk", "walker", "wall", "wallet", "walnut", "wander", "want", "war", "warden", "warm", "warmth", "warn", "warp", "wary", "was", "wash", "wasp", "waste", "watch", "water", "watery", "wave", "way", "weak", "weaken", "wealth", "weapon", "wear", "weary", "wedge", "wee", - "weed", "week", "weekly", "weep", "weight", "weird", "well", "were", "wet", "whale", "wharf", "what", "wheat", "wheel", "when", "whence", "where", "which", "whiff", "whig", "while", "whim", "whip", - "whisky", - "white", "who", "whole", "wholly", "whom", - "whore", "whose", "why", "wide", "widely", "widen", "wider", "widow", "width", "wife", "wild", "wildly", "wilful", "will", "willow", "win", "wind", "window", "windy", "wine", "wing", "wink", "winner", "winter", "wipe", "wire", "wisdom", "wise", "wish", "wit", "witch", "with", "within", "witty", "wizard", "woke", "wolf", "wolves", "woman", "womb", "won", "wonder", "wood", "wooden", "woods", "woody", "wool", "word", "work", "worker", "world", "worm", "worry", "worse", "worst", "worth", "worthy", "would", "wound", "wrap", "wrath", "wreath", "wreck", "wright", "wrist", "writ", "write", "writer", "wrong", "xerox", "yacht", "yard", "yarn", "yeah", "year", "yeast", - "yellow", "yet", "yield", "yogurt", "yolk", "you", "young", "your", "youth", "zeal", "zebra", "zenith", "zero", "zigzag", "zinc", "zombie", - "zone" + "zone", + "Africa", /* we had "Europe" */ + "Asia", + "South", /* we had the other 3, but not this one */ + "anyone", /* some previously missed very common English words */ + "born", + "its", + "little", + "man", + "new", + "news", + "others", + "yes", + "admin", /* words common in computing */ + "avatar", + "codec", + "laptop", + "online", + "tweet", + "aisle", /* BIP-0039 */ + "amused", + "arena", + "armed", + "banner", + "boring", + "buddy", + "bunker", + "burger", + "cannon", + "casino", + "churn", + "clog", + "clump", + "coyote", + "cram", + "crouch", + "daring", + "embody", + "enact", + "enrich", + "erase", + "erupt", + "ethics", + "fiber", + "flip", + "gym", + "hen", + "hip", + "hub", + "inhale", + "jar", + "jeans", + "kit", + "kiwi", + "lab", + "lumber", + "lyrics", + "mango", + "marine", + "math", + "mixed", + "muffin", + "naive", + "net", + "noodle", + "nut", + "odor", + "panda", + "peanut", + "pet", + "pluck", + "purity", + "ranch", + "renew", + "reopen", + "rib", + "rookie", + "rubber", + "rug", + "salon", + "scheme", + "setup", + "shed", + "shove", + "skate", + "ski", + "slush", + "sniff", + "spawn", + "spike", + "spy", + "stairs", + "subway", + "tag", + "tired", + "toe", + "topple", + "toy", + "trash", + "undo", + "unfold", + "unlock", + "unveil", + "upper", + "used", + "wagon", + "weasel", + "web", + "zoo", + "cookie", /* more previously missed common English words */ + "cop", + "cops", + "dig", + "digger", + "dining", + "extent", + "gifted", + "hunter", + "living", + "lots", + "makeup", + "nearby", + "nod", + "odds", + "pan", + "pant", + "porch", + "rating", + "sales", + "saving", + "scared", + "sin", + "sinner", + "sins", + "terms", + "thanks", + "united", + "unless", + "viewer", + "voter", + "voters", + "weigh", + "works", + "yell", + "yours", + "arms", /* gutenberg-19xx-lowercase-words-1000plus.txt top 1000 */ + "asked", + "became", + "been", + "began", + "books", + "boys", + "called", + "cannot", + "closed", + "comes", + "coming", + "days", + "died", + "does", + "doing", + "drawn", + "ears", + "eyes", + "faces", + "fallen", + "fixed", + "girls", + "giving", + "goes", + "going", + "hands", + "having", + "horses", + "hours", + "houses", + "knows", + "laid", + "leaves", + "legs", + "lifted", + "liked", + "lines", + "lips", + "lived", + "lives", + "looked", + "looks", + "loved", + "makes", + "making", + "means", + "men", + "miles", + "months", + "moved", + "moving", + "needed", + "nodded", + "opened", + "papers", + "passed", + "paused", + "picked", + "placed", + "places", + "played", + "pounds", + "pulled", + "raised", + "ran", + "rooms", + "sat", + "saw", + "saying", + "says", + "seemed", + "seems", + "seen", + "showed", + "smiled", + "spent", + "stared", + "steps", + "stood", + "struck", + "taken", + "taking", + "talked", + "tears", + "things", + "threw", + "times", + "trees", + "trying", + "turned", + "waited", + "walked", + "walls", + "wanted", + "ways", + "weeks", + "went", + "wished", + "women", + "words", + "wore", + "worked", + "wrote", + "years", + "asking", /* gutenberg-19xx-lowercase-words-1000plus.txt top 2000 */ + "begun", + "birds", + "bodies", + "bones", + "boots", + "bowed", + "built", + "calls", + "cards", + "cared", + "cases", + "caused", + "ceased", + "chairs", + "cheeks", + "clouds", + "crying", + "damned", + "dared", + "dim", + "dogs", + "doors", + "dreams", + "driven", + "ended", + "ends", + "events", + "faced", + "facts", + "faded", + "failed", + "feared", + "fields", + "fired", + "flying", + "forced", + "forgot", + "formed", + "fought", + "gazed", + "gets", + "gives", + "guests", + "guns", + "handed", + "hated", + "heads", + "hearts", + "helped", + "hers", + "hills", + "hoped", + "ideas", + "joined", + "jumped", + "kissed", + "knees", + "ladies", + "larger", + "laws", + "lies", + "lights", + "locked", + "marked", + "minds", + "missed", + "named", + "names", + "needs", + "nerves", + "nights", + "noted", + "notes", + "ones", + "orders", + "parts", + "pieces", + "plans", + "points", + "powers", + "proved", + "pushed", + "rang", + "riding", + "rising", + "rocks", + "rolled", + "rushed", + "sank", + "saved", + "seated", + "seized", + "served", + "ships", + "shoes", + "shown", + "sides", + "sighed", + "signs", + "slept", + "sought", + "sounds", + "spoken", + "sprang", + "stars", + "stayed", + "stones", + "stuck", + "swept", + "swung", + "takes", + "taught", + "thinks", + "thrown", + "tied", + "turns", + "voices", + "wants", + "waters", + "whilst", + "worn", + "yards", + "acted", /* gutenberg-19xx-lowercase-words-1000plus.txt top 3000 */ + "acting", + "ages", + "arose", + "banks", + "beaten", + "beg", + "begged", + "begins", + "beings", + "blank", + "boats", + "bored", + "brains", + "brow", + "bushes", + "cars", + "cent", + "cities", + "coldly", + "cries", + "cursed", + "dashed", + "deeper", + "depths", + "duties", + "easier", + "eating", + "eggs", + "facing", + "fears", + "feels", + "finds", + "fires", + "fitted", + "folded", + "folks", + "forces", + "forms", + "gained", + "gasped", + "gates", + "gazing", + "gods", + "goods", + "grimly", + "habits", + "halted", + "hatred", + "heels", + "hid", + "hiding", + "hopes", + "hoping", + "inches", + "jewels", + "keeps", + "kinds", + "landed", + "lands", + "likes", + "limbs", + "longed", + "losing", + "loves", + "loving", + "marks", + "mob", + "ours", + "packed", + "pages", + "parted", + "paying", + "posted", + "poured", + "rested", + "rights", + "risen", + "roads", + "roots", + "roses", + "roused", + "ruined", + "runs", + "sailed", + "sang", + "sees", + "senses", + "shone", + "shows", + "signed", + "sons", + "sorts", + "souls", + "stands", + "stated", + "stole", + "stolen", + "strode", + "tables", + "tells", + "tones", + "tore", + "torn", + "towns", + "tracks", + "troops", + "urged", + "using", + "views", + "warned", + "washed", + "wasted", + "waved", + "waves", + "wicked", + "winds", + "wings", + "wishes", + "wives", + "acres", /* gutenberg-19xx-lowercase-words-1000plus.txt top 4000 */ + "acts", + "adding", + "aged", + "amazed", + "apt", + "ashes", + "awoke", + "backed", + "backs", + "bade", + "bags", + "bars", + "based", + "beasts", + "beds", + "bells", + "bills", + "bits", + "blown", + "blows", + "borne", + "boxes", + "brings", + "brows", + "buying", + "carved", + "causes", + "choked", + "cliffs", + "coolly", + "cows", + "crops", + "danced", + "dealt", + "denied", + "dimly", + "dishes", + "dismay", + "doubts", + "drinks", + "drops", + "earned", + "echoed", + "errand", + "falls", + "finest", + "flames", + "flies", + "fools", + "games", + "ghosts", + "gifts", + "gloves", + "groups", + "grows", + "hanged", + "hats", + "hay", + "headed", + "hired", + "holds", + "holes", + "homes", + "intent", + "issued", + "jerked", + "keys", + "kicked", + "lamps", + "lasted", + "laying", + "leaped", + "leapt", + "liking", + "lined", + "loaded", + "masses", + "meals", + "obeyed", + "outfit", + "owned", + "pains", + "passes", + "pearls", + "peered", + "piled", + "plains", + "plants", + "plays", + "porter", + "prayed", + "puts", + "races", + "racing", + "ragged", + "rays", + "rings", + "roared", + "robbed", + "rows", + "rubbed", + "ruins", + "rules", + "scenes", + "seas", + "seats", + "sets", + "shaken", + "shared", + "sheets", + "shops", + "sits", + "skirts", + "smiles", + "smoked", + "songs", + "speaks", + "sticks", + "stores", + "sunk", + "tales", + "talks", + "tapped", + "theirs", + "tossed", + "tribes", + "tricks", + "unseen", + "veins", + "viewed", + "visits", + "wages", + "walks", + "warmly", + "waving", + "wheels", + "wiped", + "wits", + "yelled", + "yonder", + "agents", /* the rest of gutenberg-19xx-lowercase-words-1000plus.txt */ + "alas", + "argued", + "arts", + "asks", + "behold", + "boiled", + "boldly", + "cares", + "chaps", + "claims", + "crimes", + "crowds", + "curled", + "darted", + "dazed", + "dined", + "dug", + "firing", + "flowed", + "francs", + "gaiety", + "gaily", + "git", + "gladly", + "glared", + "hinted", + "hunted", + "ink", + "jobs", + "judged", + "keenly", + "kings", + "knelt", + "lacked", + "leads", + "morrow", + "nails", + "noises", + "novels", + "owed", + "palms", + "patted", + "plates", + "poets", + "prey", + "prices", + "quoted", + "ranks", + "risks", + "rivers", + "rum", + "sealed", + "shaped", + "shares", + "shells", + "shots", + "spared", + "spots", + "starts", + "states", + "suited", + "sworn", + "tents", + "tools", + "traced", + "traces", + "trains", + "tramp", + "varied", + "veiled", + "waking", + "wax", + "wept", + "wiser", + "worlds", + "writes", + "affix", /* eff_short_wordlist_1.txt */ + "aging", + "aids", + "ajar", + "alias", + "aloe", + "aloha", + "amino", + "aqua", + "bagel", + "baked", + "balmy", + "banjo", + "bash", + "bask", + "bats", + "blimp", + "bloat", + "blob", + "blog", + "blurt", + "bok", + "boned", + "boney", + "botch", + "boxer", + "bribe", + "broil", + "bud", + "bunt", + "cadet", + "cameo", + "canon", + "carve", + "cedar", + "chili", + "chomp", + "chow", + "chuck", + "chump", + "chute", + "cinch", + "clamp", + "cleat", + "cleft", + "clink", + "cod", + "cola", + "coma", + "comma", + "cot", + "cozy", + "cramp", + "crank", + "crave", + "creme", + "crepe", + "crib", + "crumb", + "cub", + "cupid", + "curvy", + "cushy", + "dab", + "dandy", + "darn", + "dart", + "debug", + "decaf", + "decal", + "decoy", + "denim", + "dent", + "dill", + "dime", + "diner", + "dingy", + "ditzy", + "dodge", + "donut", + "dot", + "dowry", + "doze", + "drab", + "drone", + "droop", + "dude", + "duo", + "eats", + "ebay", + "ebony", + "ebook", + "eel", + "eject", + "elf", + "elk", + "elm", + "elope", + "emu", + "etch", + "fable", + "fang", + "fax", + "femur", + "finch", + "flaky", + "fling", + "flop", + "floss", + "foe", + "folic", + "fray", + "frill", + "frisk", + "froth", + "froze", + "gag", + "gains", + "gecko", + "geek", + "gem", + "gig", + "gills", + "giver", + "gooey", + "goofy", + "gore", + "grope", + "growl", + "grub", + "gulp", + "gummy", + "gush", + "halo", + "heave", + "herbs", + "hug", + "hula", + "hump", + "hunk", + "icy", + "igloo", + "ion", + "ivy", + "jab", + "jam", + "jaws", + "jiffy", + "jog", + "jot", + "junky", + "juror", + "keg", + "kilt", + "kitty", + "koala", + "kung", + "ladle", + "lair", + "lapel", + "lash", + "lasso", + "lilac", + "lily", + "limes", + "lint", + "lurch", + "lurk", + "mace", + "mama", + "mardi", + "mash", + "mocha", + "mold", + "mop", + "morse", + "motto", + "mousy", + "mower", + "mug", + "mulch", + "mull", + "mumbo", + "mural", + "muse", + "musky", + "nacho", + "nag", + "nanny", + "nap", + "nerd", + "nutty", + "oat", + "ooze", + "opal", + "opt", + "ouch", + "pager", + "pants", + "panty", + "pasta", + "payer", + "pecan", + "pep", + "perky", + "perm", + "petal", + "petri", + "plaza", + "plow", + "poach", + "pod", + "pogo", + "poise", + "poker", + "polio", + "polka", + "poser", + "pout", + "prank", + "prong", + "props", + "prude", + "pry", + "pug", + "puma", + "purr", + "quack", + "quill", + "rabid", + "rake", + "rant", + "ream", + "recap", + "remix", + "repay", + "repel", + "rerun", + "reset", + "rigor", + "ritzy", + "romp", + "runny", + "rut", + "salsa", + "santa", + "savor", + "sax", + "scam", + "scoff", + "scold", + "scoop", + "scoot", + "scowl", + "scuba", + "scuff", + "sedan", + "sepia", + "shack", + "showy", + "shred", + "shun", + "shush", + "sift", + "silo", + "sip", + "skew", + "skid", + "skier", + "skies", + "skit", + "slash", + "slaw", + "sled", + "sleet", + "slob", + "slurp", + "smirk", + "smog", + "snare", + "snarl", + "sneer", + "snore", + "snort", + "snout", + "snub", + "snuff", + "spew", + "spied", + "spiny", + "spoof", + "spool", + "spout", + "stank", + "stash", + "stays", + "stomp", + "stoop", + "strut", + "stump", + "stung", + "suds", + "sulk", + "sushi", + "swab", + "swipe", + "swoop", + "tacky", + "taco", + "talon", + "tamer", + "taper", + "taps", + "tarot", + "taunt", + "thaw", + "theft", + "thong", + "throb", + "thump", + "tiara", + "tint", + "tug", + "tulip", + "tummy", + "tusk", + "tutu", + "tux", + "tweak", + "twine", + "twins", + "twirl", + "uncut", + "untie", + "usher", + "vegan", + "visor", + "vixen", + "volt", + "wad", + "wafer", + "wager", + "wand", + "wavy", + "whoop", + "wick", + "wifi", + "wilt", + "wimp", + "wired", + "wiry", + "wispy", + "wok", + "woozy", + "woven", + "yahoo", + "yam", + "yelp", + "yodel", + "yoga", + "yummy", + "zesty", + "zippy", + "zoom", + "abacus", /* eff_large_wordlist.txt */ + "affirm", + "aflame", + "afoot", + "ahoy", + "aliens", + "alto", + "alumni", + "amends", + "amigo", + "amply", + "amuck", + "amulet", + "amuser", + "anemia", + "anemic", + "angled", + "angler", + "angles", + "anime", + "annex", + "antics", + "antler", + "antsy", + "anvil", + "aorta", + "apache", + "aptly", + "arming", + "armory", + "ascend", + "ashy", + "askew", + "aspire", + "atop", + "atrium", + "attest", + "attire", + "autism", + "awning", + "awry", + "babble", + "babied", + "baboon", + "backer", + "badass", + "baffle", + "bagful", + "bagged", + "baggie", + "baking", + "banish", + "banked", + "banker", + "banter", + "barbed", + "barman", + "basics", + "batboy", + "bauble", + "blah", + "blazer", + "bleach", + "bleep", + "bling", + "blinks", + "bluish", + "blurb", + "blurry", + "bobbed", + "bobble", + "bobcat", + "bogged", + "boggle", + "bonded", + "bonsai", + "booted", + "bootie", + "boozy", + "borax", + "botany", + "bouncy", + "boxcar", + "boxing", + "boxy", + "breezy", + "briar", + "broker", + "bronco", + "browse", + "brunch", + "brunt", + "bubbly", + "bucked", + "buffed", + "buffer", + "bulgur", + "bungee", + "bunion", + "busboy", + "busily", + "cabana", + "cabbie", + "cackle", + "cacti", + "caddie", + "caddy", + "camper", + "canned", + "canola", + "capped", + "carat", + "carded", + "caring", + "carton", + "casing", + "casket", + "catchy", + "catnap", + "catnip", + "catsup", + "catty", + "caucus", + "caviar", + "cavity", + "chafe", + "chaste", + "chatty", + "cheesy", + "chemo", + "cherub", + "chevy", + "chewer", + "chewy", + "chimp", + "chirpy", + "chive", + "choosy", + "chubby", + "chug", + "chummy", + "citric", + "citrus", + "clamor", + "clang", + "clench", + "clique", + "clover", + "clunky", + "cobweb", + "coerce", + "collie", + "comfy", + "conch", + "copied", + "copier", + "coping", + "cornea", + "corned", + "corny", + "corral", + "corset", + "cozily", + "crayon", + "crazed", + "crease", + "creole", + "crier", + "crimp", + "cringe", + "crispy", + "croak", + "crock", + "croon", + "crummy", + "cuddle", + "cuddly", + "cupped", + "curdle", + "curing", + "curler", + "curly", + "curtly", + "curtsy", + "cusp", + "cussed", + "cymbal", + "dainty", + "dander", + "dangle", + "dares", + "dating", + "daybed", + "deacon", + "debunk", + "deceit", + "decode", + "deduct", + "deem", + "deepen", + "deface", + "defame", + "defile", + "defog", + "deftly", + "defuse", + "deluge", + "deluxe", + "demote", + "deport", + "depose", + "derail", + "deuce", + "diaper", + "dicing", + "dilute", + "dimmed", + "dimmer", + "dimple", + "dingo", + "dipped", + "dipper", + "disarm", + "disown", + "ditto", + "diving", + "doable", + "dodgy", + "doily", + "dollop", + "doodle", + "doozy", + "dork", + "dosage", + "dotted", + "douche", + "dreamt", + "dreamy", + "drench", + "drier", + "drippy", + "drool", + "drudge", + "dubbed", + "ducky", + "duffel", + "dugout", + "duh", + "duller", + "dupe", + "duplex", + "duvet", + "dweeb", + "earful", + "earthy", + "earwig", + "easing", + "eatery", + "ecard", + "eclair", + "edging", + "edgy", + "egging", + "eggnog", + "elated", + "elixir", + "ember", + "emboss", + "emcee", + "emote", + "encode", + "encore", + "ending", + "engulf", + "enrage", + "entity", + "entomb", + "entrap", + "entree", + "erased", + "eraser", + "eskimo", + "ether", + "exes", + "exhale", + "exhume", + "expel", + "expend", + "extras", + "fading", + "faucet", + "fedora", + "feisty", + "feline", + "fender", + "ferret", + "ferris", + "fervor", + "fester", + "filing", + "finer", + "flail", + "flashy", + "flatly", + "flier", + "flinch", + "fondue", + "footer", + "frayed", + "frays", + "frolic", + "frying", + "gab", + "gaffe", + "galore", + "gaming", + "gander", + "gangly", + "gargle", + "garnet", + "garter", + "gating", + "gauze", + "gawk", + "geiger", + "gents", + "gerbil", + "getup", + "giblet", + "giddy", + "giggly", + "gilled", + "girdle", + "gizmo", + "glider", + "glitch", + "glitzy", + "gluten", + "gnarly", + "gnat", + "gonad", + "google", + "goon", + "gopher", + "gorged", + "gotten", + "gout", + "graded", + "grader", + "granny", + "graves", + "grime", + "grimy", + "grinch", + "groggy", + "groovy", + "grout", + "grower", + "grunge", + "gurgle", + "gusto", + "gusty", + "guts", + "gutter", + "hacked", + "hacker", + "haiku", + "halved", + "halves", + "hamper", + "hangup", + "hankie", + "hanky", + "hardy", + "hatbox", + "hazily", + "hazing", + "header", + "helper", + "henna", + "herbal", + "hermit", + "hertz", + "hubcap", + "huddle", + "huff", + "hulk", + "humbly", + "hummus", + "humped", + "humvee", + "hurled", + "hurler", + "hurray", + "husked", + "icky", + "idiocy", + "iguana", + "impale", + "impart", + "impish", + "impure", + "iodine", + "iodize", + "ipad", + "iphone", + "ipod", + "irate", + "irk", + "itunes", + "jackal", + "jailer", + "jaunt", + "jawed", + "jester", + "jigsaw", + "jimmy", + "jingle", + "jinx", + "jogger", + "jovial", + "judo", + "juggle", + "junkie", + "jurist", + "justly", + "kabob", + "karma", + "kebab", + "kelp", + "kennel", + "kiln", + "kimono", + "kindle", + "kisser", + "knoll", + "kooky", + "kosher", + "kudos", + "lagged", + "lanky", + "lapdog", + "lapped", + "lard", + "lark", + "lather", + "laurel", + "lazily", + "legged", + "lego", + "legume", + "levers", + "lifter", + "lilly", + "lingo", + "lining", + "linked", + "lisp", + "litmus", + "litter", + "lugged", + "lushly", + "luster", + "lusty", + "macaw", + "maggot", + "maimed", + "manger", + "mangle", + "mangy", + "manila", + "manly", + "manned", + "mantis", + "mantra", + "marlin", + "maroon", + "marrow", + "marshy", + "mascot", + "mashed", + "mating", + "matron", + "matted", + "mauve", + "mayday", + "moaner", + "mocker", + "mockup", + "mooing", + "mooned", + "mossy", + "mowing", + "mulled", + "mumble", + "mumps", + "muppet", + "mushy", + "musket", + "muster", + "musty", + "mutate", + "mutt", + "naming", + "napped", + "nappy", + "nebula", + "nectar", + "nervy", + "neuron", + "neuter", + "nibble", + "nifty", + "nimbly", + "ninja", + "nuclei", + "nugget", + "numbly", + "nutmeg", + "nuzzle", + "oaf", + "oblong", + "obtuse", + "ocelot", + "octane", + "ogle", + "oink", + "onyx", + "oops", + "oozy", + "outage", + "outbid", + "outing", + "outlet", + "outwit", + "ovary", + "paced", + "pacify", + "padded", + "paging", + "paltry", + "pang", + "pantry", + "papaya", + "parka", + "parlor", + "parole", + "pasted", + "pasty", + "patchy", + "pauper", + "paver", + "paving", + "pawing", + "payday", + "payee", + "pebble", + "pebbly", + "pectin", + "pellet", + "pelt", + "penpal", + "pesky", + "peso", + "pester", + "petted", + "phobia", + "phoney", + "phony", + "plated", + "pleat", + "plod", + "plop", + "pointy", + "poking", + "poncho", + "poplar", + "popper", + "porous", + "portly", + "posing", + "possum", + "poster", + "pounce", + "powwow", + "pox", + "prance", + "precut", + "prelaw", + "prepay", + "preppy", + "preset", + "prewar", + "pried", + "primer", + "primp", + "prissy", + "pronto", + "proofs", + "prozac", + "pucker", + "pueblo", + "pumice", + "pummel", + "purist", + "pusher", + "pushup", + "python", + "quail", + "qualm", + "quench", + "racoon", + "radial", + "raging", + "raider", + "raisin", + "raking", + "ramble", + "ramrod", + "ranged", + "ranger", + "ranked", + "rants", + "rascal", + "ravage", + "ravine", + "raving", + "rebate", + "reboot", + "reborn", + "rebuff", + "recant", + "recast", + "recede", + "recite", + "recoil", + "recopy", + "refill", + "reflex", + "reflux", + "refold", + "refund", + "refute", + "regain", + "reggae", + "rehab", + "reheat", + "rehire", + "rejoin", + "relive", + "reload", + "relock", + "remake", + "remold", + "rename", + "rented", + "renter", + "repave", + "replay", + "repose", + "repost", + "reps", + "resale", + "reseal", + "resend", + "resize", + "retake", + "retold", + "retool", + "retry", + "retype", + "reuse", + "reverb", + "revert", + "revoke", + "rewash", + "rewind", + "rewire", + "reword", + "rework", + "rewrap", + "riches", + "richly", + "ridden", + "rimmed", + "rind", + "rink", + "roamer", + "rocker", + "roping", + "roster", + "roving", + "ruckus", + "rumor", + "runner", + "runt", + "ruse", + "sadden", + "saggy", + "salami", + "sandal", + "sanded", + "sappy", + "sassy", + "saucy", + "savior", + "scabby", + "scion", + "scone", + "scorch", + "scored", + "scorer", + "scouts", + "scribe", + "scurvy", + "sedate", + "seduce", + "septic", + "septum", + "sesame", + "shaded", + "shale", + "shank", + "shanty", + "sheath", + "shelve", + "shifty", + "shimmy", + "shorts", + "shorty", + "shriek", + "shrubs", + "shrunk", + "siding", + "sierra", + "siesta", + "silica", + "silt", + "simile", + "sitcom", + "sitter", + "sizing", + "sizzle", + "skater", + "skewed", + "skewer", + "skied", + "skiing", + "skype", + "slacks", + "sliced", + "slicer", + "slider", + "slinky", + "sliver", + "sloped", + "sludge", + "sly", + "smite", + "smith", + "smock", + "smudge", + "smudgy", + "smugly", + "snazzy", + "sneeze", + "snide", + "snitch", + "snooze", + "snugly", + "specks", + "sphinx", + "spiffy", + "spilt", + "spleen", + "splice", + "spoils", + "spongy", + "spooky", + "spore", + "sports", + "sporty", + "spotty", + "sprain", + "sprawl", + "sprig", + "sprite", + "sprout", + "spruce", + "sprung", + "spry", + "spud", + "squall", + "squeak", + "squint", + "squire", + "starry", + "steed", + "stilt", + "stingy", + "stinky", + "stoic", + "stoke", + "stooge", + "strep", + "strewn", + "strobe", + "strum", + "strung", + "stucco", + "stupor", + "stylus", + "suave", + "sublet", + "subpar", + "sudoku", + "suffix", + "suing", + "sulfur", + "sultry", + "surfer", + "swerve", + "swivel", + "swoosh", + "tabby", + "talcum", + "tamale", + "tamper", + "tanned", + "tarmac", + "tartar", + "tartly", + "tassel", + "tattle", + "thinly", + "thrash", + "thrift", + "tibia", + "tidbit", + "tiling", + "timing", + "tingle", + "tingly", + "tinker", + "tinsel", + "tipoff", + "tipped", + "tipper", + "tiptop", + "tiring", + "traps", + "triage", + "tripod", + "trowel", + "trunks", + "tubby", + "turret", + "twerp", + "twig", + "twisty", + "twitch", + "tyke", + "udder", + "unbend", + "unbent", + "unclad", + "unclip", + "unclog", + "uncork", + "undead", + "undone", + "unease", + "uneven", + "unglue", + "unholy", + "unhook", + "unison", + "unkind", + "unlit", + "unmade", + "unpack", + "unpaid", + "unplug", + "unread", + "unreal", + "unripe", + "unroll", + "unsafe", + "unsaid", + "unsent", + "unsnap", + "unsold", + "unsure", + "untidy", + "untold", + "untrue", + "unused", + "unwary", + "unwed", + "unwell", + "unwind", + "unworn", + "unzip", + "upbeat", + "upload", + "uproot", + "upside", + "uptown", + "upwind", + "urchin", + "utopia", + "vacate", + "valium", + "vastly", + "veal", + "veggie", + "velcro", + "vibes", + "viper", + "vista", + "voting", + "vowed", + "waffle", + "waged", + "waggle", + "walrus", + "waltz", + "wasabi", + "washer", + "whacky", + "wham", + "whinny", + "whiny", + "whoops", + "widget", + "wilder", + "willed", + "wince", + "wiring", + "wobble", + "wobbly", + "woof", + "wooing", + "wow", + "wrench", + "xbox", + "yearly", + "yen", + "yin", + "yippee", + "zap", + "zen", + "zips", + "zit", + "zodiac", + "zoning", + "abyss", /* eff_short_wordlist_2_0.txt */ + "algae", + "amoeba", + "anklet", + "apnea", + "azalea", + "boiler", + "cashew", + "dibs", + "dozed", + "dryer", + "edged", + "eulogy", + "fillet", + "foggy", + "geyser", + "iconic", + "jetski", + "kayak", + "kiosk", + "llama", + "luau", + "ocular", + "oomph", + "owlish", + "peony", + "pewter", + "pulley", + "scythe", + "shovel", + "skulk", + "sloth", + "tirade", + "trucks", + "tryout", + "tuxedo", + "untied", + "upkeep", + "wakeup", + "yuppie", + "zealot", + "bush", /* these two were merely words, now may be taken as referring to specific people */ + "trump", + "church", /* religious references (more of these are among the capitalized words below) */ + "devil", + "devils", + "easter", + "gospel", + "satan", + "black", /* races, skin colors, and ethnicities */ + "blacks", + "brown", + "white", + "yellow", + "Afghan", /* assorted capitalized words undesirable in passphrases, including: */ + "Allah", /* religious references */ + "Anglo", /* ethnicities, nationalities (but country names stay), other categories of people */ + "Arab", + "Asian", + "Bach", /* specific people */ + "Basque", + "Bible", + "Briton", + "Buddha", + "Caesar", + "Celtic", + "Christ", + "Danish", + "Darwin", + "Dutch", + "Exodus", + "French", + "Gandhi", + "Gaul", + "German", + "God", + "Greek", + "Hebrew", + "Hindu", + "Hitler", + "Indian", + "Irish", + "Islam", + "Jesus", + "Jewish", + "Judas", + "Koran", + "Madame", /* has an inappropriate meaning */ + "Moses", + "Moslem", + "Mrs", /* abbreviation */ + "Muslim", + "Nazi", + "Nobel", + "Pascal", + "Polish", + "Ritz", /* brands (the EFF lists also include some, but might not be reached) */ + "Saudi", + "Saxon", + "Scot", + "Sony", /* "Amazon" stayed for its original meaning */ + "Soviet", + "Stalin", + "Swiss", + "Turk", + "abort", /* generally politically incorrect */ + "master", + "slave", + "slaves", + "booze", /* some references to drugs and alcohol (but specific drink names stay above) */ + "crack", + "heroin", + "joint", + "opium", + "pot", + "weed", + "kill", /* violence */ + "killed", + "killer", + "murder", + "rape", + "arouse", /* obscene, intimate anatomy, sexually explicit or suggestive */ + "ass", + "babe", + "bang", + "bitch", + "blew", + "blow", + "breast", + "butt", + "carnal", + "cervix", + "chick", + "climax", + "cock", + "daddy", + "erect", + "erotic", + "escort", + "facial", + "flirt", + "gay", + "gigolo", + "horny", + "incest", + "lick", + "lover", + "lovers", + "naked", + "nude", + "oral", + "orgasm", + "penis", + "piss", + "pussy", + "queer", + "rectal", + "sadism", + "screw", + "sex", + "sexual", + "sexy", + "shit", + "sperm", + "squirt", + "strip", + "suck", + "thrust", /* Ubuntu bug 1407629 claims "Probe!thrust6scorn" is inappropriate */ + "urine", + "uterus", /* "womb" is on EFF lists, so assumed OK - more figurative than anatomical */ + "vagina", + "virgin", + "whore", + "advice", /* too similar */ + "advise", + "armor", /* multiple spellings */ + "armour", + "burned", + "burnt", + "center", + "centre", + "cheque", /* or "check", which is preserved above for different meaning */ + "color", + "colour", + "email", /* or "e-mail" */ + "enroll", /* or "enrol" */ + "favor", + "favour", + "flavor", /* or "flavour" */ + "gipsy", + "gray", + "grey", + "gypsy", + "harbor", /* or "harbour" */ + "hippie", + "hippy", + "honor", + "honour", + "humor", + "humour", + "labor", + "labour", + "leaned", + "leant", + "learnt", /* or "learned" */ + "mom", + "mum", + "tire", + "tyre", + "vapor", + "vapour", + "whisky", /* or "whiskey" */ + "yoyo", /* or "yo-yo" */ + "etc", /* Latin, not English */ +#if 0 /* obsolete, dialectal, and non-words from gutenberg-19xx-lowercase-words-1000plus.txt */ + "des", + "ere", + "fer", + "fro", + "hath", + "thee", + "thet", + "thou", + "thy", + "unto", + "von", + "wid", + "yer", + "html", /* obvious noise from how gutenberg-19xx-lowercase-words-1000plus.txt was created */ + "http", + "txt", +#endif + "" /* end of list marker */ }; diff --git a/contrib/pam_modules/pam_passwdqc/wordset_4k.h b/contrib/pam_modules/pam_passwdqc/wordset_4k.h new file mode 100644 index 000000000000..5f212bbecd26 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/wordset_4k.h @@ -0,0 +1,13 @@ +/* + * Written by Solar Designer and placed in the + * public domain. + */ + +#ifndef WORDSET_4K_H__ +#define WORDSET_4K_H__ + +#define WORDSET_4K_LENGTH_MAX 6 + +extern const char _passwdqc_wordset_4k[][WORDSET_4K_LENGTH_MAX]; + +#endif /* WORDSET_4K_H__ */