Skip to content

Commit 68a2069

Browse files
author
Jonathan Warren
committed
Merge pull request #306 from Atheros1/master
Key file permissions
2 parents 823363b + 08694ec commit 68a2069

File tree

6 files changed

+141
-55
lines changed

6 files changed

+141
-55
lines changed

src/bitmessageqt/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from pyelliptic.openssl import OpenSSL
3030
import pickle
3131
import platform
32+
import debug
33+
from debug import logger
3234

3335
try:
3436
from PyQt4 import QtCore, QtGui
@@ -1874,7 +1876,14 @@ def click_actionSettings(self):
18741876
shared.knownNodesLock.release()
18751877
os.remove(shared.appdata + 'keys.dat')
18761878
os.remove(shared.appdata + 'knownnodes.dat')
1879+
previousAppdataLocation = shared.appdata
18771880
shared.appdata = ''
1881+
debug.restartLoggingInUpdatedAppdataLocation()
1882+
try:
1883+
os.remove(previousAppdataLocation + 'debug.log')
1884+
os.remove(previousAppdataLocation + 'debug.log.1')
1885+
except:
1886+
pass
18781887

18791888
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...
18801889
shared.appdata = shared.lookupAppdataFolder()
@@ -1894,6 +1903,12 @@ def click_actionSettings(self):
18941903
shared.knownNodesLock.release()
18951904
os.remove('keys.dat')
18961905
os.remove('knownnodes.dat')
1906+
debug.restartLoggingInUpdatedAppdataLocation()
1907+
try:
1908+
os.remove('debug.log')
1909+
os.remove('debug.log.1')
1910+
except:
1911+
pass
18971912

18981913
def click_radioButtonBlacklist(self):
18991914
if shared.config.get('bitmessagesettings', 'blackwhitelist') == 'white':

src/class_sqlThread.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil # used for moving the messages.dat file
66
import sys
77
import os
8+
from debug import logger
89

910
# This thread exists because SQLITE3 is so un-threadsafe that we must
1011
# submit queries to it and it puts results back in a different queue. They

src/debug.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,59 @@
2323
# TODO(xj9): Get from a config file.
2424
log_level = 'DEBUG'
2525

26-
logging.config.dictConfig({
27-
'version': 1,
28-
'formatters': {
29-
'default': {
30-
'format': '%(asctime)s - %(levelname)s - %(message)s',
26+
def configureLogging():
27+
logging.config.dictConfig({
28+
'version': 1,
29+
'formatters': {
30+
'default': {
31+
'format': '%(asctime)s - %(levelname)s - %(message)s',
32+
},
3133
},
32-
},
33-
'handlers': {
34-
'console': {
35-
'class': 'logging.StreamHandler',
36-
'formatter': 'default',
37-
'level': log_level,
38-
'stream': 'ext://sys.stdout'
34+
'handlers': {
35+
'console': {
36+
'class': 'logging.StreamHandler',
37+
'formatter': 'default',
38+
'level': log_level,
39+
'stream': 'ext://sys.stdout'
40+
},
41+
'file': {
42+
'class': 'logging.handlers.RotatingFileHandler',
43+
'formatter': 'default',
44+
'level': log_level,
45+
'filename': shared.appdata + 'debug.log',
46+
'maxBytes': 2097152, # 2 MiB
47+
'backupCount': 1,
48+
}
49+
},
50+
'loggers': {
51+
'console_only': {
52+
'handlers': ['console'],
53+
'propagate' : 0
54+
},
55+
'file_only': {
56+
'handlers': ['file'],
57+
'propagate' : 0
58+
},
59+
'both': {
60+
'handlers': ['console', 'file'],
61+
'propagate' : 0
62+
},
3963
},
40-
'file': {
41-
'class': 'logging.handlers.RotatingFileHandler',
42-
'formatter': 'default',
64+
'root': {
4365
'level': log_level,
44-
'filename': shared.appdata + 'debug.log',
45-
'maxBytes': 2097152, # 2 MiB
46-
'backupCount': 1,
47-
}
48-
},
49-
'loggers': {
50-
'console_only': {
5166
'handlers': ['console'],
52-
'propagate' : 0
53-
},
54-
'file_only': {
55-
'handlers': ['file'],
56-
'propagate' : 0
5767
},
58-
'both': {
59-
'handlers': ['console', 'file'],
60-
'propagate' : 0
61-
},
62-
},
63-
'root': {
64-
'level': log_level,
65-
'handlers': ['console'],
66-
},
67-
})
68+
})
6869
# TODO (xj9): Get from a config file.
6970
#logger = logging.getLogger('console_only')
71+
configureLogging()
7072
logger = logging.getLogger('both')
73+
74+
def restartLoggingInUpdatedAppdataLocation():
75+
global logger
76+
for i in list(logger.handlers):
77+
logger.removeHandler(i)
78+
i.flush()
79+
i.close()
80+
configureLogging()
81+
logger = logging.getLogger('both')

src/helper_bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def dns():
3333
print 'Adding', item[4][0], 'to knownNodes based on DNS boostrap method'
3434
shared.knownNodes[1][item[4][0]] = (8080, int(time.time()))
3535
except:
36-
print 'bootstrap8080.bitmessage.org DNS bootstraping failed.'
36+
print 'bootstrap8080.bitmessage.org DNS bootstrapping failed.'
3737
try:
3838
for item in socket.getaddrinfo('bootstrap8444.bitmessage.org', 80):
3939
print 'Adding', item[4][0], 'to knownNodes based on DNS boostrap method'

src/helper_startup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,7 @@ def loadConfig():
7474
print 'Creating new config files in', shared.appdata
7575
if not os.path.exists(shared.appdata):
7676
os.makedirs(shared.appdata)
77+
if not sys.platform.startswith('win'):
78+
os.umask(0o077)
7779
with open(shared.appdata + 'keys.dat', 'wb') as configfile:
7880
shared.config.write(configfile)

src/shared.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,26 @@
88
useVeryEasyProofOfWorkForTesting = False # If you set this to True while on the normal network, you won't be able to send or sometimes receive messages.
99

1010

11-
import threading
12-
import sys
13-
from addresses import *
14-
import highlevelcrypto
15-
import Queue
16-
import pickle
17-
import os
18-
import time
11+
# Libraries.
1912
import ConfigParser
20-
import socket
13+
import os
14+
import pickle
15+
import Queue
2116
import random
17+
import socket
18+
import sys
19+
import stat
20+
import threading
21+
import time
22+
23+
# Project imports.
24+
from addresses import *
2225
import highlevelcrypto
2326
import shared
2427
import helper_startup
2528

2629

30+
2731
config = ConfigParser.SafeConfigParser()
2832
myECCryptorObjects = {}
2933
MyECSubscriptionCryptorObjects = {}
@@ -143,6 +147,7 @@ def lookupAppdataFolder():
143147
else:
144148
print stringToLog
145149
except IOError:
150+
# Old directory may not exist.
146151
pass
147152
dataFolder = dataFolder + '/'
148153
return dataFolder
@@ -188,23 +193,26 @@ def isAddressInMyAddressBookSubscriptionsListOrWhitelist(address):
188193
return False
189194

190195
def safeConfigGetBoolean(section,field):
191-
try:
192-
return config.getboolean(section,field)
193-
except:
194-
return False
196+
try:
197+
return config.getboolean(section,field)
198+
except:
199+
return False
195200

196201
def decodeWalletImportFormat(WIFstring):
197202
fullString = arithmetic.changebase(WIFstring,58,256)
198203
privkey = fullString[:-4]
199204
if fullString[-4:] != hashlib.sha256(hashlib.sha256(privkey).digest()).digest()[:4]:
200-
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))
205+
logger.error('Major problem! When trying to decode one of your private keys, the checksum '
206+
'failed. Here is the PRIVATE key: %s\n' % str(WIFstring))
201207
return ""
202208
else:
203209
#checksum passed
204210
if privkey[0] == '\x80':
205211
return privkey[1:]
206212
else:
207-
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))
213+
logger.error('Major problem! When trying to decode one of your private keys, the '
214+
'checksum passed but the key doesn\'t begin with hex 80. Here is the '
215+
'PRIVATE key: %s\n' % str(WIFstring))
208216
return ""
209217

210218

@@ -213,19 +221,32 @@ def reloadMyAddressHashes():
213221
myECCryptorObjects.clear()
214222
myAddressesByHash.clear()
215223
#myPrivateKeys.clear()
224+
225+
keyfileSecure = checkSensitiveFilePermissions(appdata + 'keys.dat')
216226
configSections = config.sections()
227+
hasEnabledKeys = False
217228
for addressInKeysFile in configSections:
218229
if addressInKeysFile <> 'bitmessagesettings':
219230
isEnabled = config.getboolean(addressInKeysFile, 'enabled')
220231
if isEnabled:
232+
hasEnabledKeys = True
221233
status,addressVersionNumber,streamNumber,hash = decodeAddress(addressInKeysFile)
222234
if addressVersionNumber == 2 or addressVersionNumber == 3:
223-
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
235+
# Returns a simple 32 bytes of information encoded in 64 Hex characters,
236+
# or null if there was an error.
237+
privEncryptionKey = decodeWalletImportFormat(
238+
config.get(addressInKeysFile, 'privencryptionkey')).encode('hex')
239+
224240
if len(privEncryptionKey) == 64:#It is 32 bytes encoded as 64 hex characters
225241
myECCryptorObjects[hash] = highlevelcrypto.makeCryptor(privEncryptionKey)
226242
myAddressesByHash[hash] = addressInKeysFile
243+
227244
else:
228-
sys.stderr.write('Error in reloadMyAddressHashes: Can\'t handle address versions other than 2 or 3.\n')
245+
logger.error('Error in reloadMyAddressHashes: Can\'t handle address '
246+
'versions other than 2 or 3.\n')
247+
248+
if not keyfileSecure:
249+
fixSensitiveFilePermissions(appdata + 'keys.dat', hasEnabledKeys)
229250

230251
def reloadBroadcastSendersForWhichImWatching():
231252
logger.debug('reloading subscriptions...')
@@ -276,6 +297,7 @@ def doCleanShutdown():
276297
sqlSubmitQueue.put('exit')
277298
sqlLock.release()
278299
logger.info('Finished flushing inventory.')
300+
279301
# Wait long enough to guarantee that any running proof of work worker threads will check the
280302
# shutdown variable and exit. If the main thread closes before they do then they won't stop.
281303
time.sleep(.25)
@@ -313,5 +335,40 @@ def fixPotentiallyInvalidUTF8Data(text):
313335
output = 'Part of the message is corrupt. The message cannot be displayed the normal way.\n\n' + repr(text)
314336
return output
315337

338+
# Checks sensitive file permissions for inappropriate umask during keys.dat creation.
339+
# (Or unwise subsequent chmod.)
340+
#
341+
# Returns true iff file appears to have appropriate permissions.
342+
def checkSensitiveFilePermissions(filename):
343+
if sys.platform == 'win32':
344+
# TODO: This might deserve extra checks by someone familiar with
345+
# Windows systems.
346+
return True
347+
else:
348+
present_permissions = os.stat(filename)[0]
349+
disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO
350+
return present_permissions & disallowed_permissions == 0
351+
352+
# Fixes permissions on a sensitive file.
353+
def fixSensitiveFilePermissions(filename, hasEnabledKeys):
354+
if hasEnabledKeys:
355+
logger.warning('Keyfile had insecure permissions, and there were enabled keys. '
356+
'The truly paranoid should stop using them immediately.')
357+
else:
358+
logger.warning('Keyfile had insecure permissions, but there were no enabled keys.')
359+
try:
360+
present_permissions = os.stat(filename)[0]
361+
disallowed_permissions = stat.S_IRWXG | stat.S_IRWXO
362+
allowed_permissions = ((1<<32)-1) ^ disallowed_permissions
363+
new_permissions = (
364+
allowed_permissions & present_permissions)
365+
os.chmod(filename, new_permissions)
366+
367+
logger.info('Keyfile permissions automatically fixed.')
368+
369+
except Exception, e:
370+
logger.exception('Keyfile permissions could not be fixed.')
371+
raise
372+
316373
helper_startup.loadConfig()
317374
from debug import logger

0 commit comments

Comments
 (0)