IMP: Added encrypt_passwords (True/False) to ini as controller, IMP: If encrypt_passwords is enabled, will encrypt all passwords, and apikeys in the ini so as to obfuscate the items

This commit is contained in:
evilhero 2019-03-29 10:18:08 -04:00
parent a89bf56b08
commit 5805ed3877
3 changed files with 159 additions and 9 deletions

View File

@ -27,7 +27,7 @@ from cgi import escape
import urllib
import re
import mylar
from mylar import logger
from mylar import logger, encrypted
SESSION_KEY = '_cp_username'
@ -37,10 +37,18 @@ def check_credentials(username, password):
# Adapt to your needs
forms_user = cherrypy.request.config['auth.forms_username']
forms_pass = cherrypy.request.config['auth.forms_password']
if username == forms_user and password == forms_pass:
return None
edc = encrypted.Encryptor(forms_pass)
ed_chk = edc.decrypt_it()
if mylar.CONFIG.ENCRYPT_PASSWORDS is True:
if username == forms_user and all([ed_chk['status'] is True, ed_chk['password'] == password]):
return None
else:
return u"Incorrect username or password."
else:
return u"Incorrect username or password."
if username == forms_user and password == forms_pass:
return None
else:
return u"Incorrect username or password."
def check_auth(*args, **kwargs):
"""A tool that looks in config for 'auth.require'. If found and it

View File

@ -10,7 +10,7 @@ import threading
import re
import ConfigParser
import mylar
from mylar import logger, helpers
from mylar import logger, helpers, encrypted
config = ConfigParser.SafeConfigParser()
@ -78,6 +78,7 @@ _CONFIG_DEFINITIONS = OrderedDict({
'FORMAT_BOOKTYPE': (bool, 'General', False),
'CLEANUP_CACHE': (bool, 'General', False),
'SECURE_DIR': (str, 'General', None),
'ENCRYPT_PASSWORDS': (bool, 'General', False),
'RSS_CHECKINTERVAL': (int, 'Scheduler', 20),
'SEARCH_INTERVAL': (int, 'Scheduler', 360),
@ -385,7 +386,7 @@ class Config(object):
count = sum(1 for line in open(self._config_file))
else:
count = 0
self.newconfig = 9
self.newconfig = 10
if count == 0:
CONFIG_VERSION = 0
MINIMALINI = False
@ -505,9 +506,11 @@ class Config(object):
shutil.move(self._config_file, os.path.join(mylar.DATA_DIR, 'config.ini.backup'))
except:
print('Unable to make proper backup of config file in %s' % os.path.join(mylar.DATA_DIR, 'config.ini.backup'))
if self.CONFIG_VERSION < 9:
if self.CONFIG_VERSION < 10:
print('Attempting to update configuration..')
#torznab multiple entries merged into extra_torznabs value
#8-torznab multiple entries merged into extra_torznabs value
#9-remote rtorrent ssl option
#10-encryption of all keys/passwords.
self.config_update()
setattr(self, 'CONFIG_VERSION', str(self.newconfig))
config.set('General', 'CONFIG_VERSION', str(self.newconfig))
@ -555,7 +558,7 @@ class Config(object):
config.remove_option('Torznab', 'torznab_category')
config.remove_option('Torznab', 'torznab_verify')
print('Successfully removed outdated config entries.')
if self.newconfig == 9:
if self.newconfig < 9:
#rejig rtorrent settings due to change.
try:
if all([self.RTORRENT_SSL is True, not self.RTORRENT_HOST.startswith('http')]):
@ -565,6 +568,15 @@ class Config(object):
pass
config.remove_option('Rtorrent', 'rtorrent_ssl')
print('Successfully removed oudated config entries.')
if self.newconfig < 10:
#encrypt all passwords / apikeys / usernames in ini file.
#leave non-ini items (ie. memory) as un-encrypted items.
try:
if self.ENCRYPT_PASSWORDS is True:
self.encrypt_items(mode='encrypt', updateconfig=True)
except Exception as e:
print('Error: %s' % e)
print('Successfully updated config to version 10 ( password / apikey - .ini encryption )')
print('Configuration upgraded to version %s' % self.newconfig)
def check_section(self, section, key):
@ -713,6 +725,10 @@ class Config(object):
else:
pass
if self.ENCRYPT_PASSWORDS is True:
self.encrypt_items(mode='encrypt')
def writeconfig(self, values=None):
logger.fdebug("Writing configuration to file")
self.provider_sequence()
@ -741,6 +757,74 @@ class Config(object):
except IOError as e:
logger.warn("Error writing configuration file: %s", e)
def encrypt_items(self, mode='encrypt', updateconfig=False):
encryption_list = OrderedDict({
#key section key value
'HTTP_PASSWORD': ('Interface', 'http_password', self.HTTP_PASSWORD),
'SAB_PASSWORD': ('SABnzbd', 'sab_password', self.SAB_PASSWORD),
'SAB_APIKEY': ('SABnzbd', 'sab_apikey', self.SAB_APIKEY),
'NZBGET_PASSWORD': ('NZBGet', 'nzbget_password', self.NZBGET_PASSWORD),
'NZBSU_APIKEY': ('NZBsu', 'nzbsu_apikey', self.NZBSU_APIKEY),
'DOGNZB_APIKEY': ('DOGnzb', 'dognzb_apikey', self.DOGNZB_APIKEY),
'UTORRENT_PASSWORD': ('uTorrent', 'utorrent_password', self.UTORRENT_PASSWORD),
'TRANSMISSION_PASSWORD': ('Transmission', 'transmission_password', self.TRANSMISSION_PASSWORD),
'DELUGE_PASSWORD': ('Deluge', 'deluge_password', self.DELUGE_PASSWORD),
'QBITTORRENT_PASSWORD': ('qBittorrent', 'qbittorrent_password', self.QBITTORRENT_PASSWORD),
'RTORRENT_PASSWORD': ('Rtorrent', 'rtorrent_password', self.RTORRENT_PASSWORD),
'PROWL_KEYS': ('Prowl', 'prowl_keys', self.PROWL_KEYS),
'PUSHOVER_APIKEY': ('PUSHOVER', 'pushover_apikey', self.PUSHOVER_APIKEY),
'PUSHOVER_USERKEY': ('PUSHOVER', 'pushover_userkey', self.PUSHOVER_USERKEY),
'BOXCAR_TOKEN': ('BOXCAR', 'boxcar_token', self.BOXCAR_TOKEN),
'PUSHBULLET_APIKEY': ('PUSHBULLET', 'pushbullet_apikey', self.PUSHBULLET_APIKEY),
'TELEGRAM_TOKEN': ('TELEGRAM', 'telegram_token', self.TELEGRAM_TOKEN),
'COMICVINE_API': ('CV', 'comicvine_api', self.COMICVINE_API),
'PASSWORD_32P': ('32P', 'password_32p', self.PASSWORD_32P),
'PASSKEY_32P': ('32P', 'passkey_32p', self.PASSKEY_32P),
'USERNAME_32P': ('32P', 'username_32p', self.USERNAME_32P),
'SEEDBOX_PASS': ('Seedbox', 'seedbox_pass', self.SEEDBOX_PASS),
'TAB_PASS': ('Tablet', 'tab_pass', self.TAB_PASS),
'API_KEY': ('API', 'api_key', self.API_KEY),
'OPDS_PASSWORD': ('OPDS', 'opds_password', self.OPDS_PASSWORD),
'PP_SSHPASSWD': ('AutoSnatch', 'pp_sshpasswd', self.PP_SSHPASSWD),
})
new_encrypted = 0
for k,v in encryption_list.iteritems():
value = []
for x in v:
value.append(x)
if value[2] is not None:
if value[2][:5] == '^~$z$':
if mode == 'decrypt':
hp = encrypted.Encryptor(value[2])
decrypted_password = hp.decrypt_it()
if decrypted_password['status'] is False:
logger.warn('Password unable to decrypt - you might have to manually edit the ini for %s to reset the value' % value[1])
else:
if k != 'HTTP_PASSWORD':
setattr(self, k, decrypted_password['password'])
config.set(value[0], value[1], decrypted_password['password'])
else:
if k == 'HTTP_PASSWORD':
hp = encrypted.Encryptor(value[2])
decrypted_password = hp.decrypt_it()
if decrypted_password['status'] is False:
logger.warn('Password unable to decrypt - you might have to manually edit the ini for %s to reset the value' % value[1])
else:
setattr(self, k, decrypted_password['password'])
else:
hp = encrypted.Encryptor(value[2])
encrypted_password = hp.encrypt_it()
if encrypted_password['status'] is False:
logger.warn('Unable to encrypt password for %s - it has not been encrypted. Keeping it as it is.' % value[1])
else:
if k == 'HTTP_PASSWORD':
#make sure we set the http_password for signon to the encrypted value otherwise won't match
setattr(self, k, encrypted_password['password'])
config.set(value[0], value[1], encrypted_password['password'])
new_encrypted+=1
def configure(self, update=False, startup=False):
#force alt_pull = 2 on restarts regardless of settings
@ -880,6 +964,9 @@ class Config(object):
elif all([self.HTTP_USERNAME is None, self.HTTP_PASSWORD is None]):
self.AUTHENTICATION = 0
if self.ENCRYPT_PASSWORDS is True:
self.encrypt_items(mode='decrypt')
if all([self.IGNORE_TOTAL is True, self.IGNORE_HAVETOTAL is True]):
self.IGNORE_TOTAL = False
self.IGNORE_HAVETOTAL = False

55
mylar/encrypted.py Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Mylar.
#
# Mylar is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Mylar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Mylar. If not, see <http://www.gnu.org/licenses/>.
import random
import base64
import re
import sys
import os
import mylar
from mylar import logger
class Encryptor(object):
def __init__(self, password, chk_password=None):
self.password = password.encode('utf-8')
def encrypt_it(self):
try:
salt = os.urandom(8)
saltedhash = [salt[i] for i in range (0, len(salt))]
salted_pass = base64.b64encode('%s%s' % (self.password,salt))
except Exception as e:
logger.warn('Error when encrypting: %s' % e)
return {'status': False}
else:
return {'status': True, 'password': '^~$z$' + salted_pass}
def decrypt_it(self):
try:
if not self.password.startswith('^~$z$'):
logger.warn('Error not an encryption that I recognize.')
return {'status': False}
passd = base64.b64decode(self.password[5:]) #(base64.decodestring(self.password))
saltedhash = [bytes(passd[-8:])]
except Exception as e:
logger.warn('Error when decrypting password: %s' % e)
return {'status': False}
else:
return {'status': True, 'password': passd[:-8]}