Repository: Bitmessage/PyBitmessage

Issue:
Don't create keyfile with insecure permissions
Issue #258 is similar, but slightly different.

Even with the fix from pull/#262, there is a (milder) vulnerability: on some systems a malicious process can get a file handle during the short time that keys.dat remains in a readable state. If the user creates a key while the malicious process has a valid handle, the malware can read the key. Even after permissions change!

 The fix is to ensure the keys.dat file is never readable except by the owner. On posix systems this should be a simple os.umask(0o077) before any file creation. But I don't know how to do this in a platform-independent way, and I have very little time for a few weeks.



Discussion:

Resolving patch:
diff --git a/src/helper_bootstrap.py b/src/helper_bootstrap.py
--- 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
--- a/src/helper_startup.py
+++ b/src/helper_startup.py
@@ -74,5 +74,7 @@ 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
--- a/src/shared.py
+++ b/src/shared.py
@@ -8,21 +8,26 @@
 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 *
+from debug import logger
 import highlevelcrypto
 import shared
 from debug import logger
 
+
 config = ConfigParser.SafeConfigParser()
 myECCryptorObjects = {}
 MyECSubscriptionCryptorObjects = {}
@@ -137,6 +142,7 @@ def lookupAppdataFolder():
             logger.info("Moving data folder to %s" % (dataFolder))
             move(path.join(environ["HOME"], ".%s" % APPNAME), dataFolder)
         except IOError:
+            # Old directory may not exist.
             pass
         dataFolder = dataFolder + '/'
     return dataFolder
@@ -182,23 +188,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 ""
 
 
@@ -207,19 +216,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...')
@@ -270,6 +292,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,3 +329,39 @@ def fixPotentiallyInvalidUTF8Data(text):
     except:
         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
+


Test patch: