예제 #1
0
class TagLayoutForm(forms.ModelForm):
    layout = JsonField(widget=SchemeWidget(registry.fields.keys()))
    default_verifiers = MultipleOpenChoiceField(widget=ListWidget(
        items={"format_type": "url"}, item_label=_("Url to Verifier")),
                                                required=False)

    class Meta:
        model = TagLayout
        fields = ["name", "unique", "layout", "default_verifiers"]

    usertaglayout = None

    def __init__(self, usertaglayout, **kwargs):
        self.usertaglayout = usertaglayout
        super().__init__(**kwargs)

    def _save_m2m(self):
        self.instance.usertag = self.usertaglayout.associated
        self.instance.name = self.usertaglayout.associated.name
        self.instance.description = self.usertaglayout.associated.description
        self.instance.save()
        self.instance.usertag.refresh_from_db()
        return super()._save_m2m()

    def clean_default_verifiers(self):
        values = self.cleaned_data["default_verifiers"]
        if not isinstance(values, list):
            raise forms.ValidationError(_("Invalid format"),
                                        code='invalid_format')
        values = list(map(merge_get_url, values))
        return values

    def clean(self):
        self.usertaglayout.full_clean()
        return super().clean()

    def save(self, commit=True):
        if commit:
            self.usertaglayout.save()
            self._save_m2m()
        else:
            self.save_m2m = self._save_m2m
        return self.usertaglayout
예제 #2
0
class TagLayoutAdminForm(forms.ModelForm):
    layout = JsonField(widget=SchemeWidget(registry.fields.keys()))
    default_verifiers = MultipleOpenChoiceField(widget=ListWidget(
        items={"format_type": "url"}, item_label=_("Url to Verifier")),
                                                required=False)

    class Meta:
        model = TagLayout
        fields = ["name", "unique", "layout", "default_verifiers", "usertag"]

    class Media:
        css = {
            'all':
            ['node_modules/@fortawesome/fontawesome-free/css/all.min.css']
        }

    def clean_default_verifiers(self):
        values = self.cleaned_data["default_verifiers"]
        if not isinstance(values, list):
            raise forms.ValidationError(_("Invalid format"),
                                        code='invalid_format')
        values = list(map(merge_get_url, values))
        return values
예제 #3
0
class FileForm(LicenseForm):
    request = None
    file = forms.FileField()
    key_list = JsonField(widget=forms.HiddenInput(),
                         initial=None,
                         required=False)
    quota_fields = {"key_list": None}
    quota_fields.update(LicenseForm.quota_fields)

    def __init__(self, request, uc=None, initial=None, **kwargs):
        if initial is None:
            initial = {}
        if not getattr(kwargs.get("instance", None), "id", None):
            initial.setdefault("license_name",
                               DEFAULT_LICENSE_FILE(uc, request.user))
        initial2 = {}
        if kwargs.get("instance", None):
            initial2.update(kwargs["instance"].free_data)
        initial2.update(initial)
        super().__init__(initial=initial2, **kwargs)
        if self.instance.pk:
            self.initial["file"] = \
                self.instance.associated.attachedfiles.filter(
                    name="file"
                ).first()
            if self.initial["file"]:
                self.initial["file"] = self.initial["file"].file
        setattr(self.fields['file'], "hashable", True)
        # sources should not be hashed as they don't affect result
        setattr(self.fields['sources'], "hashable", False)
        setattr(self.fields['license_name'], "hashable", True)
        setattr(self.fields['license_url'], "hashable", True)
        if request.user.is_superuser:
            # no upload limit
            pass
        elif request.user.is_staff:
            self.fields["file"].max_length = getattr(
                settings, "SPIDER_MAX_FILE_SIZE_STAFF", None)
        else:
            self.fields["file"].max_length = getattr(settings,
                                                     "SPIDER_MAX_FILE_SIZE",
                                                     None)
        if request.is_owner:
            # self.user = request.user
            return
        self.fields["file"].editable = False
        self.fields["name"].editable = False
        # for SPIDER_UPLOAD_FILTER
        self.request = request

    def clean_key_list(self):
        ret = self.cleaned_data["key_list"]
        if not ret:
            return None
        if not isinstance(ret, dict):
            raise forms.ValidationError(_("key_list is not a dictionary"))
        for val in ret.values():
            # key is extended by padding + base64 so extra buffer
            if len(val) > 8000:
                raise forms.ValidationError(_("key has invalid length"))
        return ret

    def clean(self):
        ret = super().clean()
        if "file" not in ret:
            return ret
        # has to raise ValidationError
        get_settings_func("SPIDER_UPLOAD_FILTER",
                          "spkcspider.apps.spider.functions.allow_all_filter")(
                              self.request, ret["file"], self)
        return ret

    def get_prepared_attachements(self):
        if "file" not in self.changed_data:
            return {}
        f = None
        if self.instance.pk:
            f = self.instance.associated.attachedfiles.filter(
                name="file").first()
        if not f:
            f = AttachedFile(unique=True,
                             name="file",
                             content=self.instance.associated)
        f.file = self.cleaned_data["file"]
        return {"attachedfiles": [f]}
예제 #4
0
class TextForm(LicenseForm):
    text = SanitizedHtmlField(widget=TrumbowygWidget(), localize=True)
    editable_from = forms.ModelMultipleChoiceField(
        queryset=UserComponent.objects.all(),
        required=False,
        initial=[],
        widget=SelectizeWidget(allow_multiple_selected=True,
                               attrs={"style":
                                      "min-width: 150px; width:100%"}))
    push = forms.BooleanField(required=False,
                              initial=False,
                              help_text=_("Improve ranking of this Text."))
    key_list = JsonField(widget=forms.HiddenInput(),
                         initial=None,
                         required=False)
    file = forms.FileField(required=False, initial=None)

    free_fields = {"push": False}
    free_fields.update(LicenseForm.free_fields)
    quota_fields = {"key_list": None}
    quota_fields.update(LicenseForm.quota_fields)

    class Media:
        js = ['spider_filets/description_helper.js']

    def __init__(self, request, source, scope, initial=None, **kwargs):
        if initial is None:
            initial = {}
        if not getattr(kwargs.get("instance", None), "id", None):
            initial.setdefault("license_name",
                               DEFAULT_LICENSE_TEXT(source, request.user))
        initial2 = {}
        if kwargs.get("instance", None):
            initial2.update(kwargs["instance"].free_data)
        initial2.update(initial)
        super().__init__(initial=initial2, **kwargs)
        if self.instance.pk and not self.instance.quota_data.get("key_list"):
            self.initial["text"] = \
                self.instance.associated.attachedblobs.filter(
                    name="text"
                ).first()
            if self.initial["text"] is not None:
                self.initial["text"] = \
                    self.initial["text"].as_bytes.decode("utf8")
        if scope in ("add", "update"):
            self.fields["editable_from"].help_text = \
                _(
                    "Allow editing from selected components. "
                    "Requires protection strength >=%s."
                ) % settings.SPIDER_MIN_STRENGTH_EVELATION

            query = models.Q(pk=self.instance.associated.usercomponent_id)
            if scope == "update":
                query |= models.Q(
                    contents__references=self.instance.associated)
            query &= models.Q(
                strength__gte=settings.SPIDER_MIN_STRENGTH_EVELATION)
            query &= models.Q(strength__lt=9)
            self.fields["editable_from"].queryset = \
                self.fields["editable_from"].queryset.filter(query).distinct()
            return

        del self.fields["editable_from"]
        del self.fields["push"]
        self.fields["license_name"].editable = False
        self.fields["license_url"].editable = False

        allow_edit = scope == "update_guest"

        if self.instance.quota_data.get("key_list"):
            self.fields["file"].editable = allow_edit
            self.fields["key_list"].editable = allow_edit
            del self.fields["text"]
        else:
            self.fields["text"].editable = allow_edit
            del self.fields["file"]
            del self.fields["key_list"]
        # sources stay enabled
        self.fields["sources"].editable = allow_edit

    def clean_key_list(self):
        ret = self.cleaned_data["key_list"]
        if not ret:
            return None
        if not isinstance(ret, dict):
            raise forms.ValidationError(_("key_list is not a dictionary"))
        for val in ret.values():
            # key is extended by padding + base64 so extra buffer
            if len(val) > 8000:
                raise forms.ValidationError(_("key has invalid length"))
        return ret

    def clean(self):
        ret = super().clean()
        if (self.instance.pk and self.instance.quota_data.get("key_list")):
            if not ret.get("key_list") or not ret.get("file"):
                raise forms.ValidationError(_("Cannot switch to unencrypted"))

        if not ret.get("key_list") and ret.get("file"):
            raise forms.ValidationError(
                _("Can only use file in connection with key_list"))

        if "editable_from" in self.fields:
            self.instance.free_data["editable_from"] = \
                list(self.cleaned_data["editable_from"].values_list(
                    "id", flat=True
                ))
        return ret

    def get_prepared_attachements(self):
        changed_data = self.changed_data
        if "text" not in changed_data and "file" not in changed_data:
            return {}
        b = None
        if self.instance.pk:
            b = self.instance.associated.attachedblobs.filter(
                name="text").first()
        if not b:
            b = AttachedBlob(unique=True,
                             name="text",
                             content=self.instance.associated)
        if self.cleaned_data.get("file"):
            b.blob = self.cleaned_data["file"].read()
        else:
            b.blob = self.cleaned_data["text"].encode("utf-8")
        return {"attachedblobs": [b]}
예제 #5
0
class AddressForm(DataContentForm):
    name = forms.CharField()
    setattr(
        name, "spkc_datatype", XSD.base64Binary
    )
    url = forms.CharField()
    setattr(
        url, "spkc_datatype", XSD.base64Binary
    )
    nonce = forms.CharField(
        widget=forms.HiddenInput(),
        required=False, initial=""
    )
    setattr(
        nonce, "spkc_datatype", XSD.base64Binary
    )
    key_list = JsonField(
        initial=None, required=False
    )

    quota_fields = {
        "name": "",
        "url": "",
        "nonce": None,
        "key_list": {}
    }

    def clean(self):
        ret = super().clean()
        if not self.cleaned_data.get("nonce"):
            keys = self.instance.usercomponent.contents.filter(
                ctype__name="PublicKey",
                info__contains="\x1epubkeyhash="
            ).exclude(
                info__contains="\x1ethirdparty\x1e"
            )
            if not keys:
                raise forms.ValidationError(
                    _(
                        "No keys found and no keys specified"
                    )
                )
            aes_key = os.urandom(32)
            self.cleaned_data["nonce"] = os.urandom(13)
            cipher = Cipher(
                algorithms.AES(aes_key),
                modes.GCM(self.cleaned_data["nonce"]),
                backend=default_backend()
            )
            self.cleaned_data["name"] = \
                base64.b64encode(cipher.encryptor().update(
                    self.cleaned_data["name"].encode("utf8")
                ))
            self.cleaned_data["url"] = \
                base64.b64encode(cipher.encryptor().update(
                    self.cleaned_data["url"].encode("utf8")
                ))
            self.cleaned_data["key_list"] = {}
            for key in keys:
                k = key.content.get_key_ob()
                if k:
                    algo_hash = key.getlist("pubkeyhash", amount=1)[0]
                    algo = getattr(
                        hashes, algo_hash.split("=", 1)[0].upper()
                    )()
                    enc = k.encrypt(
                        aes_key,
                        padding.OAEP(
                            mgf=padding.MGF1(algorithm=algo),
                            algorithm=self.hash_algo, label=None
                        )
                    )
                    self.cleaned_data["key_list"][
                        algo_hash
                    ] = base64.b64encode(enc).decode("ascii")

        if not self.cleaned_data.get("key_list"):
            self.add_error(
                None, forms.ValidationError(
                    _(
                        "key_list is missing: either specify complete "
                        "or leave nonce out"
                    )
                )
            )
        return ret
예제 #6
0
class MessageForm(DataContentForm):
    own_hash = forms.CharField(
        widget=forms.HiddenInput(),
        required=False
    )
    fetch_url = forms.CharField(disabled=True, required=False, initial="")
    was_retrieved = forms.BooleanField(
        disabled=True, required=False, initial=False, help_text=_(
            "Retrieved by recipient"
        )
    )
    # by own client(s)
    received = forms.BooleanField(
        disabled=True, required=False, initial=False, help_text=_(
            "Already received by own client"
        )
    )
    key_list = JsonField(
        initial=dict, widget=forms.Textarea()
    )
    tokens = MultipleOpenChoiceField(initial=list, disabled=True)
    amount_tokens = forms.IntegerField(min_value=0, initial=1, required=False)
    encrypted_content = forms.FileField()

    hash_algorithm = forms.CharField(
        disabled=False, required=False
    )
    setattr(hash_algorithm, "hashable", False)

    first_run = False

    free_fields = {"hash_algorithm": settings.SPIDER_HASH_ALGORITHM.name}
    quota_fields = {"fetch_url": None, "key_list": dict}

    def __init__(self, request, **kwargs):
        super().__init__(**kwargs)
        if self.instance.id:
            self.fields["hash_algorithm"].disabled = True
            self.initial["tokens"] = \
                [
                    token.token
                    for token in self.instance.associated.attached_tokens.all()
            ]
            # hack around for current bad default JsonField widget
            self.initial["key_list"] = json.dumps(self.initial["key_list"])
            setattr(self.fields["key_list"], "spkc_datatype", XSD.string)

            self.initial["fetch_url"] = \
                "{}://{}{}?".format(
                    request.scheme,
                    request.get_host(),
                    reverse(
                        "spider_messages:message"
                    )
                )
            self.initial["encrypted_content"] = \
                self.instance.associated.attachedfiles.get(
                    name="encrypted_content"
                ).file
            setattr(
                self.fields["encrypted_content"],
                "download_url",
                self.instance.associated.get_absolute_url("download")
            )
            setattr(self.fields["encrypted_content"], "hashable", False)
            setattr(
                self.fields["encrypted_content"],
                "view_form_field_template",
                "spider_messages/partials/fields/view_encrypted_content.html"
            )
            self.initial["was_retrieved"] = \
                self.instance.associated.smarttags.filter(
                    name="received", target=None
                ).exists()
            keyhashes = self.data.getlist("keyhash")
            keyhashes_q = info_or(
                pubkeyhash=keyhashes, hash=keyhashes,
                info_fieldname="target__info"
            )
            if keyhashes:
                self.initial["received"] = \
                    self.instance.asspciated.smarttags.filter(
                        name="received"
                    ).filter(keyhashes_q).count() == len(keyhashes)
            else:
                del self.fields["received"]
            del self.fields["amount_tokens"]
            self.first_run = False
        else:
            del self.fields["fetch_url"]
            del self.fields["was_retrieved"]
            del self.fields["received"]
            del self.fields["tokens"]

            if not self.initial.get("hash_algorithm"):
                self.initial["hash_algorithm"] = \
                    settings.SPIDER_HASH_ALGORITHM.name
            self.initial["was_retrieved"] = False
            self.first_run = True

    def clean_hash_algorithm(self):
        ret = self.cleaned_data["hash_algorithm"]
        if ret and not hasattr(hashes, ret.upper()):
            raise forms.ValidationError(
                _("invalid hash algorithm")
            )
        return ret

    def clean(self):
        super().clean()
        if (
            "hash_algorithm" in self.initial and
            not self.cleaned_data.get("hash_algorithm")
        ):
            self.cleaned_data["hash_algorithm"] = \
                self.initial["hash_algorithm"]
        if self.first_run:
            postbox = \
                self.instance.associated.usercomponent.contents.filter(
                    ctype__name="PostBox"
                ).first()
            if postbox:
                self.instance.associated.attached_to_content = postbox
            else:
                self.add_error(None, forms.ValidationError(
                    _("This usercomponent has no Postbox")
                ))
        return self.cleaned_data

    def is_valid(self):
        # cannot update retrieved message
        if (
            self.initial["was_retrieved"] and
            (
                "encrypted_content" in self.changed_data or
                "key_list" in self.changed_data
            )
        ):
            return False

        return super().is_valid()

    def get_prepared_attachements(self):
        ret = {}
        changed_data = self.changed_data
        # create or update keys
        if (
            "key_list" in changed_data or "encrypted_content" in changed_data
        ):
            self.initial["received"] = False
            if self.first_run:
                keyhashes_q = info_or(
                    hash=self.cleaned_data["key_list"],
                    pubkeyhash=self.cleaned_data["key_list"]
                )
                ret["smarttags"] = [
                    SmartTag(
                        content=self.instance.associated,
                        unique=True,
                        name="unread",
                        target=t,
                        data={"hash": t.getlist("hash", 1)[0]}
                    ) for t in self.instance.associated.usercomponent.contents.filter(  # noqa: E501
                        ctype__name="PublicKey"
                    ).filter(keyhashes_q)
                ]

                ret["smarttags"].append(
                    SmartTag(
                        content=self.instance.associated,
                        unique=True,
                        name="unread",
                        target=None
                    )
                )
            else:
                ret["smarttags"] = self.instance.associated.smarttags.all()

            for smartkey in ret["smarttags"]:
                h1 = None
                h2 = None
                if smartkey.target:
                    h1 = smartkey.target.getlist("hash", 1)[0]
                    h2 = smartkey.target.getlist("pubkeyhash", 1)[0]
                if self.cleaned_data["own_hash"] in {h1, h2}:
                    self.initial["received"] = True
                    smartkey.name = "received"
        # don't allow new tokens after the first run
        if self.first_run:
            # update own references to add messagecontent
            #   without updating PostBox
            ret["referenced_by"] = self.instance.associated.attached_to_content
            ret["attached_tokens"] = [
                AuthToken(
                    persist=0,
                    usercomponent=self.instance.associated.usercomponent,
                    attached_to_content=self.instance.associated,
                    extra={
                        # don't allow anything than accessing content via
                        # view
                        "ids": []
                    }
                ) for _ in range(self.cleaned_data.get("amount_tokens", 1))
            ]
            # self.initial["tokens"] = [
            #     x.token for x in ret["attached_tokens"]
            # ]
        if "encrypted_content" in self.changed_data:
            f = None
            if self.instance.pk:
                f = self.instance.associated.attachedfiles.filter(
                    name="encrypted_content"
                ).first()
            if not f:
                f = AttachedFile(
                    unique=True, name="encrypted_content",
                    content=self.instance.associated
                )
            f.file = self.cleaned_data["encrypted_content"]
            ret["attachedfiles"] = [f]
        return ret
예제 #7
0
class PostBoxForm(DataContentForm):
    max_receive_size = forms.IntegerField(
        initial=None, required=False,
        help_text=_(
            "maximal message size received"
        )
    )
    setattr(max_receive_size, "hashable", False)
    only_persistent = forms.BooleanField(required=False)
    setattr(only_persistent, "hashable", False)
    # TODO: functionality, currently nothing logically. Cleanup
    shared = forms.BooleanField(required=False, initial=True)
    setattr(shared, "hashable", False)
    keys = ContentMultipleChoiceField(
        queryset=AssignedContent.objects.filter(
            info__contains="\x1etype=PublicKey\x1e"
        ).filter(
            info__contains="\x1epubkeyhash="
        ), to_field_name="id",
    )
    setattr(keys, "hashable", True)
    webreferences = ContentMultipleChoiceField(
        queryset=AssignedContent.objects.filter(
            ctype__name="WebReference"
        ), to_field_name="id", disabled=True, required=False
    )
    message_objects = ContentMultipleChoiceField(
        queryset=AssignedContent.objects.filter(
            ctype__name="MessageContent"
        ), to_field_name="id", disabled=True, required=False
    )
    setattr(message_objects, "hashable", False)
    attestation = forms.CharField(
        label=_("PostBox Attestation"), help_text=_(
            "Re-sign with every active key for activating new key "
            "or removing a key"
        ), required=False,
        widget=forms.TextInput(
            attrs={
                "readonly": True,
                "style": "width:100%"
            }
        )
    )
    setattr(attestation, "hashable", True)
    setattr(
        attestation, "spkc_datatype", XSD.base64Binary
    )
    setattr(
        attestation,
        "view_form_field_template",
        "spider_messages/partials/fields/view_combined_keyhash.html"
    )
    hash_algorithm = forms.CharField(
        widget=forms.HiddenInput(), disabled=True, required=False
    )
    setattr(hash_algorithm, "hashable", False)
    signatures = JsonField(
        widget=SignatureWidget(
            item_label=_("Signature"), hash_label=_("Hash")
        )
    )
    setattr(signatures, "hashable", False)
    setattr(
        signatures,
        "view_form_field_template",
        "spider_messages/partials/fields/view_signatures.html"
    )

    extract_pubkeyhash = re.compile("\x1epubkeyhash=([^\x1e=]+)=([^\x1e=]+)")

    free_fields = {
        "only_persistent": False,
        "shared": True,  # TODO: specify default_mode
        "max_receive_size": None
    }

    def __init__(self, scope, request, **kwargs):
        super().__init__(**kwargs)
        self.initial["hash_algorithm"] = settings.SPIDER_HASH_ALGORITHM.name
        self.fields["keys"].queryset = \
            self.fields["keys"].queryset.filter(
                usercomponent=self.instance.associated.usercomponent
            )
        if scope in {"view", "raw", "list"} and request.is_owner:
            self.initial["webreferences"] = \
                self.instance.associated.attached_contents.filter(
                    ctype__name="WebReference"
                )
            self.initial["message_objects"] = \
                self.instance.associated.attached_contents.filter(
                    ctype__name="MessageContent"
                )
            keyhashes = request.POST.getlist("keyhash")
            if self.data.get("view_all", "") != "true" and keyhashes:
                self.initial["message_objects"] = \
                    self.initial["message_objects"].filter(
                        info_or(pubkeyhash=keyhashes, hash=keyhashes)
                    )
            if scope != "view":
                self.initial["webreferences"] = \
                    self.initial["webreferences"].values_list("id", flat=True)

                self.initial["message_objects"] = \
                    self.initial["message_objects"].values_list(
                        "id", flat=True
                    )
        else:
            del self.fields["webreferences"]
            del self.fields["message_objects"]

        if scope not in {"add", "update", "export"}:
            del self.fields["keys"]
        if self.instance.id:
            if "keys" in self.fields:
                self.initial["keys"] = self.instance.associated.smarttags.filter(  # noqa: E501
                    name="key"
                ).values_list("target", flat=True)
            signatures = self.instance.associated.smarttags.filter(
                name="key"
            )
            mapped_hashes = map(
                lambda x: self.extract_pubkeyhash.search(x).group(2),
                signatures.values_list(
                    "target__info", flat=True
                )
            )
            mapped_hashes = sorted(mapped_hashes)
            hasher = get_hashob()
            for mh in mapped_hashes:
                hasher.update(binascii.unhexlify(mh))
            hasher = hasher.finalize()
            self.initial["attestation"] = \
                base64.b64encode(hasher).decode("ascii")
            self.initial["signatures"] = [
                {
                    None: x.target,
                    "hash": x.target.getlist("hash", 1)[0],
                    "signature": x.data["signature"]
                } for x in signatures.all()
            ]
            setattr(
                self.fields["signatures"],
                "spkc_datatype",
                {
                    None: spkcgraph["Content"],
                    "hash": XSD.string,
                    "signature": XSD.string
                }
            )
        else:
            del self.fields["attestation"]
            del self.fields["signatures"]

    def clean_signatures(self):
        ret = self.cleaned_data["signatures"]
        if len(ret) == 0:
            raise forms.ValidationError(
                _("Requires signatures")
            )
        try:
            for i in ret:
                i["hash"] and i["signature"]
        except KeyError:
            raise forms.ValidationError(
                _("invalid signature format")
            )
        return ret

    def get_prepared_attachements(self):
        ret = {
            "smarttags": []
        }
        if self.instance.id:
            smarttags = self.instance.associated.smarttags.filter(
                name="key"
            )
        else:
            smarttags = SmartTag.objects.none()
        signatures = dict(
            map(
                lambda x: (x["hash"], x.get("signature") or ""),
                self.cleaned_data.get("signatures", [])
            )
        )
        for pubkey in self.cleaned_data.get("keys", []):
            smarttag = smarttags.filter(target=pubkey).first()
            if not smarttag:
                smarttag = SmartTag(
                    content=self.instance.associated,
                    unique=True,
                    name="key",
                    target=pubkey,
                    data={
                        "signature": None
                    }
                )
            if pubkey.getlist("hash", 1)[0] in signatures:
                # shown hash of key, it includes some extra information
                smarttag.data["signature"] = \
                    signatures[pubkey.getlist("hash", 1)[0]]
            elif pubkey.getlist("pubkeyhash", 1)[0] in signatures:
                # in case only the pubkeyhash is available it must be accepted
                # this is the case for automatic repair
                smarttag.data["signature"] = \
                    signatures[pubkey.getlist("pubkeyhash", 1)[0]]
            ret["smarttags"].append(smarttag)
        return ret
예제 #8
0
class ReferenceForm(DataContentForm):
    url = forms.URLField(max_length=400)
    key_list = JsonField(
        widget=forms.Textarea()
    )
    setattr(key_list, "spkc_datatype", XSD.string)

    hash_algorithm = forms.CharField(
        required=False, disabled=False
    )
    setattr(hash_algorithm, "hashable", False)

    create = False

    free_fields = {"hash_algorithm": settings.SPIDER_HASH_ALGORITHM.name}
    quota_fields = {"url": None, "key_list": dict}

    def __init__(self, create=False, **kwargs):
        self.create = create
        super().__init__(**kwargs)
        if not self.initial.get("hash_algorithm"):
            self.initial["hash_algorithm"] = \
                settings.SPIDER_HASH_ALGORITHM.name
        if not self.create:
            self.fields["hash_algorithm"].disabled = True

    def clean_hash_algorithm(self):
        ret = self.cleaned_data["hash_algorithm"]
        if ret and not hasattr(hashes, ret.upper()):
            raise forms.ValidationError(
                _("invalid hash algorithm")
            )
        return ret

    def clean_key_list(self):
        ret = self.cleaned_data["key_list"]
        for val in ret.values():
            # key is extended by padding + base64 so extra buffer
            if len(val) > 8000:
                raise forms.ValidationError(
                    _("key has invalid length")
                )
        return ret

    def clean(self):
        ret = super().clean()
        if "key_list" not in self.cleaned_data:
            return ret
        if (
            "hash_algorithm" in self.initial and
            not self.cleaned_data.get("hash_algorithm")
        ):
            self.cleaned_data["hash_algorithm"] = \
                self.initial["hash_algorithm"]
        q = info_or(
            pubkeyhash=list(self.cleaned_data["key_list"].keys()),
            info_fieldname="target__info"
        )

        # get from postbox key smarttags with signature
        self.cleaned_data["signatures"] = \
            self.instance.associated.attached_to_content.smarttags.filter(
                name="key"
        ).filter(q)

        # check if key_list matches with signatures;
        # otherwise MITM injection of keys are possible
        if (
            self.cleaned_data["signatures"].count() !=
            len(self.cleaned_data["key_list"])
        ):
            self.add_error("key_list", forms.ValidationError(
                _("invalid keys"),
                code="invalid_keys"
            ))
        return ret

    def get_prepared_attachements(self):
        ret = {}
        if self.create:
            # update own references to add webrerence
            #   without updating PostBox
            ret["referenced_by"] = self.instance.associated.attached_to_content
            ret["smarttags"] = [
                SmartTag(
                    content=self.instance.associated,
                    unique=True,
                    name="unread",
                    target=h.target,
                    free=True
                )
                # signatures are prepared and only hold keys in key_list
                for h in self.cleaned_data["signatures"]
            ]
        return ret