diff options
author | Gordon Tetlow <gordon@FreeBSD.org> | 2023-11-03 17:37:48 +0000 |
---|---|---|
committer | Gordon Tetlow <gordon@FreeBSD.org> | 2023-11-03 17:37:48 +0000 |
commit | e4fe068d29c902225fa3733db09af214cbfb3c02 (patch) | |
tree | f8f9cbff2b52e2e05c2e8e64aea2fc3210583273 | |
parent | 00d65bdc4b6c36b0692588d71ca18ff080826a75 (diff) |
Vendor import of pam_passwdqc v2.0.3.vendor/pam_modules/passwdqc/v2.0.3vendor/pam_modules
53 files changed, 9062 insertions, 822 deletions
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 @@ -9,22 +9,47 @@ 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,8 +1,8 @@ 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 @@ -15,27 +15,35 @@ 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. @@ -46,35 +54,36 @@ 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. @@ -82,29 +91,65 @@ 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] @@ -115,20 +160,20 @@ 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 [] @@ -139,5 +184,8 @@ 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 <solar@openwall.com> + 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.h>' '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 <solar at openwall.com> and placed in the + * public domain. + */ + +#include <string.h> +#include <stdarg.h> +#include <stdlib.h> +#include <limits.h> +#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 <stdio.h> + +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 <passwdqc.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdio.h> + +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 <solar at openwall.com> + * + * 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 <string.h> + +#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 <solar at openwall.com> + * + * 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 <openssl/md4.h> +#elif !defined(_MD4_H) +#define _MD4_H + +#include <stddef.h> /* 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 @@ -2,27 +2,41 @@ * These macros are partially based on Linux-PAM's <security/_pam_macros.h>, * 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 <string.h> #include <stdlib.h> -#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,10 +1,19 @@ /* - * 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 <stdio.h> #include <stdlib.h> #include <stdarg.h> @@ -15,6 +24,10 @@ #ifdef HAVE_SHADOW #include <shadow.h> #endif +#ifdef HAVE_LIBAUDIT +#include <security/pam_modutil.h> +#include <libaudit.h> +#endif #define PAM_SM_PASSWORD #ifndef LINUX_PAM @@ -32,119 +45,164 @@ #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); } @@ -166,19 +224,20 @@ static int say(pam_handle_t *pamh, int style, const char *format, ...) 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; } @@ -188,152 +247,96 @@ static int check_max(params_t *params, pam_handle_t *pamh, const char *newpass) 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); @@ -341,86 +344,80 @@ PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, 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; @@ -431,45 +428,48 @@ retry: 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); @@ -477,33 +477,36 @@ retry: 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; @@ -515,7 +518,7 @@ retry: 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) { @@ -523,17 +526,19 @@ retry: 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); @@ -541,13 +546,13 @@ retry: 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, 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 <solar@owl.openwall.com> -- 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 <mci@owl.openwall.com> -- Enforce our new spec file conventions. - -* Sun Nov 04 2001 Solar Designer <solar@owl.openwall.com> -- 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 <solar@owl.openwall.com> -- Install the module as mode 755. - -* Tue Dec 19 2000 Solar Designer <solar@owl.openwall.com> -- Added "-Wall -fPIC" to the CFLAGS. - -* Mon Oct 30 2000 Solar Designer <solar@owl.openwall.com> -- 0.3: portability fixes (this might build on non-Linux-PAM now). - -* Fri Sep 22 2000 Solar Designer <solar@owl.openwall.com> -- 0.2: added "use_authtok", added README. - -* Fri Aug 18 2000 Solar Designer <solar@owl.openwall.com> -- 0.1, "retry_wanted" bugfix. - -* Sun Jul 02 2000 Solar Designer <solar@owl.openwall.com> -- 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,11 +1,30 @@ /* - * 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 <pwd.h> +#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; @@ -13,12 +32,51 @@ typedef struct { 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 <ldv-at-owl.openwall.com> 2.0.3-owl1 +- wordset_4k: Move "enroll" to the multiple spellings list (by Solar Designer) +- Don't #include <endian.h> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 2.0.0-owl2 +- Update the package description to include pwqfilter. + +* Tue Feb 16 2021 Solar Designer <solar-at-owl.openwall.com> 2.0.0-owl1 +- Introduce and use passwdqc_params_free(). + +* Fri Jan 29 2021 Solar Designer <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <ldv-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 1.0.1-owl1 +- Further compiler warning fixes on LP64 platforms. + +* Fri Mar 25 2005 Solar Designer <solar-at-owl.openwall.com> 1.0-owl1 +- Corrected the source code to not break C strict aliasing rules. + +* Wed Jan 26 2005 Solar Designer <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 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 <solar-at-owl.openwall.com> 0.7.3.1-owl1 +- Added URL. + +* Thu Oct 31 2002 Solar Designer <solar-at-owl.openwall.com> 0.7.3-owl1 +- When compiling with gcc, also link with gcc. +- Use $(MAKE) to invoke sub-makes. + +* Fri Oct 04 2002 Solar Designer <solar-at-owl.openwall.com> +- Solaris 9 notes in PLATFORMS. + +* Wed Sep 18 2002 Solar Designer <solar-at-owl.openwall.com> +- 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 <solar-at-owl.openwall.com> +- Have the same pam_passwdqc binary work for both trusted and non-trusted +HP-UX, from Kevin Steves. + +* Fri Sep 06 2002 Solar Designer <solar-at-owl.openwall.com> +- 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 <solar-at-owl.openwall.com> +- Call it 0.6. + +* Sat Jul 27 2002 Solar Designer <solar-at-owl.openwall.com> +- Documented that the man page is under the 3-clause BSD-style license. +- HP-UX 11 support. + +* Tue Jul 23 2002 Solar Designer <solar-at-owl.openwall.com> +- 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 <solar-at-owl.openwall.com> +- 0.5.1: imported the pam_passwdqc(8) manual page back from FreeBSD. + +* Tue Apr 16 2002 Solar Designer <solar-at-owl.openwall.com> +- 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 <mci-at-owl.openwall.com> +- Enforce our new spec file conventions. + +* Sun Nov 04 2001 Solar Designer <solar-at-owl.openwall.com> +- 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 <solar-at-owl.openwall.com> +- Install the module as mode 755. + +* Tue Dec 19 2000 Solar Designer <solar-at-owl.openwall.com> +- Added "-Wall -fPIC" to the CFLAGS. + +* Mon Oct 30 2000 Solar Designer <solar-at-owl.openwall.com> +- 0.3: portability fixes (this might build on non-Linux-PAM now). + +* Fri Sep 22 2000 Solar Designer <solar-at-owl.openwall.com> +- 0.2: added "use_authtok", added README. + +* Fri Aug 18 2000 Solar Designer <solar-at-owl.openwall.com> +- 0.1, "retry_wanted" bugfix. + +* Sun Jul 02 2000 Solar Designer <solar-at-owl.openwall.com> +- 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,37 +1,57 @@ /* - * 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 <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> -#include <pwd.h> -#include "passwdqc.h" +#include "passwdqc.h" /* also provides <pwd.h> 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 @@ -39,7 +59,7 @@ 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). */ @@ -49,7 +69,8 @@ static int expected_different(int charset, int length) 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); @@ -59,8 +80,16 @@ static int expected_different(int charset, int length) * 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; @@ -72,66 +101,89 @@ static int is_simple(passwdqc_params_t *params, const char *newpass) 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; @@ -140,13 +192,13 @@ static int is_simple(passwdqc_params_t *params, const char *newpass) 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; @@ -154,9 +206,28 @@ static char *unify(const char *src) 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; @@ -181,25 +252,27 @@ static char *reverse(const char *src) 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; @@ -207,31 +280,76 @@ static int is_based(passwdqc_params_t *params, 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); @@ -239,123 +357,280 @@ static int is_based(passwdqc_params_t *params, 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 <stdio.h> /* for SEEK_SET and SEEK_END */ +#include <io.h> +#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 <unistd.h> +#define OPEN_FLAGS O_RDONLY +#endif + +#include <stdint.h> +#include <limits.h> +#include <fcntl.h> + +#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 <stdint.h> + +/* 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 <string.h> /* for strcspn() */ +#if !defined(_MSC_VER) && !defined(__APPLE__) +#include <endian.h> +#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 <libintl.h> +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <errno.h> +#include <sys/stat.h> + +#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 <windows.h> +#else +#include <string.h> +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#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,30 +1,86 @@ /* - * 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 <stdio.h> -#include <string.h> +#ifdef _MSC_VER +#define _CRT_NONSTDC_NO_WARNINGS /* we use POSIX function names */ +#include <windows.h> +#include <wincrypt.h> +#else +#include <limits.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> +#endif + +#include <string.h> #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; @@ -32,61 +88,147 @@ static int read_loop(int fd, char *buffer, int count) 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 <mcpain@altlinux.org>, 2017. +# Andrey Cherepanov <cas@altlinux.org>, 2017. +# Dmitry V. Levin <ldv@altlinux.org>, 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 <ldv@altlinux.org>\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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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 @@ +<?php + +/* + * Copyright (c) 2010 by Solar Designer + * See LICENSE + * + * This file was originally written as part of demos for the "How to manage a + * PHP application's users and passwords" article submitted to "the Month of + * PHP Security" (which was May 2010): + * + * https://www.openwall.com/articles/PHP-Users-Passwords#enforcing-password-policy + * + * The pwqcheck() function is a wrapper around the pwqcheck(1) program from + * the passwdqc package: + * + * https://www.openwall.com/passwdqc/ + * + * Returns 'OK' if the new password/passphrase passes the requirements. + * Otherwise returns a message explaining one of the reasons why the + * password/passphrase is rejected. + * + * $newpass and $oldpass are the new and current/old passwords/passphrases, + * respectively. Only $newpass is required. + * + * $user is the username. + * + * $aux may be the user's full name, e-mail address, and/or other textual + * info specific to the user (multiple items may be separated with spaces). + * + * $args are additional arguments to pass to pwqcheck(1), to override the + * default password policy. + */ +function pwqcheck($newpass, $oldpass = '', $user = '', $aux = '', $args = '') +{ +// pwqcheck(1) itself returns the same message on internal error + $retval = 'Bad passphrase (check failed)'; + + $descriptorspec = array( + 0 => 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 <io.h> +#else +#include <unistd.h> /* for unlink() */ +#endif + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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,63 +1,103 @@ /* - * 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 <dianelos at tecapro.com>, 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", @@ -65,43 +105,30 @@ char _passwdqc_wordset_4k[0x1000][6] = { "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", @@ -111,20 +138,16 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Japan", "Java", "Jersey", - "Jesus", - "Jewish", "Jim", "John", "Jordan", "Joseph", - "Judas", "Judy", "July", "June", "Kansas", "Karl", "Kenya", - "Koran", "Korea", "Kuwait", "Laos", @@ -138,7 +161,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Louvre", "Lucy", "Luther", - "Madame", "Madrid", "Malta", "Maria", @@ -153,17 +175,11 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Monaco", "Monday", "Moscow", - "Moses", - "Moslem", - "Mrs", "Munich", - "Muslim", "Naples", - "Nazi", "Nepal", "Newark", "Nile", - "Nobel", "North", "Norway", "Ohio", @@ -172,19 +188,16 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Oxford", "Panama", "Paris", - "Pascal", "Paul", "Peking", "Peru", "Peter", "Philip", "Poland", - "Polish", "Prague", "Quebec", "Rex", "Rhine", - "Ritz", "Robert", "Roman", "Rome", @@ -193,20 +206,13 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Sahara", "Sam", "Saturn", - "Saudi", - "Saxon", - "Scot", "Seoul", "Somali", - "Sony", - "Soviet", "Spain", - "Stalin", "Sudan", "Suez", "Sunday", "Sweden", - "Swiss", "Sydney", "Syria", "Taiwan", @@ -219,7 +225,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "Tibet", "Tokyo", "Tom", - "Turk", "Turkey", "Uganda", "Venice", @@ -242,7 +247,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "able", "aboard", "abode", - "abort", "abound", "about", "above", @@ -398,7 +402,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "army", "aroma", "around", - "arouse", "array", "arrest", "arrive", @@ -466,7 +469,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "axis", "axle", "aye", - "babe", "baby", "back", "backup", @@ -491,7 +493,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "banal", "banana", "band", - "bang", "bank", "bar", "barber", @@ -585,10 +586,8 @@ char _passwdqc_wordset_4k[0x1000][6] = { "birth", "bishop", "bit", - "bitch", "bite", "bitter", - "black", "blade", "blame", "bland", @@ -597,7 +596,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "bleak", "blend", "bless", - "blew", "blind", "blink", "blip", @@ -610,7 +608,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "bloom", "blot", "blouse", - "blow", "blue", "bluff", "blunt", @@ -637,7 +634,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "boost", "boot", "booth", - "booze", "border", "bore", "borrow", @@ -673,7 +669,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "breach", "bread", "break", - "breast", "breath", "bred", "breed", @@ -695,7 +690,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "bronze", "brook", "broom", - "brown", "bruise", "brush", "brutal", @@ -727,18 +721,15 @@ char _passwdqc_wordset_4k[0x1000][6] = { "buried", "burly", "burn", - "burnt", "burrow", "burst", "bury", "bus", - "bush", "bust", "bustle", "busy", "but", "butler", - "butt", "butter", "button", "buy", @@ -786,7 +777,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "career", "caress", "cargo", - "carnal", "carp", "carpet", "carrot", @@ -815,7 +805,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "censor", "census", "cereal", - "cervix", "chain", "chair", "chalk", @@ -846,7 +835,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "chest", "chew", "chic", - "chick", "chief", "child", "chill", @@ -865,7 +853,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "chrome", "chunk", "chunky", - "church", "cider", "cigar", "cinema", @@ -895,7 +882,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "click", "client", "cliff", - "climax", "climb", "clinch", "cling", @@ -926,7 +912,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "cobalt", "cobra", "coca", - "cock", "cocoa", "code", "coffee", @@ -996,7 +981,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "coward", "cowboy", "crab", - "crack", "cradle", "craft", "crafty", @@ -1062,7 +1046,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "cyclic", "cynic", "dad", - "daddy", "dagger", "daily", "dairy", @@ -1147,7 +1130,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "detest", "detour", "device", - "devil", "devise", "devoid", "devote", @@ -1273,7 +1255,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "ease", "easel", "easily", - "easter", "easy", "eat", "eaten", @@ -1339,14 +1320,10 @@ char _passwdqc_wordset_4k[0x1000][6] = { "equip", "equity", "era", - "erase", - "erect", "erode", - "erotic", "errant", "error", "escape", - "escort", "essay", "estate", "esteem", @@ -1388,7 +1365,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "eyed", "fabric", "face", - "facial", "fact", "factor", "fade", @@ -1508,7 +1484,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "flight", "flimsy", "flint", - "flirt", "float", "flock", "flood", @@ -1616,7 +1591,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "future", "fuzzy", "gadget", - "gag", "gain", "gala", "galaxy", @@ -1640,7 +1614,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "gauge", "gaunt", "gave", - "gay", "gaze", "gear", "geese", @@ -1697,7 +1670,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "gorge", "gory", "gosh", - "gospel", "gossip", "got", "govern", @@ -1717,7 +1689,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "grave", "gravel", "gravy", - "gray", "grease", "greasy", "great", @@ -1726,7 +1697,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "green", "greet", "grew", - "grey", "grid", "grief", "grill", @@ -1765,7 +1735,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "guru", "gut", "guy", - "gypsy", "habit", "hack", "had", @@ -1852,7 +1821,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "hernia", "hero", "heroic", - "heroin", "hey", "heyday", "hick", @@ -1865,7 +1833,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "him", "hind", "hint", - "hippy", "hire", "his", "hiss", @@ -1888,7 +1855,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "hook", "hope", "horn", - "horny", "horrid", "horror", "horse", @@ -1939,7 +1905,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "imply", "import", "impose", - "incest", "inch", "income", "incur", @@ -2011,7 +1976,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "jock", "jockey", "join", - "joint", "joke", "jolly", "jolt", @@ -2044,8 +2008,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "kid", "kidnap", "kidney", - "kill", - "killer", "kin", "kind", "kindly", @@ -2150,7 +2112,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "liar", "libel", "lice", - "lick", "lid", "lie", "lied", @@ -2217,7 +2178,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "lousy", "love", "lovely", - "lover", "low", "lower", "lowest", @@ -2292,7 +2252,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "mason", "mass", "mast", - "master", "match", "mate", "matrix", @@ -2432,7 +2391,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "muddy", "mule", "mummy", - "murder", "murky", "murmur", "muscle", @@ -2454,7 +2412,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "myth", "nadir", "nail", - "naked", "name", "namely", "nape", @@ -2521,7 +2478,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "novice", "now", "nozzle", - "nude", "null", "numb", "number", @@ -2571,19 +2527,16 @@ char _passwdqc_wordset_4k[0x1000][6] = { "open", "openly", "opera", - "opium", "oppose", "optic", "option", "oracle", - "oral", "orange", "orbit", "orchid", "ordeal", "order", "organ", - "orgasm", "orient", "origin", "ornate", @@ -2672,7 +2625,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "penal", "pence", "pencil", - "penis", "penny", "people", "pepper", @@ -2717,7 +2669,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "pious", "pipe", "pirate", - "piss", "pistol", "piston", "pit", @@ -2787,7 +2738,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "posh", "post", "postal", - "pot", "potato", "potent", "pouch", @@ -2856,7 +2806,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "pursue", "push", "pushy", - "pussy", "put", "putt", "puzzle", @@ -2866,7 +2815,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "quartz", "quay", "queen", - "queer", "query", "quest", "queue", @@ -2905,7 +2853,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "range", "rank", "ransom", - "rape", "rapid", "rare", "rarely", @@ -3089,7 +3036,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "sacred", "sad", "saddle", - "sadism", "sadly", "safari", "safe", @@ -3119,7 +3065,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "sandy", "sane", "sash", - "satan", "satin", "satire", "sauce", @@ -3148,7 +3093,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "scrap", "scream", "screen", - "screw", "script", "scroll", "scrub", @@ -3198,9 +3142,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "seven", "severe", "sewage", - "sex", - "sexual", - "sexy", "shabby", "shade", "shadow", @@ -3234,7 +3175,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "ship", "shire", "shirt", - "shit", "shiver", "shock", "shoe", @@ -3307,7 +3247,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "slap", "slate", "slater", - "slave", "sleek", "sleep", "sleepy", @@ -3413,7 +3352,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "speedy", "spell", "spend", - "sperm", "sphere", "spice", "spicy", @@ -3524,7 +3462,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "strife", "strike", "string", - "strip", "strive", "stroke", "stroll", @@ -3543,7 +3480,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "subtly", "suburb", "such", - "suck", "sudden", "sue", "suffer", @@ -3692,7 +3628,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "throne", "throng", "throw", - "thrust", "thud", "thug", "thumb", @@ -3860,19 +3795,16 @@ char _passwdqc_wordset_4k[0x1000][6] = { "urge", "urgent", "urging", - "urine", "usable", "usage", "use", "useful", "user", "usual", - "uterus", "utmost", "utter", "vacant", "vacuum", - "vagina", "vague", "vain", "valet", @@ -3925,7 +3857,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "violet", "violin", "viral", - "virgin", "virtue", "virus", "visa", @@ -3983,7 +3914,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "weary", "wedge", "wee", - "weed", "week", "weekly", "weep", @@ -4006,13 +3936,10 @@ char _passwdqc_wordset_4k[0x1000][6] = { "while", "whim", "whip", - "whisky", - "white", "who", "whole", "wholly", "whom", - "whore", "whose", "why", "wide", @@ -4088,7 +4015,6 @@ char _passwdqc_wordset_4k[0x1000][6] = { "yeah", "year", "yeast", - "yellow", "yet", "yield", "yogurt", @@ -4104,5 +4030,2268 @@ char _passwdqc_wordset_4k[0x1000][6] = { "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 <solar at openwall.com> 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__ */ |