483 lines
15 KiB
Python
483 lines
15 KiB
Python
# pylint: disable=too-many-boolean-expressions,too-many-return-statements,too-many-locals,too-many-statements
|
|
"""
|
|
protocol.py
|
|
===========
|
|
|
|
Low-level protocol-related functions.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import base64
|
|
from binascii import hexlify
|
|
import hashlib
|
|
import os
|
|
import random
|
|
import socket
|
|
import ssl
|
|
from struct import pack, unpack, Struct
|
|
import sys
|
|
import time
|
|
import traceback
|
|
|
|
import defaults
|
|
import highlevelcrypto
|
|
import state
|
|
from addresses import calculateInventoryHash, encodeVarint, decodeVarint, decodeAddress, varintDecodeError
|
|
from bmconfigparser import BMConfigParser
|
|
from debug import logger
|
|
from helper_sql import sqlExecute
|
|
from version import softwareVersion
|
|
import inventory
|
|
import queues
|
|
import workprover.utils
|
|
|
|
|
|
# Service flags
|
|
NODE_NETWORK = 1
|
|
NODE_SSL = 2
|
|
NODE_DANDELION = 8
|
|
|
|
# Bitfield flags
|
|
BITFIELD_DOESACK = 1
|
|
|
|
# Error types
|
|
STATUS_WARNING = 0
|
|
STATUS_ERROR = 1
|
|
STATUS_FATAL = 2
|
|
|
|
# Object types
|
|
OBJECT_GETPUBKEY = 0
|
|
OBJECT_PUBKEY = 1
|
|
OBJECT_MSG = 2
|
|
OBJECT_BROADCAST = 3
|
|
OBJECT_I2P = 0x493250
|
|
OBJECT_ADDR = 0x61646472
|
|
|
|
eightBytesOfRandomDataUsedToDetectConnectionsToSelf = pack(
|
|
'>Q', random.randrange(1, 18446744073709551615))
|
|
|
|
# Compiled struct for packing/unpacking headers
|
|
# New code should use CreatePacket instead of Header.pack
|
|
Header = Struct('!L12sL4s')
|
|
|
|
VersionPacket = Struct('>LqQ20s4s36sH')
|
|
|
|
def calculateDoubleHash(data):
|
|
return hashlib.sha512(hashlib.sha512(data).digest()).digest()
|
|
|
|
def calculateRipeHash(data):
|
|
return hashlib.new("ripemd160", hashlib.sha512(data).digest()).digest()
|
|
|
|
def calculateAddressTag(version, stream, ripe):
|
|
doubleHash = calculateDoubleHash(
|
|
encodeVarint(version) +
|
|
encodeVarint(stream) +
|
|
ripe
|
|
)
|
|
|
|
return doubleHash[: 32], doubleHash[32: ]
|
|
|
|
# ip addresses
|
|
|
|
|
|
def encodeHost(host):
|
|
"""Encode a given host to be used in low-level socket operations"""
|
|
if host.find('.onion') > -1:
|
|
return '\xfd\x87\xd8\x7e\xeb\x43' + base64.b32decode(host.split(".")[0], True)
|
|
elif host.find(':') == -1:
|
|
return '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + \
|
|
socket.inet_aton(host)
|
|
return socket.inet_pton(socket.AF_INET6, host)
|
|
|
|
|
|
def networkType(host):
|
|
"""Determine if a host is IPv4, IPv6 or an onion address"""
|
|
if host.find('.onion') > -1:
|
|
return 'onion'
|
|
elif host.find(':') == -1:
|
|
return 'IPv4'
|
|
return 'IPv6'
|
|
|
|
|
|
def checkIPAddress(host, private=False):
|
|
"""Returns hostStandardFormat if it is a valid IP address, otherwise returns False"""
|
|
if host[0:12] == '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF':
|
|
hostStandardFormat = socket.inet_ntop(socket.AF_INET, host[12:])
|
|
return checkIPv4Address(host[12:], hostStandardFormat, private)
|
|
elif host[0:6] == '\xfd\x87\xd8\x7e\xeb\x43':
|
|
# Onion, based on BMD/bitcoind
|
|
hostStandardFormat = base64.b32encode(host[6:]).lower() + ".onion"
|
|
if private:
|
|
return False
|
|
return hostStandardFormat
|
|
else:
|
|
try:
|
|
hostStandardFormat = socket.inet_ntop(socket.AF_INET6, host)
|
|
except ValueError:
|
|
return False
|
|
if hostStandardFormat == "":
|
|
# This can happen on Windows systems which are not 64-bit compatible
|
|
# so let us drop the IPv6 address.
|
|
return False
|
|
return checkIPv6Address(host, hostStandardFormat, private)
|
|
|
|
|
|
def checkIPv4Address(host, hostStandardFormat, private=False):
|
|
"""Returns hostStandardFormat if it is an IPv4 address, otherwise returns False"""
|
|
if host[0] == '\x7F': # 127/8
|
|
if not private:
|
|
logger.debug('Ignoring IP address in loopback range: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
if host[0] == '\x0A': # 10/8
|
|
if not private:
|
|
logger.debug('Ignoring IP address in private range: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
if host[0:2] == '\xC0\xA8': # 192.168/16
|
|
if not private:
|
|
logger.debug('Ignoring IP address in private range: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
if host[0:2] >= '\xAC\x10' and host[0:2] < '\xAC\x20': # 172.16/12
|
|
if not private:
|
|
logger.debug('Ignoring IP address in private range: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
return False if private else hostStandardFormat
|
|
|
|
|
|
def checkIPv6Address(host, hostStandardFormat, private=False):
|
|
"""Returns hostStandardFormat if it is an IPv6 address, otherwise returns False"""
|
|
if host == ('\x00' * 15) + '\x01':
|
|
if not private:
|
|
logger.debug('Ignoring loopback address: %s', hostStandardFormat)
|
|
return False
|
|
if host[0] == '\xFE' and (ord(host[1]) & 0xc0) == 0x80:
|
|
if not private:
|
|
logger.debug('Ignoring local address: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
if (ord(host[0]) & 0xfe) == 0xfc:
|
|
if not private:
|
|
logger.debug('Ignoring unique local address: %s', hostStandardFormat)
|
|
return hostStandardFormat if private else False
|
|
return False if private else hostStandardFormat
|
|
|
|
|
|
def haveSSL(server=False):
|
|
"""
|
|
Predicate to check if ECDSA server support is required and available
|
|
|
|
python < 2.7.9's ssl library does not support ECDSA server due to
|
|
missing initialisation of available curves, but client works ok
|
|
"""
|
|
if not server:
|
|
return True
|
|
elif sys.version_info >= (2, 7, 9):
|
|
return True
|
|
return False
|
|
|
|
|
|
def checkSocksIP(host):
|
|
"""Predicate to check if we're using a SOCKS proxy"""
|
|
try:
|
|
if state.socksIP is None or not state.socksIP:
|
|
state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname"))
|
|
# uninitialised
|
|
except NameError:
|
|
state.socksIP = socket.gethostbyname(BMConfigParser().get("bitmessagesettings", "sockshostname"))
|
|
# resolving failure
|
|
except socket.gaierror:
|
|
state.socksIP = BMConfigParser().get("bitmessagesettings", "sockshostname")
|
|
return state.socksIP == host
|
|
|
|
|
|
# Packet creation
|
|
|
|
|
|
def CreatePacket(command, payload=''):
|
|
"""Construct and return a number of bytes from a payload"""
|
|
payload_length = len(payload)
|
|
checksum = hashlib.sha512(payload).digest()[0:4]
|
|
|
|
b = bytearray(Header.size + payload_length)
|
|
Header.pack_into(b, 0, 0xE9BEB4D9, command, payload_length, checksum)
|
|
b[Header.size:] = payload
|
|
return bytes(b)
|
|
|
|
|
|
def assembleVersionMessage(remoteHost, remotePort, participatingStreams, server=False, nodeid=None):
|
|
"""Construct the payload of a version message, return the resultng bytes of running CreatePacket() on it"""
|
|
payload = ''
|
|
payload += pack('>L', 3) # protocol version.
|
|
# bitflags of the services I offer.
|
|
payload += pack(
|
|
'>q',
|
|
NODE_NETWORK |
|
|
(NODE_SSL if haveSSL(server) else 0) |
|
|
(NODE_DANDELION if state.dandelion else 0)
|
|
)
|
|
payload += pack('>q', int(time.time()))
|
|
|
|
payload += pack(
|
|
'>q', 1) # boolservices of remote connection; ignored by the remote host.
|
|
if checkSocksIP(remoteHost) and server: # prevent leaking of tor outbound IP
|
|
payload += encodeHost('127.0.0.1')
|
|
payload += pack('>H', 8444)
|
|
else:
|
|
payload += encodeHost(remoteHost)
|
|
payload += pack('>H', remotePort) # remote IPv6 and port
|
|
|
|
# bitflags of the services I offer.
|
|
payload += pack(
|
|
'>q',
|
|
NODE_NETWORK |
|
|
(NODE_SSL if haveSSL(server) else 0) |
|
|
(NODE_DANDELION if state.dandelion else 0)
|
|
)
|
|
# = 127.0.0.1. This will be ignored by the remote host. The actual remote connected IP will be used.
|
|
payload += '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF' + pack('>L', 2130706433)
|
|
# we have a separate extPort and incoming over clearnet or outgoing through clearnet
|
|
if BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and state.extPort \
|
|
and ((server and not checkSocksIP(remoteHost)) or
|
|
(BMConfigParser().get("bitmessagesettings", "socksproxytype") == "none" and not server)):
|
|
payload += pack('>H', state.extPort)
|
|
elif checkSocksIP(remoteHost) and server: # incoming connection over Tor
|
|
payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'onionport'))
|
|
else: # no extPort and not incoming over Tor
|
|
payload += pack('>H', BMConfigParser().getint('bitmessagesettings', 'port'))
|
|
|
|
random.seed()
|
|
if nodeid is not None:
|
|
payload += nodeid[0:8]
|
|
else:
|
|
payload += eightBytesOfRandomDataUsedToDetectConnectionsToSelf
|
|
userAgent = '/PyBitmessage:' + softwareVersion + '/'
|
|
payload += encodeVarint(len(userAgent))
|
|
payload += userAgent
|
|
|
|
# Streams
|
|
payload += encodeVarint(len(participatingStreams))
|
|
count = 0
|
|
for stream in sorted(participatingStreams):
|
|
payload += encodeVarint(stream)
|
|
count += 1
|
|
# protocol limit, see specification
|
|
if count >= 160000:
|
|
break
|
|
|
|
return CreatePacket('version', payload)
|
|
|
|
|
|
def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''):
|
|
"""Construct the payload of an error message, return the resultng bytes of running CreatePacket() on it"""
|
|
payload = encodeVarint(fatal)
|
|
payload += encodeVarint(banTime)
|
|
payload += encodeVarint(len(inventoryVector))
|
|
payload += inventoryVector
|
|
payload += encodeVarint(len(errorText))
|
|
payload += errorText
|
|
return CreatePacket('error', payload)
|
|
|
|
|
|
# Packet decoding
|
|
|
|
def decryptAndCheckV4Pubkey(payload, address, cryptor):
|
|
status, version, stream, ripe = decodeAddress(address)
|
|
|
|
readPosition = 20
|
|
|
|
try:
|
|
embeddedVersion, readLength = decodeVarint(payload[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
|
|
embeddedStream, readLength = decodeVarint(payload[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
except:
|
|
return None
|
|
|
|
if embeddedVersion != 4:
|
|
logger.info("Pubkey decryption failed due to address version mismatch")
|
|
|
|
return None
|
|
|
|
if embeddedStream != stream:
|
|
logger.info("Pubkey decryption failed due to stream number mismatch")
|
|
|
|
return None
|
|
|
|
result = payload[20: readPosition]
|
|
|
|
tag = payload[readPosition: readPosition + 32]
|
|
readPosition += 32
|
|
|
|
if len(tag) < 32:
|
|
return None
|
|
|
|
signedData = payload[8: readPosition]
|
|
ciphertext = payload[readPosition: ]
|
|
|
|
try:
|
|
plaintext = cryptor.decrypt(ciphertext)
|
|
except:
|
|
logger.info("Pubkey decryption failed")
|
|
|
|
return None
|
|
|
|
readPosition = 0
|
|
|
|
try:
|
|
bitfield = unpack(">I", plaintext[readPosition: readPosition + 4])
|
|
readPosition += 4
|
|
except:
|
|
return None
|
|
|
|
publicSigningKey = "\x04" + plaintext[readPosition: readPosition + 64]
|
|
readPosition += 64
|
|
|
|
publicEncryptionKey = "\x04" + plaintext[readPosition: readPosition + 64]
|
|
readPosition += 64
|
|
|
|
if len(publicSigningKey) != 65 or len(publicEncryptionKey) != 65:
|
|
return None
|
|
|
|
embeddedRipe = calculateRipeHash(publicSigningKey + publicEncryptionKey)
|
|
|
|
if embeddedRipe != ripe:
|
|
logger.info("Pubkey decryption failed due to RIPE mismatch")
|
|
|
|
return None
|
|
|
|
try:
|
|
byteDifficulty, readLength = decodeVarint(plaintext[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
|
|
lengthExtension, readLength = decodeVarint(plaintext[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
except:
|
|
return None
|
|
|
|
result += plaintext[: readPosition]
|
|
signedData += plaintext[: readPosition]
|
|
|
|
signatureLength, readLength = decodeVarint(plaintext[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
|
|
signature = plaintext[readPosition: readPosition + signatureLength]
|
|
|
|
if len(signature) != signatureLength:
|
|
return None
|
|
|
|
if not highlevelcrypto.verify(signedData, signature, hexlify(publicSigningKey)):
|
|
logger.info("Invalid signature on a pubkey")
|
|
|
|
return None
|
|
|
|
return result
|
|
|
|
def checkAndShareObjectWithPeers(payload):
|
|
if len(payload) > 2 ** 18:
|
|
logger.info("The payload length of this object is too large (%i bytes)", len(payload))
|
|
|
|
return None
|
|
|
|
if not workprover.utils.checkWorkSufficient(
|
|
payload,
|
|
defaults.networkDefaultProofOfWorkNonceTrialsPerByte,
|
|
defaults.networkDefaultPayloadLengthExtraBytes
|
|
):
|
|
logger.info("Proof of work is insufficient")
|
|
|
|
return None
|
|
|
|
readPosition = 8
|
|
|
|
try:
|
|
expiryTime, objectType = unpack(">QI", payload[readPosition: readPosition + 12])
|
|
readPosition += 12
|
|
|
|
version, readLength = decodeVarint(payload[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
|
|
stream, readLength = decodeVarint(payload[readPosition: readPosition + 9])
|
|
readPosition += readLength
|
|
except:
|
|
logger.info("Error parsing object header")
|
|
|
|
return None
|
|
|
|
tag = payload[readPosition: readPosition + 32]
|
|
|
|
TTL = expiryTime - int(time.time())
|
|
|
|
# TTL may not be lesser than -1 hour or larger than 28 days + 3 hours of wiggle room
|
|
|
|
if TTL < -3600:
|
|
logger.info("This object\'s expiry time was more than an hour ago: %s", expiryTime)
|
|
|
|
return None
|
|
elif TTL > 28 * 24 * 60 * 60 + 10800:
|
|
logger.info("This object\'s expiry time is too far in the future: %s", expiryTime)
|
|
|
|
return None
|
|
|
|
if stream not in state.streamsInWhichIAmParticipating:
|
|
logger.info("The stream number %i isn\'t one we are interested in", stream)
|
|
|
|
return None
|
|
|
|
if objectType == 0:
|
|
if len(payload) < 42:
|
|
logger.info("Too short \"getpubkey\" message")
|
|
|
|
return None
|
|
elif objectType == 1:
|
|
if len(payload) < 146 or len(payload) > 440:
|
|
logger.info("Invalid length \"pubkey\"")
|
|
|
|
return None
|
|
elif objectType == 3:
|
|
if len(payload) < 180:
|
|
logger.info("Too short \"broadcast\" message")
|
|
|
|
return None
|
|
|
|
if version == 1:
|
|
logger.info("Obsolete \"broadcast\" message version")
|
|
|
|
return None
|
|
|
|
inventoryHash = calculateDoubleHash(payload)[: 32]
|
|
|
|
if inventoryHash in inventory.Inventory():
|
|
logger.info("We already have this object")
|
|
|
|
return inventoryHash
|
|
else:
|
|
inventory.Inventory()[inventoryHash] = objectType, stream, payload, expiryTime, buffer(tag)
|
|
queues.invQueue.put((stream, inventoryHash))
|
|
|
|
logger.info("Broadcasting inventory object with hash: %s", hexlify(inventoryHash))
|
|
|
|
queues.objectProcessorQueue.put((objectType, payload))
|
|
|
|
return inventoryHash
|
|
|
|
# sslProtocolVersion
|
|
if sys.version_info >= (2, 7, 13):
|
|
# this means TLSv1 or higher
|
|
# in the future change to
|
|
# ssl.PROTOCOL_TLS1.2
|
|
sslProtocolVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member
|
|
elif sys.version_info >= (2, 7, 9):
|
|
# this means any SSL/TLS. SSLv2 and 3 are excluded with an option after context is created
|
|
sslProtocolVersion = ssl.PROTOCOL_SSLv23
|
|
else:
|
|
# this means TLSv1, there is no way to set "TLSv1 or higher" or
|
|
# "TLSv1.2" in < 2.7.9
|
|
sslProtocolVersion = ssl.PROTOCOL_TLSv1
|
|
|
|
|
|
# ciphers
|
|
if ssl.OPENSSL_VERSION_NUMBER >= 0x10100000 and not ssl.OPENSSL_VERSION.startswith("LibreSSL"):
|
|
sslProtocolCiphers = "AECDH-AES256-SHA@SECLEVEL=0"
|
|
else:
|
|
sslProtocolCiphers = "AECDH-AES256-SHA"
|