Skip to content

Commit

Permalink
remove Code alerts and change password hash
Browse files Browse the repository at this point in the history
  • Loading branch information
cdhigh committed Jun 28, 2024
1 parent 3d587f2 commit 2847dea
Show file tree
Hide file tree
Showing 15 changed files with 158 additions and 146 deletions.
20 changes: 14 additions & 6 deletions application/back_end/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#数据库结构定义,使用这个文件隔离sql和nosql的差异,尽量向外提供一致的接口
#Visit <https://github.com/cdhigh/KindleEar> for the latest version
#Author: cdhigh <https://github.com/cdhigh>
import os, sys, random, hashlib, datetime
import os, sys, random, datetime
from operator import attrgetter
from ..utils import ke_encrypt, ke_decrypt, tz_now
from ..utils import PasswordManager, ke_encrypt, ke_decrypt, tz_now

if os.getenv('DATABASE_URL', '').startswith(("datastore", "mongodb", "redis", "pickle")):
from .db_models_nosql import *
Expand Down Expand Up @@ -136,12 +136,20 @@ def get_send_mail_service(self):
return srv

#使用自身的密钥加密和解密字符串
def encrypt(self, txt):
def encrypt(self, txt) -> str:
return ke_encrypt((txt or ''), self.cfg('secret_key')) #type:ignore
def decrypt(self, txt):
def decrypt(self, txt) -> str:
return ke_decrypt((txt or ''), self.cfg('secret_key')) #type:ignore
def hash_text(self, txt):
return hashlib.md5((txt + self.cfg('secret_key')).encode('utf-8')).hexdigest()
def hash_text(self, txt) -> str:
return PasswordManager(self.cfg('secret_key')).create_hash(txt)
def verify_password(self, password) -> bool:
new_hash = PasswordManager(self.cfg('secret_key')).migrate_password(self.passwd_hash, password)
if not new_hash:
return False
if new_hash != self.passwd_hash: #迁移至更安全的hash
self.passwd_hash = new_hash
self.save()
return True

#自定义字典的设置
def set_custom(self, item, value):
Expand Down
8 changes: 4 additions & 4 deletions application/lib/calibre/ebooks/conversion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def __init__(self, extra_opts=None, log=None):
self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"(softbreak|whitespace)\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.anyblank = re.compile(r'\s*(?P<openline><p[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>(\s*<div[^>]*>\s*</div>\s*)*){2,}(?!\s*<h\d)', re.IGNORECASE)
self.any_multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>(\s*<div[^>]*>\s*</div>\s*)*){2,}', re.IGNORECASE)
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>(?:\s*<div[^>]*>\s*</div>\s*)*){2,}(?!\s*<h\d)', re.IGNORECASE)
self.any_multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>(?:\s*<div[^>]*>\s*</div>\s*)*){2,}', re.IGNORECASE)
self.line_open = (
r"<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*"
r"(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*")
Expand Down Expand Up @@ -500,10 +500,10 @@ def merge_matches(match):
em = base_em + (em_per_line * lines)
if to_merge.find('whitespace'):
newline = self.any_multi_blank.sub('\n<p class="whitespace'+str(int(em * 10))+
'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', match.group(0))
'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', to_merge)
else:
newline = self.any_multi_blank.sub('\n<p class="softbreak'+str(int(em * 10))+
'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', match.group(0))
'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', to_merge)
return newline

html = self.any_multi_blank.sub(merge_matches, html)
Expand Down
2 changes: 1 addition & 1 deletion application/lib/calibre/ebooks/oeb/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):

MS_COVER_TYPE = 'other.ms-coverimage-standard'

ENTITY_RE = re.compile(r'&([a-zA-Z_:][a-zA-Z0-9.-_:]+);')
ENTITY_RE = re.compile(r'&([a-zA-Z_:][a-zA-Z0-9\.-_:]+);')
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
QNAME_RE = re.compile(r'^[{][^{}]+[}][^{}]+$')
PREFIXNAME_RE = re.compile(r'^[^:]+[:][^:]+')
Expand Down
11 changes: 2 additions & 9 deletions application/lib/opml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ class OutlineElement(object):

def __init__(self, root):
"""Initialize from the root <outline> node."""

self._root = root

def __getattr__(self, attr):

if attr in self._root.attrib:
return self._root.attrib[attr]
else:
Expand All @@ -22,7 +20,6 @@ def __getattr__(self, attr):
@property
def _outlines(self):
"""Return the available sub-outline objects as a seqeunce."""

return [OutlineElement(n) for n in self._root.xpath('./outline')]

def __len__(self):
Expand All @@ -36,13 +33,11 @@ class Opml(object):

def __init__(self, xml_tree):
"""Initialize the object using the parsed XML tree."""

self._tree = xml_tree

def __getattr__(self, attr):
"""Fall back attribute handler -- attempt to find the attribute in
the OPML <head>."""

result = self._tree.xpath('/opml/head/%s/text()' % attr)
if len(result) == 1:
return result[0]
Expand All @@ -52,7 +47,6 @@ def __getattr__(self, attr):
@property
def _outlines(self):
"""Return the available sub-outline objects as a seqeunce."""

return [OutlineElement(n) for n in self._tree.xpath(
'/opml/body/outline')]

Expand All @@ -63,11 +57,10 @@ def __getitem__(self, index):
return self._outlines[index]

def from_string(opml_text):

return Opml(lxml.etree.fromstring(opml_text))
parser = lxml.etree.XMLParser(resolve_entities=False)
return Opml(lxml.etree.fromstring(opml_text, parser=parser))

def parse(opml_url):

return Opml(lxml.etree.parse(opml_url))


Expand Down
2 changes: 1 addition & 1 deletion application/static/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ function DeleteUploadRecipe(id, title) {
});
}

//在页面下发插入bookmarklet
//在页面下方插入bookmarklet
function insertBookmarkletGmailThis(subscribeUrl, mailPrefix) {
var parser = $('<a>', {href: subscribeUrl});
var host = parser.prop('hostname');
Expand Down
8 changes: 1 addition & 7 deletions application/static/reader.css
Original file line number Diff line number Diff line change
Expand Up @@ -361,17 +361,11 @@ body::-webkit-scrollbar-thumb {
height: 60px;
}
}
@media (min-height: 768px) and (max-height: 1023px) {
@media (min-height: 768px) {
.nav-popmenu-row {
height: 80px;
}
}
@media (min-height: 1024px) {
.nav-popmenu-row {
height: 100px;
}
}

.tr-result {
position: absolute;
top: 50%;
Expand Down
57 changes: 55 additions & 2 deletions application/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,68 @@ def xml_unescape(txt):
txt = txt.replace("&apos;", "'")
return txt

#-----------以下几个函数为安全相关的
def new_secret_key(length=12):
#-----------以下为安全相关的工具函数--------------------

#使用此密码管理器逐步将以前的md5哈希的密码迁移到sha256密码
#尽管sha256不是密码哈希算法,但是已经足够好(配合加盐16字节)
#PBKDF2在3.10之后默认不提供,其他更好的bcrypt/scrypt等不是标准库
class PasswordManager:
def __init__(self, secretKey: str):
"""secretKey: 密码的salt"""
self.secretKey = secretKey
#迭代计算次数,大多数免费VPS性能不强,次数太多了影响正常使用体验
#何况sha256不是专门的密码哈希算法,单纯增加迭代次数带来的安全性提升有限
self.iterations = 1000

def migrate_password(self, stored_hash: str, password: str) -> str:
"""密码校验和哈希迁移函数,校验成功返回sha256哈希,否则返回空串
stored_hash: 数据库保存的密码哈希值
password: 需要校验的密码"""
password = password or ''
if len(stored_hash) == 32: #MD5哈希长度为32,sha256哈希长度为64(base64后44字节)
pwd_hash = ''
try:
pwd_hash = hashlib.md5((password + self.secretKey).encode('utf-8')).hexdigest()
except Exception as e:
print(f'PasswordManager migrate_password hash failed: {e}')
return self.create_hash(password) if pwd_hash == stored_hash else ''
else:
return stored_hash if self.verify_password(stored_hash, password) else ''

def create_hash(self, password: str) -> str:
"""创建密码的sha256哈希结果,返回base64字符串"""
hash_value = self._sha256_hash(password)
return base64.b64encode(hash_value).decode('utf-8')

def verify_password(self, stored_hash: str, password: str) -> bool:
"""使用sha256算法校验密码是否正确
stored_hash: 数据库保存的密码哈希值
password: 需要校验的密码"""
try:
decoded = base64.b64decode(stored_hash or '')
return self._sha256_hash(password) == decoded
except Exception as e:
print(f'PasswordManager b64decode password failed: {e}')
return False

def _sha256_hash(self, password: str) -> bytes:
"""计算密码的hash值,返回bytes"""
result = ((password or '') + self.secretKey).encode('utf-8')
for _ in range(self.iterations):
result = hashlib.sha256(result).digest()
return result

def new_secret_key(length=16):
"""生成一个安全密钥,为了更通用,只包含数字和ASCII大小写字母,不包含特殊字符"""
allchars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZYabcdefghijklmnopqrstuvwxyz'
return ''.join([secrets.choice(allchars) for i in range(length)])

def ke_encrypt(txt: str, key: str):
"""简单可逆加密"""
return _ke_auth_code(txt, key, 'encode')

def ke_decrypt(txt: str, key: str):
"""简单可逆解密"""
return _ke_auth_code(txt, key, 'decode')

def _ke_auth_code(txt: str, key: str, act: str='decode'):
Expand Down
73 changes: 31 additions & 42 deletions application/view/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,63 +158,52 @@ def AdminAccountChangePost(name: str, user: KeUser):
elif (p1 or p2) and (p1 != p2):
tips = _("The two new passwords are dismatch.")
else:
try:
if p1 or p2:
dbItem.passwd_hash = dbItem.hash_text(p1)
except:
tips = _("The password includes non-ascii chars.")
if p1 and p2: #只有提供了两个密码才修改数据库中保存的密码
dbItem.passwd_hash = dbItem.hash_text(p1)

dbItem.expiration_days = expiration
if expiration:
dbItem.expires = datetime.datetime.utcnow() + datetime.timedelta(days=expiration)
else:
dbItem.expiration_days = expiration
if expiration:
dbItem.expires = datetime.datetime.utcnow() + datetime.timedelta(days=expiration)
else:
dbItem.expires = None
if smType == 'admin':
dbItem.send_mail_service = {'service': 'admin'}
elif dbItem.send_mail_service.get('service') == 'admin': #从和管理员一致变更为独立设置
dbItem.send_mail_service = {}
dbItem.set_cfg('email', email)
dbItem.save()
tips = _("Change success.")
dbItem.expires = None
if smType == 'admin':
dbItem.send_mail_service = {'service': 'admin'}
elif dbItem.send_mail_service.get('service') == 'admin': #从和管理员一致变更为独立设置
dbItem.send_mail_service = {}
dbItem.set_cfg('email', email)
dbItem.save()
tips = _("Change success.")

return render_template('user_account.html', tips=tips, formTitle=_('Edit account'),
submitTitle=_('Change'), user=dbItem, tab='admin')

#修改一个账号的密码,返回执行结果字符串
def ChangePassword(user, orgPwd, p1, p2, email, shareKey):
try:
oldPwd = user.hash_text(orgPwd)
newPwd = user.hash_text(p1)
except:
return _("The password includes non-ascii chars.")

tips = _("Changes saved successfully.")
if not email or not shareKey:
tips = _("Some parameters are missing or wrong.")
elif p1 != p2:
tips = _("The two new passwords are dismatch.")
elif any((orgPwd, p1, p2)) and not user.verify_password(orgPwd):
#如果不修改密码,则三个密码都必须为空,有任何一个不为空,都表示要修改密码
tips = _("The old password is wrong.")
else:
if not (orgPwd or p1 or p2): # 如果不修改密码,则三个密码都必须为空
newPwd = user.passwd_hash
oldPwd = user.passwd_hash
tips = _("Changes saved successfully.")

if user.passwd_hash != oldPwd:
tips = _("The old password is wrong.")
else:
user.passwd_hash = newPwd
user.set_cfg('email', email)
shareLinks = user.share_links
shareLinks['key'] = shareKey
user.share_links = shareLinks
if user.name == app.config['ADMIN_NAME']: #如果管理员修改email,也同步更新其他用户的发件地址
if any((orgPwd, p1, p2)):
user.passwd_hash = user.hash_text(p1)
user.set_cfg('email', email)
shareLinks = user.share_links
shareLinks['key'] = shareKey
user.share_links = shareLinks
if user.name == app.config['ADMIN_NAME']: #如果管理员修改email,也同步更新其他用户的发件地址
user.set_cfg('sender', email)
SyncSenderAddress(user)
else: #其他人修改自己的email,根据设置确定是否要同步到发件地址
sm_service = user.send_mail_service
if not sm_service or sm_service.get('service', 'admin') != 'admin':
user.set_cfg('sender', email)
SyncSenderAddress(user)
else: #其他人修改自己的email,根据设置确定是否要同步到发件地址
sm_service = user.send_mail_service
if not sm_service or sm_service.get('service', 'admin') != 'admin':
user.set_cfg('sender', email)

user.save()
user.save()
return tips

#将管理员的email同步到所有用户
Expand Down
5 changes: 3 additions & 2 deletions application/view/adv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re, io, textwrap, json
from urllib.parse import unquote, urljoin, urlparse
from bs4 import BeautifulSoup
from html import escape
from flask import Blueprint, url_for, render_template, redirect, session, send_file, abort, current_app as app
from flask_babel import gettext as _
from PIL import Image
Expand Down Expand Up @@ -456,7 +457,7 @@ def DbImage(id_: str, user: KeUser):
@login_required()
def AdvOAuth2(authType: str, user: KeUser):
if authType.lower() != 'pocket':
return 'Auth Type ({}) Unsupported!'.format(authType)
return 'Auth Type ({}) Unsupported!'.format(escape(authType))

cbUrl = urljoin(app.config['APP_DOMAIN'], '/oauth2cb/pocket?redirect={}'.format(url_for("bpAdv.AdvArchive")))
pocket = Pocket(app.config['POCKET_CONSUMER_KEY'], cbUrl)
Expand All @@ -475,7 +476,7 @@ def AdvOAuth2(authType: str, user: KeUser):
@login_required()
def AdvOAuth2Callback(authType: str, user: KeUser):
if authType.lower() != 'pocket':
return 'Auth Type ({}) Unsupported!'.format(authType)
return 'Auth Type ({}) Unsupported!'.format(escape(authType))

pocket = Pocket(app.config['POCKET_CONSUMER_KEY'])
request_token = session.get('pocket_request_token', '')
Expand Down
7 changes: 4 additions & 3 deletions application/view/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#Author: cdhigh<https://github.com/cdhigh>
import json, re
from urllib.parse import unquote, urljoin
from html import escape
from bs4 import BeautifulSoup
from flask import Blueprint, request, make_response, current_app as app
from flask_babel import gettext as _
Expand All @@ -28,7 +29,7 @@ def ExtRemoveJsRoute():
url = args.get('url')
user = KeUser.get_or_none(KeUser.name == userName)
if not user or user.share_links.get('key') != key:
return HTML_TPL.format(_("The username '{}' does not exist.").format(userName))
return HTML_TPL.format(_("The username '{}' does not exist.").format(escape(userName)))
elif not url:
return HTML_TPL.format(_("Some parameters are missing or wrong."))

Expand All @@ -44,7 +45,7 @@ def ExtRemoveJsRoute():
resp.headers['Access-Control-Allow-Origin'] = '*' #允许跨域访问CSS/FONT之类的
return resp
else:
return HTML_TPL.format(GetRespErrorInfo(resp, url))
return HTML_TPL.format(GetRespErrorInfo(resp, escape(url)))


#接受扩展程序的请求,下载一个页面,将js全部去掉,根据特定的规则提取正文内容,然后返回
Expand All @@ -70,7 +71,7 @@ def ExtRenderWithRules():
opener = UrlOpener()
resp = opener.open(unquote(url))
if resp.status_code != 200:
return HTML_TPL.format(GetRespErrorInfo(resp, url))
return HTML_TPL.format(GetRespErrorInfo(resp, escape(url)))

encoding = resp.encoding or resp.apparent_encoding or 'utf-8'
rawHtml = resp.text
Expand Down
Loading

0 comments on commit 2847dea

Please sign in to comment.