From 16d0e647338dffebf9ab46c6832417e95a2b5eb8 Mon Sep 17 00:00:00 2001 From: Peter Avalos Date: Sun, 4 Jan 2009 11:25:06 -0500 Subject: [PATCH] Import pam_passwdqc 1.0.5 from the Openwall project. --- contrib/pam_passwdqc/INTERNALS | 3 + contrib/pam_passwdqc/LICENSE | 18 + contrib/pam_passwdqc/README | 148 + contrib/pam_passwdqc/pam_macros.h | 40 + contrib/pam_passwdqc/pam_passwdqc.8 | 269 ++ contrib/pam_passwdqc/pam_passwdqc.c | 628 ++++ contrib/pam_passwdqc/pam_passwdqc.spec | 154 + contrib/pam_passwdqc/passwdqc.h | 24 + contrib/pam_passwdqc/passwdqc_check.c | 361 +++ contrib/pam_passwdqc/passwdqc_random.c | 104 + contrib/pam_passwdqc/wordset_4k.c | 4108 ++++++++++++++++++++++++ 11 files changed, 5857 insertions(+) create mode 100644 contrib/pam_passwdqc/INTERNALS create mode 100644 contrib/pam_passwdqc/LICENSE create mode 100644 contrib/pam_passwdqc/README create mode 100644 contrib/pam_passwdqc/pam_macros.h create mode 100644 contrib/pam_passwdqc/pam_passwdqc.8 create mode 100644 contrib/pam_passwdqc/pam_passwdqc.c create mode 100644 contrib/pam_passwdqc/pam_passwdqc.spec create mode 100644 contrib/pam_passwdqc/passwdqc.h create mode 100644 contrib/pam_passwdqc/passwdqc_check.c create mode 100644 contrib/pam_passwdqc/passwdqc_random.c create mode 100644 contrib/pam_passwdqc/wordset_4k.c diff --git a/contrib/pam_passwdqc/INTERNALS b/contrib/pam_passwdqc/INTERNALS new file mode 100644 index 0000000000..1026c05f09 --- /dev/null +++ b/contrib/pam_passwdqc/INTERNALS @@ -0,0 +1,3 @@ +The functions defined in passwdqc.h may be used without PAM at all. +They will eventually be moved into a libpasswdqc (which has already been +done in ALT Linux distributions). diff --git a/contrib/pam_passwdqc/LICENSE b/contrib/pam_passwdqc/LICENSE new file mode 100644 index 0000000000..58f11961b3 --- /dev/null +++ b/contrib/pam_passwdqc/LICENSE @@ -0,0 +1,18 @@ +pam_passwdqc.8 is under the 3-clause BSD-style license as specified +within the file itself. + +wordset_4k.c is in the public domain. + +The rest of the files in this package fall under the following terms: + +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. + +$Owl: Owl/packages/pam_passwdqc/pam_passwdqc/LICENSE,v 1.3 2005/11/16 13:28:57 solar Exp $ diff --git a/contrib/pam_passwdqc/README b/contrib/pam_passwdqc/README new file mode 100644 index 0000000000..7085a57e35 --- /dev/null +++ b/contrib/pam_passwdqc/README @@ -0,0 +1,148 @@ +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 ones. All features are optional and +can be (re-)configured without rebuilding. + +This module should be stacked before your usual password changing +module (such as pam_unix or pam_pwdb) in the password management group +(the "password" lines in /etc/pam.d/passwd or /etc/pam.conf). The +password changing module should then be told to use the provided new +authentication token (new password) rather than request it from the +user. There's usually the "use_authtok" option to do that. If your +password changing module lacks the "use_authtok" option or its prompts +are inconsistent with pam_passwdqc's, you may tell pam_passwdqc to ask +for the old password as well, with "ask_oldauthtok". In that case the +option to use with the password changing module is "use_first_pass". + +There's a number of supported options which can be used to modify the +behavior of pam_passwdqc (defaults are given in square brackets): + + 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 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 that do not meet the requirements for a passphrase. + +N2 is used for passphrases. Note that besides meeting this length +requirement, a passphrase must also consist of a sufficient number of +words (see the "passphrase" option below). + +N3 and N4 are used for passwords consisting of characters from three +and four character classes, respectively. + +When calculating the number of character classes, upper-case letters +used as the first character and digits used as the last character of a +password are not counted. + +In addition to being sufficiently long, passwords are required to +contain enough different characters for the character classes and +the minimum length they have been checked against. + + max=N [max=40] + +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: 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 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 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; it will instead be +subjected to the usual strength requirements with the weak substring +removed. + +The substring search is case-insensitive and is able to detect and +remove a common substring spelled backwards. + + similar=permit|deny [similar=deny] + +Whether a new password is allowed to be similar to the old one. The +passwords are considered to be similar when there is 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 passphrases in bits (24 to 72), 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 invocations by non-root users only. + + non-unix [] + +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 the "non-unix" option. + + retry=N [retry=3] + +The number of times the module will ask for a new password if the user +fails to provide a sufficiently strong password and enter it twice the +first time. + + ask_oldauthtok[=update] [] + +Ask for the old password as well. Normally, pam_passwdqc leaves this +task for 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 subsequent +modules. + +The primary use for this option is when "ask_oldauthtok=update" is +also specified, in which case no other module gets a chance to ask +for and validate the password. Of course, this will only work with +Unix passwords. + + use_first_pass [] + use_authtok [] + +Use the new password obtained by modules stacked before pam_passwdqc. +This disables user interaction within pam_passwdqc. With this module, +the only difference between "use_first_pass" and "use_authtok" is that +the former is incompatible with "ask_oldauthtok". + +-- +Solar Designer + +$Owl: Owl/packages/pam_passwdqc/pam_passwdqc/README,v 1.11 2008/02/12 19:43:33 solar Exp $ diff --git a/contrib/pam_passwdqc/pam_macros.h b/contrib/pam_passwdqc/pam_macros.h new file mode 100644 index 0000000000..3006af7c82 --- /dev/null +++ b/contrib/pam_passwdqc/pam_macros.h @@ -0,0 +1,40 @@ +/* + * These macros are partially based on Linux-PAM's , + * which were organized by Cristian Gafton and I believe are in the public + * domain. + */ + +#ifndef PAM_PASSWDQC_MACROS_H__ +#define PAM_PASSWDQC_MACROS_H__ + +#include +#include + +#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 pwqc_drop_pam_reply(/* struct pam_response* */ reply, /* int */ replies) \ +do { \ + if (reply) { \ + int reply_i; \ +\ + 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); \ + } \ +} while (0) + +#endif /* PAM_PASSWDQC_MACROS_H__ */ diff --git a/contrib/pam_passwdqc/pam_passwdqc.8 b/contrib/pam_passwdqc/pam_passwdqc.8 new file mode 100644 index 0000000000..8247652825 --- /dev/null +++ b/contrib/pam_passwdqc/pam_passwdqc.8 @@ -0,0 +1,269 @@ +.\" Copyright (c) 2000-2003,2005,2008 Solar Designer. +.\" All rights reserved. +.\" Copyright (c) 2001 Networks Associates Technology, Inc. +.\" 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. +.\" +.\" $FreeBSD: src/lib/libpam/modules/pam_passwdqc/pam_passwdqc.8,v 1.4 2002/05/30 14:49:57 ru Exp $ +.\" $Owl: Owl/packages/pam_passwdqc/pam_passwdqc/pam_passwdqc.8,v 1.11 2008/02/12 20:33:09 solar Exp $ +.\" +.Dd February 12, 2008 +.Dt PAM_PASSWDQC 8 +.Os +.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 following options may be passed to the module: +.Bl -tag -width indent +.It Xo +.Sm off +.Cm min No = Ar N0 , N1 , N2 , N3 , N4 +.Sm on +.Xc +.Sm off +.Pq Cm min No = Cm disabled , No 24 , 11 , 8 , 7 +.Sm on +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 Cm max Ns = Ns 40 +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 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 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 removed. +.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 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 removed +would be weak. +.It Xo +.Sm off +.Cm random No = Ar N Op , Cm only +.Sm on +.Xc +.Pq Cm random Ns = Ns 42 +The size of randomly-generated passphrases in bits (24 to 72), +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. +.It Xo +.Sm off +.Cm enforce No = Cm none | users | everyone +.Sm on +.Xc +.Pq Cm enforce Ns = Ns Cm everyone +The module can be configured to warn of weak passwords only, but not +actually enforce strong passwords. +The +.Cm users +setting will enforce strong passwords for invocations by non-root users only. +.It Cm non-unix +Normally, +.Nm +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 Cm retry Ns = Ns 3 +The number of times the module will ask for a new password if the user +fails to provide a sufficiently strong password and enter it twice the +first time. +.It Cm ask_oldauthtok Ns Op = Ns Cm update +Ask for the old password as well. +Normally, +.Nm +leaves this task for subsequent modules. +With no argument, the +.Cm ask_oldauthtok +option will cause +.Nm +to ask for the old password during the preliminary check phase. +If the +.Cm ask_oldauthtok +option is specified with the +.Cm update +argument, +.Nm +will do that during the update phase. +.It Cm check_oldauthtok +This tells +.Nm +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 modules stacked before +.Nm . +This disables user interaction within +.Nm . +The only difference between +.Cm use_first_pass +and +.Cm use_authtok +is that the former is incompatible with +.Cm ask_oldauthtok . +.El +.Sh SEE ALSO +.Xr getpwnam 3 , +.Xr pam.conf 5 , +.Xr pam 8 +.Sh AUTHORS +The +.Nm +module was written for Openwall GNU/*/Linux by +.An Solar Designer Aq solar at openwall.com . +This manual page, 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_passwdqc/pam_passwdqc.c b/contrib/pam_passwdqc/pam_passwdqc.c new file mode 100644 index 0000000000..e7e339033a --- /dev/null +++ b/contrib/pam_passwdqc/pam_passwdqc.c @@ -0,0 +1,628 @@ +/* + * Copyright (c) 2000-2003,2005 by Solar Designer. See LICENSE. + */ + +#ifdef __FreeBSD__ +/* For vsnprintf(3) */ +#define _XOPEN_SOURCE 600 +#else +#define _XOPEN_SOURCE 500 +#define _XOPEN_SOURCE_EXTENDED +#define _XOPEN_VERSION 500 +#endif +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SHADOW +#include +#endif + +#define PAM_SM_PASSWORD +#ifndef LINUX_PAM +#include +#endif +#include + +#include "pam_macros.h" + +#if !defined(PAM_EXTERN) && !defined(PAM_STATIC) +#define PAM_EXTERN extern +#endif + +#if !defined(PAM_AUTHTOK_RECOVERY_ERR) && defined(PAM_AUTHTOK_RECOVER_ERR) +#define PAM_AUTHTOK_RECOVERY_ERR PAM_AUTHTOK_RECOVER_ERR +#endif + +#if (defined(__sun) || defined(__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, 11, 8, 7}, /* min */ + 40, /* max */ + 3, /* passphrase_words */ + 4, /* match_length */ + 1, /* similar_deny */ + 42 /* random_bits */ + }, + F_ENFORCE_EVERYONE, /* flags */ + 3 /* retry */ +}; + +#define PROMPT_OLDPASS \ + "Enter current password: " +#define PROMPT_NEWPASS1 \ + "Enter new password: " +#define PROMPT_NEWPASS2 \ + "Re-type new password: " + +#define MESSAGE_MISCONFIGURED \ + "System configuration error. Please contact your administrator." +#define MESSAGE_INVALID_OPTION \ + "pam_passwdqc: Invalid option: \"%s\"." +#define MESSAGE_INTRO_PASSWORD \ + "\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_1CLASS \ + "A good 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.\n" +#define MESSAGE_EXPLAIN_PASSWORD_CLASSES \ + "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 %d of these 4 classes.\n" \ + "An upper case letter that begins the password and a digit that\n" \ + "ends it do not count towards the number of character classes used.\n" +#define MESSAGE_EXPLAIN_PASSWORD_ALL_CLASSES \ + "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 all of these classes. An upper\n" \ + "case letter that begins the password and a digit that ends it do\n" \ + "not count towards the number of character classes used.\n" +#define MESSAGE_EXPLAIN_PASSWORD_ALT \ + "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. An upper case letter that begins the password and a\n" \ + "digit that ends it do not count towards the number of character\n" \ + "classes used.\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" +#define MESSAGE_RANDOM \ + "Alternatively, if noone else can see your terminal now, you can\n" \ + "pick this as 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" +#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." +#define MESSAGE_TOOLONG \ + "This password may be too long for some services. Choose another." +#define MESSAGE_TRUNCATED \ + "Warning: your longer password will be truncated to 8 characters." +#define MESSAGE_WEAKPASS \ + "Weak password: %s." +#define MESSAGE_NOTRANDOM \ + "Sorry, you've mistyped the password that was generated for you." +#define MESSAGE_MISTYPED \ + "Sorry, passwords do not match." +#define MESSAGE_RETRY \ + "Try again." + +static int converse(pam_handle_t *pamh, int style, l_const char *text, + struct pam_response **resp) +{ + pam_item_t item; + const struct pam_conv *conv; + struct pam_message msg, *pmsg; + int status; + + *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; + + return conv->conv(1, (lo_const struct pam_message **)&pmsg, resp, + conv->appdata_ptr); +} + +#ifdef __GNUC__ +__attribute__ ((format (printf, 3, 4))) +#endif +static int say(pam_handle_t *pamh, int style, const char *format, ...) +{ + va_list args; + char buffer[0x800]; + int needed; + struct pam_response *resp; + int status; + + va_start(args, format); + needed = vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + + if ((unsigned int)needed < sizeof(buffer)) { + status = converse(pamh, style, buffer, &resp); + pwqc_overwrite_string(buffer); + pwqc_drop_pam_reply(resp, 1); + } else { + status = PAM_ABORT; + memset(buffer, 0, sizeof(buffer)); + } + + return status; +} + +static int check_max(params_t *params, pam_handle_t *pamh, const char *newpass) +{ + if ((int)strlen(newpass) > params->qc.max) { + if (params->qc.max != 8) { + say(pamh, PAM_ERROR_MSG, MESSAGE_TOOLONG); + return -1; + } + say(pamh, PAM_TEXT_INFO, MESSAGE_TRUNCATED); + } + + return 0; +} + +static int check_pass(struct passwd *pw, const char *pass) +{ +#ifdef HAVE_SHADOW + struct spwd *spw; + const char *hash; + int retval; + +#ifdef __hpux + if (iscomsec()) { +#else + if (!strcmp(pw->pw_passwd, "x")) { +#endif + spw = getspnam(pw->pw_name); + endspent(); + if (!spw) + return -1; +#ifdef __hpux + hash = bigcrypt(pass, spw->sp_pwdp); +#else + hash = crypt(pass, spw->sp_pwdp); +#endif + retval = strcmp(hash, spw->sp_pwdp) ? -1 : 0; + memset(spw->sp_pwdp, 0, strlen(spw->sp_pwdp)); + return retval; + } +#endif + + return strcmp(crypt(pass, pw->pw_passwd), pw->pw_passwd) ? -1 : 0; +} + +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"); +} + +static int parse(params_t *params, pam_handle_t *pamh, + int argc, const char **argv) +{ + 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 && v < 24) || v > 72) 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++; + } + + if (argc) { + say(pamh, PAM_ERROR_MSG, am_root(pamh) ? + MESSAGE_INVALID_OPTION : MESSAGE_MISCONFIGURED, *argv); + return PAM_ABORT; + } + + return PAM_SUCCESS; +} + +PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + params_t params; + struct pam_response *resp; + struct passwd *pw, fake_pw; + pam_item_t item; + const char *user, *oldpass, *newpass; + char *trypass, *randompass; + const char *reason; + int ask_oldauthtok; + int randomonly, enforce, retries_left, retry_wanted; + int status; + + params = defaults; + status = parse(¶ms, pamh, argc, argv); + if (status != PAM_SUCCESS) + return status; + + ask_oldauthtok = 0; + if (flags & PAM_PRELIM_CHECK) { + if (params.flags & F_ASK_OLDAUTHTOK_PRELIM) + ask_oldauthtok = 1; + } else + if (flags & PAM_UPDATE_AUTHTOK) { + if (params.flags & F_ASK_OLDAUTHTOK_UPDATE) + ask_oldauthtok = 1; + } else + return PAM_SERVICE_ERR; + + if (ask_oldauthtok && !am_root(pamh)) { + status = converse(pamh, PAM_PROMPT_ECHO_OFF, + PROMPT_OLDPASS, &resp); + + if (status == PAM_SUCCESS) { + if (resp && resp->resp) { + status = pam_set_item(pamh, + PAM_OLDAUTHTOK, resp->resp); + pwqc_drop_pam_reply(resp, 1); + } else + status = PAM_AUTHTOK_RECOVERY_ERR; + } + + if (status != PAM_SUCCESS) + return status; + } + + if (flags & PAM_PRELIM_CHECK) + return status; + + status = pam_get_item(pamh, PAM_USER, &item); + if (status != PAM_SUCCESS) + return status; + user = item; + + status = pam_get_item(pamh, PAM_OLDAUTHTOK, &item); + if (status != PAM_SUCCESS) + return status; + oldpass = item; + + if (params.flags & F_NON_UNIX) { + pw = &fake_pw; + pw->pw_name = (char *)user; + pw->pw_gecos = ""; + } else { + pw = getpwnam(user); + endpwent(); + if (!pw) + return PAM_USER_UNKNOWN; + if ((params.flags & F_CHECK_OLDAUTHTOK) && !am_root(pamh) && + (!oldpass || check_pass(pw, oldpass))) + status = PAM_AUTH_ERR; + memset(pw->pw_passwd, 0, strlen(pw->pw_passwd)); + if (status != PAM_SUCCESS) + return status; + } + + randomonly = params.qc.min[4] > params.qc.max; + + if (am_root(pamh)) + enforce = params.flags & F_ENFORCE_ROOT; + else + enforce = params.flags & F_ENFORCE_USERS; + + if (params.flags & F_USE_AUTHTOK) { + status = pam_get_item(pamh, PAM_AUTHTOK, &item); + if (status != PAM_SUCCESS) + return status; + newpass = item; + if (!newpass || (check_max(¶ms, pamh, newpass) && enforce)) + return PAM_AUTHTOK_ERR; + reason = _passwdqc_check(¶ms.qc, newpass, oldpass, pw); + if (reason) { + say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, reason); + if (enforce) + status = PAM_AUTHTOK_ERR; + } + return status; + } + + retries_left = params.retry; + +retry: + retry_wanted = 0; + + if (!randomonly && + params.qc.passphrase_words && params.qc.min[2] <= params.qc.max) + status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_BOTH); + else + status = say(pamh, PAM_TEXT_INFO, MESSAGE_INTRO_PASSWORD); + if (status != PAM_SUCCESS) + return status; + + if (!randomonly && params.qc.min[0] == params.qc.min[4]) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_1CLASS, + params.qc.min[4] == 8 || params.qc.min[4] == 11 ? "n" : "", + params.qc.min[4]); + else + if (!randomonly && params.qc.min[3] == params.qc.min[4]) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_CLASSES, + params.qc.min[4] == 8 || params.qc.min[4] == 11 ? "n" : "", + 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] == 8 || params.qc.min[4] == 11 ? "n" : "", + params.qc.min[4]); + else + if (!randomonly) + status = say(pamh, PAM_TEXT_INFO, + MESSAGE_EXPLAIN_PASSWORD_ALT, + 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; + + 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.min[2], params.qc.max); + if (status != PAM_SUCCESS) + return status; + } + + randompass = _passwdqc_random(¶ms.qc); + if (randompass) { + status = say(pamh, PAM_TEXT_INFO, randomonly ? + MESSAGE_RANDOMONLY : MESSAGE_RANDOM, randompass); + if (status != PAM_SUCCESS) { + pwqc_overwrite_string(randompass); + randompass = NULL; + } + } else + if (randomonly) { + say(pamh, PAM_ERROR_MSG, am_root(pamh) ? + MESSAGE_RANDOMFAILED : MESSAGE_MISCONFIGURED); + return PAM_AUTHTOK_ERR; + } + + status = converse(pamh, PAM_PROMPT_ECHO_OFF, PROMPT_NEWPASS1, &resp); + if (status == PAM_SUCCESS && (!resp || !resp->resp)) + status = PAM_AUTHTOK_ERR; + + if (status != PAM_SUCCESS) { + pwqc_overwrite_string(randompass); + return status; + } + + trypass = strdup(resp->resp); + + pwqc_drop_pam_reply(resp, 1); + + if (!trypass) { + pwqc_overwrite_string(randompass); + return PAM_AUTHTOK_ERR; + } + + if (check_max(¶ms, pamh, trypass) && enforce) { + status = PAM_AUTHTOK_ERR; + retry_wanted = 1; + } + + reason = NULL; + if (status == PAM_SUCCESS && + (!randompass || !strstr(trypass, randompass)) && + (randomonly || + (reason = _passwdqc_check(¶ms.qc, trypass, oldpass, pw)))) { + if (randomonly) + say(pamh, PAM_ERROR_MSG, MESSAGE_NOTRANDOM); + else + say(pamh, PAM_ERROR_MSG, MESSAGE_WEAKPASS, reason); + if (enforce) { + status = PAM_AUTHTOK_ERR; + retry_wanted = 1; + } + } + + if (status == PAM_SUCCESS) + status = converse(pamh, PAM_PROMPT_ECHO_OFF, + PROMPT_NEWPASS2, &resp); + if (status == PAM_SUCCESS) { + if (resp && resp->resp) { + if (strcmp(trypass, resp->resp)) { + status = say(pamh, + PAM_ERROR_MSG, MESSAGE_MISTYPED); + if (status == PAM_SUCCESS) { + status = PAM_AUTHTOK_ERR; + retry_wanted = 1; + } + } + pwqc_drop_pam_reply(resp, 1); + } else + status = PAM_AUTHTOK_ERR; + } + + if (status == PAM_SUCCESS) + status = pam_set_item(pamh, PAM_AUTHTOK, trypass); + + pwqc_overwrite_string(randompass); + pwqc_overwrite_string(trypass); + pwqc_drop_mem(trypass); + + if (retry_wanted && --retries_left > 0) { + status = say(pamh, PAM_TEXT_INFO, MESSAGE_RETRY); + if (status == PAM_SUCCESS) + goto retry; + } + + return status; +} + +#ifdef PAM_MODULE_ENTRY +PAM_MODULE_ENTRY("pam_passwdqc"); +#elif defined(PAM_STATIC) +struct pam_module _pam_passwdqc_modstruct = { + "pam_passwdqc", + NULL, + NULL, + NULL, + NULL, + NULL, + pam_sm_chauthtok +}; +#endif diff --git a/contrib/pam_passwdqc/pam_passwdqc.spec b/contrib/pam_passwdqc/pam_passwdqc.spec new file mode 100644 index 0000000000..2273f0d043 --- /dev/null +++ b/contrib/pam_passwdqc/pam_passwdqc.spec @@ -0,0 +1,154 @@ +# $Owl: Owl/packages/pam_passwdqc/pam_passwdqc/pam_passwdqc.spec,v 1.38 2008/02/12 20:28:48 solar Exp $ + +Summary: Pluggable password quality-control module. +Name: pam_passwdqc +Version: 1.0.5 +Release: owl1 +License: BSD-compatible +Group: System Environment/Base +URL: http://www.openwall.com/passwdqc/ +Source: ftp://ftp.openwall.com/pub/projects/pam/modules/%name/%name-%version.tar.gz +BuildRequires: pam-devel +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 ones. All features are optional and +can be (re-)configured without rebuilding. + +%prep +%setup -q + +%build +%__make CFLAGS="-Wall -fPIC -DLINUX_PAM %optflags" + +%install +rm -rf %buildroot +%__make install DESTDIR=%buildroot MANDIR=%_mandir SECUREDIR=/%_lib/security + +%files +%defattr(-,root,root) +%doc LICENSE README +/%_lib/security/pam_passwdqc.so +%_mandir/man*/* + +%changelog +* Tue Feb 12 2008 Solar Designer 1.0.5-owl1 +- Replaced the separator characters with some of those defined by RFC 3986 +as being safe within "userinfo" part of URLs without encoding. +- Reduced the default value for the N2 parameter to min=... (the minimum +length for passphrases) from 12 to 11. +- Corrected the potentially misleading description of N2 (Debian bug #310595). +- Applied minor grammar and style corrections to the documentation, a +pam_passwdqc message, and source code comments. + +* Tue Apr 04 2006 Dmitry V. Levin 1.0.4-owl1 +- Changed Makefile to pass list of libraries to linker after regular +object files, to fix build with -Wl,--as-needed. +- Corrected specfile to make it build on x86_64. + +* Wed Aug 17 2005 Dmitry V. Levin 1.0.3-owl1 +- Fixed potential memory leak in conversation wrapper. +- Restricted list of global symbols exported by the PAM module +to standard set of six pam_sm_* functions. + +* Wed May 18 2005 Solar Designer 1.0.2-owl1 +- Fixed compiler warnings seen on FreeBSD 5.3. +- Updated the Makefile to not require editing on FreeBSD. +- Updated the FreeBSD-specific notes in PLATFORMS. + +* Sun Mar 27 2005 Solar Designer 1.0.1-owl1 +- Further compiler warning fixes on LP64 platforms. + +* Fri Mar 25 2005 Solar Designer 1.0-owl1 +- Corrected the source code to not break C strict aliasing rules. + +* Wed Jan 26 2005 Solar Designer 0.7.6-owl1 +- Disallow unreasonable random= settings. +- Clarified the allowable bit sizes for randomly-generated passphrases and +the lack of relationship between passphrase= and random= options. + +* Fri Oct 31 2003 Solar Designer 0.7.5-owl1 +- Assume invocation by root only if both the UID is 0 and the PAM service +name is "passwd"; this should solve changing expired passwords on Solaris +and HP-UX and make "enforce=users" safe. +- Produce proper English explanations for a wider variety of settings. +- Moved the "-c" out of CFLAGS, renamed FAKEROOT to DESTDIR. + +* Sat Jun 21 2003 Solar Designer 0.7.4-owl1 +- Documented that "enforce=users" may not always work for services other +than the passwd command. +- Applied a patch to PLATFORMS from Mike Gerdts of GE Medical Systems +to reflect how Solaris 8 patch 108993-18 (or 108994-18 on x86) changes +Solaris 8's PAM implementation to look like Solaris 9. + +* Mon Jun 02 2003 Solar Designer 0.7.3.1-owl1 +- Added URL. + +* Thu Oct 31 2002 Solar Designer 0.7.3-owl1 +- When compiling with gcc, also link with gcc. +- Use $(MAKE) to invoke sub-makes. + +* Fri Oct 04 2002 Solar Designer +- Solaris 9 notes in PLATFORMS. + +* Wed Sep 18 2002 Solar Designer +- Build with Sun's C compiler cleanly, from Kevin Steves. +- Use install -c as that actually makes a difference on at least HP-UX +(otherwise install would possibly move files and not change the owner). + +* Fri Sep 13 2002 Solar Designer +- Have the same pam_passwdqc binary work for both trusted and non-trusted +HP-UX, from Kevin Steves. + +* Fri Sep 06 2002 Solar Designer +- Use bigcrypt() on HP-UX whenever necessary, from Kevin Steves of Atomic +Gears LLC. +- Moved the old password checking into a separate function. + +* Wed Jul 31 2002 Solar Designer +- Call it 0.6. + +* Sat Jul 27 2002 Solar Designer +- Documented that the man page is under the 3-clause BSD-style license. +- HP-UX 11 support. + +* Tue Jul 23 2002 Solar Designer +- Applied minor corrections to the man page and at the same time eliminated +unneeded/unimportant differences between it and the README. + +* Sun Jul 21 2002 Solar Designer +- 0.5.1: imported the pam_passwdqc(8) manual page back from FreeBSD. + +* Tue Apr 16 2002 Solar Designer +- 0.5: preliminary OpenPAM (FreeBSD-current) support in the code and related +code cleanups (thanks to Dag-Erling Smorgrav). + +* Thu Feb 07 2002 Michail Litvak +- Enforce our new spec file conventions. + +* Sun Nov 04 2001 Solar Designer +- Updated to 0.4: +- Added "ask_oldauthtok" and "check_oldauthtok" as needed for stacking with +the Solaris pam_unix; +- Permit for stacking of more than one instance of this module (no statics). + +* Tue Feb 13 2001 Solar Designer +- Install the module as mode 755. + +* Tue Dec 19 2000 Solar Designer +- Added "-Wall -fPIC" to the CFLAGS. + +* Mon Oct 30 2000 Solar Designer +- 0.3: portability fixes (this might build on non-Linux-PAM now). + +* Fri Sep 22 2000 Solar Designer +- 0.2: added "use_authtok", added README. + +* Fri Aug 18 2000 Solar Designer +- 0.1, "retry_wanted" bugfix. + +* Sun Jul 02 2000 Solar Designer +- Initial version (non-public). diff --git a/contrib/pam_passwdqc/passwdqc.h b/contrib/pam_passwdqc/passwdqc.h new file mode 100644 index 0000000000..75f1dffcd4 --- /dev/null +++ b/contrib/pam_passwdqc/passwdqc.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + */ + +#ifndef _PASSWDQC_H +#define _PASSWDQC_H + +#include + +typedef struct { + int min[5], max; + int passphrase_words; + int match_length; + int similar_deny; + int random_bits; +} passwdqc_params_t; + +extern char _passwdqc_wordset_4k[0x1000][6]; + +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); + +#endif diff --git a/contrib/pam_passwdqc/passwdqc_check.c b/contrib/pam_passwdqc/passwdqc_check.c new file mode 100644 index 0000000000..2d5f7143f7 --- /dev/null +++ b/contrib/pam_passwdqc/passwdqc_check.c @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + */ + +#include +#include +#include +#include + +#include "passwdqc.h" + +#define REASON_ERROR \ + "check failed" + +#define REASON_SAME \ + "is the same as the old one" +#define REASON_SIMILAR \ + "is based on the old one" + +#define REASON_SHORT \ + "too short" +#define REASON_LONG \ + "too long" + +#define REASON_SIMPLESHORT \ + "not enough different characters or classes for this length" +#define REASON_SIMPLE \ + "not enough different characters or classes" + +#define REASON_PERSONAL \ + "based on personal login information" + +#define REASON_WORD \ + "based on a dictionary word and not a passphrase" + +#define FIXED_BITS 15 + +typedef unsigned long fixed; + +/* + * Calculates the expected number of different characters for a random + * password of a given length. The result is rounded down. We use this + * with the _requested_ minimum length (so longer passwords don't have + * to meet this strict requirement for their length). + */ +static int expected_different(int charset, int length) +{ + fixed x, y, z; + + x = ((fixed)(charset - 1) << FIXED_BITS) / charset; + y = x; + while (--length > 0) y = (y * x) >> FIXED_BITS; + z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y); + + return (int)(z >> FIXED_BITS); +} + +/* + * A password is too simple if it is too short for its class, or doesn't + * contain enough different characters for its class, or doesn't contain + * enough words for a passphrase. + */ +static int is_simple(passwdqc_params_t *params, const char *newpass) +{ + int length, classes, words, chars; + int digits, lowers, uppers, others, unknowns; + int c, p; + + length = classes = words = chars = 0; + digits = lowers = uppers = others = unknowns = 0; + p = ' '; + while ((c = (unsigned char)newpass[length])) { + length++; + + if (!isascii(c)) unknowns++; else + if (isdigit(c)) digits++; else + if (islower(c)) lowers++; else + if (isupper(c)) uppers++; else + others++; + + if (isascii(c) && isalpha(c) && isascii(p) && !isalpha(p)) + words++; + p = c; + + if (!strchr(&newpass[length], c)) + chars++; + } + + 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--; + c = (unsigned char)newpass[length - 1]; + if (digits && isascii(c) && isdigit(c)) 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++; + + for (; classes > 0; classes--) + switch (classes) { + case 1: + if (length >= params->min[0] && + chars >= expected_different(10, params->min[0]) - 1) + return 0; + return 1; + + case 2: + if (length >= 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] && + chars >= expected_different(27, params->min[2]) - 1) + return 0; + continue; + + case 3: + if (length >= params->min[3] && + chars >= expected_different(62, params->min[3]) - 1) + return 0; + continue; + + case 4: + if (length >= params->min[4] && + chars >= expected_different(95, params->min[4]) - 1) + return 0; + continue; + } + + return 1; +} + +static char *unify(const char *src) +{ + const char *sptr; + char *dst, *dptr; + int c; + + if (!(dst = malloc(strlen(src) + 1))) + return NULL; + + sptr = src; + dptr = dst; + do { + c = (unsigned char)*sptr; + if (isascii(c) && isupper(c)) + *dptr++ = tolower(c); + else + *dptr++ = *sptr; + } while (*sptr++); + + return dst; +} + +static char *reverse(const char *src) +{ + const char *sptr; + char *dst, *dptr; + + if (!(dst = malloc(strlen(src) + 1))) + return NULL; + + sptr = &src[strlen(src)]; + dptr = dst; + while (sptr > src) + *dptr++ = *--sptr; + *dptr = '\0'; + + return dst; +} + +static void clean(char *dst) +{ + if (dst) { + memset(dst, 0, strlen(dst)); + free(dst); + } +} + +/* + * 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. + */ +static int is_based(passwdqc_params_t *params, + const char *haystack, const char *needle, const char *original) +{ + char *scratch; + int length; + int i, j; + const char *p; + int match; + + if (!params->match_length) /* disabled */ + return 0; + + if (params->match_length < 0) /* misconfigured */ + return 1; + + if (strstr(haystack, needle)) /* based on haystack entirely */ + return 1; + + scratch = NULL; + + length = strlen(needle); + for (i = 0; i <= length - params->match_length; i++) + for (j = params->match_length; i + j <= length; j++) { + match = 0; + for (p = haystack; *p; p++) + if (*p == needle[i] && !strncmp(p, &needle[i], j)) { + match = 1; + if (!scratch) { + if (!(scratch = malloc(length + 1))) + 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; + } + } + if (!match) break; + } + + clean(scratch); + + return 0; +} + +/* + * 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 + * 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 + * 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. + */ +static int is_word_based(passwdqc_params_t *params, + const char *needle, 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; + } + clean(unified); + } + + return 0; +} + +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; + + reversed = NULL; + u_newpass = u_reversed = NULL; + u_oldpass = NULL; + u_name = u_gecos = NULL; + + reason = NULL; + + if (oldpass && !strcmp(oldpass, newpass)) + reason = REASON_SAME; + + length = strlen(newpass); + + if (!reason && length < params->min[4]) + reason = REASON_SHORT; + + if (!reason && length > params->max) { + if (params->max == 8) { + truncated[0] = '\0'; + strncat(truncated, newpass, 8); + newpass = truncated; + if (oldpass && !strncmp(oldpass, newpass, 8)) + reason = REASON_SAME; + } else + reason = REASON_LONG; + } + + if (!reason && is_simple(params, newpass)) { + if (length < params->min[1] && params->min[1] <= params->max) + reason = REASON_SIMPLESHORT; + else + reason = REASON_SIMPLE; + } + + 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 (!reason && oldpass && params->similar_deny && + (is_based(params, u_oldpass, u_newpass, newpass) || + is_based(params, u_oldpass, u_reversed, reversed))) + reason = REASON_SIMILAR; + + 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))) + reason = REASON_PERSONAL; + + 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; + + memset(truncated, 0, sizeof(truncated)); + clean(reversed); + clean(u_newpass); clean(u_reversed); + clean(u_oldpass); + clean(u_name); clean(u_gecos); + + return reason; +} diff --git a/contrib/pam_passwdqc/passwdqc_random.c b/contrib/pam_passwdqc/passwdqc_random.c new file mode 100644 index 0000000000..4183cf6bbf --- /dev/null +++ b/contrib/pam_passwdqc/passwdqc_random.c @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2000-2002,2005,2008 by Solar Designer. See LICENSE. + */ + +#include +#include +#include +#include +#include + +#include "passwdqc.h" + +/* + * We separate words in the generated "passphrases" with random special + * characters out of a set of 8 (so we encode 3 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 characters. + */ +#define SEPARATORS "-_!$&*+=" + +static int read_loop(int fd, unsigned char *buffer, int count) +{ + int offset, block; + + offset = 0; + while (count > 0) { + block = read(fd, &buffer[offset], count); + + if (block < 0) { + if (errno == EINTR) continue; + return block; + } + if (!block) return offset; + + offset += block; + count -= block; + } + + return offset; +} + +char *_passwdqc_random(passwdqc_params_t *params) +{ + static char output[0x100]; + int bits; + int use_separators, count, i; + unsigned int length, extra; + char *start, *end; + int fd; + unsigned char bytes[2]; + + bits = params->random_bits; + if (bits < 24 || bits > 128) + return NULL; + + count = 1 + (bits + (14 - 12)) / 15; + use_separators = ((bits + 11) / 12 != count); + + length = count * 7 - 1; + if (length >= sizeof(output) || (int)length > params->max) + return NULL; + + if ((fd = open("/dev/urandom", O_RDONLY)) < 0) return NULL; + + length = 0; + do { + if (read_loop(fd, bytes, sizeof(bytes)) != sizeof(bytes)) { + close(fd); + return NULL; + } + + i = (((int)bytes[1] & 0x0f) << 8) | (int)bytes[0]; + start = _passwdqc_wordset_4k[i]; + end = memchr(start, '\0', 6); + if (!end) end = start + 6; + extra = end - start; + if (length + extra >= sizeof(output) - 1) { + close(fd); + return NULL; + } + memcpy(&output[length], start, extra); + length += extra; + bits -= 12; + + if (use_separators && bits > 3) { + i = ((int)bytes[1] & 0x70) >> 4; + output[length++] = SEPARATORS[i]; + bits -= 3; + } else + if (bits > 0) + output[length++] = ' '; + } while (bits > 0); + + memset(bytes, 0, sizeof(bytes)); + output[length] = '\0'; + + close(fd); + + return output; +} diff --git a/contrib/pam_passwdqc/wordset_4k.c b/contrib/pam_passwdqc/wordset_4k.c new file mode 100644 index 0000000000..1a604d49b0 --- /dev/null +++ b/contrib/pam_passwdqc/wordset_4k.c @@ -0,0 +1,4108 @@ +/* + * 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. + * + * I've replaced two 7-character words to save space. + */ + +#include "passwdqc.h" + +char _passwdqc_wordset_4k[0x1000][6] = { + "Adam", + "Afghan", + "Alaska", + "Alice", + "Allah", + "Amazon", + "Andrew", + "Anglo", + "Angola", + "Antony", + "April", + "Arab", + "Arctic", + "Athens", + "Austin", + "Bach", + "Baltic", + "Basque", + "Berlin", + "Bible", + "Bombay", + "Bonn", + "Boston", + "Brazil", + "Briton", + "Buddha", + "Burma", + "Caesar", + "Cairo", + "Canada", + "Carl", + "Carol", + "Celtic", + "Chile", + "China", + "Christ", + "Congo", + "Cuba", + "Cyprus", + "Czech", + "Dallas", + "Danish", + "Darwin", + "David", + "Delhi", + "Derby", + "Diana", + "Dublin", + "Dutch", + "East", + "Eden", + "Edward", + "Eric", + "Essex", + "Europe", + "Eve", + "Exodus", + "France", + "French", + "Friday", + "Gandhi", + "Gaul", + "Gemini", + "Geneva", + "George", + "German", + "Gloria", + "God", + "Gothic", + "Greece", + "Greek", + "Hague", + "Haiti", + "Hanoi", + "Harry", + "Havana", + "Hawaii", + "Hebrew", + "Henry", + "Hermes", + "Hindu", + "Hitler", + "Idaho", + "Inca", + "India", + "Indian", + "Iowa", + "Iran", + "Iraq", + "Irish", + "Isaac", + "Isabel", + "Islam", + "Israel", + "Italy", + "Ivan", + "Jack", + "Jacob", + "James", + "Japan", + "Java", + "Jersey", + "Jesus", + "Jewish", + "Jim", + "John", + "Jordan", + "Joseph", + "Judas", + "Judy", + "July", + "June", + "Kansas", + "Karl", + "Kenya", + "Koran", + "Korea", + "Kuwait", + "Laos", + "Latin", + "Leo", + "Libya", + "Lima", + "Lisbon", + "Liz", + "London", + "Louvre", + "Lucy", + "Luther", + "Madame", + "Madrid", + "Malta", + "Maria", + "Mars", + "Mary", + "Maya", + "Mecca", + "Mexico", + "Miami", + "Mickey", + "Milan", + "Monaco", + "Monday", + "Moscow", + "Moses", + "Moslem", + "Mrs", + "Munich", + "Muslim", + "Naples", + "Nazi", + "Nepal", + "Newark", + "Nile", + "Nobel", + "North", + "Norway", + "Ohio", + "Oscar", + "Oslo", + "Oxford", + "Panama", + "Paris", + "Pascal", + "Paul", + "Peking", + "Peru", + "Peter", + "Philip", + "Poland", + "Polish", + "Prague", + "Quebec", + "Rex", + "Rhine", + "Ritz", + "Robert", + "Roman", + "Rome", + "Rosa", + "Russia", + "Sahara", + "Sam", + "Saturn", + "Saudi", + "Saxon", + "Scot", + "Seoul", + "Somali", + "Sony", + "Soviet", + "Spain", + "Stalin", + "Sudan", + "Suez", + "Sunday", + "Sweden", + "Swiss", + "Sydney", + "Syria", + "Taiwan", + "Tarzan", + "Taurus", + "Tehran", + "Teresa", + "Texas", + "Thomas", + "Tibet", + "Tokyo", + "Tom", + "Turk", + "Turkey", + "Uganda", + "Venice", + "Venus", + "Vienna", + "Viking", + "Virgo", + "Warsaw", + "West", + "Yale", + "Yemen", + "York", + "Zaire", + "Zurich", + "aback", + "abbey", + "abbot", + "abide", + "ablaze", + "able", + "aboard", + "abode", + "abort", + "abound", + "about", + "above", + "abroad", + "abrupt", + "absent", + "absorb", + "absurd", + "abuse", + "accent", + "accept", + "access", + "accord", + "accuse", + "ace", + "ache", + "aching", + "acid", + "acidic", + "acorn", + "acre", + "across", + "act", + "action", + "active", + "actor", + "actual", + "acute", + "adapt", + "add", + "added", + "addict", + "adept", + "adhere", + "adjust", + "admire", + "admit", + "adobe", + "adopt", + "adrift", + "adult", + "adverb", + "advert", + "aerial", + "afar", + "affair", + "affect", + "afford", + "afield", + "afloat", + "afraid", + "afresh", + "after", + "again", + "age", + "agency", + "agenda", + "agent", + "aghast", + "agile", + "ago", + "agony", + "agree", + "agreed", + "ahead", + "aid", + "aide", + "aim", + "air", + "airman", + "airy", + "akin", + "alarm", + "albeit", + "album", + "alert", + "alibi", + "alien", + "alight", + "align", + "alike", + "alive", + "alkali", + "all", + "alley", + "allied", + "allow", + "alloy", + "ally", + "almond", + "almost", + "aloft", + "alone", + "along", + "aloof", + "aloud", + "alpha", + "alpine", + "also", + "altar", + "alter", + "always", + "amaze", + "amber", + "ambush", + "amen", + "amend", + "amid", + "amidst", + "amiss", + "among", + "amount", + "ample", + "amuse", + "anchor", + "and", + "anew", + "angel", + "anger", + "angle", + "angry", + "animal", + "ankle", + "annoy", + "annual", + "answer", + "anthem", + "anti", + "any", + "anyhow", + "anyway", + "apart", + "apathy", + "apex", + "apiece", + "appeal", + "appear", + "apple", + "apply", + "apron", + "arcade", + "arcane", + "arch", + "ardent", + "are", + "area", + "argue", + "arid", + "arise", + "arm", + "armful", + "armpit", + "army", + "aroma", + "around", + "arouse", + "array", + "arrest", + "arrive", + "arrow", + "arson", + "art", + "artery", + "artful", + "artist", + "ascent", + "ashen", + "ashore", + "aside", + "ask", + "asleep", + "aspect", + "assay", + "assent", + "assert", + "assess", + "asset", + "assign", + "assist", + "assume", + "assure", + "asthma", + "astute", + "asylum", + "ate", + "atlas", + "atom", + "atomic", + "attach", + "attack", + "attain", + "attend", + "attic", + "auburn", + "audio", + "audit", + "august", + "aunt", + "auntie", + "aura", + "author", + "auto", + "autumn", + "avail", + "avenge", + "avenue", + "avert", + "avid", + "avoid", + "await", + "awake", + "awaken", + "award", + "aware", + "awash", + "away", + "awful", + "awhile", + "axes", + "axiom", + "axis", + "axle", + "aye", + "babe", + "baby", + "back", + "backup", + "bacon", + "bad", + "badge", + "badly", + "bag", + "baggy", + "bail", + "bait", + "bake", + "baker", + "bakery", + "bald", + "ball", + "ballad", + "ballet", + "ballot", + "bamboo", + "ban", + "banal", + "banana", + "band", + "bang", + "bank", + "bar", + "barber", + "bare", + "barely", + "barge", + "bark", + "barley", + "barn", + "baron", + "barrel", + "barren", + "basalt", + "base", + "basic", + "basil", + "basin", + "basis", + "basket", + "bass", + "bat", + "batch", + "bath", + "baton", + "battle", + "bay", + "beach", + "beacon", + "beak", + "beam", + "bean", + "bear", + "beard", + "beast", + "beat", + "beauty", + "become", + "bed", + "beech", + "beef", + "beefy", + "beep", + "beer", + "beet", + "beetle", + "before", + "beggar", + "begin", + "behalf", + "behave", + "behind", + "beige", + "being", + "belief", + "bell", + "belly", + "belong", + "below", + "belt", + "bench", + "bend", + "benign", + "bent", + "berry", + "berth", + "beset", + "beside", + "best", + "bestow", + "bet", + "beta", + "betray", + "better", + "beware", + "beyond", + "bias", + "biceps", + "bicker", + "bid", + "big", + "bigger", + "bike", + "bile", + "bill", + "binary", + "bind", + "biopsy", + "birch", + "bird", + "birdie", + "birth", + "bishop", + "bit", + "bitch", + "bite", + "bitter", + "black", + "blade", + "blame", + "bland", + "blast", + "blaze", + "bleak", + "blend", + "bless", + "blew", + "blind", + "blink", + "blip", + "bliss", + "blitz", + "block", + "blond", + "blood", + "bloody", + "bloom", + "blot", + "blouse", + "blow", + "blue", + "bluff", + "blunt", + "blur", + "blush", + "boar", + "board", + "boast", + "boat", + "bodily", + "body", + "bogus", + "boil", + "bold", + "bolt", + "bomb", + "bond", + "bone", + "bonnet", + "bonus", + "bony", + "book", + "boom", + "boost", + "boot", + "booth", + "booze", + "border", + "bore", + "borrow", + "bosom", + "boss", + "both", + "bother", + "bottle", + "bottom", + "bought", + "bounce", + "bound", + "bounty", + "bout", + "bovine", + "bow", + "bowel", + "bowl", + "box", + "boy", + "boyish", + "brace", + "brain", + "brainy", + "brake", + "bran", + "branch", + "brand", + "brandy", + "brass", + "brave", + "bravo", + "breach", + "bread", + "break", + "breast", + "breath", + "bred", + "breed", + "breeze", + "brew", + "brick", + "bride", + "bridge", + "brief", + "bright", + "brim", + "brine", + "bring", + "brink", + "brisk", + "broad", + "broke", + "broken", + "bronze", + "brook", + "broom", + "brown", + "bruise", + "brush", + "brutal", + "brute", + "bubble", + "buck", + "bucket", + "buckle", + "budget", + "buffet", + "buggy", + "build", + "bulb", + "bulge", + "bulk", + "bulky", + "bull", + "bullet", + "bully", + "bump", + "bumpy", + "bunch", + "bundle", + "bunk", + "bunny", + "burden", + "bureau", + "burial", + "buried", + "burly", + "burn", + "burnt", + "burrow", + "burst", + "bury", + "bus", + "bush", + "bust", + "bustle", + "busy", + "but", + "butler", + "butt", + "butter", + "button", + "buy", + "buyer", + "buzz", + "bye", + "byte", + "cab", + "cabin", + "cable", + "cache", + "cactus", + "cage", + "cake", + "calf", + "call", + "caller", + "calm", + "calmly", + "came", + "camel", + "camera", + "camp", + "campus", + "can", + "canal", + "canary", + "cancel", + "cancer", + "candid", + "candle", + "candy", + "cane", + "canine", + "canoe", + "canopy", + "canvas", + "canyon", + "cap", + "cape", + "car", + "carbon", + "card", + "care", + "career", + "caress", + "cargo", + "carnal", + "carp", + "carpet", + "carrot", + "carry", + "cart", + "cartel", + "case", + "cash", + "cask", + "cast", + "castle", + "casual", + "cat", + "catch", + "cater", + "cattle", + "caught", + "causal", + "cause", + "cave", + "cease", + "celery", + "cell", + "cellar", + "cement", + "censor", + "census", + "cereal", + "cervix", + "chain", + "chair", + "chalk", + "chalky", + "champ", + "chance", + "change", + "chant", + "chaos", + "chap", + "chapel", + "charge", + "charm", + "chart", + "chase", + "chat", + "cheap", + "cheat", + "check", + "cheek", + "cheeky", + "cheer", + "cheery", + "cheese", + "chef", + "cherry", + "chess", + "chest", + "chew", + "chic", + "chick", + "chief", + "child", + "chill", + "chilly", + "chin", + "chip", + "choice", + "choir", + "choose", + "chop", + "choppy", + "chord", + "chorus", + "chose", + "chosen", + "chrome", + "chunk", + "chunky", + "church", + "cider", + "cigar", + "cinema", + "circa", + "circle", + "circus", + "cite", + "city", + "civic", + "civil", + "clad", + "claim", + "clammy", + "clan", + "clap", + "clash", + "clasp", + "class", + "clause", + "claw", + "clay", + "clean", + "clear", + "clergy", + "clerk", + "clever", + "click", + "client", + "cliff", + "climax", + "climb", + "clinch", + "cling", + "clinic", + "clip", + "cloak", + "clock", + "clone", + "close", + "closer", + "closet", + "cloth", + "cloud", + "cloudy", + "clout", + "clown", + "club", + "clue", + "clumsy", + "clung", + "clutch", + "coach", + "coal", + "coarse", + "coast", + "coat", + "coax", + "cobalt", + "cobra", + "coca", + "cock", + "cocoa", + "code", + "coffee", + "coffin", + "cohort", + "coil", + "coin", + "coke", + "cold", + "collar", + "colon", + "colony", + "colt", + "column", + "comb", + "combat", + "come", + "comedy", + "comic", + "commit", + "common", + "compel", + "comply", + "concur", + "cone", + "confer", + "consul", + "convex", + "convey", + "convoy", + "cook", + "cool", + "cope", + "copper", + "copy", + "coral", + "cord", + "core", + "cork", + "corn", + "corner", + "corps", + "corpse", + "corpus", + "cortex", + "cosmic", + "cosmos", + "cost", + "costly", + "cosy", + "cotton", + "couch", + "cough", + "could", + "count", + "county", + "coup", + "couple", + "coupon", + "course", + "court", + "cousin", + "cove", + "cover", + "covert", + "cow", + "coward", + "cowboy", + "crab", + "crack", + "cradle", + "craft", + "crafty", + "crag", + "crane", + "crap", + "crash", + "crate", + "crater", + "crawl", + "crazy", + "creak", + "cream", + "creamy", + "create", + "credit", + "creed", + "creek", + "creep", + "creepy", + "crept", + "crest", + "crew", + "cried", + "crime", + "crisis", + "crisp", + "critic", + "croft", + "crook", + "crop", + "cross", + "crow", + "crowd", + "crown", + "crude", + "cruel", + "cruise", + "crunch", + "crush", + "crust", + "crux", + "cry", + "crypt", + "cube", + "cubic", + "cuckoo", + "cuff", + "cult", + "cup", + "curb", + "cure", + "curfew", + "curl", + "curry", + "curse", + "cursor", + "curve", + "custom", + "cut", + "cute", + "cycle", + "cyclic", + "cynic", + "dad", + "daddy", + "dagger", + "daily", + "dairy", + "daisy", + "dale", + "damage", + "damn", + "damp", + "dampen", + "dance", + "danger", + "dare", + "dark", + "darken", + "dash", + "data", + "date", + "dawn", + "day", + "dead", + "deadly", + "deaf", + "deal", + "dealer", + "dean", + "dear", + "death", + "debate", + "debit", + "debris", + "debt", + "debtor", + "decade", + "decay", + "decent", + "decide", + "deck", + "decor", + "decree", + "deduce", + "deed", + "deep", + "deeply", + "deer", + "defeat", + "defect", + "defend", + "defer", + "define", + "defy", + "degree", + "deity", + "delay", + "delete", + "delta", + "demand", + "demise", + "demo", + "demon", + "demure", + "denial", + "denote", + "dense", + "dental", + "deny", + "depart", + "depend", + "depict", + "deploy", + "depot", + "depth", + "deputy", + "derive", + "desert", + "design", + "desire", + "desist", + "desk", + "detail", + "detect", + "deter", + "detest", + "detour", + "device", + "devil", + "devise", + "devoid", + "devote", + "devour", + "dial", + "diary", + "dice", + "dictum", + "did", + "die", + "diesel", + "diet", + "differ", + "digest", + "digit", + "dine", + "dinghy", + "dinner", + "diode", + "dire", + "direct", + "dirt", + "dirty", + "disc", + "disco", + "dish", + "disk", + "dismal", + "dispel", + "ditch", + "dive", + "divert", + "divide", + "divine", + "dizzy", + "docile", + "dock", + "doctor", + "dog", + "dogma", + "dole", + "doll", + "dollar", + "dolly", + "domain", + "dome", + "domino", + "donate", + "done", + "donkey", + "donor", + "doom", + "door", + "dorsal", + "dose", + "double", + "doubt", + "dough", + "dour", + "dove", + "down", + "dozen", + "draft", + "drag", + "dragon", + "drain", + "drama", + "drank", + "draw", + "drawer", + "dread", + "dream", + "dreary", + "dress", + "drew", + "dried", + "drift", + "drill", + "drink", + "drip", + "drive", + "driver", + "drop", + "drove", + "drown", + "drug", + "drum", + "drunk", + "dry", + "dual", + "duck", + "duct", + "due", + "duel", + "duet", + "duke", + "dull", + "duly", + "dumb", + "dummy", + "dump", + "dune", + "dung", + "duress", + "during", + "dusk", + "dust", + "dusty", + "duty", + "dwarf", + "dwell", + "dyer", + "dying", + "dynamo", + "each", + "eager", + "eagle", + "ear", + "earl", + "early", + "earn", + "earth", + "ease", + "easel", + "easily", + "easter", + "easy", + "eat", + "eaten", + "eater", + "echo", + "eddy", + "edge", + "edible", + "edict", + "edit", + "editor", + "eerie", + "eerily", + "effect", + "effort", + "egg", + "ego", + "eight", + "eighth", + "eighty", + "either", + "elbow", + "elder", + "eldest", + "elect", + "eleven", + "elicit", + "elite", + "else", + "elude", + "elves", + "embark", + "emblem", + "embryo", + "emerge", + "emit", + "empire", + "employ", + "empty", + "enable", + "enamel", + "end", + "endure", + "enemy", + "energy", + "engage", + "engine", + "enjoy", + "enlist", + "enough", + "ensure", + "entail", + "enter", + "entire", + "entry", + "envoy", + "envy", + "enzyme", + "epic", + "epoch", + "equal", + "equate", + "equip", + "equity", + "era", + "erase", + "erect", + "erode", + "erotic", + "errant", + "error", + "escape", + "escort", + "essay", + "estate", + "esteem", + "ethic", + "ethnic", + "evade", + "even", + "event", + "ever", + "every", + "evict", + "evil", + "evoke", + "evolve", + "exact", + "exam", + "exceed", + "excel", + "except", + "excess", + "excise", + "excite", + "excuse", + "exempt", + "exert", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expert", + "expire", + "export", + "expose", + "extend", + "extra", + "eye", + "eyed", + "fabric", + "face", + "facial", + "fact", + "factor", + "fade", + "fail", + "faint", + "fair", + "fairly", + "fairy", + "faith", + "fake", + "falcon", + "fall", + "false", + "falter", + "fame", + "family", + "famine", + "famous", + "fan", + "fancy", + "far", + "farce", + "fare", + "farm", + "farmer", + "fast", + "fasten", + "faster", + "fat", + "fatal", + "fate", + "father", + "fatty", + "fault", + "faulty", + "fauna", + "fear", + "feast", + "feat", + "fed", + "fee", + "feeble", + "feed", + "feel", + "feet", + "fell", + "fellow", + "felt", + "female", + "fence", + "fend", + "ferry", + "fetal", + "fetch", + "feudal", + "fever", + "few", + "fewer", + "fiance", + "fiasco", + "fiddle", + "field", + "fiend", + "fierce", + "fiery", + "fifth", + "fifty", + "fig", + "fight", + "figure", + "file", + "fill", + "filled", + "filler", + "film", + "filter", + "filth", + "filthy", + "final", + "finale", + "find", + "fine", + "finger", + "finish", + "finite", + "fire", + "firm", + "firmly", + "first", + "fiscal", + "fish", + "fisher", + "fist", + "fit", + "fitful", + "five", + "fix", + "flag", + "flair", + "flak", + "flame", + "flank", + "flap", + "flare", + "flash", + "flask", + "flat", + "flaw", + "fled", + "flee", + "fleece", + "fleet", + "flesh", + "fleshy", + "flew", + "flick", + "flight", + "flimsy", + "flint", + "flirt", + "float", + "flock", + "flood", + "floor", + "floppy", + "flora", + "floral", + "flour", + "flow", + "flower", + "fluent", + "fluffy", + "fluid", + "flung", + "flurry", + "flush", + "flute", + "flux", + "fly", + "flyer", + "foal", + "foam", + "focal", + "focus", + "fog", + "foil", + "fold", + "folk", + "follow", + "folly", + "fond", + "fondly", + "font", + "food", + "fool", + "foot", + "for", + "forbid", + "force", + "ford", + "forest", + "forge", + "forget", + "fork", + "form", + "formal", + "format", + "former", + "fort", + "forth", + "forty", + "forum", + "fossil", + "foster", + "foul", + "found", + "four", + "fourth", + "fox", + "foyer", + "frail", + "frame", + "franc", + "frank", + "fraud", + "free", + "freed", + "freely", + "freer", + "freeze", + "frenzy", + "fresh", + "friar", + "fridge", + "fried", + "friend", + "fright", + "fringe", + "frock", + "frog", + "from", + "front", + "frost", + "frosty", + "frown", + "frozen", + "frugal", + "fruit", + "fudge", + "fuel", + "fulfil", + "full", + "fully", + "fun", + "fund", + "funny", + "fur", + "furry", + "fury", + "fuse", + "fusion", + "fuss", + "fussy", + "futile", + "future", + "fuzzy", + "gadget", + "gag", + "gain", + "gala", + "galaxy", + "gale", + "gall", + "galley", + "gallon", + "gallop", + "gamble", + "game", + "gamma", + "gang", + "gap", + "garage", + "garden", + "garlic", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaunt", + "gave", + "gay", + "gaze", + "gear", + "geese", + "gender", + "gene", + "genial", + "genius", + "genre", + "gentle", + "gently", + "gentry", + "genus", + "get", + "ghetto", + "ghost", + "giant", + "gift", + "giggle", + "gill", + "gilt", + "ginger", + "girl", + "give", + "given", + "glad", + "glade", + "glance", + "gland", + "glare", + "glass", + "glassy", + "gleam", + "glee", + "glide", + "global", + "globe", + "gloom", + "gloomy", + "glory", + "gloss", + "glossy", + "glove", + "glow", + "glue", + "goal", + "goat", + "gold", + "golden", + "golf", + "gone", + "gong", + "good", + "goose", + "gorge", + "gory", + "gosh", + "gospel", + "gossip", + "got", + "govern", + "gown", + "grab", + "grace", + "grade", + "grain", + "grand", + "grant", + "grape", + "graph", + "grasp", + "grass", + "grassy", + "grate", + "grave", + "gravel", + "gravy", + "gray", + "grease", + "greasy", + "great", + "greed", + "greedy", + "green", + "greet", + "grew", + "grey", + "grid", + "grief", + "grill", + "grim", + "grin", + "grind", + "grip", + "grit", + "gritty", + "groan", + "groin", + "groom", + "groove", + "gross", + "ground", + "group", + "grove", + "grow", + "grown", + "growth", + "grudge", + "grunt", + "guard", + "guess", + "guest", + "guide", + "guild", + "guilt", + "guilty", + "guise", + "guitar", + "gulf", + "gully", + "gun", + "gunman", + "guru", + "gut", + "guy", + "gypsy", + "habit", + "hack", + "had", + "hail", + "hair", + "hairy", + "hale", + "half", + "hall", + "halt", + "hamlet", + "hammer", + "hand", + "handle", + "handy", + "hang", + "hangar", + "happen", + "happy", + "harass", + "hard", + "harder", + "hardly", + "hare", + "harem", + "harm", + "harp", + "harsh", + "has", + "hash", + "hassle", + "haste", + "hasten", + "hasty", + "hat", + "hatch", + "hate", + "haul", + "haunt", + "have", + "haven", + "havoc", + "hawk", + "hazard", + "haze", + "hazel", + "hazy", + "head", + "heal", + "health", + "heap", + "hear", + "heard", + "heart", + "hearth", + "hearty", + "heat", + "heater", + "heaven", + "heavy", + "heck", + "hectic", + "hedge", + "heel", + "hefty", + "height", + "heir", + "held", + "helium", + "helix", + "hell", + "hello", + "helm", + "helmet", + "help", + "hemp", + "hence", + "her", + "herald", + "herb", + "herd", + "here", + "hereby", + "hernia", + "hero", + "heroic", + "heroin", + "hey", + "heyday", + "hick", + "hidden", + "hide", + "high", + "higher", + "highly", + "hill", + "him", + "hind", + "hint", + "hippy", + "hire", + "his", + "hiss", + "hit", + "hive", + "hoard", + "hoarse", + "hobby", + "hockey", + "hold", + "holder", + "hole", + "hollow", + "holly", + "holy", + "home", + "honest", + "honey", + "hood", + "hook", + "hope", + "horn", + "horny", + "horrid", + "horror", + "horse", + "hose", + "host", + "hot", + "hotel", + "hound", + "hour", + "house", + "hover", + "how", + "huge", + "hull", + "human", + "humane", + "humble", + "humid", + "hung", + "hunger", + "hungry", + "hunt", + "hurdle", + "hurl", + "hurry", + "hurt", + "hush", + "hut", + "hybrid", + "hymn", + "hyphen", + "ice", + "icing", + "icon", + "idea", + "ideal", + "idiom", + "idiot", + "idle", + "idly", + "idol", + "ignite", + "ignore", + "ill", + "image", + "immune", + "impact", + "imply", + "import", + "impose", + "incest", + "inch", + "income", + "incur", + "indeed", + "index", + "indoor", + "induce", + "inept", + "inert", + "infant", + "infect", + "infer", + "influx", + "inform", + "inject", + "injure", + "injury", + "inlaid", + "inland", + "inlet", + "inmate", + "inn", + "innate", + "inner", + "input", + "insane", + "insect", + "insert", + "inset", + "inside", + "insist", + "insult", + "insure", + "intact", + "intake", + "intend", + "inter", + "into", + "invade", + "invent", + "invest", + "invite", + "invoke", + "inward", + "iron", + "ironic", + "irony", + "island", + "isle", + "issue", + "itch", + "item", + "itself", + "ivory", + "jacket", + "jade", + "jaguar", + "jail", + "jargon", + "jaw", + "jazz", + "jeep", + "jelly", + "jerky", + "jest", + "jet", + "jewel", + "job", + "jock", + "jockey", + "join", + "joint", + "joke", + "jolly", + "jolt", + "joy", + "joyful", + "joyous", + "judge", + "juice", + "juicy", + "jumble", + "jumbo", + "jump", + "jungle", + "junior", + "junk", + "junta", + "jury", + "just", + "karate", + "keel", + "keen", + "keep", + "keeper", + "kept", + "kernel", + "kettle", + "key", + "khaki", + "kick", + "kid", + "kidnap", + "kidney", + "kill", + "killer", + "kin", + "kind", + "kindly", + "king", + "kiss", + "kite", + "kitten", + "knack", + "knee", + "knew", + "knife", + "knight", + "knit", + "knob", + "knock", + "knot", + "know", + "known", + "label", + "lace", + "lack", + "lad", + "ladder", + "laden", + "lady", + "lagoon", + "laity", + "lake", + "lamb", + "lame", + "lamp", + "lance", + "land", + "lane", + "lap", + "lapse", + "large", + "larval", + "laser", + "last", + "latch", + "late", + "lately", + "latent", + "later", + "latest", + "latter", + "laugh", + "launch", + "lava", + "lavish", + "law", + "lawful", + "lawn", + "lawyer", + "lay", + "layer", + "layman", + "lazy", + "lead", + "leader", + "leaf", + "leafy", + "league", + "leak", + "leaky", + "lean", + "leap", + "learn", + "lease", + "leash", + "least", + "leave", + "led", + "ledge", + "left", + "leg", + "legacy", + "legal", + "legend", + "legion", + "lemon", + "lend", + "length", + "lens", + "lent", + "leper", + "lesion", + "less", + "lessen", + "lesser", + "lesson", + "lest", + "let", + "lethal", + "letter", + "level", + "lever", + "levy", + "lewis", + "liable", + "liar", + "libel", + "lice", + "lick", + "lid", + "lie", + "lied", + "life", + "lift", + "light", + "like", + "likely", + "limb", + "lime", + "limit", + "limp", + "line", + "linear", + "linen", + "linger", + "link", + "lion", + "lip", + "liquid", + "liquor", + "list", + "listen", + "lit", + "live", + "lively", + "liver", + "lizard", + "load", + "loaf", + "loan", + "lobby", + "lobe", + "local", + "locate", + "lock", + "locus", + "lodge", + "loft", + "lofty", + "log", + "logic", + "logo", + "lone", + "lonely", + "long", + "longer", + "look", + "loop", + "loose", + "loosen", + "loot", + "lord", + "lorry", + "lose", + "loss", + "lost", + "lot", + "lotion", + "lotus", + "loud", + "loudly", + "lounge", + "lousy", + "love", + "lovely", + "lover", + "low", + "lower", + "lowest", + "loyal", + "lucid", + "luck", + "lucky", + "lull", + "lump", + "lumpy", + "lunacy", + "lunar", + "lunch", + "lung", + "lure", + "lurid", + "lush", + "lust", + "lute", + "luxury", + "lying", + "lymph", + "lynch", + "lyric", + "macho", + "macro", + "mad", + "madam", + "made", + "mafia", + "magic", + "magma", + "magnet", + "magnum", + "maid", + "maiden", + "mail", + "main", + "mainly", + "major", + "make", + "maker", + "male", + "malice", + "mall", + "malt", + "mammal", + "manage", + "mane", + "mania", + "manic", + "manner", + "manor", + "mantle", + "manual", + "manure", + "many", + "map", + "maple", + "marble", + "march", + "mare", + "margin", + "marina", + "mark", + "market", + "marry", + "marsh", + "martin", + "martyr", + "mask", + "mason", + "mass", + "mast", + "master", + "match", + "mate", + "matrix", + "matter", + "mature", + "maxim", + "may", + "maybe", + "mayor", + "maze", + "mead", + "meadow", + "meal", + "mean", + "meant", + "meat", + "medal", + "media", + "median", + "medic", + "medium", + "meet", + "mellow", + "melody", + "melon", + "melt", + "member", + "memo", + "memory", + "menace", + "mend", + "mental", + "mentor", + "menu", + "mercy", + "mere", + "merely", + "merge", + "merger", + "merit", + "merry", + "mesh", + "mess", + "messy", + "met", + "metal", + "meter", + "method", + "methyl", + "metric", + "metro", + "mid", + "midday", + "middle", + "midst", + "midway", + "might", + "mighty", + "mild", + "mildew", + "mile", + "milk", + "milky", + "mill", + "mimic", + "mince", + "mind", + "mine", + "mini", + "mink", + "minor", + "mint", + "minus", + "minute", + "mirror", + "mirth", + "misery", + "miss", + "mist", + "misty", + "mite", + "mix", + "moan", + "moat", + "mobile", + "mock", + "mode", + "model", + "modem", + "modern", + "modest", + "modify", + "module", + "moist", + "molar", + "mole", + "molten", + "moment", + "money", + "monies", + "monk", + "monkey", + "month", + "mood", + "moody", + "moon", + "moor", + "moral", + "morale", + "morbid", + "more", + "morgue", + "mortal", + "mortar", + "mosaic", + "mosque", + "moss", + "most", + "mostly", + "moth", + "mother", + "motion", + "motive", + "motor", + "mould", + "mount", + "mourn", + "mouse", + "mouth", + "move", + "movie", + "much", + "muck", + "mucus", + "mud", + "muddle", + "muddy", + "mule", + "mummy", + "murder", + "murky", + "murmur", + "muscle", + "museum", + "music", + "mussel", + "must", + "mutant", + "mute", + "mutiny", + "mutter", + "mutton", + "mutual", + "muzzle", + "myopic", + "myriad", + "myself", + "mystic", + "myth", + "nadir", + "nail", + "naked", + "name", + "namely", + "nape", + "napkin", + "narrow", + "nasal", + "nasty", + "nation", + "native", + "nature", + "nausea", + "naval", + "nave", + "navy", + "near", + "nearer", + "nearly", + "neat", + "neatly", + "neck", + "need", + "needle", + "needy", + "negate", + "neon", + "nephew", + "nerve", + "nest", + "neural", + "never", + "newly", + "next", + "nice", + "nicely", + "niche", + "nickel", + "niece", + "night", + "nimble", + "nine", + "ninety", + "ninth", + "noble", + "nobody", + "node", + "noise", + "noisy", + "non", + "none", + "noon", + "nor", + "norm", + "normal", + "nose", + "nosy", + "not", + "note", + "notice", + "notify", + "notion", + "nought", + "noun", + "novel", + "novice", + "now", + "nozzle", + "nude", + "null", + "numb", + "number", + "nurse", + "nylon", + "nymph", + "oak", + "oasis", + "oath", + "obese", + "obey", + "object", + "oblige", + "oboe", + "obtain", + "occult", + "occupy", + "occur", + "ocean", + "octave", + "odd", + "off", + "offend", + "offer", + "office", + "offset", + "often", + "oil", + "oily", + "okay", + "old", + "older", + "oldest", + "olive", + "omega", + "omen", + "omit", + "once", + "one", + "onion", + "only", + "onset", + "onto", + "onus", + "onward", + "opaque", + "open", + "openly", + "opera", + "opium", + "oppose", + "optic", + "option", + "oracle", + "oral", + "orange", + "orbit", + "orchid", + "ordeal", + "order", + "organ", + "orgasm", + "orient", + "origin", + "ornate", + "orphan", + "other", + "otter", + "ought", + "ounce", + "our", + "out", + "outer", + "output", + "outset", + "oval", + "oven", + "over", + "overt", + "owe", + "owing", + "owl", + "own", + "owner", + "oxide", + "oxygen", + "oyster", + "ozone", + "pace", + "pack", + "packet", + "pact", + "paddle", + "paddy", + "pagan", + "page", + "paid", + "pain", + "paint", + "pair", + "palace", + "pale", + "palm", + "panel", + "panic", + "papa", + "papal", + "paper", + "parade", + "parcel", + "pardon", + "parent", + "parish", + "park", + "parody", + "parrot", + "part", + "partly", + "party", + "pass", + "past", + "paste", + "pastel", + "pastor", + "pastry", + "pat", + "patch", + "patent", + "path", + "patio", + "patrol", + "patron", + "pause", + "pave", + "pawn", + "pay", + "peace", + "peach", + "peak", + "pear", + "pearl", + "pedal", + "peel", + "peer", + "pelvic", + "pelvis", + "pen", + "penal", + "pence", + "pencil", + "penis", + "penny", + "people", + "pepper", + "per", + "perch", + "peril", + "period", + "perish", + "permit", + "person", + "pest", + "petite", + "petrol", + "petty", + "phase", + "phone", + "photo", + "phrase", + "piano", + "pick", + "picket", + "picnic", + "pie", + "piece", + "pier", + "pierce", + "piety", + "pig", + "pigeon", + "piggy", + "pike", + "pile", + "pill", + "pillar", + "pillow", + "pilot", + "pin", + "pinch", + "pine", + "pink", + "pint", + "pious", + "pipe", + "pirate", + "piss", + "pistol", + "piston", + "pit", + "pitch", + "pity", + "pivot", + "pixel", + "pizza", + "place", + "placid", + "plague", + "plain", + "plan", + "plane", + "planet", + "plank", + "plant", + "plasma", + "plate", + "play", + "player", + "plea", + "plead", + "please", + "pledge", + "plenty", + "plenum", + "plight", + "plot", + "ploy", + "plug", + "plum", + "plump", + "plunge", + "plural", + "plus", + "plush", + "pocket", + "poem", + "poet", + "poetic", + "poetry", + "point", + "poison", + "polar", + "pole", + "police", + "policy", + "polite", + "poll", + "pollen", + "polo", + "pond", + "ponder", + "pony", + "pool", + "poor", + "poorly", + "pop", + "pope", + "poppy", + "pore", + "pork", + "port", + "portal", + "pose", + "posh", + "post", + "postal", + "pot", + "potato", + "potent", + "pouch", + "pound", + "pour", + "powder", + "power", + "praise", + "pray", + "prayer", + "preach", + "prefer", + "prefix", + "press", + "pretty", + "price", + "pride", + "priest", + "primal", + "prime", + "prince", + "print", + "prior", + "prism", + "prison", + "privy", + "prize", + "probe", + "profit", + "prompt", + "prone", + "proof", + "propel", + "proper", + "prose", + "proton", + "proud", + "prove", + "proven", + "proxy", + "prune", + "psalm", + "pseudo", + "psyche", + "pub", + "public", + "puff", + "pull", + "pulp", + "pulpit", + "pulsar", + "pulse", + "pump", + "punch", + "punish", + "punk", + "pupil", + "puppet", + "puppy", + "pure", + "purely", + "purge", + "purify", + "purple", + "purse", + "pursue", + "push", + "pushy", + "pussy", + "put", + "putt", + "puzzle", + "quaint", + "quake", + "quarry", + "quartz", + "quay", + "queen", + "queer", + "query", + "quest", + "queue", + "quick", + "quid", + "quiet", + "quilt", + "quirk", + "quit", + "quite", + "quiver", + "quiz", + "quota", + "quote", + "rabbit", + "race", + "racial", + "racism", + "rack", + "racket", + "radar", + "radio", + "radish", + "radius", + "raffle", + "raft", + "rage", + "raid", + "rail", + "rain", + "rainy", + "raise", + "rally", + "ramp", + "random", + "range", + "rank", + "ransom", + "rape", + "rapid", + "rare", + "rarely", + "rarity", + "rash", + "rat", + "rate", + "rather", + "ratify", + "ratio", + "rattle", + "rave", + "raven", + "raw", + "ray", + "razor", + "reach", + "react", + "read", + "reader", + "ready", + "real", + "really", + "realm", + "reap", + "rear", + "reason", + "rebel", + "recall", + "recent", + "recess", + "recipe", + "reckon", + "record", + "recoup", + "rector", + "red", + "redeem", + "reduce", + "reed", + "reef", + "refer", + "reform", + "refuge", + "refuse", + "regal", + "regard", + "regent", + "regime", + "region", + "regret", + "reign", + "reject", + "relate", + "relax", + "relay", + "relic", + "relief", + "relish", + "rely", + "remain", + "remark", + "remedy", + "remind", + "remit", + "remote", + "remove", + "renal", + "render", + "rent", + "rental", + "repair", + "repeal", + "repeat", + "repent", + "reply", + "report", + "rescue", + "resent", + "reside", + "resign", + "resin", + "resist", + "resort", + "rest", + "result", + "resume", + "retail", + "retain", + "retina", + "retire", + "return", + "reveal", + "review", + "revise", + "revive", + "revolt", + "reward", + "rhino", + "rhyme", + "rhythm", + "ribbon", + "rice", + "rich", + "rick", + "rid", + "ride", + "rider", + "ridge", + "rife", + "rifle", + "rift", + "right", + "rigid", + "ring", + "rinse", + "riot", + "ripe", + "ripen", + "ripple", + "rise", + "risk", + "risky", + "rite", + "ritual", + "rival", + "river", + "road", + "roar", + "roast", + "rob", + "robe", + "robin", + "robot", + "robust", + "rock", + "rocket", + "rocky", + "rod", + "rode", + "rodent", + "rogue", + "role", + "roll", + "roof", + "room", + "root", + "rope", + "rose", + "rosy", + "rotate", + "rotor", + "rotten", + "rouge", + "rough", + "round", + "route", + "rover", + "row", + "royal", + "rubble", + "ruby", + "rudder", + "rude", + "rugby", + "ruin", + "rule", + "ruler", + "rumble", + "rump", + "run", + "rune", + "rung", + "runway", + "rural", + "rush", + "rust", + "rustic", + "rusty", + "sack", + "sacred", + "sad", + "saddle", + "sadism", + "sadly", + "safari", + "safe", + "safely", + "safer", + "safety", + "saga", + "sage", + "said", + "sail", + "sailor", + "saint", + "sake", + "salad", + "salary", + "sale", + "saline", + "saliva", + "salmon", + "saloon", + "salt", + "salty", + "salute", + "same", + "sample", + "sand", + "sandy", + "sane", + "sash", + "satan", + "satin", + "satire", + "sauce", + "sauna", + "savage", + "save", + "say", + "scale", + "scalp", + "scan", + "scant", + "scar", + "scarce", + "scare", + "scarf", + "scary", + "scene", + "scenic", + "scent", + "school", + "scope", + "score", + "scorn", + "scotch", + "scout", + "scrap", + "scream", + "screen", + "screw", + "script", + "scroll", + "scrub", + "scum", + "sea", + "seal", + "seam", + "seaman", + "search", + "season", + "seat", + "second", + "secret", + "sect", + "sector", + "secure", + "see", + "seed", + "seeing", + "seek", + "seem", + "seize", + "seldom", + "select", + "self", + "sell", + "seller", + "semi", + "senate", + "send", + "senile", + "senior", + "sense", + "sensor", + "sent", + "sentry", + "sequel", + "serene", + "serial", + "series", + "sermon", + "serum", + "serve", + "server", + "set", + "settle", + "seven", + "severe", + "sewage", + "sex", + "sexual", + "sexy", + "shabby", + "shade", + "shadow", + "shady", + "shaft", + "shaggy", + "shah", + "shake", + "shaky", + "shall", + "sham", + "shame", + "shape", + "share", + "shark", + "sharp", + "shawl", + "she", + "shear", + "sheen", + "sheep", + "sheer", + "sheet", + "shelf", + "shell", + "sherry", + "shield", + "shift", + "shine", + "shiny", + "ship", + "shire", + "shirt", + "shit", + "shiver", + "shock", + "shoe", + "shook", + "shoot", + "shop", + "shore", + "short", + "shot", + "should", + "shout", + "show", + "shower", + "shrank", + "shrewd", + "shrill", + "shrimp", + "shrine", + "shrink", + "shrub", + "shrug", + "shut", + "shy", + "shyly", + "sick", + "side", + "siege", + "sigh", + "sight", + "sigma", + "sign", + "signal", + "silent", + "silk", + "silken", + "silky", + "sill", + "silly", + "silver", + "simple", + "simply", + "since", + "sinful", + "sing", + "singer", + "single", + "sink", + "sir", + "siren", + "sister", + "sit", + "site", + "six", + "sixth", + "sixty", + "size", + "sketch", + "skill", + "skin", + "skinny", + "skip", + "skirt", + "skull", + "sky", + "slab", + "slack", + "slain", + "slam", + "slang", + "slap", + "slate", + "slater", + "slave", + "sleek", + "sleep", + "sleepy", + "sleeve", + "slice", + "slick", + "slid", + "slide", + "slight", + "slim", + "slimy", + "sling", + "slip", + "slit", + "slogan", + "slope", + "sloppy", + "slot", + "slow", + "slowly", + "slug", + "slum", + "slump", + "smack", + "small", + "smart", + "smash", + "smear", + "smell", + "smelly", + "smelt", + "smile", + "smoke", + "smoky", + "smooth", + "smug", + "snack", + "snail", + "snake", + "snap", + "snatch", + "sneak", + "snow", + "snowy", + "snug", + "soak", + "soap", + "sober", + "soccer", + "social", + "sock", + "socket", + "soda", + "sodden", + "sodium", + "sofa", + "soft", + "soften", + "softly", + "soggy", + "soil", + "solar", + "sold", + "sole", + "solely", + "solemn", + "solid", + "solo", + "solve", + "some", + "son", + "sonar", + "sonata", + "song", + "sonic", + "soon", + "sooner", + "soot", + "soothe", + "sordid", + "sore", + "sorrow", + "sorry", + "sort", + "soul", + "sound", + "soup", + "sour", + "source", + "space", + "spade", + "span", + "spare", + "spark", + "sparse", + "spasm", + "spat", + "spate", + "speak", + "spear", + "speech", + "speed", + "speedy", + "spell", + "spend", + "sperm", + "sphere", + "spice", + "spicy", + "spider", + "spiky", + "spill", + "spin", + "spinal", + "spine", + "spiral", + "spirit", + "spit", + "spite", + "splash", + "split", + "spoil", + "spoke", + "sponge", + "spoon", + "sport", + "spot", + "spouse", + "spray", + "spread", + "spree", + "spring", + "sprint", + "spur", + "squad", + "square", + "squash", + "squat", + "squid", + "stab", + "stable", + "stack", + "staff", + "stage", + "stain", + "stair", + "stake", + "stale", + "stall", + "stamp", + "stance", + "stand", + "staple", + "star", + "starch", + "stare", + "stark", + "start", + "starve", + "state", + "static", + "statue", + "status", + "stay", + "stead", + "steady", + "steak", + "steal", + "steam", + "steel", + "steep", + "steer", + "stem", + "stench", + "step", + "stereo", + "stern", + "stew", + "stick", + "sticky", + "stiff", + "stifle", + "stigma", + "still", + "sting", + "stint", + "stir", + "stitch", + "stock", + "stocky", + "stone", + "stony", + "stool", + "stop", + "store", + "storm", + "stormy", + "story", + "stout", + "stove", + "strain", + "strait", + "strand", + "strap", + "strata", + "straw", + "stray", + "streak", + "stream", + "street", + "stress", + "strict", + "stride", + "strife", + "strike", + "string", + "strip", + "strive", + "stroke", + "stroll", + "strong", + "stud", + "studio", + "study", + "stuff", + "stuffy", + "stunt", + "stupid", + "sturdy", + "style", + "submit", + "subtle", + "subtly", + "suburb", + "such", + "suck", + "sudden", + "sue", + "suffer", + "sugar", + "suit", + "suite", + "suitor", + "sullen", + "sultan", + "sum", + "summer", + "summit", + "summon", + "sun", + "sunny", + "sunset", + "super", + "superb", + "supper", + "supple", + "supply", + "sure", + "surely", + "surf", + "surge", + "survey", + "suture", + "swamp", + "swan", + "swap", + "swarm", + "sway", + "swear", + "sweat", + "sweaty", + "sweep", + "sweet", + "swell", + "swift", + "swim", + "swine", + "swing", + "swirl", + "switch", + "sword", + "swore", + "symbol", + "synod", + "syntax", + "syrup", + "system", + "table", + "tablet", + "taboo", + "tacit", + "tackle", + "tact", + "tactic", + "tail", + "tailor", + "take", + "tale", + "talent", + "talk", + "tall", + "tally", + "tame", + "tandem", + "tangle", + "tank", + "tap", + "tape", + "target", + "tariff", + "tart", + "task", + "taste", + "tasty", + "tattoo", + "taut", + "tavern", + "tax", + "taxi", + "tea", + "teach", + "teak", + "team", + "tear", + "tease", + "tech", + "teeth", + "tell", + "temper", + "temple", + "tempo", + "tempt", + "ten", + "tenant", + "tend", + "tender", + "tendon", + "tennis", + "tenor", + "tense", + "tensor", + "tent", + "tenth", + "tenure", + "term", + "terror", + "test", + "text", + "than", + "thank", + "that", + "the", + "their", + "them", + "theme", + "then", + "thence", + "theory", + "there", + "these", + "thesis", + "they", + "thick", + "thief", + "thigh", + "thin", + "thing", + "think", + "third", + "thirst", + "thirty", + "this", + "thorn", + "those", + "though", + "thread", + "threat", + "three", + "thrill", + "thrive", + "throat", + "throne", + "throng", + "throw", + "thrust", + "thud", + "thug", + "thumb", + "thus", + "thyme", + "tick", + "ticket", + "tidal", + "tide", + "tidy", + "tie", + "tier", + "tiger", + "tight", + "tile", + "till", + "tilt", + "timber", + "time", + "timid", + "tin", + "tiny", + "tip", + "tissue", + "title", + "toad", + "toast", + "today", + "toilet", + "token", + "told", + "toll", + "tomato", + "tomb", + "tonal", + "tone", + "tongue", + "tonic", + "too", + "took", + "tool", + "tooth", + "top", + "topaz", + "topic", + "torch", + "torque", + "torso", + "tort", + "toss", + "total", + "touch", + "tough", + "tour", + "toward", + "towel", + "tower", + "town", + "toxic", + "toxin", + "trace", + "track", + "tract", + "trade", + "tragic", + "trail", + "train", + "trait", + "tram", + "trance", + "trap", + "trauma", + "travel", + "tray", + "tread", + "treat", + "treaty", + "treble", + "tree", + "trek", + "tremor", + "trench", + "trend", + "trendy", + "trial", + "tribal", + "tribe", + "trick", + "tricky", + "tried", + "trifle", + "trim", + "trio", + "trip", + "triple", + "troop", + "trophy", + "trot", + "trough", + "trout", + "truce", + "truck", + "true", + "truly", + "trunk", + "trust", + "truth", + "try", + "tsar", + "tube", + "tumble", + "tuna", + "tundra", + "tune", + "tung", + "tunic", + "tunnel", + "turban", + "turf", + "turn", + "turtle", + "tutor", + "tweed", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "tycoon", + "tying", + "type", + "tyrant", + "ugly", + "ulcer", + "ultra", + "umpire", + "unable", + "uncle", + "under", + "uneasy", + "unfair", + "unify", + "union", + "unique", + "unit", + "unite", + "unity", + "unlike", + "unrest", + "unruly", + "until", + "update", + "upheld", + "uphill", + "uphold", + "upon", + "uproar", + "upset", + "upshot", + "uptake", + "upturn", + "upward", + "urban", + "urge", + "urgent", + "urging", + "urine", + "usable", + "usage", + "use", + "useful", + "user", + "usual", + "uterus", + "utmost", + "utter", + "vacant", + "vacuum", + "vagina", + "vague", + "vain", + "valet", + "valid", + "valley", + "value", + "valve", + "van", + "vanish", + "vanity", + "vary", + "vase", + "vast", + "vat", + "vault", + "vector", + "veil", + "vein", + "velvet", + "vendor", + "veneer", + "venom", + "vent", + "venue", + "verb", + "verbal", + "verge", + "verify", + "verity", + "verse", + "versus", + "very", + "vessel", + "vest", + "veto", + "via", + "viable", + "vicar", + "vice", + "victim", + "victor", + "video", + "view", + "vigil", + "vile", + "villa", + "vine", + "vinyl", + "viola", + "violet", + "violin", + "viral", + "virgin", + "virtue", + "virus", + "visa", + "vision", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "vodka", + "vogue", + "voice", + "void", + "volley", + "volume", + "vomit", + "vote", + "vowel", + "voyage", + "vulgar", + "wade", + "wage", + "waist", + "wait", + "waiter", + "wake", + "walk", + "walker", + "wall", + "wallet", + "walnut", + "wander", + "want", + "war", + "warden", + "warm", + "warmth", + "warn", + "warp", + "wary", + "was", + "wash", + "wasp", + "waste", + "watch", + "water", + "watery", + "wave", + "way", + "weak", + "weaken", + "wealth", + "weapon", + "wear", + "weary", + "wedge", + "wee", + "weed", + "week", + "weekly", + "weep", + "weight", + "weird", + "well", + "were", + "wet", + "whale", + "wharf", + "what", + "wheat", + "wheel", + "when", + "whence", + "where", + "which", + "whiff", + "whig", + "while", + "whim", + "whip", + "whisky", + "white", + "who", + "whole", + "wholly", + "whom", + "whore", + "whose", + "why", + "wide", + "widely", + "widen", + "wider", + "widow", + "width", + "wife", + "wild", + "wildly", + "wilful", + "will", + "willow", + "win", + "wind", + "window", + "windy", + "wine", + "wing", + "wink", + "winner", + "winter", + "wipe", + "wire", + "wisdom", + "wise", + "wish", + "wit", + "witch", + "with", + "within", + "witty", + "wizard", + "woke", + "wolf", + "wolves", + "woman", + "womb", + "won", + "wonder", + "wood", + "wooden", + "woods", + "woody", + "wool", + "word", + "work", + "worker", + "world", + "worm", + "worry", + "worse", + "worst", + "worth", + "worthy", + "would", + "wound", + "wrap", + "wrath", + "wreath", + "wreck", + "wright", + "wrist", + "writ", + "write", + "writer", + "wrong", + "xerox", + "yacht", + "yard", + "yarn", + "yeah", + "year", + "yeast", + "yellow", + "yet", + "yield", + "yogurt", + "yolk", + "you", + "young", + "your", + "youth", + "zeal", + "zebra", + "zenith", + "zero", + "zigzag", + "zinc", + "zombie", + "zone" +}; -- 2.41.0