Onionr/onionr/onionrblockapi.py

538 lines
17 KiB
Python
Raw Normal View History

2018-05-15 06:43:29 +00:00
'''
Onionr - P2P Anonymous Storage Network
2018-05-15 06:43:29 +00:00
This file contains the OnionrBlocks class which is a class for working with Onionr blocks
2018-05-15 06:43:29 +00:00
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
2019-07-19 19:49:56 +00:00
import logger, config, onionrexceptions, nacl.exceptions
2019-07-20 00:01:16 +00:00
import json, os, sys, datetime, base64, onionrstorage
from onionrusers import onionrusers
from onionrutils import stringvalidators, epoch
from coredb import blockmetadb
2019-07-19 19:49:56 +00:00
from onionrstorage import removeblock
import onionrblocks
2019-07-20 00:01:16 +00:00
from onionrcrypto import encryption, cryptoutils as cryptoutils, signing
2018-05-15 06:43:29 +00:00
class Block:
2018-06-14 04:17:58 +00:00
blockCacheOrder = list() # NEVER write your own code that writes to this!
blockCache = dict() # should never be accessed directly, look at Block.getCache()
2019-07-19 19:49:56 +00:00
def __init__(self, hash = None, type = None, content = None, expire=None, decrypt=False, bypassReplayCheck=False):
2018-06-20 04:36:07 +00:00
# take from arguments
# sometimes people input a bytes object instead of str in `hash`
2018-08-04 03:47:56 +00:00
if (not hash is None) and isinstance(hash, bytes):
hash = hash.decode()
2018-06-20 04:36:07 +00:00
self.hash = hash
self.btype = type
self.bcontent = content
2018-09-30 16:53:39 +00:00
self.expire = expire
self.bypassReplayCheck = bypassReplayCheck
2018-05-16 02:12:23 +00:00
2018-05-16 01:47:58 +00:00
# initialize variables
self.valid = True
self.raw = None
self.signed = False
self.signature = None
self.signedData = None
2018-05-16 03:08:42 +00:00
self.blockFile = None
2018-05-16 01:47:58 +00:00
self.bheader = {}
self.bmetadata = {}
self.isEncrypted = False
self.decrypted = False
self.signer = None
self.validSig = False
self.autoDecrypt = decrypt
2018-05-16 01:47:58 +00:00
self.update()
2018-05-15 06:43:29 +00:00
2019-02-17 20:21:03 +00:00
def decrypt(self, encodedData = True):
2018-07-30 00:37:12 +00:00
'''
Decrypt a block, loading decrypted data into their vars
'''
if self.decrypted:
return True
retData = False
2018-07-17 07:18:17 +00:00
# decrypt data
if self.getHeader('encryptType') == 'asym':
try:
2019-07-20 00:01:16 +00:00
self.bcontent = encryption.pub_key_decrypt(self.bcontent, encodedData=encodedData)
bmeta = encryption.pub_key_decrypt(self.bmetadata, encodedData=encodedData)
try:
bmeta = bmeta.decode()
except AttributeError:
# yet another bytes fix
pass
self.bmetadata = json.loads(bmeta)
2019-07-20 00:01:16 +00:00
self.signature = encryption.pub_key_decrypt(self.signature, encodedData=encodedData)
self.signer = encryption.pub_key_decrypt(self.signer, encodedData=encodedData)
self.bheader['signer'] = self.signer.decode()
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
if not self.signer is None:
if not self.verifySig():
raise onionrexceptions.SignatureError("Block has invalid signature")
# Check for replay attacks
try:
if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60:
2019-07-20 00:01:16 +00:00
assert cryptoutils.replay_validator(self.bmetadata['rply'])
2019-04-12 23:45:22 +00:00
except (AssertionError, KeyError, TypeError) as e:
if not self.bypassReplayCheck:
# Zero out variables to prevent reading of replays
self.bmetadata = {}
self.signer = ''
self.bheader['signer'] = ''
self.signedData = ''
self.signature = ''
raise onionrexceptions.ReplayAttack('Signature is too old. possible replay attack')
2018-10-08 02:25:59 +00:00
try:
assert self.bmetadata['forwardEnc'] is True
except (AssertionError, KeyError) as e:
pass
else:
2018-10-08 05:11:46 +00:00
try:
2019-07-19 19:49:56 +00:00
self.bcontent = onionrusers.OnionrUser(self.signer).forwardDecrypt(self.bcontent)
2018-10-09 23:36:52 +00:00
except (onionrexceptions.DecryptionError, nacl.exceptions.CryptoError) as e:
2019-08-06 03:10:21 +00:00
#logger.error(str(e))
2018-10-08 05:11:46 +00:00
pass
except nacl.exceptions.CryptoError:
pass
#logger.debug('Could not decrypt block. Either invalid key or corrupted data')
except onionrexceptions.ReplayAttack:
logger.warn('%s is possibly a replay attack' % (self.hash,))
else:
retData = True
self.decrypted = True
else:
logger.warn('symmetric decryption is not yet supported by this API')
return retData
2018-07-30 00:37:12 +00:00
def verifySig(self):
2018-07-30 00:37:12 +00:00
'''
Verify if a block's signature is signed by its claimed signer
'''
2019-07-20 00:01:16 +00:00
if signing.ed_verify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
self.validSig = True
else:
self.validSig = False
return self.validSig
2018-07-17 07:18:17 +00:00
2018-05-15 06:43:29 +00:00
def update(self, data = None, file = None):
'''
Loads data from a block in to the current object.
Inputs:
- data (str):
- if None: will load from file by hash
- else: will load from `data` string
- file (str):
- if None: will load from file specified in this parameter
- else: will load from wherever block is stored by hash
Outputs:
- (bool): indicates whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
try:
# import from string
blockdata = data
# import from file
if blockdata is None:
2019-02-22 21:04:03 +00:00
try:
2019-07-19 19:49:56 +00:00
blockdata = onionrstorage.getData(self.getHash()).decode()
2019-02-22 21:04:03 +00:00
except AttributeError:
raise onionrexceptions.NoDataAvailable('Block does not exist')
2018-05-16 01:47:58 +00:00
else:
self.blockFile = None
# parse block
self.raw = str(blockdata)
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:]
2018-09-27 00:40:02 +00:00
if ('encryptType' in self.bheader) and (self.bheader['encryptType'] in ('asym', 'sym')):
self.bmetadata = self.getHeader('meta', None)
self.isEncrypted = True
else:
self.bmetadata = json.loads(self.getHeader('meta', None))
2018-06-20 04:36:07 +00:00
self.btype = self.getMetadata('type', None)
2018-05-16 01:47:58 +00:00
self.signed = ('sig' in self.getHeader() and self.getHeader('sig') != '')
# TODO: detect if signer is hash of pubkey or not
self.signer = self.getHeader('signer', None)
2018-06-20 04:36:07 +00:00
self.signature = self.getHeader('sig', None)
# signed data is jsonMeta + block content (no linebreak)
self.signedData = (None if not self.isSigned() else self.getHeader('meta') + self.getContent())
self.date = blockmetadb.get_block_date(self.getHash())
self.claimedTime = self.getHeader('time', None)
2018-05-16 01:47:58 +00:00
if not self.getDate() is None:
self.date = datetime.datetime.fromtimestamp(self.getDate())
self.valid = True
if self.autoDecrypt:
self.decrypt()
2018-06-14 04:17:58 +00:00
2018-05-16 01:47:58 +00:00
return True
except Exception as e:
2019-02-22 21:04:03 +00:00
logger.warn('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False)
2018-09-25 04:16:51 +00:00
# if block can't be parsed, it's a waste of precious space. Throw it away.
if not self.delete():
2019-02-22 21:04:03 +00:00
logger.warn('Failed to delete invalid block %s.' % self.getHash(), error = e)
2018-09-27 00:40:02 +00:00
else:
logger.debug('Deleted invalid block %s.' % self.getHash(), timestamp = False)
2018-05-16 01:47:58 +00:00
self.valid = False
2018-05-15 06:43:29 +00:00
return False
def delete(self):
2018-05-16 02:12:23 +00:00
'''
Deletes the block's file and records, if they exist
Outputs:
- (bool): whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
if self.exists():
try:
os.remove(self.getBlockFile())
except TypeError:
pass
2019-07-19 19:49:56 +00:00
removeblock.remove_block(self.getHash())
2018-05-16 01:47:58 +00:00
return True
2018-05-15 06:43:29 +00:00
return False
2018-05-16 02:12:23 +00:00
def save(self, sign = False, recreate = True):
'''
Saves a block to file and imports it into Onionr
Inputs:
- sign (bool): whether or not to sign the block before saving
- recreate (bool): if the block already exists, whether or not to recreate the block and save under a new hash
Outputs:
- (bool): whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
try:
if self.isValid() is True:
2019-07-19 19:49:56 +00:00
self.hash = onionrblocks.insert(self.getRaw(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire())
2018-12-24 06:12:46 +00:00
if self.hash != False:
self.update()
2018-07-30 00:37:12 +00:00
2018-05-19 22:21:35 +00:00
return self.getHash()
2018-05-16 01:47:58 +00:00
else:
logger.warn('Not writing block; it is invalid.')
except Exception as e:
logger.error('Failed to save block.', error = e, timestamp = False)
2018-07-30 00:37:12 +00:00
return False
2018-05-15 06:43:29 +00:00
# getters
2018-10-02 05:02:05 +00:00
def getExpire(self):
'''
Returns the expire time for a block
Outputs:
- (int): the expire time for a block, or None
'''
return self.expire
2018-05-15 06:43:29 +00:00
def getHash(self):
2018-05-16 02:12:23 +00:00
'''
Returns the hash of the block if saved to file
Outputs:
- (str): the hash of the block, or None
'''
2018-05-15 06:43:29 +00:00
return self.hash
def getType(self):
2018-05-16 02:12:23 +00:00
'''
Returns the type of the block
Outputs:
- (str): the type of the block
'''
2018-05-15 06:43:29 +00:00
return self.btype
2018-05-16 01:47:58 +00:00
def getRaw(self):
2018-05-16 02:12:23 +00:00
'''
Returns the raw contents of the block, if saved to file
Outputs:
- (str): the raw contents of the block, or None
'''
2018-05-16 01:47:58 +00:00
return str(self.raw)
2018-06-20 04:36:07 +00:00
def getHeader(self, key = None, default = None):
2018-05-16 02:12:23 +00:00
'''
Returns the header information
Inputs:
- key (str): only returns the value of the key in the header
Outputs:
- (dict/str): either the whole header as a dict, or one value
'''
2018-05-16 01:47:58 +00:00
if not key is None:
2018-06-20 04:36:07 +00:00
if key in self.getHeader():
return self.getHeader()[key]
return default
return self.bheader
2018-05-16 01:47:58 +00:00
2018-06-20 04:36:07 +00:00
def getMetadata(self, key = None, default = None):
2018-05-16 02:12:23 +00:00
'''
Returns the metadata information
Inputs:
- key (str): only returns the value of the key in the metadata
Outputs:
- (dict/str): either the whole metadata as a dict, or one value
'''
2018-05-16 01:47:58 +00:00
if not key is None:
2018-06-20 04:36:07 +00:00
if key in self.getMetadata():
return self.getMetadata()[key]
return default
return self.bmetadata
2018-05-15 06:43:29 +00:00
def getContent(self):
2018-05-16 02:12:23 +00:00
'''
Returns the contents of the block
Outputs:
- (str): the contents of the block
'''
2018-05-16 01:47:58 +00:00
return str(self.bcontent)
2018-06-05 02:26:04 +00:00
2018-05-15 06:43:29 +00:00
def getDate(self):
2018-05-16 02:12:23 +00:00
'''
Returns the date that the block was received, if loaded from file
Outputs:
- (datetime): the date that the block was received
'''
2018-05-15 06:43:29 +00:00
return self.date
2018-05-16 01:47:58 +00:00
def getBlockFile(self):
2018-05-16 02:12:23 +00:00
'''
Returns the location of the block file if it is saved
Outputs:
- (str): the location of the block file, or None
'''
2018-05-16 01:47:58 +00:00
return self.blockFile
2018-05-15 06:43:29 +00:00
def isValid(self):
2018-05-16 02:12:23 +00:00
'''
Checks if the block is valid
Outputs:
- (bool): whether or not the block is valid
'''
2018-05-15 06:43:29 +00:00
return self.valid
def isSigned(self):
2018-05-16 02:12:23 +00:00
'''
Checks if the block was signed
Outputs:
- (bool): whether or not the block is signed
'''
2018-05-15 06:43:29 +00:00
return self.signed
2018-05-16 01:47:58 +00:00
def getSignature(self):
2018-05-16 02:12:23 +00:00
'''
Returns the base64-encoded signature
Outputs:
- (str): the signature, or None
'''
2018-05-16 01:47:58 +00:00
return self.signature
def getSignedData(self):
2018-05-16 02:12:23 +00:00
'''
Returns the data that was signed
Outputs:
- (str): the data that was signed, or None
'''
2018-05-16 01:47:58 +00:00
return self.signedData
def isSigner(self, signer, encodedData = True):
2018-05-16 02:12:23 +00:00
'''
Checks if the block was signed by the signer inputted
Inputs:
- signer (str): the public key of the signer to check against
- encodedData (bool): whether or not the `signer` argument is base64 encoded
Outputs:
- (bool): whether or not the signer of the block is the signer inputted
'''
2018-05-16 01:47:58 +00:00
try:
if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)):
2018-05-16 01:47:58 +00:00
return False
2019-07-20 00:01:16 +00:00
return bool(signing.ed_verify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
2018-05-16 01:47:58 +00:00
except:
return False
2018-05-15 06:43:29 +00:00
# setters
def setType(self, btype):
2018-05-16 02:12:23 +00:00
'''
Sets the type of the block
Inputs:
- btype (str): the type of block to be set to
Outputs:
- (Block): the Block instance
2018-05-16 02:12:23 +00:00
'''
2018-05-15 06:43:29 +00:00
self.btype = btype
return self
2018-06-05 02:26:04 +00:00
def setMetadata(self, key, val):
'''
Sets a custom metadata value
2018-06-05 02:26:04 +00:00
Metadata should not store block-specific data structures.
2018-06-05 02:26:04 +00:00
Inputs:
- key (str): the key
- val: the value (type is irrelevant)
2018-06-05 02:26:04 +00:00
Outputs:
- (Block): the Block instance
'''
2018-06-05 02:26:04 +00:00
self.bmetadata[key] = val
return self
2018-05-15 06:43:29 +00:00
def setContent(self, bcontent):
2018-05-16 02:12:23 +00:00
'''
Sets the contents of the block
Inputs:
- bcontent (str): the contents to be set to
Outputs:
- (Block): the Block instance
2018-05-16 02:12:23 +00:00
'''
2018-05-16 01:47:58 +00:00
self.bcontent = str(bcontent)
2018-05-15 06:43:29 +00:00
return self
2018-06-05 02:26:04 +00:00
# static functions
2018-05-15 06:43:29 +00:00
def getBlocks(type = None, signer = None, signed = None, reverse = False, limit = None):
2018-05-16 02:12:23 +00:00
'''
Returns a list of Block objects based on supplied filters
Inputs:
- type (str): filters by block type
- signer (str/list): filters by signer (one in the list has to be a signer)
- signed (bool): filters out by whether or not the block is signed
- reverse (bool): reverses the list if True
Outputs:
- (list): a list of Block objects that match the input
'''
2018-05-15 06:43:29 +00:00
2018-05-16 01:47:58 +00:00
try:
relevant_blocks = list()
blocks = (blockmetadb.get_block_list() if type is None else blockmetadb.get_blocks_by_type(type))
2018-05-16 01:47:58 +00:00
for block in blocks:
if Block.exists(block):
2019-07-19 19:49:56 +00:00
block = Block(block)
2018-05-16 01:47:58 +00:00
relevant = True
if (not signed is None) and (block.isSigned() != bool(signed)):
relevant = False
if not signer is None:
if isinstance(signer, (str,)):
signer = [signer]
2018-07-18 04:45:51 +00:00
if isinstance(signer, (bytes,)):
signer = [signer.decode()]
2018-05-16 01:47:58 +00:00
isSigner = False
for key in signer:
if block.isSigner(key):
isSigner = True
break
if not isSigner:
relevant = False
2018-08-04 03:47:56 +00:00
if relevant and (limit is None or len(relevant_Blocks) <= int(limit)):
2018-05-16 01:47:58 +00:00
relevant_blocks.append(block)
2018-07-30 00:37:12 +00:00
2018-05-16 02:12:23 +00:00
if bool(reverse):
relevant_blocks.reverse()
2018-05-16 01:47:58 +00:00
return relevant_blocks
except Exception as e:
2018-07-18 04:45:51 +00:00
logger.debug('Failed to get blocks.', error = e)
2018-05-16 01:47:58 +00:00
return list()
2018-05-15 06:43:29 +00:00
def exists(bHash):
2018-05-16 02:12:23 +00:00
'''
Checks if a block is saved to file or not
Inputs:
- hash (str/Block):
- if (Block): check if this block is saved to file
- if (str): check if a block by this hash is in file
Outputs:
- (bool): whether or not the block file exists
'''
2018-06-14 04:17:58 +00:00
# no input data? scrap it.
if bHash is None:
2018-05-16 01:47:58 +00:00
return False
2019-07-19 19:49:56 +00:00
if isinstance(bHash, Block):
bHash = bHash.getHash()
2019-07-19 19:49:56 +00:00
ret = isinstance(onionrstorage.getData(bHash), type(None))
2018-05-16 01:47:58 +00:00
return not ret