aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGordon Tetlow <gordon@FreeBSD.org>2023-11-03 17:37:48 +0000
committerGordon Tetlow <gordon@FreeBSD.org>2023-11-03 17:37:48 +0000
commite4fe068d29c902225fa3733db09af214cbfb3c02 (patch)
treef8f9cbff2b52e2e05c2e8e64aea2fc3210583273
parent00d65bdc4b6c36b0692588d71ca18ff080826a75 (diff)
Vendor import of pam_passwdqc v2.0.3.vendor/pam_modules/passwdqc/v2.0.3vendor/pam_modules
-rw-r--r--contrib/pam_modules/pam_passwdqc/.gitattributes2
-rw-r--r--contrib/pam_modules/pam_passwdqc/.github/workflows/ci.yml207
-rw-r--r--contrib/pam_modules/pam_passwdqc/.gitignore7
-rw-r--r--contrib/pam_modules/pam_passwdqc/CHANGES122
-rw-r--r--contrib/pam_modules/pam_passwdqc/INSTALL47
-rw-r--r--contrib/pam_modules/pam_passwdqc/INTERNALS4
-rw-r--r--contrib/pam_modules/pam_passwdqc/LICENSE37
-rw-r--r--contrib/pam_modules/pam_passwdqc/Makefile335
-rw-r--r--contrib/pam_modules/pam_passwdqc/PLATFORMS51
-rw-r--r--contrib/pam_modules/pam_passwdqc/README150
-rwxr-xr-xcontrib/pam_modules/pam_passwdqc/ci/install-dependencies.sh86
-rwxr-xr-xcontrib/pam_modules/pam_passwdqc/ci/run-build-and-tests.sh42
-rw-r--r--contrib/pam_modules/pam_passwdqc/concat.c70
-rw-r--r--contrib/pam_modules/pam_passwdqc/concat.h10
-rw-r--r--contrib/pam_modules/pam_passwdqc/libpasswdqc.3211
-rw-r--r--contrib/pam_modules/pam_passwdqc/libpasswdqc.map11
-rw-r--r--contrib/pam_modules/pam_passwdqc/md4.c272
-rw-r--r--contrib/pam_modules/pam_passwdqc/md4.h49
-rw-r--r--contrib/pam_modules/pam_passwdqc/pam_macros.h38
-rw-r--r--contrib/pam_modules/pam_passwdqc/pam_passwdqc.8102
-rw-r--r--contrib/pam_modules/pam_passwdqc/pam_passwdqc.c601
-rw-r--r--contrib/pam_modules/pam_passwdqc/pam_passwdqc.map7
-rw-r--r--contrib/pam_modules/pam_passwdqc/pam_passwdqc.spec67
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc.conf12
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc.conf.5312
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc.h72
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc.pc.in5
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc.spec394
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_check.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_check.c565
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_filter.c161
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_filter.h309
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_i18n.h26
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_load.c131
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_memzero.c20
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_params_free.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_params_load.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_params_parse.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_params_reset.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_parse.c215
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_random.31
-rw-r--r--contrib/pam_modules/pam_passwdqc/passwdqc_random.c244
-rw-r--r--contrib/pam_modules/pam_passwdqc/po/.gitignore2
-rw-r--r--contrib/pam_modules/pam_passwdqc/po/ru.po296
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqcheck.1269
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqcheck.c236
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqcheck.php84
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqfilter.1145
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqfilter.c1218
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqgen.186
-rw-r--r--contrib/pam_modules/pam_passwdqc/pwqgen.c74
-rw-r--r--contrib/pam_modules/pam_passwdqc/wordset_4k.c2461
-rw-r--r--contrib/pam_modules/pam_passwdqc/wordset_4k.h13
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(&params);
+ if (passwdqc_params_load(&params, &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(&params.qc, newpass, oldpass, pw);
+ if (check_result)
+ fprintf(stderr, "passwdqc_check: %s\en", check_result);
+out:
+ passwdqc_params_free(&params);
+ 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(&params, pamh, argc, argv);
- if (status != PAM_SUCCESS)
- return status;
+ passwdqc_params_reset(&params);
+ if (passwdqc_params_parse(&params, &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(&params);
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, &params);
}
- if (flags & PAM_PRELIM_CHECK)
+ if (flags & PAM_PRELIM_CHECK) {
+ passwdqc_params_free(&params);
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, &params);
+ 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, &params);
+ 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, &params);
+ 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, &params);
}
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(&params, pamh, newpass) && enforce))
- return PAM_AUTHTOK_ERR;
- reason = _passwdqc_check(&params.qc, newpass, oldpass, pw);
- if (reason) {
- say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, reason);
+ return logaudit(pamh, status, &params);
+ newpass = item;
+ if (!newpass ||
+ (check_max(&params.qc, pamh, newpass) && enforce))
+ return logaudit(pamh, PAM_AUTHTOK_ERR, &params);
+ check_reason =
+ passwdqc_check(&params.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, &params);
}
- 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, &params);
+
+ 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, &params);
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, &params);
}
- randompass = _passwdqc_random(&params.qc);
+ randompass = passwdqc_random(&params.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, &params);
}
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, &params);
}
- 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, &params);
}
- if (check_max(&params, pamh, newpass) && enforce) {
+ if (check_max(&params.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(&params.qc, newpass, oldpass, pw)))) {
+ (check_reason = passwdqc_check(&params.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, &params);
}
#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(&params);
+ if (argc > 1 &&
+ passwdqc_params_parse(&params, &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(&params.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(&params);
+
+ 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(&params);
+ if (argc > 1 &&
+ passwdqc_params_parse(&params, &reason, argc - 1, argv + 1)) {
+ fprintf(stderr, "pwqgen: %s\n",
+ (reason ? reason : "Out of memory"));
+ free(reason);
+ return 1;
+ }
+
+ pass = passwdqc_random(&params.qc);
+ passwdqc_params_free(&params);
+ 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__ */