fixed broken random IP binding and Improved Onionr code safety

This commit is contained in:
Kevin Froman 2019-09-08 19:21:36 -05:00
parent 8191854a2f
commit 57791c34a5
25 changed files with 70 additions and 48 deletions

3
.gitignore vendored
View File

@ -19,6 +19,7 @@ core
.vscode/* .vscode/*
venv/* venv/*
onionr/fs* onionr/fs*
onionr/tmp/*
*.dll *.dll
*.exe *.exe
@ -35,4 +36,4 @@ onionr/data/*.log
onionr-*.pkg.tar.gz onionr-*.pkg.tar.gz
pkg/ pkg/
src/ src/
spawnnodes.py spawnnodes.py

View File

@ -110,7 +110,7 @@ Everyone is welcome to contribute. Help is wanted for the following:
* Creation of a shared lib for use from other languages and faster proof-of-work * Creation of a shared lib for use from other languages and faster proof-of-work
* Android and IOS development * Android and IOS development
* Windows and Mac support (already partially supported, testers needed) * Windows and Mac support (already partially supported, testers needed)
* General bug fixes and development of new features * Bug fixes and development of new features
* Testing * Testing
* Translations/localizations * Translations/localizations
* UI/UX design * UI/UX design

View File

@ -20,6 +20,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
# Set the user's locale for encoding reasons # Set the user's locale for encoding reasons
import locale import locale
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')

View File

@ -118,7 +118,7 @@ def reload():
try: try:
with open(get_config_file(), 'r', encoding="utf8") as configfile: with open(get_config_file(), 'r', encoding="utf8") as configfile:
set_config(json.loads(configfile.read())) set_config(json.loads(configfile.read()))
except: except (FileNotFoundError, json.JSONDecodeError) as e:
pass pass
#logger.debug('Failed to parse configuration file.') #logger.debug('Failed to parse configuration file.')

View File

@ -26,10 +26,11 @@ def get_expired_blocks():
c = conn.cursor() c = conn.cursor()
date = int(epoch.get_epoch()) date = int(epoch.get_epoch())
execute = 'SELECT hash FROM hashes WHERE expire <= %s ORDER BY dateReceived;' % (date,) compiled = (date,)
execute = 'SELECT hash FROM hashes WHERE expire <= ? ORDER BY dateReceived;'
rows = list() rows = list()
for row in c.execute(execute): for row in c.execute(execute, compiled):
for i in row: for i in row:
rows.append(i) rows.append(i)
conn.close() conn.close()

View File

@ -34,12 +34,14 @@ def update_block_info(hash, key, data):
dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is
expire - expire date for a block expire - expire date for a block
''' '''
if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'): if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound',
return False 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'):
raise ValueError('Key must be in the allowed list')
conn = sqlite3.connect(dbfiles.block_meta_db, timeout=30) conn = sqlite3.connect(dbfiles.block_meta_db, timeout=30)
c = conn.cursor() c = conn.cursor()
args = (data, hash) args = (data, hash)
# Unfortunately, not really possible
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
conn.commit() conn.commit()
conn.close() conn.close()

View File

@ -86,10 +86,7 @@ def clear_daemon_queue():
conn = sqlite3.connect(dbfiles.daemon_queue_db, timeout=30) conn = sqlite3.connect(dbfiles.daemon_queue_db, timeout=30)
c = conn.cursor() c = conn.cursor()
try: c.execute('DELETE FROM commands;')
c.execute('DELETE FROM commands;') conn.commit()
conn.commit()
except:
pass
conn.close() conn.close()

View File

@ -66,7 +66,7 @@ def set_address_info(address, key, data):
command = (data, address) command = (data, address)
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'): if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'):
raise Exception("Got invalid database key when setting address info") raise ValueError("Got invalid database key when setting address info, must be in whitelist")
else: else:
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
conn.commit() conn.commit()

View File

@ -61,9 +61,8 @@ def set_peer_info(peer, key, data):
command = (data, peer) command = (data, peer)
# TODO: validate key on whitelist
if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'): if key not in ('id', 'name', 'pubkey', 'forwardKey', 'dateSeen', 'trust'):
raise Exception("Got invalid database key when setting peer info") raise ValueError("Got invalid database key when setting peer info")
c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command)
conn.commit() conn.commit()

View File

@ -1,12 +1,13 @@
import json import json
import onionrblockapi import onionrblockapi
from onionrutils import bytesconverter, stringvalidators from onionrutils import bytesconverter, stringvalidators
import onionrexceptions
class GetBlockData: class GetBlockData:
def __init__(self, client_api_inst=None): def __init__(self, client_api_inst=None):
return return
def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False): def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False):
assert stringvalidators.validate_hash(bHash) if not stringvalidators.validate_hash(bHash): raise onionrexceptions.InvalidHexHash("block hash not valid hash format")
bl = onionrblockapi.Block(bHash) bl = onionrblockapi.Block(bHash)
if decrypt: if decrypt:
bl.decrypt() bl.decrypt()

View File

@ -1,11 +1,30 @@
import random, socket import gevent
from gevent import socket, sleep
import secrets, random
import config, logger import config, logger
import os
# Hacky monkey patch so we can bind random localhosts without gevent trying to switch with an empty hub
socket.getfqdn = lambda n: n
def _get_acceptable_random_number()->int:
"""Return a cryptographically random number in the inclusive range (1, 255)"""
number = 0
while number == 0:
number = secrets.randbelow(0xFF)
return number
def set_bind_IP(filePath=''): def set_bind_IP(filePath=''):
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' '''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)'''
if config.get('general.random_bind_ip', True): if config.get('general.random_bind_ip', True):
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))] hostOctets = []
# Build the random localhost address
for i in range(3):
hostOctets.append(str(_get_acceptable_random_number()))
hostOctets = ['127'] + hostOctets
# Convert the localhost address to a normal string address
data = '.'.join(hostOctets) data = '.'.join(hostOctets)
# Try to bind IP. Some platforms like Mac block non normal 127.x.x.x # Try to bind IP. Some platforms like Mac block non normal 127.x.x.x
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:

View File

@ -76,7 +76,7 @@ class PrivateEndpoints:
@private_endpoints_bp.route('/waitforshare/<name>', methods=['post']) @private_endpoints_bp.route('/waitforshare/<name>', methods=['post'])
def waitforshare(name): def waitforshare(name):
'''Used to prevent the **public** api from sharing blocks we just created''' '''Used to prevent the **public** api from sharing blocks we just created'''
assert name.isalnum() if not name.isalnum(): raise ValueError('block hash needs to be alpha numeric')
if name in client_api.publicAPI.hideBlocks: if name in client_api.publicAPI.hideBlocks:
client_api.publicAPI.hideBlocks.remove(name) client_api.publicAPI.hideBlocks.remove(name)
return Response("removed") return Response("removed")

View File

@ -18,6 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import base64 import base64
import binascii
from flask import Blueprint, Response, request, abort from flask import Blueprint, Response, request, abort
import onionrblockapi, onionrexceptions import onionrblockapi, onionrexceptions
from onionrutils import stringvalidators from onionrutils import stringvalidators
@ -37,7 +38,7 @@ def site(name):
pass pass
try: try:
resp = base64.b64decode(resp) resp = base64.b64decode(resp)
except: except binascii.Error:
pass pass
if resp == 'Not Found' or not resp: if resp == 'Not Found' or not resp:
abort(404) abort(404)

View File

@ -90,8 +90,8 @@ class Block:
# Check for replay attacks # Check for replay attacks
try: try:
if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60: if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60:
assert cryptoutils.replay_validator(self.bmetadata['rply']) if not cryptoutils.replay_validator(self.bmetadata['rply']): raise onionrexceptions.ReplayAttack
except (AssertionError, KeyError, TypeError) as e: except (AssertionError, KeyError, TypeError, onionrexceptions.ReplayAttack) as e:
if not self.bypassReplayCheck: if not self.bypassReplayCheck:
# Zero out variables to prevent reading of replays # Zero out variables to prevent reading of replays
self.bmetadata = {} self.bmetadata = {}
@ -101,7 +101,7 @@ class Block:
self.signature = '' self.signature = ''
raise onionrexceptions.ReplayAttack('Signature is too old. possible replay attack') raise onionrexceptions.ReplayAttack('Signature is too old. possible replay attack')
try: try:
assert self.bmetadata['forwardEnc'] is True if not self.bmetadata['forwardEnc']: raise KeyError
except (AssertionError, KeyError) as e: except (AssertionError, KeyError) as e:
pass pass
else: else:

View File

@ -120,7 +120,7 @@ def insert_block(data: Union[str, bytes], header: str ='txt',
# ensure expire is integer and of sane length # ensure expire is integer and of sane length
if type(expire) is not type(None): if type(expire) is not type(None):
assert len(str(int(expire))) < 20 if not len(str(int(expire))) < 20: raise ValueError('expire must be valid int less than 20 digits in length')
metadata['expire'] = expire metadata['expire'] = expire
# send block data (and metadata) to POW module to get tokenized block data # send block data (and metadata) to POW module to get tokenized block data

View File

@ -32,8 +32,8 @@ def doExport(bHash):
def export_block(): def export_block():
exportDir = filepaths.export_location exportDir = filepaths.export_location
try: try:
assert stringvalidators.validate_hash(sys.argv[2]) if not stringvalidators.validate_hash(sys.argv[2]): raise ValueError
except (IndexError, AssertionError): except (IndexError, ValueError):
logger.error('No valid block hash specified.', terminal=True) logger.error('No valid block hash specified.', terminal=True)
sys.exit(1) sys.exit(1)
else: else:

View File

@ -32,8 +32,8 @@ def add_ID():
key_manager = keymanager.KeyManager() key_manager = keymanager.KeyManager()
try: try:
sys.argv[2] sys.argv[2]
assert sys.argv[2] == 'true' if not sys.argv[2].lower() == 'true': raise ValueError
except (IndexError, AssertionError) as e: except (IndexError, ValueError) as e:
newID = key_manager.addKey()[0] newID = key_manager.addKey()[0]
else: else:
logger.warn('Deterministic keys require random and long passphrases.', terminal=True) logger.warn('Deterministic keys require random and long passphrases.', terminal=True)

View File

@ -60,12 +60,11 @@ def getDifficultyForNewBlock(data, ourBlock=True):
return retData return retData
def getHashDifficulty(h): def getHashDifficulty(h: str):
''' '''
Return the amount of leading zeroes in a hex hash string (h) Return the amount of leading zeroes in a hex hash string (h)
''' '''
difficulty = 0 difficulty = 0
assert type(h) is str
for character in h: for character in h:
if character == '0': if character == '0':
difficulty += 1 difficulty += 1

View File

@ -37,7 +37,7 @@ class OnionrServices:
When a client wants to connect, contact their bootstrap address and tell them our When a client wants to connect, contact their bootstrap address and tell them our
ephemeral address for our service by creating a new ConnectionServer instance ephemeral address for our service by creating a new ConnectionServer instance
''' '''
assert stringvalidators.validate_transport(address) if not stringvalidators.validate_transport(address): raise ValueError('address must be valid')
BOOTSTRAP_TRIES = 10 # How many times to attempt contacting the bootstrap server BOOTSTRAP_TRIES = 10 # How many times to attempt contacting the bootstrap server
TRY_WAIT = 3 # Seconds to wait before trying bootstrap again TRY_WAIT = 3 # Seconds to wait before trying bootstrap again
# HTTP is fine because .onion/i2p is encrypted/authenticated # HTTP is fine because .onion/i2p is encrypted/authenticated

View File

@ -54,8 +54,8 @@ def bootstrap_client_service(peer, comm_inst=None, bootstrap_timeout=300):
http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None) http_server = WSGIServer(('127.0.0.1', bootstrap_port), bootstrap_app, log=None)
try: try:
assert comm_inst is not None if comm_inst is None: raise ValueError
except (AttributeError, AssertionError) as e: except (AttributeError, ValueError) as e:
pass pass
else: else:
comm_inst.service_greenlets.append(http_server) comm_inst.service_greenlets.append(http_server)

View File

@ -58,10 +58,10 @@ def deleteBlock(blockHash):
return True return True
def store(data, blockHash=''): def store(data, blockHash=''):
assert stringvalidators.validate_hash(blockHash) if not stringvalidators.validate_hash(blockHash): raise ValueError
ourHash = hashers.sha3_hash(data) ourHash = hashers.sha3_hash(data)
if blockHash != '': if blockHash != '':
assert ourHash == blockHash if not ourHash == blockHash: raise ValueError('Hash specified does not meet internal hash check')
else: else:
blockHash = ourHash blockHash = ourHash
@ -72,7 +72,7 @@ def store(data, blockHash=''):
blockFile.write(data) blockFile.write(data)
def getData(bHash): def getData(bHash):
assert stringvalidators.validate_hash(bHash) if not stringvalidators.validate_hash(bHash): raise ValueError
bHash = bytesconverter.bytes_to_str(bHash) bHash = bytesconverter.bytes_to_str(bHash)

View File

@ -58,8 +58,9 @@ def process_block_metadata(blockHash: str):
# Set block expire time if specified # Set block expire time if specified
try: try:
expireTime = int(myBlock.getHeader('expire')) expireTime = int(myBlock.getHeader('expire'))
assert len(str(expireTime)) < 20 # test that expire time is an integer of sane length (for epoch) # test that expire time is an integer of sane length (for epoch)
except (AssertionError, ValueError, TypeError) as e: if not len(str(expireTime)) < 20: raise ValueError('timestamp invalid')
except (ValueError, TypeError) as e:
expireTime = onionrvalues.DEFAULT_EXPIRE + curTime expireTime = onionrvalues.DEFAULT_EXPIRE + curTime
finally: finally:
expireTime = min(expireTime, curTime + onionrvalues.DEFAULT_EXPIRE) expireTime = min(expireTime, curTime + onionrvalues.DEFAULT_EXPIRE)

View File

@ -86,14 +86,14 @@ def validate_transport(id):
if peerType == 'i2p': if peerType == 'i2p':
try: try:
id.split('.b32.i2p')[2] id.split('.b32.i2p')[2]
except: except IndexError:
pass pass
else: else:
retVal = False retVal = False
elif peerType == 'onion': elif peerType == 'onion':
try: try:
id.split('.onion')[2] id.split('.onion')[2]
except: except IndexError:
pass pass
else: else:
retVal = False retVal = False

View File

@ -66,14 +66,14 @@ def validate_metadata(metadata, block_data) -> bool:
break break
elif i == 'expire': elif i == 'expire':
try: try:
assert int(metadata[i]) > epoch.get_epoch() if not int(metadata[i]) > epoch.get_epoch(): raise ValueError
except AssertionError: except ValueError:
logger.warn('Block is expired: %s less than %s' % (metadata[i], epoch.get_epoch())) logger.warn('Block is expired: %s less than %s' % (metadata[i], epoch.get_epoch()))
break break
elif i == 'encryptType': elif i == 'encryptType':
try: try:
assert metadata[i] in ('asym', 'sym', '') if not metadata[i] in ('asym', 'sym', ''): raise ValueError
except AssertionError: except ValueError:
logger.warn('Invalid encryption mode') logger.warn('Invalid encryption mode')
break break
elif i == 'sig': elif i == 'sig':

View File

@ -100,8 +100,8 @@ class PlainEncryption:
logger.info('Decrypted Message: \n\n%s' % data['data'], terminal=True) logger.info('Decrypted Message: \n\n%s' % data['data'], terminal=True)
try: try:
logger.info("Signing public key: %s" % (data['signer'],), terminal=True) logger.info("Signing public key: %s" % (data['signer'],), terminal=True)
assert signing.ed_verify(data['data'], data['signer'], data['sig']) != False if not signing.ed_verify(data['data'], data['signer'], data['sig']): raise ValueError
except (AssertionError, KeyError) as e: except (ValueError, KeyError) as e:
logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE", terminal=True) logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE", terminal=True)
else: else:
logger.info("Message has good signature.", terminal=True) logger.info("Message has good signature.", terminal=True)