Hi all, the current issue of (German) Linux Magazin has an article comparing some GnuPG frontends. One issue discussed there is the "password strength meter" that gives e.g. 25% strength indication for things like 123456789. I don't know about Kleopatra, but KGpg uses KNewPasswordDialog and it's strength meter for this. I propose to change the algorithm used to calculate the password strength to remove key sequences from the "length" calculation of the password, i.e. 123 has the same length as 1. Also punish all passwords harder that do not contain all types of characters, so a password containing only lowercase characters and numbers needs to be much longer than one also containing specials and uppercase characters.
I've attached my strength test program containing both the old and the proposed new version of the code. I've tested the new version in 2 variants, once with and without the call to toLower() before checking for sequences. These are some test passwords I used, mostly some examples of "simple" passwords users will use. The last one is a scrambled version of a password I saw used somewhere (i.e. every letter replaced with something from the same character class to retain the score) that was not totally obvious. old nEw new abcdef 45 12 12 abcdefghi 72 22 22 1 12 1 1 12 15 2 2 123 17 2 2 1234 20 2 2 12345 22 2 2 123456 25 2 2 123456789 32 2 2 qwertz 45 25 25 1234test 40 20 20 test1234 30 10 10 a1b2c3d4 100 60 60 a1b2c3d4e5 100 85 85 a1b2c3d4e5f6 100 100 100 a1a1a1a1 40 20 20 ........ 30 2 2 KKvfnDd. 90 57 55 Also I propose to change the color of the strength indicator to red below 50% and to yellow below 75%. Since this does not affect any strings and improves security I would also push this into 4.10 in noone objects. Comments? Eike
#include <QString> #include <QDebug> const int reasonablePasswordLength = 8; int effectivePasswordLength(const QString &password) { enum Category { Digit, Upper, Vowel, Consonant, Special }; Category previousCategory = Vowel; QString vowels("aeiou"); int count = 0; int catCount = 0; unsigned int catMask = 0; for (int i = 0; i < password.length(); ++i) { QChar currentChar = password.at(i); if (!password.left(i).contains(currentChar)) { Category currentCategory; switch (currentChar.category()) { case QChar::Letter_Uppercase: currentCategory = Upper; break; case QChar::Letter_Lowercase: if (vowels.contains(currentChar)) { currentCategory = Vowel; } else { currentCategory = Consonant; } break; case QChar::Number_DecimalDigit: currentCategory = Digit; break; default: currentCategory = Special; break; } switch (currentCategory) { case Vowel: if (previousCategory != Consonant) { ++count; } break; case Consonant: if (previousCategory != Vowel) { ++count; } break; default: if (previousCategory != currentCategory) { ++count; } break; } previousCategory = currentCategory; if (!(catMask & (1 << currentCategory))) { ++catCount; catMask |= (1 << currentCategory); } } } // passwords that have many category changes but few categories are weaker return (count * catCount) / 5; } int passwordStrength(const QString &password) { QString sPass = password.simplified().toLower(); if (sPass.length() < 2) return sPass.length(); int i = 0; while (i < sPass.length()) { // duplicate characters do not improve the length if (sPass[i] == sPass[i + 1]) { sPass.remove(i + 1, 1); continue; } // the sequence detection is only reliable in the ASCII range if (!sPass[i].isLetterOrNumber()) { ++i; continue; } if (sPass[i].unicode() == sPass[i + 1].unicode() + 1 || sPass[i].unicode() == sPass[i + 1].unicode() - 1) { // Remove the old one here. Otherwise we would not catch 123 as a sequence sPass.remove(i, 1); continue; } ++i; } int pwstrength = (20 * sPass.length() + 80 * effectivePasswordLength(password)) / qMax(reasonablePasswordLength, 2); if (pwstrength < 0) { pwstrength = 0; } else if (pwstrength > 100) { pwstrength = 100; } return pwstrength; } int old_effectivePasswordLength(const QString &password) { enum Category { Digit, Upper, Vowel, Consonant, Special }; Category previousCategory = Vowel; QString vowels("aeiou"); int count = 0; for (int i = 0; i < password.length(); ++i) { QChar currentChar = password.at(i); if (!password.left(i).contains(currentChar)) { Category currentCategory; switch (currentChar.category()) { case QChar::Letter_Uppercase: currentCategory = Upper; break; case QChar::Letter_Lowercase: if (vowels.contains(currentChar)) { currentCategory = Vowel; } else { currentCategory = Consonant; } break; case QChar::Number_DecimalDigit: currentCategory = Digit; break; default: currentCategory = Special; break; } switch (currentCategory) { case Vowel: if (previousCategory != Consonant) { ++count; } break; case Consonant: if (previousCategory != Vowel) { ++count; } break; default: if (previousCategory != currentCategory) { ++count; } break; } previousCategory = currentCategory; } } return count; } int old_passwordStrength(const QString &password) { int pwstrength = (20 * password.length() + 80 * old_effectivePasswordLength(password)) / qMax(reasonablePasswordLength, 2); if (pwstrength < 0) { pwstrength = 0; } else if (pwstrength > 100) { pwstrength = 100; } return pwstrength; } int main(int argc, char **argv) { for (int i = 1; i < argc; i++) qDebug() << old_passwordStrength(QString::fromAscii(argv[i])) << passwordStrength(QString::fromAscii(argv[i])); return 0; }
signature.asc
Description: This is a digitally signed message part.