1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
4  *   SPDX-License-Identifier: GPL-3.0-or-later
5  *
6  *   Contains strings from libpwquality under the terms of the
7  *   GPL-3.0-or-later (libpwquality is BSD-3-clause or GPL-2.0-or-later,
8  *   so we pick GPL-3.0-or-later).
9  *
10  *   Calamares is Free Software: see the License-Identifier above.
11  *
12  */
13 
14 #include "CheckPWQuality.h"
15 
16 #include "utils/Logger.h"
17 
18 #include <QCoreApplication>
19 #include <QString>
20 
21 #ifdef HAVE_LIBPWQUALITY
22 #include <pwquality.h>
23 #endif
24 
25 #include <memory>
26 
PasswordCheck()27 PasswordCheck::PasswordCheck()
28     : m_weight( 0 )
29     , m_message()
30     , m_accept( []( const QString& ) { return true; } )
31 {
32 }
33 
PasswordCheck(MessageFunc m,AcceptFunc a,Weight weight)34 PasswordCheck::PasswordCheck( MessageFunc m, AcceptFunc a, Weight weight )
35     : m_weight( weight )
36     , m_message( m )
37     , m_accept( a )
38 {
39 }
40 
DEFINE_CHECK_FUNC(minLength)41 DEFINE_CHECK_FUNC( minLength )
42 {
43     int minLength = -1;
44     if ( value.canConvert( QVariant::Int ) )
45     {
46         minLength = value.toInt();
47     }
48     if ( minLength > 0 )
49     {
50         cDebug() << Logger::SubEntry << "minLength set to" << minLength;
51         checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too short" ); },
52                                          [minLength]( const QString& s ) { return s.length() >= minLength; },
53                                          PasswordCheck::Weight( 10 ) ) );
54     }
55 }
56 
DEFINE_CHECK_FUNC(maxLength)57 DEFINE_CHECK_FUNC( maxLength )
58 {
59     int maxLength = -1;
60     if ( value.canConvert( QVariant::Int ) )
61     {
62         maxLength = value.toInt();
63     }
64     if ( maxLength > 0 )
65     {
66         cDebug() << Logger::SubEntry << "maxLength set to" << maxLength;
67         checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too long" ); },
68                                          [maxLength]( const QString& s ) { return s.length() <= maxLength; },
69                                          PasswordCheck::Weight( 10 ) ) );
70     }
71 }
72 
73 #ifdef HAVE_LIBPWQUALITY
74 /* NOTE:
75  *
76  * The munge*() functions are here because libpwquality uses void* to
77  * represent user-data in callbacks and as a general "pass some parameter"
78  * type. These need to be munged to the right C++ type.
79  */
80 
81 /// @brief Handle libpwquality using void* to represent a long
82 static inline long
mungeLong(void * p)83 mungeLong( void* p )
84 {
85     return static_cast< long >( reinterpret_cast< intptr_t >( p ) );
86 }
87 
88 /// @brief Handle libpwquality using void* to represent a char*
89 static inline const char*
mungeString(void * p)90 mungeString( void* p )
91 {
92     return reinterpret_cast< const char* >( p );
93 }
94 
95 /**
96  * Class that acts as a RAII placeholder for pwquality_settings_t pointers.
97  * Gets a new pointer and ensures it is deleted only once; provides
98  * convenience functions for setting options and checking passwords.
99  */
100 class PWSettingsHolder
101 {
102 public:
103     static constexpr int arbitrary_minimum_strength = 40;
104 
PWSettingsHolder()105     PWSettingsHolder()
106         : m_settings( pwquality_default_settings() )
107     {
108     }
109 
~PWSettingsHolder()110     ~PWSettingsHolder() { pwquality_free_settings( m_settings ); }
111 
112     /// Sets an option via the configuration string @p v, <key>=<value> style.
set(const QString & v)113     int set( const QString& v ) { return pwquality_set_option( m_settings, v.toUtf8().constData() ); }
114 
115     /** @brief Checks the given password @p pwd against the current configuration
116      *
117      * Resets m_errorString and m_errorCount and then sets them appropriately
118      * so that explanation() can be called afterwards. Sets m_rv as well.
119      */
120 
check(const QString & pwd)121     int check( const QString& pwd )
122     {
123         void* auxerror = nullptr;
124         m_rv = pwquality_check( m_settings, pwd.toUtf8().constData(), nullptr, nullptr, &auxerror );
125 
126         // Positive return values could be ignored; some negative ones
127         // place extra information in auxerror, which is a void* and
128         // which needs interpretation to long- or string-values.
129         m_errorCount = 0;
130         m_errorString = QString();
131 
132         switch ( m_rv )
133         {
134         case PWQ_ERROR_CRACKLIB_CHECK:
135             if ( auxerror )
136             {
137                 /* Here the string comes from cracklib, don't free? */
138                 m_errorString = mungeString( auxerror );
139             }
140             break;
141         case PWQ_ERROR_MEM_ALLOC:
142         case PWQ_ERROR_UNKNOWN_SETTING:
143         case PWQ_ERROR_INTEGER:
144         case PWQ_ERROR_NON_INT_SETTING:
145         case PWQ_ERROR_NON_STR_SETTING:
146             if ( auxerror )
147             {
148                 m_errorString = mungeString( auxerror );
149                 free( auxerror );
150             }
151             break;
152         case PWQ_ERROR_MIN_DIGITS:
153         case PWQ_ERROR_MIN_UPPERS:
154         case PWQ_ERROR_MIN_LOWERS:
155         case PWQ_ERROR_MIN_OTHERS:
156         case PWQ_ERROR_MIN_LENGTH:
157         case PWQ_ERROR_MIN_CLASSES:
158         case PWQ_ERROR_MAX_CONSECUTIVE:
159         case PWQ_ERROR_MAX_CLASS_REPEAT:
160         case PWQ_ERROR_MAX_SEQUENCE:
161             if ( auxerror )
162             {
163                 m_errorCount = mungeLong( auxerror );
164             }
165             break;
166         default:
167             break;
168         }
169 
170         return m_rv;
171     }
172 
173     /** @brief Explain the results of the last call to check()
174      *
175      * This is roughly the same as the function pwquality_strerror,
176      * only with QStrings instead, and using the Qt translation scheme.
177      * It is used under the terms of the GNU GPL v3 or later, as
178      * allowed by the libpwquality license (LICENSES/GPLv2+-libpwquality)
179      */
explanation()180     QString explanation()
181     {
182         if ( m_rv >= arbitrary_minimum_strength )
183         {
184             return QString();
185         }
186         if ( m_rv >= 0 )
187         {
188             return QCoreApplication::translate( "PWQ", "Password is too weak" );
189         }
190 
191         switch ( m_rv )
192         {
193         case PWQ_ERROR_MEM_ALLOC:
194             if ( !m_errorString.isEmpty() )
195             {
196                 return QCoreApplication::translate( "PWQ", "Memory allocation error when setting '%1'" )
197                     .arg( m_errorString );
198             }
199             return QCoreApplication::translate( "PWQ", "Memory allocation error" );
200         case PWQ_ERROR_SAME_PASSWORD:
201             return QCoreApplication::translate( "PWQ", "The password is the same as the old one" );
202         case PWQ_ERROR_PALINDROME:
203             return QCoreApplication::translate( "PWQ", "The password is a palindrome" );
204         case PWQ_ERROR_CASE_CHANGES_ONLY:
205             return QCoreApplication::translate( "PWQ", "The password differs with case changes only" );
206         case PWQ_ERROR_TOO_SIMILAR:
207             return QCoreApplication::translate( "PWQ", "The password is too similar to the old one" );
208         case PWQ_ERROR_USER_CHECK:
209             return QCoreApplication::translate( "PWQ", "The password contains the user name in some form" );
210         case PWQ_ERROR_GECOS_CHECK:
211             return QCoreApplication::translate(
212                 "PWQ", "The password contains words from the real name of the user in some form" );
213         case PWQ_ERROR_BAD_WORDS:
214             return QCoreApplication::translate( "PWQ", "The password contains forbidden words in some form" );
215         case PWQ_ERROR_MIN_DIGITS:
216             if ( m_errorCount )
217             {
218                 return QCoreApplication::translate(
219                     "PWQ", "The password contains fewer than %n digits", nullptr, m_errorCount );
220             }
221             return QCoreApplication::translate( "PWQ", "The password contains too few digits" );
222         case PWQ_ERROR_MIN_UPPERS:
223             if ( m_errorCount )
224             {
225                 return QCoreApplication::translate(
226                     "PWQ", "The password contains fewer than %n uppercase letters", nullptr, m_errorCount );
227             }
228             return QCoreApplication::translate( "PWQ", "The password contains too few uppercase letters" );
229         case PWQ_ERROR_MIN_LOWERS:
230             if ( m_errorCount )
231             {
232                 return QCoreApplication::translate(
233                     "PWQ", "The password contains fewer than %n lowercase letters", nullptr, m_errorCount );
234             }
235             return QCoreApplication::translate( "PWQ", "The password contains too few lowercase letters" );
236         case PWQ_ERROR_MIN_OTHERS:
237             if ( m_errorCount )
238             {
239                 return QCoreApplication::translate(
240                     "PWQ", "The password contains fewer than %n non-alphanumeric characters", nullptr, m_errorCount );
241             }
242             return QCoreApplication::translate( "PWQ", "The password contains too few non-alphanumeric characters" );
243         case PWQ_ERROR_MIN_LENGTH:
244             if ( m_errorCount )
245             {
246                 return QCoreApplication::translate(
247                     "PWQ", "The password is shorter than %n characters", nullptr, m_errorCount );
248             }
249             return QCoreApplication::translate( "PWQ", "The password is too short" );
250         case PWQ_ERROR_ROTATED:
251             return QCoreApplication::translate( "PWQ", "The password is a rotated version of the previous one" );
252         case PWQ_ERROR_MIN_CLASSES:
253             if ( m_errorCount )
254             {
255                 return QCoreApplication::translate(
256                     "PWQ", "The password contains fewer than %n character classes", nullptr, m_errorCount );
257             }
258             return QCoreApplication::translate( "PWQ", "The password does not contain enough character classes" );
259         case PWQ_ERROR_MAX_CONSECUTIVE:
260             if ( m_errorCount )
261             {
262                 return QCoreApplication::translate(
263                     "PWQ", "The password contains more than %n same characters consecutively", nullptr, m_errorCount );
264             }
265             return QCoreApplication::translate( "PWQ", "The password contains too many same characters consecutively" );
266         case PWQ_ERROR_MAX_CLASS_REPEAT:
267             if ( m_errorCount )
268             {
269                 return QCoreApplication::translate(
270                     "PWQ",
271                     "The password contains more than %n characters of the same class consecutively",
272                     nullptr,
273                     m_errorCount );
274             }
275             return QCoreApplication::translate(
276                 "PWQ", "The password contains too many characters of the same class consecutively" );
277         case PWQ_ERROR_MAX_SEQUENCE:
278             if ( m_errorCount )
279             {
280                 return QCoreApplication::translate(
281                     "PWQ",
282                     "The password contains monotonic sequence longer than %n characters",
283                     nullptr,
284                     m_errorCount );
285             }
286             return QCoreApplication::translate( "PWQ",
287                                                 "The password contains too long of a monotonic character sequence" );
288         case PWQ_ERROR_EMPTY_PASSWORD:
289             return QCoreApplication::translate( "PWQ", "No password supplied" );
290         case PWQ_ERROR_RNG:
291             return QCoreApplication::translate( "PWQ", "Cannot obtain random numbers from the RNG device" );
292         case PWQ_ERROR_GENERATION_FAILED:
293             return QCoreApplication::translate( "PWQ",
294                                                 "Password generation failed - required entropy too low for settings" );
295         case PWQ_ERROR_CRACKLIB_CHECK:
296             if ( !m_errorString.isEmpty() )
297             {
298                 return QCoreApplication::translate( "PWQ", "The password fails the dictionary check - %1" )
299                     .arg( m_errorString );
300             }
301             return QCoreApplication::translate( "PWQ", "The password fails the dictionary check" );
302         case PWQ_ERROR_UNKNOWN_SETTING:
303             if ( !m_errorString.isEmpty() )
304             {
305                 return QCoreApplication::translate( "PWQ", "Unknown setting - %1" ).arg( m_errorString );
306             }
307             return QCoreApplication::translate( "PWQ", "Unknown setting" );
308         case PWQ_ERROR_INTEGER:
309             if ( !m_errorString.isEmpty() )
310             {
311                 return QCoreApplication::translate( "PWQ", "Bad integer value of setting - %1" ).arg( m_errorString );
312             }
313             return QCoreApplication::translate( "PWQ", "Bad integer value" );
314         case PWQ_ERROR_NON_INT_SETTING:
315             if ( !m_errorString.isEmpty() )
316             {
317                 return QCoreApplication::translate( "PWQ", "Setting %1 is not of integer type" ).arg( m_errorString );
318             }
319             return QCoreApplication::translate( "PWQ", "Setting is not of integer type" );
320         case PWQ_ERROR_NON_STR_SETTING:
321             if ( !m_errorString.isEmpty() )
322             {
323                 return QCoreApplication::translate( "PWQ", "Setting %1 is not of string type" ).arg( m_errorString );
324             }
325             return QCoreApplication::translate( "PWQ", "Setting is not of string type" );
326         case PWQ_ERROR_CFGFILE_OPEN:
327             return QCoreApplication::translate( "PWQ", "Opening the configuration file failed" );
328         case PWQ_ERROR_CFGFILE_MALFORMED:
329             return QCoreApplication::translate( "PWQ", "The configuration file is malformed" );
330         case PWQ_ERROR_FATAL_FAILURE:
331             return QCoreApplication::translate( "PWQ", "Fatal failure" );
332         default:
333             return QCoreApplication::translate( "PWQ", "Unknown error" );
334         }
335     }
336 
337 private:
338     QString m_errorString;  ///< Textual error from last call to check()
339     int m_errorCount = 0;  ///< Count (used in %n) error from last call to check()
340     int m_rv = 0;  ///< Return value from libpwquality
341 
342     pwquality_settings_t* m_settings = nullptr;
343 };
344 
DEFINE_CHECK_FUNC(libpwquality)345 DEFINE_CHECK_FUNC( libpwquality )
346 {
347     if ( !value.canConvert( QVariant::List ) )
348     {
349         cWarning() << "libpwquality settings is not a list";
350         return;
351     }
352 
353     QVariantList l = value.toList();
354     unsigned int requirement_count = 0;
355     auto settings = std::make_shared< PWSettingsHolder >();
356     for ( const auto& v : l )
357     {
358         if ( v.type() == QVariant::String )
359         {
360             QString option = v.toString();
361             int r = settings->set( option );
362             if ( r )
363             {
364                 cWarning() << "unrecognized libpwquality setting" << option;
365             }
366             else
367             {
368                 cDebug() << Logger::SubEntry << "libpwquality setting" << option;
369                 ++requirement_count;
370             }
371         }
372         else
373         {
374             cWarning() << "unrecognized libpwquality setting" << v;
375         }
376     }
377 
378     /* Something actually added? */
379     if ( requirement_count )
380     {
381         checks.push_back( PasswordCheck( [settings]() { return settings->explanation(); },
382                                          [settings]( const QString& s ) {
383                                              int r = settings->check( s );
384                                              if ( r < 0 )
385                                              {
386                                                  cWarning() << "libpwquality error" << r
387                                                             << pwquality_strerror( nullptr, 256, r, nullptr );
388                                              }
389                                              else if ( r < settings->arbitrary_minimum_strength )
390                                              {
391                                                  cDebug() << "Password strength" << r << "too low";
392                                              }
393                                              return r >= settings->arbitrary_minimum_strength;
394                                          },
395                                          PasswordCheck::Weight( 100 ) ) );
396     }
397 }
398 #endif
399