This commit is contained in:
Thomas Ramé
2026-07-30 11:08:02 +02:00
parent 226e5c407b
commit cc250a6e0c
49 changed files with 1433 additions and 456 deletions
+20 -22
View File
@@ -73,7 +73,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
abilities = serializers.SerializerMethodField(read_only=True)
deleted_at = serializers.SerializerMethodField(read_only=True)
accesses_user_ids = serializers.SerializerMethodField(read_only=True)
accesses_fingerprints_per_user = serializers.SerializerMethodField(read_only=True)
accesses_versions_per_user = serializers.SerializerMethodField(read_only=True)
encrypted_document_symmetric_key_for_user = serializers.SerializerMethodField(
read_only=True
)
@@ -86,7 +86,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
fields = [
"id",
"abilities",
"accesses_fingerprints_per_user",
"accesses_versions_per_user",
"accesses_user_ids",
"ancestors_link_reach",
"ancestors_link_role",
@@ -178,14 +178,14 @@ class ListDocumentSerializer(serializers.ModelSerializer):
return None
return [str(uid) for uid in instance.accesses_user_ids]
def get_accesses_fingerprints_per_user(self, instance):
"""Return fingerprints of users' public keys at share time."""
def get_accesses_versions_per_user(self, instance):
"""Return versions of users' public keys at share time."""
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return None
if not instance.is_encrypted:
return None
return instance.accesses_fingerprints_per_user
return instance.accesses_versions_per_user
def get_encrypted_document_symmetric_key_for_user(self, instance):
"""Return the encrypted symmetric key for the current user."""
@@ -248,7 +248,7 @@ class DocumentSerializer(ListDocumentSerializer):
fields = [
"id",
"abilities",
"accesses_fingerprints_per_user",
"accesses_versions_per_user",
"accesses_user_ids",
"ancestors_link_reach",
"ancestors_link_role",
@@ -442,8 +442,8 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
required=False, allow_blank=True, write_only=True
)
# TODO: REQUIRED!!!
encryption_public_key_fingerprint = serializers.CharField(
required=False, allow_blank=True, max_length=16
encryption_public_key_version = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
is_pending_encryption = serializers.SerializerMethodField(read_only=True)
@@ -461,7 +461,7 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
"max_ancestors_role",
"max_role",
"encrypted_document_symmetric_key_for_user",
"encryption_public_key_fingerprint",
"encryption_public_key_version",
"is_pending_encryption",
]
read_only_fields = [
@@ -1055,23 +1055,21 @@ class EncryptDocumentSerializer(serializers.Serializer):
)
# Required: matched to the wrapped-key map. Every user sub present
# in `encryptedSymmetricKeyPerUser` must also appear here with the
# fingerprint of the public key used to wrap their copy (or null
# version of the public key used to wrap their copy (or null
# for pending users with no public key yet). Stored on the access
# row verbatim so clients can later tell which key each user's
# wrapped key was produced for — used by the key-mismatch panel
# to display "Fingerprint at the time it was shared with you".
# row verbatim so clients can later detect when a user's key has
# rotated: if the current public key version differs from this
# stored value, the access needs re-encryption.
#
# Not security-sensitive in the crypto sense — the actual wrap is
# the wrapped key itself. The fingerprint is a display hint; a
# the wrapped key itself. The version is a staleness marker; a
# malicious client could send wrong values but the worst it
# achieves is confusing the user whose client was lying.
encryptionPublicKeyFingerprintPerUser = serializers.DictField(
child=serializers.CharField(
allow_null=True, allow_blank=True, max_length=16
),
encryptionPublicKeyVersionPerUser = serializers.DictField(
child=serializers.IntegerField(allow_null=True, min_value=1),
required=True,
help_text=(
"Mapping of user OIDC sub → fingerprint of their public key "
"Mapping of user OIDC sub → version of their public key "
"at encryption time. Must cover the same set of users as "
"`encryptedSymmetricKeyPerUser`; null is valid for pending "
"users."
@@ -1104,10 +1102,10 @@ class AcceptEncryptionAccessSerializer(serializers.Serializer):
"pending → validated. To revert, delete the access row."
),
)
encryption_public_key_fingerprint = serializers.CharField(
encryption_public_key_version = serializers.IntegerField(
required=True,
allow_blank=False,
max_length=16,
allow_null=False,
min_value=1,
)
+16 -16
View File
@@ -2122,20 +2122,20 @@ class DocumentViewSet(
'provide a wrapped key for your own user.'
})
# Per-user fingerprint map — required, keyed on the same user
# Per-user version map — required, keyed on the same user
# subs as the wrapped-key map. Stored verbatim on the access
# row so clients can later tell which key each user's wrapped
# key was produced for.
fingerprint_per_user = serializer.validated_data[
'encryptionPublicKeyFingerprintPerUser'
# row so clients can later detect when a user's key has rotated
# (current version != stored version ⇒ needs re-encryption).
version_per_user = serializer.validated_data[
'encryptionPublicKeyVersionPerUser'
]
fingerprint_subs = set(fingerprint_per_user.keys())
if fingerprint_subs != provided_user_ids:
version_subs = set(version_per_user.keys())
if version_subs != provided_user_ids:
raise drf.exceptions.ValidationError({
'encryptionPublicKeyFingerprintPerUser':
'encryptionPublicKeyVersionPerUser':
'Must cover the same set of users as encryptedSymmetricKeyPerUser. '
f'Missing: {provided_user_ids - fingerprint_subs}. '
f'Extra: {fingerprint_subs - provided_user_ids}.'
f'Missing: {provided_user_ids - version_subs}. '
f'Extra: {version_subs - provided_user_ids}.'
})
# Remove old unencrypted attachment keys from the allowed list.
@@ -2166,7 +2166,7 @@ class DocumentViewSet(
transaction.on_commit(_cleanup_old_attachments)
# Store the encrypted symmetric keys + fingerprints in
# Store the encrypted symmetric keys + versions in
# DocumentAccess for each user. Keys are keyed by the user's
# OIDC `sub`, so look up by user__sub.
for sub, encrypted_key in encryptedSymmetricKeyPerUser.items():
@@ -2175,8 +2175,8 @@ class DocumentViewSet(
document=document, user__sub=sub,
)
access.encrypted_document_symmetric_key_for_user = encrypted_key
access.encryption_public_key_fingerprint = (
fingerprint_per_user.get(sub) or None
access.encryption_public_key_version = (
version_per_user.get(sub)
)
access.save()
except models.DocumentAccess.DoesNotExist:
@@ -2579,13 +2579,13 @@ class DocumentAccessViewSet(
"encrypted_document_symmetric_key_for_user"
]
)
access.encryption_public_key_fingerprint = (
serializer.validated_data["encryption_public_key_fingerprint"]
access.encryption_public_key_version = (
serializer.validated_data["encryption_public_key_version"]
)
access.save(
update_fields=[
"encrypted_document_symmetric_key_for_user",
"encryption_public_key_fingerprint",
"encryption_public_key_version",
]
)