iotaFS/next.py

343 lines
11 KiB
Python

#import pyfuse3
from iota import Iota, ProposedTransaction, Address, TryteString, Tag
from iota.crypto.addresses import AddressGenerator
from iota.crypto.types import Seed
from iota.codecs import TrytesDecodeError
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import math
from pprint import pprint
import hashlib
import sys
import random
import time
import msgpack
import asyncio
import copy
import gzip
import secrets
CHUNKSIZE = 2187 - 1
def log(txt):
print("[-] "+str(txt))
class Atom():
def __init__(self) -> None:
self.type = "null"
def load(self, data) -> None:
if data[0]==True:
self.type = "milestone"
self.milestone = data[1:]
else:
if data[1]==True:
self.type = "dir"
self.milestoneIndex = data[4]
else:
self.type = "file"
self.size = data[4]
self.name = data[2]
self.token = data[3]
def dump(self) -> bytes:
# TODO: Delimiter?
# TODO: compression?
return msgpack.dumps(self._dumpAsArray())
def _dumpAsArray(self):
if self.type=="milestone":
return [True]+self.milestone
else:
if self.type=="file":
return [False, self.type, self.name, self.token, self.size]
else:
return [False, self.type, self.name, self.token, self.milestoneIndex]
class BlobChunk():
def __init__(self, data: bytes = b'', sealed: bool = False) -> None:
self.data = data
self.sealed = sealed
def getData(self) -> bytes:
return self.data
def append(self, data: bytes) -> None:
if len(data)+len(self.data) > CHUNKSIZE:
raise Exception("That to big!")
self.data += data
def getBytesLeft(self) -> int:
if self.sealed:
return 0
return CHUNKSIZE - len(self.data)
def seal(self) -> None:
self.sealed = True
def isSealed(self) -> bool:
return self.sealed
class TangleBlob():
def __init__(self, token: bytes, iotaApi: Iota) -> None:
self.token = token
self.iotaApi = iotaApi
self.preChunks = 0
self.chunks = []
m = hashlib.sha3_512()
m.update(self.token)
trSeed = TryteString.from_bytes(m.digest())[:81]
self.adressGen = AddressGenerator(Seed(trSeed))
self.fetched = False
self.pushedNum = 0
def _requireFetched(self):
if not self.fetched:
self.fetch()
def _getKey(self, chunkNum: int) -> bytes:
m = hashlib.sha3_384()
m.update(self.token)
m.update(chunkNum.to_bytes(8, "little")) # 64 bits should be enought...
m.update(self.token)
return m.digest()
def _genBundle(self, data, addr) -> str:
txMsg = TryteString.from_bytes(data)
trans = ProposedTransaction(
address = addr,
value = 0,
tag = Tag("IOTAFS"),
message = txMsg
)
return self.iotaApi.prepare_transfer(
transfers = [trans],
inputs = [addr]
)['trytes']
def _dumpChunk(self, chunkNum: int) -> str:
key = self._getKey(chunkNum + self.preChunks)
data = self.chunks[chunkNum].getData()
cipher = AES.new(key[16:][:16], AES.MODE_CBC, key[:16])
ct_bytes = cipher.encrypt(pad(data, AES.block_size))
addr = self.adressGen.get_addresses(start=chunkNum + self.preChunks, count=1)[0]
return self._genBundle(ct_bytes, addr)
def append(self, data: bytes, newBlock: bool = False) -> None:
self._requireFetched()
if len(self.chunks) and not newBlock:
bytesLeft = self.chunks[-1].getBytesLeft()
if bytesLeft:
leftChunk = data[:bytesLeft]
data = data[bytesLeft:]
self.chunks[-1].append(leftChunk)
while len(data):
chunk = data[:CHUNKSIZE]
self.chunks.append(BlobChunk(chunk))
data = data[CHUNKSIZE:]
def getChunkLen(self) -> int:
return self.preChunks + len(self.chunks)
def getSize(self) -> int:
if len(self.chunks):
return self.getChunkLen()*CHUNKSIZE - self.chunks[-1].getBytesLeft()
return self.preChunks
def read(self) -> bytes:
data = b''
for chunk in self.chunks:
data += chunk.getData()
return data
def _dump(self) -> str:
self.chunks[-1].seal()
data = ""
for c in range(len(self.chunks)-self.pushedNum):
num = c + self.pushedNum
data += self._dumpChunk(num) # num is without preChunks
self.pushedNum = self.getChunkLen()
return data
def fetch(self, skipChunks: int = 0) -> None:
chunkNum = self.getChunkLen() + skipChunks
while True:
key = self._getKey(chunkNum)
cipher = AES.new(key[16:][:16], AES.MODE_CBC, key[:16])
addr = self.adressGen.get_addresses(start=chunkNum, count=1)[0]
txHash = self.api.find_transactions(tags=[Tag("IOTAFS")], addresses=[addr])["hashes"]
if len(txHash)==0:
break
bundles = self.api.get_bundles(txHash[0])["bundles"]
for bundle in bundles:
for tx in bundle.transactions:
# TODO: Can we just strip the 9s and call it a day?
tryteStr = TryteString(str(tx.signature_message_fragment).rstrip("9"))
try:
ct_bytes = tryteStr.as_bytes()
except TrytesDecodeError:
ct_bytes = (tryteStr+"9").as_bytes()
self.chunks.append(BlobChunk(unpad(cipher.decrypt(ct_bytes), AES.block_size), True))
chunkNum += 1
self.pushedNum = len(self.chunks)
self.fetched = True
self._afterFetch()
def _afterFetch(self) -> None:
return
def genToken(self) -> bytes:
return secrets.token_bytes(32)
class TangleFileTreeElement(TangleBlob):
def __init__(self, token: bytes, parent, iotaApi: Iota) -> None:
super(TangleFileTreeElement, self).__init__(token, iotaApi)
self.inodes = {}
self.atomStack = -1
self.api = iotaApi
self.parent = parent
def _afterFetch(self) -> None:
raw = self.read()
if raw==b'':
self.atomStack = 0
return
data = msgpack.loads(raw, raw=True)
newAtoms = []
for i, elem in enumerate(reversed(data)):
atom = Atom()
atom.load(elem)
if atom.type == "milestone":
self._applyMilestone(atom.milestone)
break
newAtoms.append(atom)
self.atomStack = len(newAtoms)
for atom in reversed(newAtoms):
self._applyAtom(atom)
def _applyMilestone(self, milestone) -> None:
self.atomStack = 0
self.inodes = {}
for stone in milestone:
atom = Atom()
atom.load(stone)
self.inodes[atom.name] = atom
def _applyAtom(self, atom: Atom) -> None:
if atom.name in self.inodes:
self.atomStack += 1
self.inodes[atom.name] = atom
if atom.type=="dir":
self.inodes[atom.name].elem = TangleFileTreeElement(atom.token, self, self.iotaApi)
elif atom.type=="file":
self.inodes[atom.name].elem = TangleFile(atom.name, self, self.api)
else:
raise Exception("How did such an atom get here?")
def _newAtom(self, atom: Atom) -> None:
self.append(atom.dump())
def getNameList(self):
self._requireFetched()
return list(self.inodes.keys())
def getInode(self, name: str) -> Atom:
self._requireFetched()
return self.inodes[name]
def mkdir(self, name: str) -> bool:
self._requireFetched()
if name in self.getNameList():
return False
atom = Atom()
atom.type="dir"
atom.name = name
atom.token = self.genToken()
atom.milestoneIndex = 0
self._newAtom(atom)
self._applyAtom(atom)
return True
def mkfile(self, name: str) -> bool:
self._requireFetched()
if name in self.getNameList():
return False
atom = Atom()
atom.type="file"
atom.name = name
atom.token = self.genToken()
atom.size = 0
self._newAtom(atom)
self._applyAtom(atom)
return True
def _updateFileSize(self, name: str, size: int):
self._requireFetched()
self.inodes[name].size = size
self._newAtom(self.inodes[name])
def _updateFileToken(self, name: str, token: bytes, size: int):
self._requireFetched()
self.inodes[name].token = token
self.inodes[name].size = size
self._newAtom(self.inodes[name])
def performMilestone(self):
stones = []
for a in self.inodes:
stones.append(self.inodes[a]._dumpAsArray())
self.atomStack = 0
# TODO: Delimiter ?
# TODO: compression ?
data = msgpack.dumps(stones)
self.append(data)
# inform parent about milestone (when merged)
class TangleFile():
def __init__(self, name: str, parent: TangleFileTreeElement, iotaApi: Iota) -> None:
self.api = iotaApi
self.name = name
self.parent = parent
self.reflexiveAtom = parent.inodes[name]
self.size = self.reflexiveAtom.size
self.token = self.reflexiveAtom.token
self.blob = TangleBlob(self.token, iotaApi)
def write(self, offset: int, data: bytes):
if offset == self.size:
self.append(data)
else:
pass
class IotaFs():
def __init__(self, token) -> None:
self.api = Iota('https://nodes.thetangle.org:443', local_pow=True)
self.genesis = TangleFileTreeElement(token, None, self.api)
log("Fetching Genesis...")
self.genesis.fetch()
log("Retrieving reference to root")
if self.genesis.getNameList()!=["/"]:
if len(self.genesis.getNameList()):
# theres another directory in our genesis chain... WTF?!
raise Exception("Corrupted Genesis-Chain:"
+ "Unknown records for no root-directory in Genesis Chain: "+str(self.genesis.getNameList()))
else:
# we dont have a root yet, lets create one...
log("Unable to reference to root: Creating new root")
self.genesis.mkdir("/")
log("Successfully Mounted!")
def createNewFile(self, name) -> None:
pass
api = Iota('https://nodes.thetangle.org:443', local_pow=True)
token = b'testToken'
genesis = TangleFileTreeElement(token, None, api)