diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index cfbfdd6c..15acf545 100644 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -720,7 +720,6 @@ if __name__ == "__main__": helper_bootstrap.knownNodes() helper_bootstrap.dns() - # Start the address generation thread addressGeneratorThread = addressGenerator() addressGeneratorThread.daemon = True # close the main program even if there are threads left diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index bd9a97f1..459b360f 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -29,6 +29,8 @@ import os from pyelliptic.openssl import OpenSSL import pickle import platform +import debug +from debug import logger try: from PyQt4 import QtCore, QtGui @@ -1876,7 +1878,14 @@ class MyForm(QtGui.QMainWindow): shared.knownNodesLock.release() os.remove(shared.appdata + 'keys.dat') os.remove(shared.appdata + 'knownnodes.dat') + previousAppdataLocation = shared.appdata shared.appdata = '' + debug.restartLoggingInUpdatedAppdataLocation() + try: + os.remove(previousAppdataLocation + 'debug.log') + os.remove(previousAppdataLocation + 'debug.log.1') + except: + pass if shared.appdata == '' and not self.settingsDialogInstance.ui.checkBoxPortableMode.isChecked(): # If we ARE using portable mode now but the user selected that we shouldn't... shared.appdata = shared.lookupAppdataFolder() @@ -1896,6 +1905,12 @@ class MyForm(QtGui.QMainWindow): shared.knownNodesLock.release() os.remove('keys.dat') os.remove('knownnodes.dat') + debug.restartLoggingInUpdatedAppdataLocation() + try: + os.remove('debug.log') + os.remove('debug.log.1') + except: + pass def click_radioButtonBlacklist(self): if shared.config.get('bitmessagesettings', 'blackwhitelist') == 'white': diff --git a/src/class_sqlThread.py b/src/class_sqlThread.py index da5ace77..f1a428d2 100644 --- a/src/class_sqlThread.py +++ b/src/class_sqlThread.py @@ -5,6 +5,7 @@ import time import shutil # used for moving the messages.dat file import sys import os +from debug import logger # This thread exists because SQLITE3 is so un-threadsafe that we must # submit queries to it and it puts results back in a different queue. They diff --git a/src/debug.py b/src/debug.py index 034d3102..fe7815e7 100644 --- a/src/debug.py +++ b/src/debug.py @@ -23,48 +23,59 @@ import shared # TODO(xj9): Get from a config file. log_level = 'DEBUG' -logging.config.dictConfig({ - 'version': 1, - 'formatters': { - 'default': { - 'format': '%(asctime)s - %(levelname)s - %(message)s', +def configureLogging(): + logging.config.dictConfig({ + 'version': 1, + 'formatters': { + 'default': { + 'format': '%(asctime)s - %(levelname)s - %(message)s', + }, }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'default', - 'level': log_level, - 'stream': 'ext://sys.stdout' + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default', + 'level': log_level, + 'stream': 'ext://sys.stdout' + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'default', + 'level': log_level, + 'filename': shared.appdata + 'debug.log', + 'maxBytes': 2097152, # 2 MiB + 'backupCount': 1, + } }, - 'file': { - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'default', + 'loggers': { + 'console_only': { + 'handlers': ['console'], + 'propagate' : 0 + }, + 'file_only': { + 'handlers': ['file'], + 'propagate' : 0 + }, + 'both': { + 'handlers': ['console', 'file'], + 'propagate' : 0 + }, + }, + 'root': { 'level': log_level, - 'filename': shared.appdata + 'debug.log', - 'maxBytes': 2097152, # 2 MiB - 'backupCount': 1, - } - }, - 'loggers': { - 'console_only': { 'handlers': ['console'], - 'propagate' : 0 }, - 'file_only': { - 'handlers': ['file'], - 'propagate' : 0 - }, - 'both': { - 'handlers': ['console', 'file'], - 'propagate' : 0 - }, - }, - 'root': { - 'level': log_level, - 'handlers': ['console'], - }, -}) + }) # TODO (xj9): Get from a config file. #logger = logging.getLogger('console_only') +configureLogging() logger = logging.getLogger('both') + +def restartLoggingInUpdatedAppdataLocation(): + global logger + for i in list(logger.handlers): + logger.removeHandler(i) + i.flush() + i.close() + configureLogging() + logger = logging.getLogger('both') \ No newline at end of file diff --git a/src/helper_bootstrap.py b/src/helper_bootstrap.py index 296dda6b..c3d5c1fd 100644 --- a/src/helper_bootstrap.py +++ b/src/helper_bootstrap.py @@ -33,7 +33,7 @@ def dns(): print 'Adding', item[4][0], 'to knownNodes based on DNS boostrap method' shared.knownNodes[1][item[4][0]] = (8080, int(time.time())) except: - print 'bootstrap8080.bitmessage.org DNS bootstraping failed.' + print 'bootstrap8080.bitmessage.org DNS bootstrapping failed.' try: for item in socket.getaddrinfo('bootstrap8444.bitmessage.org', 80): print 'Adding', item[4][0], 'to knownNodes based on DNS boostrap method' diff --git a/src/helper_startup.py b/src/helper_startup.py index aaf71709..46150d4d 100644 --- a/src/helper_startup.py +++ b/src/helper_startup.py @@ -76,6 +76,8 @@ def loadConfig(): print 'Creating new config files in', shared.appdata if not os.path.exists(shared.appdata): os.makedirs(shared.appdata) + if not sys.platform.startswith('win'): + os.umask(0o077) with open(shared.appdata + 'keys.dat', 'wb') as configfile: shared.config.write(configfile) diff --git a/src/shared.py b/src/shared.py index 5abeeb96..557b332d 100644 --- a/src/shared.py +++ b/src/shared.py @@ -8,22 +8,26 @@ maximumAgeOfNodesThatIAdvertiseToOthers = 10800 # Equals three hours useVeryEasyProofOfWorkForTesting = False # If you set this to True while on the normal network, you won't be able to send or sometimes receive messages. -import threading -import sys -from addresses import * -import highlevelcrypto -import Queue -import pickle -import os -import time +# Libraries. import ConfigParser -import socket +import os +import pickle +import Queue import random +import socket +import sys +import stat +import threading +import time + +# Project imports. +from addresses import * import highlevelcrypto import shared import helper_startup + config = ConfigParser.SafeConfigParser() myECCryptorObjects = {} MyECSubscriptionCryptorObjects = {} @@ -118,8 +122,11 @@ def lookupAppdataFolder(): if "HOME" in environ: dataFolder = path.join(os.environ["HOME"], "Library/Application Support/", APPNAME) + '/' else: - logger.critical('Could not find home folder, please report this message and your ' - 'OS X version to the BitMessage Github.') + stringToLog = 'Could not find home folder, please report this message and your OS X version to the BitMessage Github.' + if 'logger' in globals(): + logger.critical(stringToLog) + else: + print stringToLog sys.exit() elif 'win32' in sys.platform or 'win64' in sys.platform: @@ -133,9 +140,14 @@ def lookupAppdataFolder(): # Migrate existing data to the proper location if this is an existing install try: - logger.info("Moving data folder to %s" % (dataFolder)) move(path.join(environ["HOME"], ".%s" % APPNAME), dataFolder) + stringToLog = "Moving data folder to %s" % (dataFolder) + if 'logger' in globals(): + logger.info(stringToLog) + else: + print stringToLog except IOError: + # Old directory may not exist. pass dataFolder = dataFolder + '/' return dataFolder @@ -181,23 +193,26 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address): return False def safeConfigGetBoolean(section,field): - try: - return config.getboolean(section,field) - except: - return False + try: + return config.getboolean(section,field) + except: + return False def decodeWalletImportFormat(WIFstring): fullString = arithmetic.changebase(WIFstring,58,256) privkey = fullString[:-4] if fullString[-4:] != hashlib.sha256(hashlib.sha256(privkey).digest()).digest()[:4]: - sys.stderr.write('Major problem! When trying to decode one of your private keys, the checksum failed. Here is the PRIVATE key: %s\n' % str(WIFstring)) + logger.error('Major problem! When trying to decode one of your private keys, the checksum ' + 'failed. Here is the PRIVATE key: %s\n' % str(WIFstring)) return "" else: #checksum passed if privkey[0] == '\x80': return privkey[1:] else: - sys.stderr.write('Major problem! When trying to decode one of your private keys, the checksum passed but the key doesn\'t begin with hex 80. Here is the PRIVATE key: %s\n' % str(WIFstring)) + logger.error('Major problem! When trying to decode one of your private keys, the ' + 'checksum passed but the key doesn\'t begin with hex 80. Here is the ' + 'PRIVATE key: %s\n' % str(WIFstring)) return "" @@ -206,19 +221,32 @@ def reloadMyAddressHashes(): myECCryptorObjects.clear() myAddressesByHash.clear() #myPrivateKeys.clear() + + keyfileSecure = checkSensitiveFilePermissions(appdata + 'keys.dat') configSections = config.sections() + hasEnabledKeys = False for addressInKeysFile in configSections: if addressInKeysFile <> 'bitmessagesettings': isEnabled = config.getboolean(addressInKeysFile, 'enabled') if isEnabled: + hasEnabledKeys = True status,addressVersionNumber,streamNumber,hash = decodeAddress(addressInKeysFile) if addressVersionNumber == 2 or addressVersionNumber == 3: - privEncryptionKey = decodeWalletImportFormat(config.get(addressInKeysFile, 'privencryptionkey')).encode('hex') #returns a simple 32 bytes of information encoded in 64 Hex characters, or null if there was an error + # Returns a simple 32 bytes of information encoded in 64 Hex characters, + # or null if there was an error. + privEncryptionKey = decodeWalletImportFormat( + config.get(addressInKeysFile, 'privencryptionkey')).encode('hex') + if len(privEncryptionKey) == 64:#It is 32 bytes encoded as 64 hex characters myECCryptorObjects[hash] = highlevelcrypto.makeCryptor(privEncryptionKey) myAddressesByHash[hash] = addressInKeysFile + else: - sys.stderr.write('Error in reloadMyAddressHashes: Can\'t handle address versions other than 2 or 3.\n') + logger.error('Error in reloadMyAddressHashes: Can\'t handle address ' + 'versions other than 2 or 3.\n') + + if not keyfileSecure: + fixSensitiveFilePermissions(appdata + 'keys.dat', hasEnabledKeys) def reloadBroadcastSendersForWhichImWatching(): logger.debug('reloading subscriptions...') @@ -269,6 +297,7 @@ def doCleanShutdown(): sqlSubmitQueue.put('exit') sqlLock.release() logger.info('Finished flushing inventory.') + # Wait long enough to guarantee that any running proof of work worker threads will check the # shutdown variable and exit. If the main thread closes before they do then they won't stop. time.sleep(.25) @@ -306,5 +335,40 @@ def fixPotentiallyInvalidUTF8Data(text): output = 'Part of the message is corrupt. The message cannot be displayed the normal way.\n\n' + repr(text) return output +# Checks sensitive file permissions for inappropriate umask during keys.dat creation. +# (Or unwise subsequent chmod.) +# +# Returns true iff file appears to have appropriate permissions. +def checkSensitiveFilePermissions(filename): + if sys.platform == 'win32': + # TODO: This might deserve extra checks by someone familiar with + # Windows systems. + return True + else: + present_permissions = os.stat(filename)[0] + disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO + return present_permissions & disallowed_permissions == 0 + +# Fixes permissions on a sensitive file. +def fixSensitiveFilePermissions(filename, hasEnabledKeys): + if hasEnabledKeys: + logger.warning('Keyfile had insecure permissions, and there were enabled keys. ' + 'The truly paranoid should stop using them immediately.') + else: + logger.warning('Keyfile had insecure permissions, but there were no enabled keys.') + try: + present_permissions = os.stat(filename)[0] + disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO + allowed_permissions = ((1<<32)-1) ^ disallowed_permissions + new_permissions = ( + allowed_permissions & present_permissions) + os.chmod(filename, new_permissions) + + logger.info('Keyfile permissions automatically fixed.') + + except Exception, e: + logger.exception('Keyfile permissions could not be fixed.') + raise + helper_startup.loadConfig() from debug import logger \ No newline at end of file