Improved js password strength check

Improved check of CJK-Characters
pull/2892/merge
Ozzie Isaacs 2 months ago
parent f987fb0aba
commit c901ccbb01

@ -125,13 +125,6 @@ def create_app():
ub.password_change(cli_param.user_credentials) ub.password_change(cli_param.user_credentials)
if not limiter:
log.info('*** "flask-limiter" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-limiter" ***')
print('*** "flask-limiter" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-limiter" ***')
web_server.stop(True)
sys.exit(8)
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
log.info( log.info(
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, ' '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
@ -141,13 +134,6 @@ def create_app():
'please update your installation to Python3 ***') 'please update your installation to Python3 ***')
web_server.stop(True) web_server.stop(True)
sys.exit(5) sys.exit(5)
if not wtf_present:
log.info('*** "flask-WTF" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-WTF" ***')
print('*** "flask-WTF" is needed for calibre-web to run. '
'Please install it using pip: "pip install flask-WTF" ***')
web_server.stop(True)
sys.exit(7)
lm.login_view = 'web.login' lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous lm.anonymous_user = ub.Anonymous
@ -158,13 +144,21 @@ def create_app():
calibre_db.init_db() calibre_db.init_db()
updater_thread.init_updater(config, web_server) updater_thread.init_updater(config, web_server)
# Perform dry run of updater and exit afterwards # Perform dry run of updater and exit afterward
if cli_param.dry_run: if cli_param.dry_run:
updater_thread.dry_run() updater_thread.dry_run()
sys.exit(0) sys.exit(0)
updater_thread.start() updater_thread.start()
requirements = dependency_check()
for res in dependency_check() + dependency_check(True): for res in requirements:
if res['found'] == "not installed":
message = ('Cannot import {name} module, it is needed to run calibre-web, '
'please install it using "pip install {name}"').format(name=res["name"])
log.info(message)
print("*** " + message + " ***")
web_server.stop(True)
sys.exit(8)
for res in requirements + dependency_check(True):
log.info('*** "{}" version does not meet the requirements. ' log.info('*** "{}" version does not meet the requirements. '
'Should: {}, Found: {}, please consider installing required version ***' 'Should: {}, Found: {}, please consider installing required version ***'
.format(res['name'], .format(res['name'],

@ -1834,6 +1834,7 @@ def _configuration_update_helper():
_config_checkbox(to_save, "config_password_number") _config_checkbox(to_save, "config_password_number")
_config_checkbox(to_save, "config_password_lower") _config_checkbox(to_save, "config_password_lower")
_config_checkbox(to_save, "config_password_upper") _config_checkbox(to_save, "config_password_upper")
_config_checkbox(to_save, "config_password_character")
_config_checkbox(to_save, "config_password_special") _config_checkbox(to_save, "config_password_special")
if 0 < int(to_save.get("config_password_min_length", "0")) < 41: if 0 < int(to_save.get("config_password_min_length", "0")) < 41:
_config_int(to_save, "config_password_min_length") _config_int(to_save, "config_password_min_length")

@ -165,6 +165,7 @@ class _Settings(_Base):
config_password_number = Column(Boolean, default=True) config_password_number = Column(Boolean, default=True)
config_password_lower = Column(Boolean, default=True) config_password_lower = Column(Boolean, default=True)
config_password_upper = Column(Boolean, default=True) config_password_upper = Column(Boolean, default=True)
config_password_character = Column(Boolean, default=True)
config_password_special = Column(Boolean, default=True) config_password_special = Column(Boolean, default=True)
config_session = Column(Integer, default=1) config_session = Column(Integer, default=1)
config_ratelimiter = Column(Boolean, default=True) config_ratelimiter = Column(Boolean, default=True)

@ -22,6 +22,7 @@ import random
import io import io
import mimetypes import mimetypes
import re import re
import regex
import shutil import shutil
import socket import socket
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -54,7 +55,8 @@ from . import calibre_db, cli_param
from .tasks.convert import TaskConvert from .tasks.convert import TaskConvert
from . import logger, config, db, ub, fs from . import logger, config, db, ub, fs
from . import gdriveutils as gd from . import gdriveutils as gd
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES from .constants import (STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES,
SUPPORTED_CALIBRE_BINARIES)
from .subproc_wrapper import process_wait from .subproc_wrapper import process_wait
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks.mail import TaskEmail from .tasks.mail import TaskEmail
@ -694,14 +696,16 @@ def valid_password(check_password):
if config.config_password_min_length > 0: if config.config_password_min_length > 0:
verify += r"^(?=.{" + str(config.config_password_min_length) + ",}$)" verify += r"^(?=.{" + str(config.config_password_min_length) + ",}$)"
if config.config_password_number: if config.config_password_number:
verify += r"(?=.*?\d)" verify += "(?=.*?\d)"
if config.config_password_lower: if config.config_password_lower:
verify += r"(?=.*?[a-z])" verify += "(?=.*?[\p{Ll}])"
if config.config_password_upper: if config.config_password_upper:
verify += r"(?=.*?[A-Z])" verify += "(?=.*?[\p{Lu}])"
if config.config_password_character:
verify += "(?=.*?[\p{Letter}])"
if config.config_password_special: if config.config_password_special:
verify += r"(?=.*?[^A-Za-z\s0-9])" verify += "(?=.*?[^\p{Letter}\s0-9])"
match = re.match(verify, unidecode.unidecode(check_password)) match = regex.match(verify, check_password)
if not match: if not match:
raise Exception(_("Password doesn't comply with password validation rules")) raise Exception(_("Password doesn't comply with password validation rules"))
return check_password return check_password

@ -9,6 +9,7 @@
"wordSequences": "Das Passwort enthält Buchstabensequenzen", "wordSequences": "Das Passwort enthält Buchstabensequenzen",
"wordLowercase": "Bitte mindestens einen Kleinbuchstaben verwenden", "wordLowercase": "Bitte mindestens einen Kleinbuchstaben verwenden",
"wordUppercase": "Bitte mindestens einen Großbuchstaben verwenden", "wordUppercase": "Bitte mindestens einen Großbuchstaben verwenden",
"word": "Bitte mindestens einen Buchstaben verwenden",
"wordOneNumber": "Bitte mindestens eine Ziffern verwenden", "wordOneNumber": "Bitte mindestens eine Ziffern verwenden",
"wordOneSpecialChar": "Bitte mindestens ein Sonderzeichen verwenden", "wordOneSpecialChar": "Bitte mindestens ein Sonderzeichen verwenden",
"errorList": "Fehler:", "errorList": "Fehler:",

@ -8,6 +8,7 @@
"wordRepetitions": "Too many repetitions", "wordRepetitions": "Too many repetitions",
"wordSequences": "Your password contains sequences", "wordSequences": "Your password contains sequences",
"wordLowercase": "Use at least one lowercase character", "wordLowercase": "Use at least one lowercase character",
"word": "Use at least one character",
"wordUppercase": "Use at least one uppercase character", "wordUppercase": "Use at least one uppercase character",
"wordOneNumber": "Use at least one number", "wordOneNumber": "Use at least one number",
"wordOneSpecialChar": "Use at least one special character", "wordOneSpecialChar": "Use at least one special character",

@ -144,13 +144,13 @@ try {
validation.wordTwoCharacterClasses = function(options, word, score) { validation.wordTwoCharacterClasses = function(options, word, score) {
var specialCharRE = new RegExp( var specialCharRE = new RegExp(
'(.' + options.rules.specialCharClass + ')' '(.' + options.rules.specialCharClass + ')', 'u'
); );
if ( if (
word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) || word.match(/(\p{Ll}.*\p{Lu})|(\p{Lu}.*\p{Ll})/u) ||
(word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) || (word.match(/(\p{Letter})/u) && word.match(/([0-9])/)) ||
(word.match(specialCharRE) && word.match(/[a-zA-Z0-9_]/)) (word.match(specialCharRE) && word.match(/[\p{Letter}0-9_]/u))
) { ) {
return score; return score;
} }
@ -202,11 +202,15 @@ try {
}; };
validation.wordLowercase = function(options, word, score) { validation.wordLowercase = function(options, word, score) {
return word.match(/[a-z]/) && score; return word.match(/\p{Ll}/u) && score;
}; };
validation.wordUppercase = function(options, word, score) { validation.wordUppercase = function(options, word, score) {
return word.match(/[A-Z]/) && score; return word.match(/\p{Lu}/u) && score;
};
validation.word = function(options, word, score) {
return word.match(/\p{Letter}/u) && score;
}; };
validation.wordOneNumber = function(options, word, score) { validation.wordOneNumber = function(options, word, score) {
@ -218,7 +222,7 @@ try {
}; };
validation.wordOneSpecialChar = function(options, word, score) { validation.wordOneSpecialChar = function(options, word, score) {
var specialCharRE = new RegExp(options.rules.specialCharClass); var specialCharRE = new RegExp(options.rules.specialCharClass, 'u');
return word.match(specialCharRE) && score; return word.match(specialCharRE) && score;
}; };
@ -228,27 +232,27 @@ try {
options.rules.specialCharClass + options.rules.specialCharClass +
'.*' + '.*' +
options.rules.specialCharClass + options.rules.specialCharClass +
')' ')', 'u'
); );
return word.match(twoSpecialCharRE) && score; return word.match(twoSpecialCharRE) && score;
}; };
validation.wordUpperLowerCombo = function(options, word, score) { validation.wordUpperLowerCombo = function(options, word, score) {
return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score; return word.match(/(\p{Ll}.*\p{Lu})|(\p{Lu}.*\p{Ll})/u) && score;
}; };
validation.wordLetterNumberCombo = function(options, word, score) { validation.wordLetterNumberCombo = function(options, word, score) {
return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score; return word.match(/([\p{Letter}])/u) && word.match(/([0-9])/) && score;
}; };
validation.wordLetterNumberCharCombo = function(options, word, score) { validation.wordLetterNumberCharCombo = function(options, word, score) {
var letterNumberCharComboRE = new RegExp( var letterNumberCharComboRE = new RegExp(
'([a-zA-Z0-9].*' + '([\p{Letter}0-9].*' +
options.rules.specialCharClass + options.rules.specialCharClass +
')|(' + ')|(' +
options.rules.specialCharClass + options.rules.specialCharClass +
'.*[a-zA-Z0-9])' '.*[\p{Letter}0-9])', 'u'
); );
return word.match(letterNumberCharComboRE) && score; return word.match(letterNumberCharComboRE) && score;
@ -341,6 +345,7 @@ defaultOptions.rules.scores = {
wordTwoCharacterClasses: 2, wordTwoCharacterClasses: 2,
wordRepetitions: -25, wordRepetitions: -25,
wordLowercase: 1, wordLowercase: 1,
word: 1,
wordUppercase: 3, wordUppercase: 3,
wordOneNumber: 3, wordOneNumber: 3,
wordThreeNumbers: 5, wordThreeNumbers: 5,
@ -361,6 +366,7 @@ defaultOptions.rules.activated = {
wordTwoCharacterClasses: true, wordTwoCharacterClasses: true,
wordRepetitions: true, wordRepetitions: true,
wordLowercase: true, wordLowercase: true,
word: true,
wordUppercase: true, wordUppercase: true,
wordOneNumber: true, wordOneNumber: true,
wordThreeNumbers: true, wordThreeNumbers: true,
@ -372,7 +378,7 @@ defaultOptions.rules.activated = {
wordIsACommonPassword: true wordIsACommonPassword: true
}; };
defaultOptions.rules.raisePower = 1.4; defaultOptions.rules.raisePower = 1.4;
defaultOptions.rules.specialCharClass = "(?=.*?[^A-Za-z\s0-9])"; //'[!,@,#,$,%,^,&,*,?,_,~]'; defaultOptions.rules.specialCharClass = "(?=.*?[^\\p{Letter}\\s0-9])"; //'[!,@,#,$,%,^,&,*,?,_,~]';
// List taken from https://github.com/danielmiessler/SecLists (MIT License) // List taken from https://github.com/danielmiessler/SecLists (MIT License)
defaultOptions.rules.commonPasswords = [ defaultOptions.rules.commonPasswords = [
'123456', '123456',

File diff suppressed because one or more lines are too long

@ -38,22 +38,20 @@ $(document).ready(function() {
showVerdicts: false, showVerdicts: false,
} }
options.rules= { options.rules= {
specialCharClass: "(?=.*?[^A-Za-z\\s0-9])", specialCharClass: "(?=.*?[^\\p{Letter}\\s0-9])",
activated: { activated: {
wordNotEmail: false, wordNotEmail: false,
wordMinLength: $('#password').data("min"), wordMinLength: $('#password').data("min"),
// wordMaxLength: false,
// wordInvalidChar: true,
wordSimilarToUsername: false, wordSimilarToUsername: false,
wordSequences: false, wordSequences: false,
wordTwoCharacterClasses: false, wordTwoCharacterClasses: false,
wordRepetitions: false, wordRepetitions: false,
wordLowercase: $('#password').data("lower") === "True" ? true : false, wordLowercase: $('#password').data("lower") === "True" ? true : false,
wordUppercase: $('#password').data("upper") === "True" ? true : false, wordUppercase: $('#password').data("upper") === "True" ? true : false,
word: $('#password').data("word") === "True" ? true : false,
wordOneNumber: $('#password').data("number") === "True" ? true : false, wordOneNumber: $('#password').data("number") === "True" ? true : false,
wordThreeNumbers: false, wordThreeNumbers: false,
wordOneSpecialChar: $('#password').data("special") === "True" ? true : false, wordOneSpecialChar: $('#password').data("special") === "True" ? true : false,
// wordTwoSpecialChar: true,
wordUpperLowerCombo: false, wordUpperLowerCombo: false,
wordLetterNumberCombo: false, wordLetterNumberCombo: false,
wordLetterNumberCharCombo: false wordLetterNumberCharCombo: false

@ -410,6 +410,10 @@
<input type="checkbox" id="config_password_upper" name="config_password_upper" {% if config.config_password_upper %}checked{% endif %}> <input type="checkbox" id="config_password_upper" name="config_password_upper" {% if config.config_password_upper %}checked{% endif %}>
<label for="config_password_upper">{{_('Enforce uppercase characters')}}</label> <label for="config_password_upper">{{_('Enforce uppercase characters')}}</label>
</div> </div>
<div class="form-group" style="margin-left:10px;">
<input type="checkbox" id="config_password_character" name="config_password_character" {% if config.config_password_character %}checked{% endif %}>
<label for="config_password_lower">{{_('Enforce characters (needed For Chinese/Japanese/Korean Characters)')}}</label>
</div>
<div class="form-group" style="margin-left:10px;"> <div class="form-group" style="margin-left:10px;">
<input type="checkbox" id="config_password_special" name="config_password_special" {% if config.config_password_special %}checked{% endif %}> <input type="checkbox" id="config_password_special" name="config_password_special" {% if config.config_password_special %}checked{% endif %}>
<label for="config_password_special">{{_('Enforce special characters')}}</label> <label for="config_password_special">{{_('Enforce special characters')}}</label>

@ -21,7 +21,7 @@
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="password">{{_('Password')}}</label> <label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" name="password" id="password" data-lang="{{ current_user.locale }}" data-verify="{{ config.config_password_policy }}" {% if config.config_password_policy %} data-min={{ config.config_password_min_length }} data-special={{ config.config_password_special }} data-upper={{ config.config_password_upper }} data-lower={{ config.config_password_lower }} data-number={{ config.config_password_number }}{% endif %} value="" autocomplete="off"> <input type="password" class="form-control" name="password" id="password" data-lang="{{ current_user.locale }}" data-verify="{{ config.config_password_policy }}" {% if config.config_password_policy %} data-min={{ config.config_password_min_length }} data-word={{ config.config_password_character }} data-special={{ config.config_password_special }} data-upper={{ config.config_password_upper }} data-lower={{ config.config_password_lower }} data-number={{ config.config_password_number }}{% endif %} value="" autocomplete="off">
</div> </div>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
@ -177,7 +177,7 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/pwstrength/i18next.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/pwstrength/i18next.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/pwstrength/i18nextHttpBackend.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/pwstrength/i18nextHttpBackend.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/pwstrength/pwstrength-bootstrap.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/pwstrength/pwstrength-bootstrap.js') }}"></script>
<script src="{{ url_for('static', filename='js/password.js') }}"></script> <script src="{{ url_for('static', filename='js/password.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script> <script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endblock %} {% endblock %}

@ -18,3 +18,4 @@ flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.6.0 Flask-Limiter>=2.3.0,<3.6.0
regex>=2022.3.2,<2024.2.25

Loading…
Cancel
Save