def list_messages(self): queried_webrefs = {} queried_messages = {} merged_url, headers = self.merge_and_headers(self.url, raw="embed") response = self.session.get(merged_url, headers=headers) try: response.raise_for_status() except Exception as exc: raise SrcException("Could not list messages") from exc graph = Graph() graph.parse(data=response.content, format="turtle") for i in graph.query( """ SELECT DISTINCT ?base ?idvalue ?namevalue ?type WHERE { ?base a <https://spkcspider.net/static/schemes/spkcgraph#spkc:Content> ; spkc:type ?type ; spkc:properties ?propname , ?propid . ?propid spkc:name ?idname ; spkc:value ?idvalue . ?propname spkc:name ?namename ; spkc:value ?namevalue . } """, # noqa E501 initNs={"spkc": spkcgraph}, initBindings={ "idname": Literal("id", datatype=XSD.string), "namename": Literal("name", datatype=XSD.string), }, ): if i.type.toPython() == "WebReference": queried = queried_webrefs elif i.type.toPython() == "MessageContent": queried = queried_messages else: continue queried.setdefault(str(i.base), {}) queried[str(i.base)]["id"] = i.idvalue queried[str(i.base)]["name"] = i.namevalue return (queried_webrefs, queried_messages)
def update(self, token=None, graph=None, session=None, use_get_token=None): if session: self.session = session elif session is False or not self.session: self.session = requests.Session() if token: self.token = token elif use_get_token is not None: # keep setting if token does not change # first run: use_get_token is None so autodetect still starts use_get_token = self.use_get_token if use_get_token is not None: self.use_get_token = use_get_token else: self.use_get_token = False # empty graph or None if not graph: assert self.url try: # replaces graph with itself if graph is specified graph = self.retrieve_filtered_graph(self.url, out=graph) except Exception as exc: # don't retry if result is clear if use_get_token is not None: raise exc self.use_get_token = True # replaces graph with itself if graph is specified graph = self.retrieve_filtered_graph(self.url, out=graph) retrieved_url = self.retrieve_missing(graph, self.url) else: retrieved_url = self.retrieve_missing(graph) if not self.token: # auto retrieve token from url splitted = retrieved_url.split("?", 1) if len(splitted) == 2: GET = parse_qs(splitted[1]) self.token = GET.get("token", None) if GET.get("token", None): self.use_get_token = True postboxes = get_postboxes(graph) if len(postboxes) != 1: if not self.url: raise SrcException("No postbox found/more than one found") else: try: options = postboxes[self.url] except Exception: raise SrcException("No postbox found/more than one found") else: self.url, options = next(iter(postboxes.items())) self.component_url = graph.value(predicate=spkcgraph["contents"], object=URIRef(self.url)).toPython() self.hash_algo = options["hash_algorithm"] digest = hashes.Hash(self.hash_algo, backend=default_backend()) digest.update(self.pem_key_public) self.hash_key_public = digest.finalize() atth, errored, self.client_list = \ self.attestation_checker.check_signatures( map( lambda x: (x["key"], x["signature"]), options["signatures"].values() ), algo=self.hash_algo ) errored = set(map(lambda x: x[0], errored)) own_key_found = list( filter(lambda x: x[0] == self.hash_key_public, self.client_list)) if not own_key_found: raise ValidationError("Own key was not found") if self.hash_key_public in errored: self.state = AttestationResult.error else: if errored: self.state = AttestationResult.partial_success else: self.state = AttestationResult.success # auto add if signed by own key self.attestation_checker.add(self.url, self.client_list, self.hash_algo, attestation=atth)
def sign(self, confirm=False): url, headers = self.merge_and_headers(self.url) try: check_result = self.simple_check( url, token=headers.get("X-TOKEN"), session=self.session, ) attestation = check_result["attestation"] errored = set(map(lambda x: x[0], check_result["errors"])) key_list = check_result["key_list"] key_hashes = set(map(lambda x: x[0], key_list)) except CheckError as exc: if not exc.key_list or not exc.errored or not exc.attestation: raise exc attestation = exc.attestation errored = set(map(lambda x: x[0], exc.errored)) key_list = exc.key_list key_hashes = set(map(lambda x: x[0], key_list)) if self.hash_key_public not in key_hashes: raise CheckError("Key is not part of chain") if not confirm: return (self.hash_key_public in errored, key_list) # change to update url postbox_update = merge_get_url(replace_action(self.url, "update/"), raw="embed", search="\x1etype=PostBox\x1e") # retrieve csrftoken response = self.session.get(postbox_update, headers=headers) graph = Graph() graph.parse(data=response.content, format="html") csrftoken = list( graph.objects(predicate=spkcgraph["csrftoken"]))[0].toPython() fields = dict( map( lambda x: (x[0].toPython(), x[1].toPython()), graph.query( """ SELECT DISTINCT ?fieldname ?value WHERE { ?base spkc:fieldname ?fieldname ; spkc:value ?value . } """, initNs={"spkc": spkcgraph}, ))) fields["signatures"] = [] own_signature = None for key in self.client_list: if key[0] != self.hash_key_public: signature = key[2] else: # currently only one priv key is supported signature = self.priv_key.sign( attestation, padding.PSS(mgf=padding.MGF1(self.hash_algo), salt_length=padding.PSS.MAX_LENGTH), self.hash_algo) if signature: fields["signatures"].append({ "hash": f"{self.hash_algo.name}={key[0].hex()}", "signature": "{}={}".format(self.hash_algo.name, base64.b64encode(signature).decode("ascii")) }) if key[0] == self.hash_key_public: own_signature = fields["signatures"][-1]["signature"] fields["signatures"] = json.dumps(fields["signatures"]) # update response = self.session.post(postbox_update, data=fields, headers={ "X-CSRFToken": csrftoken, **headers }) try: response.raise_for_status() graph = Graph() graph.parse(data=response.content, format="html") vals = set(extract_property(graph, "signature").values()) breakpoint() if own_signature not in vals: raise SrcException("could not update signature", own_signature) except SrcException as exc: raise exc except Exception as exc: raise SrcException("could not update signature", own_signature) from exc return (self.hash_key_public in errored, key_list)
def receive(self, message_id, outfp=None, access_method=AccessMethod.view, extra_key_hashes=None, max_size=None): if not self.ok: raise NotReady() merged_url, headers = self.merge_and_headers(self.url, raw="embed") response = self.session.get(merge_get_url(self.url, raw="embed"), headers=headers) response.raise_for_status() graph = Graph() graph.parse(data=response.content, format="turtle") self.retrieve_missing(graph, merged_url) result = list( graph.query( """ SELECT DISTINCT ?base ?hash_algorithm ?type WHERE { ?base a <https://spkcspider.net/static/schemes/spkcgraph#spkc:Content> ; spkc:type ?type ; spkc:properties ?propalg , ?propid . ?propid spkc:name ?idname ; spkc:value ?idvalue . ?propalg spkc:name ?algname ; spkc:value ?hash_algorithm . } """, # noqa E501 initNs={"spkc": spkcgraph}, initBindings={ "idvalue": Literal(message_id), "algname": Literal("hash_algorithm", datatype=XSD.string), "idname": Literal("id", datatype=XSD.string), })) if not result or result[0].type.toPython() not in { "WebReference", "MessageContent" }: raise SrcException("No Message") # every object has it's own copy of the hash algorithm, used hash_algo = getattr(hashes, result[0].hash_algorithm.upper())() if not outfp: outfp = tempfile.TempFile() if hash_algo == self.hash_algo: pub_key_hashalg = "%s=%s" % (hash_algo.name, self.hash_key_public.hex()) else: digest = hashes.Hash(hash_algo, backend=default_backend()) digest.update(self.pem_key_public) pub_key_hashalg = "%s=%s" % (hash_algo.name, digest.finalize().hex()) key_hashes = list() if extra_key_hashes: extra_key_hashes = set(extra_key_hashes) extra_key_hashes.discard(pub_key_hashalg) for key in self.client_list: if key[0] in extra_key_hashes: if hash_algo == self.hash_algo: key_hashalg = "%s=%s" % (hash_algo.name, key[0]) else: digest = hashes.Hash(hash_algo, backend=default_backend()) digest.update(key[1]) key_hashalg = "%s=%s" % (hash_algo.name, digest.finalize().hex()) key_hashes.append(key_hashalg) if access_method == AccessMethod.view: key_hashes.append(pub_key_hashalg) retrieve_url, headers = self.merge_and_headers( replace_action( result[0].base, "bypass/" if (access_method == AccessMethod.bypass) else "message/")) data = {} if access_method != AccessMethod.bypass: data.update({"max_size": max_size or "", "keyhash": key_hashes}) response = self.session.post(retrieve_url, stream=True, headers=headers, data=data) try: response.raise_for_status() except Exception as exc: raise DestException("Message retrieval failed", response.text) from exc key_list = json.loads(response.headers["X-KEYLIST"]) key = key_list.get(pub_key_hashalg, None) if not key: raise WrongRecipient("message not for me") decrypted_key = self.priv_key.decrypt( base64.b64decode(key), padding.OAEP(mgf=padding.MGF1(algorithm=hash_algo), algorithm=hash_algo, label=None)) headblock = b"" fdecryptor = None eparser = emailparser.BytesFeedParser(policy=policy.default) headers = None for chunk in response.iter_content(chunk_size=256): blob = None if not fdecryptor: headblock = b"%b%b" % (headblock, chunk) if b"\0" in headblock: nonce, headblock = headblock.split(b"\0", 1) nonce = base64.b64decode(nonce) fdecryptor = Cipher(algorithms.AES(decrypted_key), modes.GCM(nonce), backend=default_backend()).decryptor() blob = fdecryptor.update(headblock[:-16]) headblock = headblock[-16:] else: continue else: blob = fdecryptor.update(b"%b%b" % (headblock, chunk[:-16])) headblock = chunk[-16:] if not headers: if b"\n\n" not in blob: eparser.feed(blob) continue headersrest, blob = blob.split(b"\n\n", 1) eparser.feed(headersrest) headers = eparser.close() # check what to do t = headers.get("SPKC-Type", MessageType.email) if t == MessageType.email: outfp.write( headers.as_bytes(unixfrom=True, policy=policy.SMTP)) outfp.write(blob) outfp.write(fdecryptor.finalize_with_tag(headblock)) return outfp, headers, decrypted_key
def send(self, inp, receivers, headers=b"\n", mode=SendMethod.shared, aes_key=None): if not self.ok: raise NotReady() if isinstance(receivers, str): receivers = [receivers] if isinstance(inp, (bytes, memoryview)): inp = io.BytesIO(bytes(inp)) elif isinstance(inp, str): inp = io.BytesIO(inp.encode("utf8")) # 256 bit if not aes_key: aes_key = os.urandom(32) assert len(aes_key) == 32 nonce = os.urandom(13) fencryptor = Cipher(algorithms.AES(aes_key), modes.GCM(nonce), backend=default_backend()).encryptor() src_key_list = {} if mode == SendMethod.stealth: pass elif mode == SendMethod.private: enc = self.priv_key.public_key().encrypt( aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=self.hash_algo), algorithm=self.hash_algo, label=None)) # encrypt decryption key src_key_list["%s=%s" % (self.hash_algo.name, self.pub_key_hash )] = base64.b64encode(enc).decode("ascii") elif mode == SendMethod.shared: for k in self.client_list: enc = k[1].encrypt( aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=self.hash_algo), algorithm=self.hash_algo, label=None)) # encrypt decryption key src_key_list["%s=%s" % (self.hash_algo.name, k[0].hex() )] = base64.b64encode(enc).decode("ascii") else: raise NotImplementedError() # remove raw as we parse html message_create_url, src_headers = self.merge_and_headers( replace_action(self.component_url, "add/MessageContent/"), raw=None) response = self.session.get(message_create_url, headers=src_headers) try: response.raise_for_status() except Exception as exc: raise SrcException("retrieval csrftoken failed", response.text) from exc g = Graph() g.parse(data=response.content, format="html") csrftoken = list(g.objects(predicate=spkcgraph["csrftoken"]))[0] # create message object response = self.session.post( message_create_url, data={ "own_hash": self.hash_key_public, "key_list": json.dumps(src_key_list), "amount_tokens": len(receivers) }, headers={ "X-CSRFToken": csrftoken, **src_headers # only for src }, files={ "encrypted_content": EncryptedFile(fencryptor, inp, nonce, headers) }) try: response.raise_for_status() except Exception as exc: raise SrcException("Message creation failed", response.text) from exc if message_create_url == response.url: raise SrcException("Message creation failed", response.text) g = Graph() g.parse(data=response.content, format="html") fetch_url = list( map( lambda x: x.value, g.query(""" SELECT ?value WHERE { ?property spkc:name ?name ; spkc:value ?value . } """, initNs={"spkc": spkcgraph}, initBindings={ "name": Literal("fetch_url", datatype=XSD.string) }))) tokens = list( map( lambda x: x.value, g.query(""" SELECT ?value WHERE { ?property spkc:name ?name ; spkc:value ?value . } """, initNs={"spkc": spkcgraph}, initBindings={ "name": Literal("tokens", datatype=XSD.string) }))) if not fetch_url or not tokens: raise SrcException("Message creation failed", response.text) fetch_url = fetch_url[0].toPython() exceptions = [] final_fetch_urls = [] for receiver, token in zip(receivers, tokens): furl = merge_get_url(fetch_url, token=token.toPython()) try: self._send_dest(aes_key, furl, receiver) final_fetch_urls.append(furl) except Exception as exc: exceptions.append(exc) # for autoremoval simulate access self.session.get(furl) return exceptions, final_fetch_urls, aes_key