mirror of
https://github.com/suitenumerique/drive.git
synced 2026-09-26 19:45:04 +02:00
wip
This commit is contained in:
@@ -105,8 +105,8 @@ class ItemAccessSerializer(serializers.ModelSerializer):
|
||||
encrypted_item_symmetric_key_for_user = serializers.CharField(
|
||||
required=False, allow_blank=True, write_only=True
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -126,7 +126,7 @@ class ItemAccessSerializer(serializers.ModelSerializer):
|
||||
"item",
|
||||
"is_explicit",
|
||||
"encrypted_item_symmetric_key_for_user",
|
||||
"encryption_public_key_fingerprint",
|
||||
"encryption_public_key_version",
|
||||
"is_pending_encryption",
|
||||
]
|
||||
read_only_fields = [
|
||||
@@ -253,18 +253,18 @@ class ListItemSerializer(serializers.ModelSerializer):
|
||||
is_pending_encryption_for_user = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
encryption_public_key_fingerprint_for_user = serializers.SerializerMethodField(
|
||||
encryption_public_key_version_for_user = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
|
||||
def get_encryption_public_key_fingerprint_for_user(self, item):
|
||||
"""Fingerprint of the user's public key AT THE TIME they were
|
||||
granted access. Stored on the ItemAccess row that holds their
|
||||
wrapped symmetric key.
|
||||
def get_encryption_public_key_version_for_user(self, item):
|
||||
"""Version of the user's encryption public key AT THE TIME they
|
||||
were granted access. Stored on the ItemAccess row that holds
|
||||
their wrapped symmetric key.
|
||||
|
||||
Clients use this to tell the user which key the document was
|
||||
encrypted for if decryption fails with "wrong secret key" —
|
||||
the user can compare it to their current key's fingerprint and
|
||||
the user can compare it to their current key's version and
|
||||
understand that a collaborator needs to re-add them so the key
|
||||
gets wrapped against their current public key.
|
||||
|
||||
@@ -283,7 +283,7 @@ class ListItemSerializer(serializers.ModelSerializer):
|
||||
user=request.user,
|
||||
encrypted_item_symmetric_key_for_user__isnull=False,
|
||||
)
|
||||
.values_list("encryption_public_key_fingerprint", flat=True)
|
||||
.values_list("encryption_public_key_version", flat=True)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -390,7 +390,7 @@ class ListItemSerializer(serializers.ModelSerializer):
|
||||
"is_encryption_root",
|
||||
"is_inside_encrypted_subtree",
|
||||
"is_pending_encryption_for_user",
|
||||
"encryption_public_key_fingerprint_for_user",
|
||||
"encryption_public_key_version_for_user",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
@@ -430,7 +430,7 @@ class ListItemSerializer(serializers.ModelSerializer):
|
||||
"is_encryption_root",
|
||||
"is_inside_encrypted_subtree",
|
||||
"is_pending_encryption_for_user",
|
||||
"encryption_public_key_fingerprint_for_user",
|
||||
"encryption_public_key_version_for_user",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
@@ -665,7 +665,7 @@ class ItemSerializer(ListItemSerializer):
|
||||
"is_wopi_supported",
|
||||
"encrypted_item_symmetric_key_for_user",
|
||||
"is_pending_encryption_for_user",
|
||||
"encryption_public_key_fingerprint_for_user",
|
||||
"encryption_public_key_version_for_user",
|
||||
"accesses_user_ids",
|
||||
]
|
||||
read_only_fields = [
|
||||
@@ -702,7 +702,7 @@ class ItemSerializer(ListItemSerializer):
|
||||
"is_wopi_supported",
|
||||
"encrypted_item_symmetric_key_for_user",
|
||||
"is_pending_encryption_for_user",
|
||||
"encryption_public_key_fingerprint_for_user",
|
||||
"encryption_public_key_version_for_user",
|
||||
"accesses_user_ids",
|
||||
]
|
||||
|
||||
@@ -1069,7 +1069,7 @@ class MoveItemSerializer(serializers.Serializer):
|
||||
{"target_item_id": "..." | null,
|
||||
"is_encryption_root": true,
|
||||
"per_user_encrypted_keys": {"<sub>": "<base64>" | null, ...},
|
||||
"encryption_public_key_fingerprints": {"<sub>": "<fp>" | null, ...}}
|
||||
"encryption_public_key_versions": {"<sub>": <version:int> | null, ...}}
|
||||
- The caller's own sub MUST map to a non-null wrapped key. Other
|
||||
users may be `null` (access row stored as pending until they
|
||||
finish encryption onboarding — symmetric to /encrypt/).
|
||||
@@ -1107,8 +1107,8 @@ class MoveItemSerializer(serializers.Serializer):
|
||||
child=serializers.CharField(allow_null=True),
|
||||
required=False,
|
||||
)
|
||||
encryption_public_key_fingerprints = serializers.DictField(
|
||||
child=serializers.CharField(allow_null=True, allow_blank=True, max_length=16),
|
||||
encryption_public_key_versions = serializers.DictField(
|
||||
child=serializers.IntegerField(allow_null=True, min_value=1),
|
||||
required=False,
|
||||
)
|
||||
# Encrypt-on-move payload (plaintext → encrypted). Snake_case here
|
||||
@@ -1167,10 +1167,10 @@ class AcceptEncryptionAccessSerializer(serializers.Serializer):
|
||||
"→ validated. To revert, delete the access row instead."
|
||||
),
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -1198,29 +1198,27 @@ class EncryptItemSerializer(serializers.Serializer):
|
||||
"never null."
|
||||
),
|
||||
)
|
||||
encryption_public_key_fingerprint_per_user = serializers.DictField(
|
||||
# Required: the client must send a fingerprint entry for every
|
||||
encryption_public_key_version_per_user = serializers.DictField(
|
||||
# Required: the client must send a version entry for every
|
||||
# user it sent a wrapped-key entry for. Symmetric keys and
|
||||
# fingerprints travel as matched pairs — keeping them coupled
|
||||
# versions travel as matched pairs — keeping them coupled
|
||||
# means an encrypted access row always has the "what key was
|
||||
# this wrapped for" display hint stored alongside.
|
||||
# this wrapped for" staleness marker stored alongside.
|
||||
#
|
||||
# Not security-sensitive in the crypto sense — the actual wrap
|
||||
# is the wrapped key itself. The fingerprint is a display hint
|
||||
# is the wrapped key itself. The version is a staleness marker
|
||||
# surfaced in the client's key-mismatch panel. Since it comes
|
||||
# from the encrypting client, we trust it as we trust any
|
||||
# other client-provided metadata; a malicious client could
|
||||
# send wrong values, but the worst it achieves is confusing
|
||||
# the very user whose client was sending the lie.
|
||||
child=serializers.CharField(
|
||||
allow_null=True, allow_blank=True, max_length=16
|
||||
),
|
||||
child=serializers.IntegerField(allow_null=True, min_value=1),
|
||||
required=True,
|
||||
help_text=(
|
||||
"Mapping of user OIDC sub → fingerprint of their public key "
|
||||
"at encryption time. Must cover the same set of users as "
|
||||
"Mapping of user OIDC sub → version of their encryption public "
|
||||
"key at encryption time. Must cover the same set of users as "
|
||||
"`encrypted_symmetric_key_per_user`; null is valid for "
|
||||
"pending users (no public key to fingerprint yet)."
|
||||
"pending users (no public key to version yet)."
|
||||
),
|
||||
)
|
||||
encrypted_keys_for_descendants = serializers.DictField(
|
||||
|
||||
@@ -1050,8 +1050,8 @@ class ItemViewSet(
|
||||
encrypted_symmetric_key = validated_data.get("encrypted_symmetric_key")
|
||||
is_encryption_root_flag = validated_data.get("is_encryption_root")
|
||||
per_user_encrypted_keys = validated_data.get("per_user_encrypted_keys")
|
||||
fingerprint_per_user = validated_data.get(
|
||||
"encryption_public_key_fingerprints", {}
|
||||
version_per_user = validated_data.get(
|
||||
"encryption_public_key_versions", {}
|
||||
)
|
||||
# Encrypt-on-move payload: the presence of either field is the
|
||||
# signal we're looking at the plaintext-into-chain shape (an
|
||||
@@ -1268,25 +1268,25 @@ class ItemViewSet(
|
||||
code="item_move_caller_wrap_required",
|
||||
)
|
||||
|
||||
# Per-user fingerprints must cover the same set as the wraps.
|
||||
# Per-user versions must cover the same set as the wraps.
|
||||
provided_user_subs = set(per_user_encrypted_keys.keys())
|
||||
fingerprint_subs = set(fingerprint_per_user.keys())
|
||||
if fingerprint_subs != provided_user_subs:
|
||||
fp_missing = provided_user_subs - fingerprint_subs
|
||||
fp_extra = fingerprint_subs - provided_user_subs
|
||||
version_subs = set(version_per_user.keys())
|
||||
if version_subs != provided_user_subs:
|
||||
version_missing = provided_user_subs - version_subs
|
||||
version_extra = version_subs - provided_user_subs
|
||||
errors = {}
|
||||
if fp_missing:
|
||||
errors["missing_users"] = sorted(fp_missing)
|
||||
if fp_extra:
|
||||
errors["extra_users"] = sorted(fp_extra)
|
||||
if version_missing:
|
||||
errors["missing_users"] = sorted(version_missing)
|
||||
if version_extra:
|
||||
errors["extra_users"] = sorted(version_extra)
|
||||
raise drf.exceptions.ValidationError(
|
||||
{
|
||||
"encryption_public_key_fingerprints": _(
|
||||
"Fingerprint set must match per-user key set."
|
||||
"encryption_public_key_versions": _(
|
||||
"Version set must match per-user key set."
|
||||
),
|
||||
**errors,
|
||||
},
|
||||
code="item_move_fingerprints_mismatch",
|
||||
code="item_move_versions_mismatch",
|
||||
)
|
||||
|
||||
elif is_encryption_root_flag is False:
|
||||
@@ -1395,13 +1395,13 @@ class ItemViewSet(
|
||||
access.encrypted_item_symmetric_key_for_user = (
|
||||
per_user_encrypted_keys[user_sub]
|
||||
)
|
||||
access.encryption_public_key_fingerprint = (
|
||||
fingerprint_per_user.get(user_sub) or None
|
||||
access.encryption_public_key_version = (
|
||||
version_per_user.get(user_sub) or None
|
||||
)
|
||||
access.save(
|
||||
update_fields=[
|
||||
"encrypted_item_symmetric_key_for_user",
|
||||
"encryption_public_key_fingerprint",
|
||||
"encryption_public_key_version",
|
||||
]
|
||||
)
|
||||
remaining_user_subs.discard(user_sub)
|
||||
@@ -1428,8 +1428,8 @@ class ItemViewSet(
|
||||
encrypted_item_symmetric_key_for_user=per_user_encrypted_keys[
|
||||
user_sub
|
||||
],
|
||||
encryption_public_key_fingerprint=(
|
||||
fingerprint_per_user.get(user_sub) or None
|
||||
encryption_public_key_version=(
|
||||
version_per_user.get(user_sub) or None
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2162,27 +2162,27 @@ class ItemViewSet(
|
||||
# over the role they currently hold via inheritance so permissions
|
||||
# don't change — the new ItemAccess exists purely to hold the
|
||||
# encrypted key material.
|
||||
# Per-user fingerprint map — required, must cover the same set
|
||||
# Per-user version map — required, must cover the same set
|
||||
# of user subs as the wrapped-key map. Stored alongside the
|
||||
# wrapped key so clients can later tell which key the file was
|
||||
# encrypted for (surfaced in the "key mismatch" panel when
|
||||
# decrypt fails on a rotated key).
|
||||
fingerprint_per_user = serializer.validated_data[
|
||||
"encryption_public_key_fingerprint_per_user"
|
||||
version_per_user = serializer.validated_data[
|
||||
"encryption_public_key_version_per_user"
|
||||
]
|
||||
fingerprint_subs = set(fingerprint_per_user.keys())
|
||||
if fingerprint_subs != provided_user_subs:
|
||||
fp_missing = provided_user_subs - fingerprint_subs
|
||||
fp_extra = fingerprint_subs - provided_user_subs
|
||||
version_subs = set(version_per_user.keys())
|
||||
if version_subs != provided_user_subs:
|
||||
version_missing = provided_user_subs - version_subs
|
||||
version_extra = version_subs - provided_user_subs
|
||||
errors = {}
|
||||
if fp_missing:
|
||||
errors["missing_users"] = list(fp_missing)
|
||||
if fp_extra:
|
||||
errors["extra_users"] = list(fp_extra)
|
||||
if version_missing:
|
||||
errors["missing_users"] = list(version_missing)
|
||||
if version_extra:
|
||||
errors["extra_users"] = list(version_extra)
|
||||
return drf.response.Response(
|
||||
{
|
||||
"detail": _(
|
||||
"Provided fingerprints do not match the users in "
|
||||
"Provided versions do not match the users in "
|
||||
"encrypted_symmetric_key_per_user."
|
||||
),
|
||||
**errors,
|
||||
@@ -2200,11 +2200,11 @@ class ItemViewSet(
|
||||
user_sub
|
||||
]
|
||||
update_fields = ["encrypted_item_symmetric_key_for_user"]
|
||||
if user_sub in fingerprint_per_user:
|
||||
access.encryption_public_key_fingerprint = (
|
||||
fingerprint_per_user[user_sub] or None
|
||||
if user_sub in version_per_user:
|
||||
access.encryption_public_key_version = (
|
||||
version_per_user[user_sub] or None
|
||||
)
|
||||
update_fields.append("encryption_public_key_fingerprint")
|
||||
update_fields.append("encryption_public_key_version")
|
||||
access.save(update_fields=update_fields)
|
||||
remaining_user_subs.discard(user_sub)
|
||||
|
||||
@@ -2228,8 +2228,8 @@ class ItemViewSet(
|
||||
encrypted_item_symmetric_key_for_user=encrypted_key_per_user[
|
||||
user_sub
|
||||
],
|
||||
encryption_public_key_fingerprint=(
|
||||
fingerprint_per_user.get(user_sub) or None
|
||||
encryption_public_key_version=(
|
||||
version_per_user.get(user_sub) or None
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2368,7 +2368,7 @@ class ItemViewSet(
|
||||
# Clear all per-user encrypted keys on this item's accesses
|
||||
models.ItemAccess.objects.filter(item=item).update(
|
||||
encrypted_item_symmetric_key_for_user=None,
|
||||
encryption_public_key_fingerprint=None,
|
||||
encryption_public_key_version=None,
|
||||
)
|
||||
|
||||
# Collect file items in the effective scope for post-commit
|
||||
@@ -3157,13 +3157,13 @@ class ItemAccessViewSet(
|
||||
access.encrypted_item_symmetric_key_for_user = (
|
||||
serializer.validated_data["encrypted_item_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_item_symmetric_key_for_user",
|
||||
"encryption_public_key_fingerprint",
|
||||
"encryption_public_key_version",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0021_item_encryption_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="itemaccess",
|
||||
name="encryption_public_key_fingerprint",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemaccess",
|
||||
name="encryption_public_key_version",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text=(
|
||||
"Version of the user's encryption public key at the time of sharing. "
|
||||
"Used to detect key changes — if the user's current public key version "
|
||||
"differs from this value, the access needs re-encryption."
|
||||
),
|
||||
null=True,
|
||||
verbose_name="encryption public key version",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1223,13 +1223,13 @@ class ItemAccess(BaseModel):
|
||||
"This is the user's entry point into the key chain."
|
||||
),
|
||||
)
|
||||
encryption_public_key_fingerprint = models.CharField(
|
||||
max_length=16,
|
||||
encryption_public_key_version = models.PositiveIntegerField(
|
||||
_("encryption public key version"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Fingerprint of the user's public key at the time of sharing. "
|
||||
"Used to detect key changes — if the user's current public key fingerprint "
|
||||
"Version of the user's encryption public key at the time of sharing. "
|
||||
"Used to detect key changes — if the user's current public key version "
|
||||
"differs from this value, the access needs re-encryption."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_api_items_encrypt_standalone_file():
|
||||
f"/api/v1.0/items/{item.id!s}/encrypt/",
|
||||
{
|
||||
"encrypted_symmetric_key_per_user": {user.sub: "encrypted_key_for_user"},
|
||||
"encryption_public_key_fingerprint_per_user": {user.sub: "fp"},
|
||||
"encryption_public_key_version_per_user": {user.sub: 1},
|
||||
"encrypted_keys_for_descendants": {},
|
||||
},
|
||||
format="json",
|
||||
@@ -118,7 +118,7 @@ def test_api_items_encrypt_folder_with_children():
|
||||
f"/api/v1.0/items/{folder.id!s}/encrypt/",
|
||||
{
|
||||
"encrypted_symmetric_key_per_user": {user.sub: "root_key_for_user"},
|
||||
"encryption_public_key_fingerprint_per_user": {user.sub: "fp"},
|
||||
"encryption_public_key_version_per_user": {user.sub: 1},
|
||||
"encrypted_keys_for_descendants": {
|
||||
str(subfolder.pk): "subfolder_wrapped_key",
|
||||
str(file_item.pk): "file_wrapped_key",
|
||||
@@ -202,7 +202,7 @@ def test_api_items_encrypt_missing_user_keys():
|
||||
f"/api/v1.0/items/{item.id!s}/encrypt/",
|
||||
{
|
||||
"encrypted_symmetric_key_per_user": {user1.sub: "key1"},
|
||||
"encryption_public_key_fingerprint_per_user": {user1.sub: "fp1"},
|
||||
"encryption_public_key_version_per_user": {user1.sub: 1},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -388,7 +388,7 @@ def test_api_items_key_chain_prefers_root_over_hybrid_wrap():
|
||||
user=user,
|
||||
role="owner",
|
||||
encrypted_item_symmetric_key_for_user="hybrid_side_door_wrap",
|
||||
encryption_public_key_fingerprint="fp",
|
||||
encryption_public_key_version=1,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
@@ -439,7 +439,7 @@ def test_api_items_key_chain_falls_back_to_hybrid_for_outsiders():
|
||||
user=outsider,
|
||||
role="reader",
|
||||
encrypted_item_symmetric_key_for_user="outsider_wrap",
|
||||
encryption_public_key_fingerprint="fp-outsider",
|
||||
encryption_public_key_version=4,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
|
||||
@@ -859,7 +859,7 @@ def _encrypted_root(user, role="owner"):
|
||||
user=user,
|
||||
role=role,
|
||||
encrypted_item_symmetric_key_for_user="WRAP-USER-ROOT",
|
||||
encryption_public_key_fingerprint="fp-root",
|
||||
encryption_public_key_version=1,
|
||||
)
|
||||
return item
|
||||
|
||||
@@ -953,7 +953,7 @@ def test_api_items_move_re_anchor_to_plaintext():
|
||||
user=other,
|
||||
role="reader",
|
||||
encrypted_item_symmetric_key_for_user="WRAP-OTHER-ROOT",
|
||||
encryption_public_key_fingerprint="fp-other",
|
||||
encryption_public_key_version=2,
|
||||
)
|
||||
file_item = _encrypted_child(root, item_type=models.ItemTypeChoices.FILE)
|
||||
|
||||
@@ -971,9 +971,9 @@ def test_api_items_move_re_anchor_to_plaintext():
|
||||
user.sub: "WRAP-USER-NEW-ROOT",
|
||||
other.sub: "WRAP-OTHER-NEW-ROOT",
|
||||
},
|
||||
"encryption_public_key_fingerprints": {
|
||||
user.sub: "fp-user",
|
||||
other.sub: "fp-other",
|
||||
"encryption_public_key_versions": {
|
||||
user.sub: 3,
|
||||
other.sub: 2,
|
||||
},
|
||||
},
|
||||
format="json",
|
||||
@@ -988,7 +988,7 @@ def test_api_items_move_re_anchor_to_plaintext():
|
||||
# item with the wrap.
|
||||
user_access = models.ItemAccess.objects.get(item=file_item, user=user)
|
||||
assert user_access.encrypted_item_symmetric_key_for_user == "WRAP-USER-NEW-ROOT"
|
||||
assert user_access.encryption_public_key_fingerprint == "fp-user"
|
||||
assert user_access.encryption_public_key_version == 3
|
||||
other_access = models.ItemAccess.objects.get(item=file_item, user=other)
|
||||
assert other_access.encrypted_item_symmetric_key_for_user == "WRAP-OTHER-NEW-ROOT"
|
||||
|
||||
@@ -1008,7 +1008,7 @@ def test_api_items_move_re_anchor_with_pending_collaborator():
|
||||
factories.UserItemAccessFactory(
|
||||
item=root, user=pending, role="reader",
|
||||
encrypted_item_symmetric_key_for_user=None, # pending on root too
|
||||
encryption_public_key_fingerprint=None,
|
||||
encryption_public_key_version=None,
|
||||
)
|
||||
file_item = _encrypted_child(root, item_type=models.ItemTypeChoices.FILE)
|
||||
|
||||
@@ -1025,8 +1025,8 @@ def test_api_items_move_re_anchor_with_pending_collaborator():
|
||||
user.sub: "WRAP-USER-NEW-ROOT",
|
||||
pending.sub: None,
|
||||
},
|
||||
"encryption_public_key_fingerprints": {
|
||||
user.sub: "fp-user",
|
||||
"encryption_public_key_versions": {
|
||||
user.sub: 3,
|
||||
pending.sub: None,
|
||||
},
|
||||
},
|
||||
@@ -1036,7 +1036,7 @@ def test_api_items_move_re_anchor_with_pending_collaborator():
|
||||
assert response.status_code == 200, response.json()
|
||||
pending_access = models.ItemAccess.objects.get(item=file_item, user=pending)
|
||||
assert pending_access.encrypted_item_symmetric_key_for_user is None
|
||||
assert pending_access.encryption_public_key_fingerprint is None
|
||||
assert pending_access.encryption_public_key_version is None
|
||||
|
||||
|
||||
def test_api_items_move_re_anchor_caller_must_have_wrap():
|
||||
@@ -1062,7 +1062,7 @@ def test_api_items_move_re_anchor_caller_must_have_wrap():
|
||||
"target_item_id": str(plain_target.id),
|
||||
"is_encryption_root": True,
|
||||
"per_user_encrypted_keys": {user.sub: None},
|
||||
"encryption_public_key_fingerprints": {user.sub: None},
|
||||
"encryption_public_key_versions": {user.sub: None},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -1087,7 +1087,7 @@ def test_api_items_move_re_anchor_into_encrypted_rejected():
|
||||
"target_item_id": str(root_b.id),
|
||||
"is_encryption_root": True,
|
||||
"per_user_encrypted_keys": {user.sub: "WRAP"},
|
||||
"encryption_public_key_fingerprints": {user.sub: "fp"},
|
||||
"encryption_public_key_versions": {user.sub: 1},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -1116,7 +1116,7 @@ def test_api_items_move_demote_self_rooted_into_chain():
|
||||
user=user,
|
||||
role="owner",
|
||||
encrypted_item_symmetric_key_for_user="WRAP-USER-FILE-ROOT",
|
||||
encryption_public_key_fingerprint="fp-user",
|
||||
encryption_public_key_version=3,
|
||||
)
|
||||
|
||||
dest_root = _encrypted_root(user)
|
||||
@@ -1142,7 +1142,7 @@ def test_api_items_move_demote_self_rooted_into_chain():
|
||||
# — chain users use the chain, originals use their per-user wrap.
|
||||
user_access = models.ItemAccess.objects.get(item=file_item, user=user)
|
||||
assert user_access.encrypted_item_symmetric_key_for_user == "WRAP-USER-FILE-ROOT"
|
||||
assert user_access.encryption_public_key_fingerprint == "fp-user"
|
||||
assert user_access.encryption_public_key_version == 3
|
||||
|
||||
|
||||
def test_api_items_move_demote_preserves_outsider_wrap():
|
||||
@@ -1167,12 +1167,12 @@ def test_api_items_move_demote_preserves_outsider_wrap():
|
||||
factories.UserItemAccessFactory(
|
||||
item=file_item, user=user, role="owner",
|
||||
encrypted_item_symmetric_key_for_user="WRAP-USER-FILE",
|
||||
encryption_public_key_fingerprint="fp-user",
|
||||
encryption_public_key_version=3,
|
||||
)
|
||||
factories.UserItemAccessFactory(
|
||||
item=file_item, user=outsider, role="reader",
|
||||
encrypted_item_symmetric_key_for_user="WRAP-OUTSIDER-FILE",
|
||||
encryption_public_key_fingerprint="fp-outsider",
|
||||
encryption_public_key_version=4,
|
||||
)
|
||||
|
||||
# Destination tree only `user` has access to.
|
||||
@@ -1194,7 +1194,7 @@ def test_api_items_move_demote_preserves_outsider_wrap():
|
||||
# their wrap on this item directly.
|
||||
outsider_access = models.ItemAccess.objects.get(item=file_item, user=outsider)
|
||||
assert outsider_access.encrypted_item_symmetric_key_for_user == "WRAP-OUTSIDER-FILE"
|
||||
assert outsider_access.encryption_public_key_fingerprint == "fp-outsider"
|
||||
assert outsider_access.encryption_public_key_version == 4
|
||||
|
||||
|
||||
def test_api_items_move_demote_requires_chain_wrap():
|
||||
@@ -1210,7 +1210,7 @@ def test_api_items_move_demote_requires_chain_wrap():
|
||||
)
|
||||
factories.UserItemAccessFactory(
|
||||
item=file_item, user=user, role="owner",
|
||||
encrypted_item_symmetric_key_for_user="WRAP", encryption_public_key_fingerprint="fp",
|
||||
encrypted_item_symmetric_key_for_user="WRAP", encryption_public_key_version=1,
|
||||
)
|
||||
dest_root = _encrypted_root(user)
|
||||
|
||||
@@ -1240,7 +1240,7 @@ def test_api_items_move_demote_to_plaintext_rejected():
|
||||
)
|
||||
factories.UserItemAccessFactory(
|
||||
item=file_item, user=user, role="owner",
|
||||
encrypted_item_symmetric_key_for_user="WRAP", encryption_public_key_fingerprint="fp",
|
||||
encrypted_item_symmetric_key_for_user="WRAP", encryption_public_key_version=1,
|
||||
)
|
||||
plain_target = factories.ItemFactory(
|
||||
type=models.ItemTypeChoices.FOLDER, users=[(user, "owner")],
|
||||
|
||||
@@ -5,7 +5,7 @@ export type DTOCreateAccess = {
|
||||
userId: string;
|
||||
role: Role;
|
||||
encrypted_item_symmetric_key_for_user?: string;
|
||||
encryption_public_key_fingerprint?: string;
|
||||
encryption_public_key_version?: number;
|
||||
};
|
||||
|
||||
export type DTOUpdateAccess = {
|
||||
|
||||
@@ -202,7 +202,7 @@ export abstract class Driver {
|
||||
itemId: string,
|
||||
data: {
|
||||
encryptedSymmetricKeyPerUser: Record<string, string | null>;
|
||||
encryptionPublicKeyFingerprintPerUser: Record<string, string | null>;
|
||||
encryptionPublicKeyVersionPerUser: Record<string, number | null>;
|
||||
encryptedKeysForDescendants: Record<string, string>;
|
||||
fileKeyMapping?: Record<string, string>;
|
||||
}
|
||||
@@ -241,7 +241,7 @@ export abstract class Driver {
|
||||
accessId: string,
|
||||
data: {
|
||||
encrypted_item_symmetric_key_for_user: string;
|
||||
encryption_public_key_fingerprint: string;
|
||||
encryption_public_key_version: number;
|
||||
}
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fetchAPI } from "@/features/api/fetchApi";
|
||||
import { fromBase64, toBase64 } from "@/features/encryption/recursive/binary";
|
||||
import { fetchRegisteredKeys } from "@/features/encryption/fetchRegisteredKeys";
|
||||
import {
|
||||
Driver,
|
||||
Entitlements,
|
||||
@@ -213,10 +214,10 @@ export class StandardDriver extends Driver {
|
||||
// via the share UI once the user has onboarded.
|
||||
let perUserEncryptedKeys: Record<string, string | null> | undefined;
|
||||
// Matched pair with `perUserEncryptedKeys`: same key set, value is
|
||||
// the fingerprint of the user's pubkey at wrap time (or null when
|
||||
// the user is pending). Backend stores it on the access row so
|
||||
// the version of the user's encryption pubkey at wrap time (or null
|
||||
// when the user is pending). Backend stores it on the access row so
|
||||
// clients can later detect a key-rotation mismatch.
|
||||
let perUserFingerprints: Record<string, string | null> | undefined;
|
||||
let perUserVersions: Record<string, number | null> | undefined;
|
||||
let isEncryptionRoot: boolean | undefined; // present iff the move toggles the flag
|
||||
|
||||
if (sourceEncrypted && targetEncrypted && sourceIsRoot) {
|
||||
@@ -327,7 +328,8 @@ export class StandardDriver extends Driver {
|
||||
'No users with access — cannot re-anchor item as its own encryption root.',
|
||||
);
|
||||
}
|
||||
const { publicKeys } = await vaultClient.fetchPublicKeys(userSubs);
|
||||
const { publicKeys, versions } =
|
||||
await fetchRegisteredKeys(userSubs);
|
||||
const usersWithKeys = new Set(Object.keys(publicKeys));
|
||||
|
||||
// The only catastrophic case is the OPERATOR (the user doing
|
||||
@@ -352,25 +354,35 @@ export class StandardDriver extends Driver {
|
||||
}
|
||||
|
||||
// Wrap K_item only for users who have a pubkey; the rest get a
|
||||
// `null` placeholder that the backend will treat as pending.
|
||||
// `null` placeholder that the backend will treat as pending. Pass a
|
||||
// labeled recipient map (sub → {email, name}) built from the same
|
||||
// access rows — the vault resolves + trust-checks each key (binding +
|
||||
// TOFU); the labels are display-only, shown if the trust modal opens.
|
||||
const recipients: Record<string, { email: string; name?: string }> = {};
|
||||
for (const a of accesses) {
|
||||
if (a.user.sub && publicKeys[a.user.sub]) {
|
||||
recipients[a.user.sub] = {
|
||||
email: a.user.email,
|
||||
name: a.user.full_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
const { encryptedKeys } = await vaultClient.shareKeys(
|
||||
entryKey,
|
||||
publicKeys,
|
||||
recipients,
|
||||
chainToItem.length > 0 ? chainToItem : undefined,
|
||||
);
|
||||
perUserEncryptedKeys = {};
|
||||
// Fingerprints travel as matched pairs with the per-user wraps —
|
||||
// Versions travel as matched pairs with the per-user wraps —
|
||||
// the backend writes `(encrypted_item_symmetric_key_for_user,
|
||||
// encryption_public_key_fingerprint)` together on each access
|
||||
// encryption_public_key_version)` together on each access
|
||||
// row, identical to /encrypt/.
|
||||
perUserFingerprints = {};
|
||||
perUserVersions = {};
|
||||
for (const userSub of userSubs) {
|
||||
const wrap = encryptedKeys[userSub];
|
||||
perUserEncryptedKeys[userSub] = wrap ? toBase64(wrap) : null;
|
||||
const pubKey = publicKeys[userSub];
|
||||
perUserFingerprints[userSub] = pubKey
|
||||
? await vaultClient.computeKeyFingerprint(pubKey)
|
||||
: null;
|
||||
perUserVersions[userSub] = pubKey ? versions[userSub] : null;
|
||||
}
|
||||
const pendingCount = userSubs.length - usersWithKeys.size;
|
||||
if (pendingCount > 0) {
|
||||
@@ -394,8 +406,8 @@ export class StandardDriver extends Driver {
|
||||
...(perUserEncryptedKeys
|
||||
? { per_user_encrypted_keys: perUserEncryptedKeys }
|
||||
: {}),
|
||||
...(perUserFingerprints
|
||||
? { encryption_public_key_fingerprints: perUserFingerprints }
|
||||
...(perUserVersions
|
||||
? { encryption_public_key_versions: perUserVersions }
|
||||
: {}),
|
||||
};
|
||||
await fetchAPI(`items/${id}/move/`, {
|
||||
@@ -419,9 +431,9 @@ export class StandardDriver extends Driver {
|
||||
body.encrypted_item_symmetric_key_for_user =
|
||||
data.encrypted_item_symmetric_key_for_user;
|
||||
}
|
||||
if (data.encryption_public_key_fingerprint) {
|
||||
body.encryption_public_key_fingerprint =
|
||||
data.encryption_public_key_fingerprint;
|
||||
if (data.encryption_public_key_version) {
|
||||
body.encryption_public_key_version =
|
||||
data.encryption_public_key_version;
|
||||
}
|
||||
await fetchAPI(`items/${data.itemId}/accesses/`, {
|
||||
method: "POST",
|
||||
@@ -781,7 +793,7 @@ export class StandardDriver extends Driver {
|
||||
itemId: string,
|
||||
data: {
|
||||
encryptedSymmetricKeyPerUser: Record<string, string | null>;
|
||||
encryptionPublicKeyFingerprintPerUser: Record<string, string | null>;
|
||||
encryptionPublicKeyVersionPerUser: Record<string, number | null>;
|
||||
encryptedKeysForDescendants: Record<string, string>;
|
||||
fileKeyMapping?: Record<string, string>;
|
||||
},
|
||||
@@ -792,8 +804,8 @@ export class StandardDriver extends Driver {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
encrypted_symmetric_key_per_user: data.encryptedSymmetricKeyPerUser,
|
||||
encryption_public_key_fingerprint_per_user:
|
||||
data.encryptionPublicKeyFingerprintPerUser,
|
||||
encryption_public_key_version_per_user:
|
||||
data.encryptionPublicKeyVersionPerUser,
|
||||
encrypted_keys_for_descendants: data.encryptedKeysForDescendants,
|
||||
file_key_mapping: data.fileKeyMapping ?? {},
|
||||
}),
|
||||
@@ -860,7 +872,7 @@ export class StandardDriver extends Driver {
|
||||
accessId: string,
|
||||
data: {
|
||||
encrypted_item_symmetric_key_for_user: string;
|
||||
encryption_public_key_fingerprint: string;
|
||||
encryption_public_key_version: number;
|
||||
},
|
||||
): Promise<void> {
|
||||
await fetchAPI(
|
||||
|
||||
@@ -58,7 +58,7 @@ export type Item = {
|
||||
is_encryption_root?: boolean;
|
||||
is_inside_encrypted_subtree?: boolean;
|
||||
is_pending_encryption_for_user?: boolean;
|
||||
encryption_public_key_fingerprint_for_user?: string | null;
|
||||
encryption_public_key_version_for_user?: number | null;
|
||||
encrypted_item_symmetric_key_for_user?: string;
|
||||
accesses_user_ids?: string[];
|
||||
is_favorite?: boolean;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchRegisteredKeys } from '@/features/encryption/fetchRegisteredKeys';
|
||||
|
||||
/**
|
||||
* True when the SDK threw a `VaultError` carrying the
|
||||
* `WRONG_SECRET_KEY` code. In drive this almost always means the file
|
||||
@@ -21,29 +23,30 @@ export const isWrongSecretKeyError = (
|
||||
|
||||
interface KeyMismatchPanelProps {
|
||||
/**
|
||||
* Fingerprint stored on the user's access row at share time (i.e.
|
||||
* the fingerprint of the key the file was actually encrypted for).
|
||||
* Comes from `item.encryption_public_key_fingerprint_for_user` on
|
||||
* Drive or `doc.accesses_fingerprints_per_user[currentUser.sub]` on
|
||||
* Docs. Optional — if absent, only the current key is shown.
|
||||
* Encryption public key VERSION stored on the user's access row at
|
||||
* share time (i.e. the version of the key the file was actually
|
||||
* encrypted for). Comes from
|
||||
* `item.encryption_public_key_version_for_user` on Drive. Optional —
|
||||
* if absent, only the current version is shown. This is the per-access
|
||||
* staleness marker: when it lags behind the user's current version the
|
||||
* access needs re-wrapping.
|
||||
*/
|
||||
shareTimeFingerprint?: string | null;
|
||||
shareTimeVersion?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Friendly panel shown when `isWrongSecretKeyError` is true. Explains
|
||||
* the situation and surfaces BOTH the fingerprint the file was
|
||||
* encrypted for (stored at share time) and the user's current key's
|
||||
* fingerprint, so they can see the mismatch concretely and give the
|
||||
* new fingerprint to whoever re-adds them.
|
||||
* the situation and surfaces BOTH the key version the file was
|
||||
* encrypted for (stored at share time) and the user's CURRENT key
|
||||
* version, so they can see the staleness concretely: the access was
|
||||
* wrapped for version N of their key, their current version is M, so a
|
||||
* re-encryption is needed.
|
||||
*/
|
||||
export const KeyMismatchPanel = ({
|
||||
shareTimeFingerprint,
|
||||
shareTimeVersion,
|
||||
}: KeyMismatchPanelProps = {}) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentFingerprint, setCurrentFingerprint] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [currentVersion, setCurrentVersion] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const vault = window.__driveVaultClient;
|
||||
@@ -51,12 +54,12 @@ export const KeyMismatchPanel = ({
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const { publicKey } = await vault.getPublicKey();
|
||||
const raw = await vault.computeKeyFingerprint(publicKey);
|
||||
const formatted = vault.formatFingerprint(raw);
|
||||
if (!cancelled) setCurrentFingerprint(formatted);
|
||||
const sub = vault.getAuthContext()?.suiteUserId;
|
||||
if (!sub) return;
|
||||
const { versions } = await fetchRegisteredKeys([sub]);
|
||||
if (!cancelled) setCurrentVersion(versions[sub] ?? null);
|
||||
} catch {
|
||||
// Ignore — we just won't show the fingerprint row.
|
||||
// Ignore — we just won't show the current version row.
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
@@ -64,16 +67,9 @@ export const KeyMismatchPanel = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatShareTime = (() => {
|
||||
const vault = window.__driveVaultClient;
|
||||
if (!shareTimeFingerprint) return null;
|
||||
if (!vault) return shareTimeFingerprint;
|
||||
try {
|
||||
return vault.formatFingerprint(shareTimeFingerprint);
|
||||
} catch {
|
||||
return shareTimeFingerprint;
|
||||
}
|
||||
})();
|
||||
const hasShareTimeVersion =
|
||||
shareTimeVersion !== undefined && shareTimeVersion !== null;
|
||||
const hasCurrentVersion = currentVersion !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -117,7 +113,7 @@ export const KeyMismatchPanel = ({
|
||||
'The file was encrypted for you at a time when you were using a different encryption key — possibly before you reset your keys or switched device without restoring a backup. Your current key can no longer decrypt it. Ask an owner or administrator of this file to remove you from the access list and add you back so it gets re-encrypted for your current key.'
|
||||
)}
|
||||
</span>
|
||||
{(formatShareTime || currentFingerprint) && (
|
||||
{(hasShareTimeVersion || hasCurrentVersion) && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -128,11 +124,11 @@ export const KeyMismatchPanel = ({
|
||||
'var(--c--contextuals--content--semantic--neutral--tertiary)',
|
||||
}}
|
||||
>
|
||||
{formatShareTime && (
|
||||
{hasShareTimeVersion && (
|
||||
<div>
|
||||
{t(
|
||||
'explorer.encrypted.key_mismatch.share_time_fingerprint_label',
|
||||
'Fingerprint at the time it was shared with you:'
|
||||
'explorer.encrypted.key_mismatch.share_time_version_label',
|
||||
'Key version at the time it was shared with you:'
|
||||
)}{' '}
|
||||
<code
|
||||
style={{
|
||||
@@ -142,15 +138,15 @@ export const KeyMismatchPanel = ({
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{formatShareTime}
|
||||
{shareTimeVersion}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{currentFingerprint && (
|
||||
{hasCurrentVersion && (
|
||||
<div>
|
||||
{t(
|
||||
'explorer.encrypted.key_mismatch.fingerprint_label',
|
||||
'Your current key fingerprint:'
|
||||
'explorer.encrypted.key_mismatch.current_version_label',
|
||||
'Your current key version:'
|
||||
)}{' '}
|
||||
<code
|
||||
style={{
|
||||
@@ -160,7 +156,7 @@ export const KeyMismatchPanel = ({
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{currentFingerprint}
|
||||
{currentVersion}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,16 @@ import {
|
||||
} from './MissingEncryptionKeysModal';
|
||||
import { ModalEncryptionOnboarding } from './ModalEncryptionOnboarding';
|
||||
|
||||
// Drive-owned handle to the live VaultClient, stashed on window so non-React
|
||||
// call sites (drivers, deep helpers) can reach it without threading it through.
|
||||
// Not part of the SDK surface, so it lives here rather than in the vendored
|
||||
// declaration or its shim.
|
||||
declare global {
|
||||
interface Window {
|
||||
__driveVaultClient: VaultClient | null;
|
||||
}
|
||||
}
|
||||
|
||||
const VAULT_URL = process.env.NEXT_PUBLIC_VAULT_URL ?? 'http://localhost:7201';
|
||||
const INTERFACE_URL =
|
||||
process.env.NEXT_PUBLIC_INTERFACE_URL ?? 'http://localhost:7202';
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
export declare interface AuthContext {
|
||||
/**
|
||||
* The identity provider's `sub` claim for the logged-in user. Products only
|
||||
* ever deal in subs — their own APIs know users by sub, and every SDK
|
||||
* operation (recipients, fingerprints, profiles) takes subs. The vault
|
||||
* resolves them to its internal, migration-stable user ids at its boundary
|
||||
* (local alias map, then public registry); that internal id never has to be
|
||||
* handled, stored, or even seen by a product.
|
||||
*/
|
||||
suiteUserId: string;
|
||||
}
|
||||
|
||||
export declare interface EncryptionClientEventMap {
|
||||
/** Fired when the hidden vault iframe is ready for encrypt/decrypt operations */
|
||||
[MSG_VAULT_READY]: void;
|
||||
/** Fired when the user completes onboarding (key generation + backup) */
|
||||
'onboarding:complete': {
|
||||
publicKey: string;
|
||||
};
|
||||
/** Fired when the user cancels or closes the interface */
|
||||
[MSG_INTERFACE_CLOSED]: void;
|
||||
/** Fired on errors from the vault or the interface */
|
||||
error: Error;
|
||||
/** Fired when keys changed from another tab/product (via BroadcastChannel) */
|
||||
'keys-changed': void;
|
||||
/** Fired when keys were destroyed from another tab/product (via BroadcastChannel) */
|
||||
'keys-destroyed': void;
|
||||
/** Fired when a fingerprint is accepted or refused in the local registry */
|
||||
'fingerprint-changed': void;
|
||||
}
|
||||
|
||||
export declare interface EncryptionClientOptions {
|
||||
/** URL of the vault domain (data.encryption), e.g. "https://data.encryption.numerique.gouv.fr" */
|
||||
vaultUrl: string;
|
||||
/** URL of the interface domain (encryption), e.g. "https://encryption.numerique.gouv.fr" */
|
||||
interfaceUrl: string;
|
||||
/** Timeout in ms for vault operations (default: 30000) */
|
||||
timeout?: number;
|
||||
/**
|
||||
* Cunningham theme name for the interface iframe.
|
||||
* Standard names: "default", "dark", "dsfr", "dsfr-dark", "anct", "anct-dark".
|
||||
* Default: "default"
|
||||
*/
|
||||
theme?: string;
|
||||
/** Language code for the interface iframe: "fr", "en", etc. (default: browser language) */
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export declare const isVaultError: (err: unknown) => err is VaultError;
|
||||
|
||||
declare type Listener<K extends keyof EncryptionClientEventMap> = (data: EncryptionClientEventMap[K]) => void;
|
||||
|
||||
declare const MSG_INTERFACE_CLOSED = "interface:closed";
|
||||
|
||||
declare const MSG_VAULT_READY = "vault:ready";
|
||||
|
||||
export declare type RecipientLabel = {
|
||||
email: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Union on `verified` on purpose: `encryptionPublicKey` is a usable
|
||||
* `ArrayBuffer` only in the verified branch (`null` otherwise), so a caller
|
||||
* cannot wrap a key for a forged / incoherent directory entry without the
|
||||
* compiler stopping them. Identity and encryption key live in one per-user
|
||||
* entry so the two can never disagree for a user.
|
||||
*/
|
||||
export declare type RegisteredUser = {
|
||||
verified: true;
|
||||
signaturePublicKey: ArrayBuffer;
|
||||
identityFingerprint: string;
|
||||
version: number;
|
||||
createdAtMillis: number;
|
||||
encryptionPublicKey: ArrayBuffer;
|
||||
} | {
|
||||
verified: false;
|
||||
signaturePublicKey: ArrayBuffer;
|
||||
identityFingerprint: string;
|
||||
version: number;
|
||||
createdAtMillis: number;
|
||||
encryptionPublicKey: null;
|
||||
};
|
||||
|
||||
export declare class VaultClient {
|
||||
private vaultIframe;
|
||||
private interfaceIframe;
|
||||
private pending;
|
||||
private listeners;
|
||||
private vaultReadyResolve;
|
||||
private initTimeoutId;
|
||||
private messageHandler;
|
||||
private vaultUrl;
|
||||
private interfaceUrl;
|
||||
private vaultOrigin;
|
||||
private interfaceOrigin;
|
||||
private timeout;
|
||||
private theme;
|
||||
private lang;
|
||||
private authContext;
|
||||
private verifyOverlay;
|
||||
private verifyResolve;
|
||||
private emergencyOverlay;
|
||||
private emergencySurfaced;
|
||||
private emergencyWatchdog;
|
||||
private pendingContext;
|
||||
constructor(options: EncryptionClientOptions);
|
||||
/**
|
||||
* Update the Cunningham theme. If the interface iframe is open, sends it a
|
||||
* theme message so it re-themes in place.
|
||||
* @param theme - Cunningham theme name: "default", "dark", "dsfr", "dsfr-dark", "anct", "anct-dark", etc.
|
||||
*/
|
||||
setTheme(theme: string): void;
|
||||
/**
|
||||
* Set the authentication context. Must be called before opening the interface.
|
||||
* The suiteUserId is passed to the interface iframe so it can register
|
||||
* public keys on the server and perform device transfers.
|
||||
*/
|
||||
setAuthContext(context: AuthContext): void;
|
||||
/**
|
||||
* Read back the auth context that was set via {@link setAuthContext}.
|
||||
* Returns `null` until `setAuthContext` has been called. Useful when
|
||||
* the host app needs to identify the currently-bound suite user from
|
||||
* code that doesn't otherwise have access to the auth state — e.g.
|
||||
* the Drive driver's encryption-aware move handler, which has to
|
||||
* verify the operator (mover) keeps a per-user wrap during a
|
||||
* re-anchor and would otherwise need to thread the user identifier
|
||||
* through every call site.
|
||||
*/
|
||||
getAuthContext(): AuthContext | null;
|
||||
/**
|
||||
* Initialize the encryption client.
|
||||
* Creates a hidden vault iframe (data.encryption) and waits for it to be ready.
|
||||
*/
|
||||
init(): Promise<void>;
|
||||
/**
|
||||
* Clean up all iframes and event listeners.
|
||||
*/
|
||||
destroy(): void;
|
||||
/** Check if the user has encryption keys on this device. */
|
||||
hasKeys(): Promise<{
|
||||
hasKeys: boolean;
|
||||
}>;
|
||||
/** Get the user's public key as ArrayBuffer. */
|
||||
getPublicKey(): Promise<{
|
||||
publicKey: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Create a new ROOT encrypted resource. Mints a fresh symmetric key,
|
||||
* encrypts `data` with it, and wraps the new key once per recipient.
|
||||
*
|
||||
* Pass recipients as a labeled map (OIDC sub → {email, name?}), not public
|
||||
* keys — the subs your product already holds for its users. The vault
|
||||
* resolves + trust-checks each recipient (binding + TOFU 'trusted' with
|
||||
* matching fingerprint, keyed internally so trust survives an OIDC provider
|
||||
* migration) before wrapping, and throws UNTRUSTED_RECIPIENT if any is
|
||||
* unverified or untrusted. Call checkFingerprints first to resolve any
|
||||
* 'unknown' contacts. The labels are display-only and used only if the trust
|
||||
* modal opens; the vault request itself sees just the subs.
|
||||
*
|
||||
* Use this for standalone encrypted files (no parent) and for the root
|
||||
* folder of an encrypted subtree.
|
||||
*
|
||||
* @returns encryptedContent and encryptedKeys (userId → wrappedKey)
|
||||
*/
|
||||
encryptWithoutKey(data: ArrayBuffer, recipients: Record<string, RecipientLabel>, options?: {
|
||||
optimizeMemory?: boolean;
|
||||
}): Promise<{
|
||||
encryptedContent: ArrayBuffer;
|
||||
encryptedKeys: Record<string, ArrayBuffer>;
|
||||
}>;
|
||||
private encryptWithoutKeyRequest;
|
||||
/**
|
||||
* Create a new NESTED encrypted resource inside an existing encrypted
|
||||
* subtree. Resolves `entry + chain` to the parent folder's key, mints a
|
||||
* fresh symmetric key, encrypts `data` with it, and wraps the new key
|
||||
* under the parent's key.
|
||||
*
|
||||
* Use this for creating a file inside an already-encrypted folder.
|
||||
* Persist the returned `wrappedKey` on the new item's DB row.
|
||||
*
|
||||
* @param data - ArrayBuffer content to encrypt
|
||||
* @param encryptedSymmetricKey - caller's entry key (asymmetric bootstrap
|
||||
* into the subtree, same value used for decryptWithKey)
|
||||
* @param encryptedKeyChain - optional symmetric wrappings from entry down
|
||||
* to the parent folder (exclusive of the new resource, since it doesn't
|
||||
* exist yet). Empty/omitted means the entry key IS the parent's key.
|
||||
*/
|
||||
encryptNestedWithoutKey(data: ArrayBuffer, encryptedSymmetricKey: ArrayBuffer, encryptedKeyChain?: ArrayBuffer[], options?: {
|
||||
optimizeMemory?: boolean;
|
||||
}): Promise<{
|
||||
encryptedContent: ArrayBuffer;
|
||||
wrappedKey: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Encrypt content with an EXISTING symmetric key — pure symmetric mirror
|
||||
* of `decryptWithKey`. No new key is minted.
|
||||
*
|
||||
* Without `encryptedKeyChain`: resolves `encryptedSymmetricKey` (user's
|
||||
* entry key) and encrypts with it. Used by the flat model (Docs).
|
||||
*
|
||||
* With `encryptedKeyChain`: resolves entry + chain to the terminal
|
||||
* symmetric key and encrypts with it. Used by the collaborative relay
|
||||
* to encrypt messages tied to an existing file inside an encrypted
|
||||
* hierarchy, so that sender and receiver converge on the same K_file.
|
||||
*/
|
||||
encryptWithKey(data: ArrayBuffer, encryptedSymmetricKey: ArrayBuffer, encryptedKeyChain?: ArrayBuffer[], options?: {
|
||||
optimizeMemory?: boolean;
|
||||
}): Promise<{
|
||||
encryptedData: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Decrypt content using a separately provided encrypted symmetric key.
|
||||
* The symmetric key decryption is cached per session for performance.
|
||||
*
|
||||
* @param encryptedData - ArrayBuffer ciphertext to decrypt
|
||||
* @param encryptedSymmetricKey - user's encrypted copy of the symmetric key
|
||||
* @param keyVersion - the recipient's encryption-key VERSION this wrap was
|
||||
* produced against, as stored by the product on the access row. The vault
|
||||
* unwraps with exactly that retained key (a version this device no longer
|
||||
* holds throws WRONG_SECRET_KEY). For Drive chains it is the version of the
|
||||
* ENTRY-point key; the chain links themselves are symmetric.
|
||||
* @param encryptedKeyChain - optional chain of wrapped keys for Drive's key hierarchy.
|
||||
* When provided, resolves the chain from entry point to target before decrypting.
|
||||
*/
|
||||
decryptWithKey(encryptedData: ArrayBuffer, encryptedSymmetricKey: ArrayBuffer, keyVersion: number, encryptedKeyChain?: ArrayBuffer[], options?: {
|
||||
optimizeMemory?: boolean;
|
||||
}): Promise<{
|
||||
data: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Re-wrap a nested resource's symmetric key from one parent chain onto
|
||||
* another. Used when MOVING an encrypted file/folder between positions
|
||||
* inside the same encrypted subtree: the file's content is left
|
||||
* untouched (still encrypted with K_file), but K_file's wrapping
|
||||
* follows its new parent.
|
||||
*
|
||||
* @param encryptedSymmetricKey - the user's entry-point key (root key
|
||||
* wrapped under the user's pubkey). Same value for both old and new
|
||||
* chains since this operation stays within a single encrypted root.
|
||||
* @param oldEncryptedKey - the resource's K_file as currently stored,
|
||||
* wrapped under its OLD parent's key.
|
||||
* @param oldEncryptedKeyChain - chain of wrapped folder keys from the
|
||||
* entry point down to (and including) the OLD parent's key. Omit /
|
||||
* pass `undefined` when the OLD parent IS the encryption root.
|
||||
* @param newEncryptedKeyChain - chain entry → NEW parent. Omit when
|
||||
* the NEW parent is the encryption root.
|
||||
* @returns the resource's K_file re-wrapped under the NEW parent's
|
||||
* key — caller persists this on the resource's DB row, replacing
|
||||
* the old wrapping.
|
||||
*/
|
||||
rewrapNestedKey(encryptedSymmetricKey: ArrayBuffer, oldEncryptedKey: ArrayBuffer, oldEncryptedKeyChain?: ArrayBuffer[], newEncryptedKeyChain?: ArrayBuffer[]): Promise<{
|
||||
newEncryptedKey: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Wrap an existing per-user-anchored symmetric key under a parent
|
||||
* chain. Symmetric reverse of {@link shareKeys} (which goes 1→N
|
||||
* chain→per-user); this goes 1→1 per-user→chain.
|
||||
*
|
||||
* Used when MOVING a self-rooted encrypted resource (per-user
|
||||
* wraps on its access rows) INTO an encrypted subtree: K_item is
|
||||
* recovered from the user's per-user wrap, then wrapped under the
|
||||
* destination parent's chain so the resource stops being a root
|
||||
* and joins the destination tree.
|
||||
*
|
||||
* @param userEncryptedKey - the resource's per-user wrap from the
|
||||
* caller's access row (`encrypted_item_symmetric_key_for_user`).
|
||||
* @param newEntryEncryptedSymmetricKey - user's entry-point key
|
||||
* for the destination tree (the tree's root key wrapped under
|
||||
* their pubkey — same value `getKeyChain` returns as
|
||||
* `encrypted_key_for_user` for any item under that tree).
|
||||
* @param newEncryptedKeyChain - chain entry → NEW parent. Omit
|
||||
* when the new parent IS the destination tree's root.
|
||||
* @returns the resource's K_item wrapped under the new parent's
|
||||
* key — caller persists this on the resource's row.
|
||||
*/
|
||||
wrapNestedKey(userEncryptedKey: ArrayBuffer, newEntryEncryptedSymmetricKey: ArrayBuffer, newEncryptedKeyChain?: ArrayBuffer[]): Promise<{
|
||||
newEncryptedKey: ArrayBuffer;
|
||||
}>;
|
||||
/**
|
||||
* Share an existing document's or item's symmetric key with additional users.
|
||||
*
|
||||
* Pass recipients as a labeled map (OIDC sub → {email, name?}), not public
|
||||
* keys: the vault resolves each recipient's encryption key from the directory
|
||||
* itself and wraps ONLY for identities whose binding verifies AND that you have marked
|
||||
* 'trusted' (TOFU) with a matching fingerprint. If any recipient is unverified
|
||||
* or untrusted it throws UNTRUSTED_RECIPIENT and wraps for none — call
|
||||
* checkFingerprints (and resolve any 'unknown') first. Recipients are resolved
|
||||
* in one batched request. The labels are display-only and used only if the
|
||||
* trust modal opens; the vault request itself sees just the userIds.
|
||||
*
|
||||
* @param encryptedSymmetricKey - current user's encrypted copy of the key
|
||||
* @param recipients - map of userId → display label to share with
|
||||
* @param encryptedKeyChain - optional chain of wrapped keys for Drive's key hierarchy.
|
||||
* When provided, resolves the chain from entry point to the target item's key
|
||||
* before re-encrypting for the target users.
|
||||
* @returns encryptedKeys - Record of userId → ArrayBuffer encrypted symmetric key for each user
|
||||
*/
|
||||
shareKeys(encryptedSymmetricKey: ArrayBuffer, recipients: Record<string, RecipientLabel>, encryptedKeyChain?: ArrayBuffer[]): Promise<{
|
||||
encryptedKeys: Record<string, ArrayBuffer>;
|
||||
}>;
|
||||
private shareKeysRequest;
|
||||
/**
|
||||
* Fetch registered users for a list of OIDC subs (the ids your product
|
||||
* already holds). The vault calls the encryption server itself (products
|
||||
* never touch it) and verifies each record's binding signature before
|
||||
* returning. The map is keyed by the subs you queried; subs with no active
|
||||
* registration are absent (that person never onboarded encryption). Use it
|
||||
* when building a sharing UI: it tells you who has keys, their fingerprint,
|
||||
* and whether the directory record is coherent (`verified`).
|
||||
*/
|
||||
fetchPublicKeys(subs: string[]): Promise<Record<string, RegisteredUser>>;
|
||||
/**
|
||||
* Check fingerprints provided by the product against the vault's local registry.
|
||||
* The product sends the fingerprints it stored at share time, keyed by OIDC
|
||||
* sub; results echo the same subs back (the vault translates to its internal
|
||||
* trust keys on its side).
|
||||
*
|
||||
* Returns results with status: "trusted", "refused", or "unknown" (needs user decision).
|
||||
*/
|
||||
checkFingerprints(userFingerprints: Record<string, string>): Promise<{
|
||||
results: Array<{
|
||||
userId: string;
|
||||
knownFingerprint: string | null;
|
||||
providedFingerprint: string;
|
||||
status: 'trusted' | 'refused' | 'unknown' | 'mismatch';
|
||||
}>;
|
||||
}>;
|
||||
/**
|
||||
* Get all known fingerprints with their status from the local registry.
|
||||
*/
|
||||
getKnownFingerprints(): Promise<{
|
||||
fingerprints: Record<string, {
|
||||
fingerprint: string;
|
||||
status: 'trusted' | 'refused' | 'unknown';
|
||||
}>;
|
||||
}>;
|
||||
/**
|
||||
* Compute a 128-bit DECIMAL fingerprint of a public key: the first 16 bytes of
|
||||
* its SHA-256, read big-endian as a fixed-width 40-digit decimal. Matches the
|
||||
* device-pairing fingerprint so every surface shows the same value.
|
||||
* This is a pure client-side operation — no vault iframe needed.
|
||||
*
|
||||
* @param publicKey - The public key as ArrayBuffer (from fetchPublicKeys or getPublicKey)
|
||||
*/
|
||||
computeKeyFingerprint(publicKey: ArrayBuffer): Promise<string>;
|
||||
/**
|
||||
* Format a raw decimal fingerprint for display, grouped in blocks of five.
|
||||
*/
|
||||
formatFingerprint(fingerprint: string): string;
|
||||
/**
|
||||
* Open the encryption interface for onboarding (key generation + backup).
|
||||
* The product provides a container element where the interface iframe will be mounted.
|
||||
* The product is responsible for showing/hiding this container (e.g. in a modal).
|
||||
*
|
||||
* Listen to 'onboarding:complete' and 'interface:closed' events for results.
|
||||
*/
|
||||
openOnboarding(container: HTMLElement): void;
|
||||
/**
|
||||
* Open the encryption interface for key backup/export.
|
||||
*/
|
||||
openBackup(container: HTMLElement): void;
|
||||
/**
|
||||
* Open the encryption interface for key restoration from backup.
|
||||
*/
|
||||
openRestore(container: HTMLElement): void;
|
||||
/**
|
||||
* Open the encryption settings (view fingerprint, delete keys).
|
||||
*/
|
||||
openSettings(container: HTMLElement): void;
|
||||
/**
|
||||
* Open device approval: enroll this device from another, or approve a new one.
|
||||
*/
|
||||
openDeviceApproval(container: HTMLElement): void;
|
||||
/**
|
||||
* Open the emergency-access (trusted contacts) management screen: designate
|
||||
* contacts, accept a designation, follow or refuse a running recovery.
|
||||
*/
|
||||
openEmergencyAccess(container: HTMLElement): void;
|
||||
/**
|
||||
* Open the per-recipient profile: the recipient's current trust decision, their
|
||||
* identity fingerprint (for out-of-band comparison), and Trust / Refuse actions.
|
||||
* Opened explicitly by the product (e.g. clicking a person in its share UI), so
|
||||
* it mounts in a product-provided container like the other open* methods.
|
||||
* `userId` is the recipient's OIDC sub, like every id a product passes.
|
||||
*/
|
||||
openRecipientProfile(container: HTMLElement, userId: string, label: RecipientLabel): void;
|
||||
/**
|
||||
* Close the interface iframe if it is open.
|
||||
*/
|
||||
closeInterface(): void;
|
||||
on<K extends keyof EncryptionClientEventMap>(event: K, listener: Listener<K>): void;
|
||||
off<K extends keyof EncryptionClientEventMap>(event: K, listener: Listener<K>): void;
|
||||
private openInterface;
|
||||
/**
|
||||
* Construct and configure an interface iframe for `path` (sandbox, allow,
|
||||
* theme/lang hash, context handshake). Mounting is left to the caller so the
|
||||
* same setup serves both the product-provided container (openInterface) and
|
||||
* the SDK-created full-screen overlay (openVerifyRecipients).
|
||||
*
|
||||
* `overlay` travels in the HASH, not in the postMessage context, precisely
|
||||
* because the context arrives asynchronously: a screen that renders as a page
|
||||
* when embedded and as a modal when overlaid would otherwise paint the page
|
||||
* variant first (full-width and opaque, over the product) and swap to the modal
|
||||
* only once the handshake lands, which reads as a flash.
|
||||
*/
|
||||
private buildInterfaceIframe;
|
||||
/** The context message currently owed to the interface, or null before auth. */
|
||||
private sendContext;
|
||||
/**
|
||||
* Open the SDK-owned "verify recipients" overlay on top of the product's own
|
||||
* share dialog, and resolve with the user's outcome. The SDK deliberately does
|
||||
* NOT draw any chrome here: the container is a minimal, transparent, full-
|
||||
* viewport layer, and the interface (a Cunningham Modal) draws the whole modal
|
||||
* (its own backdrop + card) inside the transparent iframe, so it matches the
|
||||
* rest of the interface UI. Tears the overlay down once the outcome is in.
|
||||
*/
|
||||
private openVerifyRecipients;
|
||||
/**
|
||||
* Auto-open the interface over the product when the vault reports actionable
|
||||
* emergency-access state: a running recovery request against the user's vault
|
||||
* (which they must be able to refuse without hunting for a menu) or a pending
|
||||
* trusted-contact designation to accept. Same transparent-overlay technique
|
||||
* as the verify-recipients flow; at most once per page load, and never while
|
||||
* another interface flow is already open (the settings screen shows the same
|
||||
* state anyway).
|
||||
*
|
||||
* This one is the ONLY flow the SDK opens on its own initiative, so it is held
|
||||
* to a stricter rule than the flows a product asked for: it stays invisible
|
||||
* until the interface says it is up, and if it never says so it is removed
|
||||
* rather than left covering the page. Both halves hang off the same signal:
|
||||
*
|
||||
* - the interface asks for its context (MSG_INTERFACE_REQUEST_CONTEXT) as soon
|
||||
* as its React app mounts, so that is "the remote page is ready";
|
||||
* - until then `visibility: hidden` keeps the blank document, then the app's
|
||||
* first frames, off the screen;
|
||||
* - if it never arrives (bundle blocked, offline, a redirect leaving an empty
|
||||
* document, a crash before mount) the watchdog removes the whole overlay, so
|
||||
* a broken interface never sits on top of the product swallowing clicks. The
|
||||
* user loses nothing: the same state is in the settings screen, and the
|
||||
* load-bearing channel for all of this is email.
|
||||
*/
|
||||
private surfaceEmergencyPending;
|
||||
/**
|
||||
* The interface app mounted. Reveal the overlay we kept hidden and stand the
|
||||
* watchdog down. No-op for every other flow (the product owns their container).
|
||||
*/
|
||||
private revealEmergencyOverlay;
|
||||
private teardownEmergencyOverlay;
|
||||
private completeVerify;
|
||||
private teardownVerifyOverlay;
|
||||
/**
|
||||
* Run a recipient-bearing operation, and on UNTRUSTED_RECIPIENT open the shared
|
||||
* verify modal for the ORIGINAL recipients (full labeled map; the interface
|
||||
* surfaces only the blocked ones). If the user trusts them all, retry the
|
||||
* operation exactly once; otherwise rethrow the original error so the product
|
||||
* sees the share failed (all-or-nothing). Any other error rethrows unchanged.
|
||||
* This is always on: whether to prompt for trust is not a product choice, so
|
||||
* there is no opt-out.
|
||||
*/
|
||||
private withRecipientVerification;
|
||||
private vaultRequest;
|
||||
private handleMessage;
|
||||
private handleVaultMessage;
|
||||
private handleInterfaceMessage;
|
||||
private removeIframe;
|
||||
private emit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error subclass tagged with a stable {@link VaultErrorCode}. Throw this
|
||||
* (instead of `new Error(...)`) anywhere inside the SDK so the boundary
|
||||
* marshaller can pass the code along to consumers.
|
||||
*
|
||||
* Note: `VaultError` instances DO NOT survive `structuredClone` — the
|
||||
* postMessage layer marshals `{ message, code }` explicitly and
|
||||
* reconstructs the class on the receiving side.
|
||||
*/
|
||||
export declare class VaultError extends Error {
|
||||
readonly code: VaultErrorCode;
|
||||
constructor(code: VaultErrorCode, message: string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable error codes that travel across the vault iframe ↔ client
|
||||
* postMessage boundary. Consumers (drive / docs / meet) match on these
|
||||
* instead of regexing error messages — message text is for logs / humans
|
||||
* and may change, the codes are part of the SDK contract.
|
||||
*
|
||||
* Why a string-keyed const (not a TS enum): keeps the runtime values
|
||||
* tree-shakeable, and the strings double as wire identifiers when
|
||||
* marshalled over postMessage.
|
||||
*/
|
||||
export declare const VaultErrorCode: {
|
||||
/** No key pair stored locally on this device — user must onboard. */
|
||||
readonly MISSING_KEYS: "MISSING_KEYS";
|
||||
/**
|
||||
* AEAD verification failed. Either the ciphertext is for a different
|
||||
* recipient (their wrapped symmetric key was encrypted against another
|
||||
* pubkey) or the underlying KEM secret didn't match. Bubbles up from
|
||||
* libsodium's "wrong secret key for the given ciphertext".
|
||||
*/
|
||||
readonly WRONG_SECRET_KEY: "WRONG_SECRET_KEY";
|
||||
/** Backup payload is corrupted, truncated, or from an unsupported version. */
|
||||
readonly INVALID_BACKUP: "INVALID_BACKUP";
|
||||
/** BIP-39-style mnemonic input that doesn't checksum. */
|
||||
readonly INVALID_MNEMONIC: "INVALID_MNEMONIC";
|
||||
/** Caller hit a vault method without first calling `init()`. */
|
||||
readonly NOT_INITIALIZED: "NOT_INITIALIZED";
|
||||
/** `setAuthContext({ suiteUserId })` was never called. */
|
||||
readonly AUTH_REQUIRED: "AUTH_REQUIRED";
|
||||
/**
|
||||
* The declared sub could not be resolved to an internal encryption user id:
|
||||
* no local alias, and no directory row (either the user never onboarded, or
|
||||
* the registry was unreachable with nothing cached). Thrown by every vault
|
||||
* operation EXCEPT `has-keys`, which responds `{ hasKeys: false }` instead
|
||||
* of throwing: for that probe, "unresolvable" and "never onboarded" are the
|
||||
* same answer, and products use it to decide whether to offer onboarding.
|
||||
*/
|
||||
readonly UNRESOLVED_USER: "UNRESOLVED_USER";
|
||||
/** Privileged operation attempted from a non-encryption-origin caller. */
|
||||
readonly PRIVILEGED_ORIGIN_REQUIRED: "PRIVILEGED_ORIGIN_REQUIRED";
|
||||
/** A vault request didn't get an answer within the configured timeout. */
|
||||
readonly TIMEOUT: "TIMEOUT";
|
||||
/** Vault module loaded outside an iframe (origin-isolation invariant). */
|
||||
readonly IFRAME_REQUIRED: "IFRAME_REQUIRED";
|
||||
/** Ciphertext / encrypted-key payload too short to be valid (truncated). */
|
||||
readonly CIPHERTEXT_TOO_SHORT: "CIPHERTEXT_TOO_SHORT";
|
||||
/** Blob's leading version byte doesn't match a format this build can decode. */
|
||||
readonly UNSUPPORTED_CRYPTO_VERSION: "UNSUPPORTED_CRYPTO_VERSION";
|
||||
/** A signature public key didn't have the expected Ed25519 length. */
|
||||
readonly INVALID_SIGNATURE_KEY: "INVALID_SIGNATURE_KEY";
|
||||
/**
|
||||
* A registry entry's binding signature did not verify against its claimed
|
||||
* identity (signature) key — the directory record is forged, tampered, or
|
||||
* incoherent. Consumers MUST refuse to trust / share with such an entry.
|
||||
*/
|
||||
readonly INVALID_KEY_BINDING: "INVALID_KEY_BINDING";
|
||||
/**
|
||||
* A pulled vault failed its integrity check: the identity-signed manifest did
|
||||
* not verify, an item's ciphertext hash or coverage did not match, or the
|
||||
* revision rolled back. Distinct from a wrong recovery phrase (which fails the
|
||||
* unlock, not the integrity check) — it means the SERVER served tampered or
|
||||
* incoherent vault data, so the user must be warned, not told to re-type.
|
||||
*/
|
||||
readonly VAULT_INTEGRITY_FAILED: "VAULT_INTEGRITY_FAILED";
|
||||
/**
|
||||
* A wrap was attempted for a recipient whose identity is not TOFU-'trusted'
|
||||
* with a matching fingerprint (refused, never verified, or a fingerprint
|
||||
* mismatch that may be a MITM-substituted key). The vault refuses to wrap the
|
||||
* symmetric key until the recipient's identity is verified. The offending
|
||||
* userIds are in the error message.
|
||||
*/
|
||||
readonly UNTRUSTED_RECIPIENT: "UNTRUSTED_RECIPIENT";
|
||||
/**
|
||||
* A write-through change (e.g. a TOFU decision) could not be pushed to the
|
||||
* server, so it was NOT kept locally either — the caller should surface a
|
||||
* "couldn't save, retry" and the local state is unchanged. Distinct from a
|
||||
* network throw (which also aborts before persisting).
|
||||
*/
|
||||
readonly SYNC_FAILED: "SYNC_FAILED";
|
||||
/**
|
||||
* The request reached a handler but its payload does not satisfy that
|
||||
* operation's contract (a missing required field, or none of a set of mutually
|
||||
* exclusive ones). A CALLER bug, not a user condition: it is surfaced rather
|
||||
* than defaulted so a malformed call fails loud instead of acting on a
|
||||
* half-specified target.
|
||||
*/
|
||||
readonly INVALID_REQUEST: "INVALID_REQUEST";
|
||||
/**
|
||||
* Catch-all for situations the SDK couldn't classify into a more
|
||||
* specific code — present so consumers always have something to switch
|
||||
* on rather than falling back to message regex.
|
||||
*/
|
||||
readonly UNKNOWN: "UNKNOWN";
|
||||
};
|
||||
|
||||
export declare type VaultErrorCode = (typeof VaultErrorCode)[keyof typeof VaultErrorCode];
|
||||
|
||||
export { }
|
||||
|
||||
export as namespace EncryptionClient;
|
||||
+14
-150
@@ -1,157 +1,21 @@
|
||||
// Ambient bindings for the VaultClient SDK.
|
||||
//
|
||||
// The full method/type surface lives in ./client-sdk.d.ts, a VERBATIM copy of
|
||||
// the encryption service's generated declaration (served at
|
||||
// <encryption-domain>/public-assets/client.d.ts). Regenerate it with:
|
||||
// curl http://encryption.localhost:7200/public-assets/client.d.ts -o client-sdk.d.ts
|
||||
// This file only re-exposes those types as the globals this codebase uses; never
|
||||
// hand-edit signatures here — fix them at the source and re-vendor.
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface VaultClient {
|
||||
init(): Promise<void>;
|
||||
destroy(): void;
|
||||
setTheme(theme: string): void;
|
||||
setAuthContext(context: { suiteUserId: string }): void;
|
||||
getAuthContext(): { suiteUserId: string } | null;
|
||||
hasKeys(): Promise<{ hasKeys: boolean }>;
|
||||
getPublicKey(): Promise<{ publicKey: ArrayBuffer }>;
|
||||
// Root creation: mint a new key and wrap it under each user's pubkey.
|
||||
//
|
||||
// Note on `optimizeMemory` (present on all four data-handling methods
|
||||
// below): defaults to `false` — safe behaviour, the SDK clones the
|
||||
// input buffer on its way to the vault and the vault clones the
|
||||
// result on its way back. Caller's input buffer stays valid. Set to
|
||||
// `true` ONLY on hot paths where the caller is happy to lose
|
||||
// ownership of the input buffer (transferred + detached) in
|
||||
// exchange for skipping two large-buffer copies. Keys / chain /
|
||||
// userId are never transferred regardless of this flag.
|
||||
encryptWithoutKey(
|
||||
data: ArrayBuffer,
|
||||
userPublicKeys: Record<string, ArrayBuffer>,
|
||||
options?: { optimizeMemory?: boolean }
|
||||
): Promise<{
|
||||
encryptedContent: ArrayBuffer;
|
||||
encryptedKeys: Record<string, ArrayBuffer>;
|
||||
}>;
|
||||
// Nested creation: mint a new key and wrap it under the parent folder's
|
||||
// key (resolved from entry + chain).
|
||||
encryptNestedWithoutKey(
|
||||
data: ArrayBuffer,
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
encryptedKeyChain?: ArrayBuffer[],
|
||||
options?: { optimizeMemory?: boolean }
|
||||
): Promise<{ encryptedContent: ArrayBuffer; wrappedKey: ArrayBuffer }>;
|
||||
// Encrypt with an existing key — purely symmetric. No mint, no wrap.
|
||||
encryptWithKey(
|
||||
data: ArrayBuffer,
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
encryptedKeyChain?: ArrayBuffer[],
|
||||
options?: { optimizeMemory?: boolean }
|
||||
): Promise<{ encryptedData: ArrayBuffer }>;
|
||||
decryptWithKey(
|
||||
encryptedData: ArrayBuffer,
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
encryptedKeyChain?: ArrayBuffer[],
|
||||
options?: { optimizeMemory?: boolean }
|
||||
): Promise<{ data: ArrayBuffer }>;
|
||||
/**
|
||||
* Re-wrap an encrypted resource's K_file from its OLD parent chain
|
||||
* to a NEW one. Called when moving a file/folder between parents
|
||||
* inside the same encrypted subtree — content stays untouched, only
|
||||
* the wrapping of K_file follows the new position. `oldEncryptedKey`
|
||||
* is the file's current wrapped K_file from its access row; the
|
||||
* returned `newEncryptedKey` replaces it.
|
||||
*/
|
||||
rewrapNestedKey(
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
oldEncryptedKey: ArrayBuffer,
|
||||
oldEncryptedKeyChain?: ArrayBuffer[],
|
||||
newEncryptedKeyChain?: ArrayBuffer[]
|
||||
): Promise<{ newEncryptedKey: ArrayBuffer }>;
|
||||
/**
|
||||
* Wrap a per-user-anchored K_item (the value stored in
|
||||
* `encrypted_item_symmetric_key_for_user` on the caller's access
|
||||
* row of a self-rooted encrypted resource) under a destination
|
||||
* parent's chain. Used when moving a self-rooted file/folder
|
||||
* INTO an encrypted subtree — symmetric reverse of `shareKeys`.
|
||||
*/
|
||||
wrapNestedKey(
|
||||
userEncryptedKey: ArrayBuffer,
|
||||
newEntryEncryptedSymmetricKey: ArrayBuffer,
|
||||
newEncryptedKeyChain?: ArrayBuffer[]
|
||||
): Promise<{ newEncryptedKey: ArrayBuffer }>;
|
||||
shareKeys(
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
userPublicKeys: Record<string, ArrayBuffer>,
|
||||
encryptedKeyChain?: ArrayBuffer[]
|
||||
): Promise<{ encryptedKeys: Record<string, ArrayBuffer> }>;
|
||||
computeKeyFingerprint(publicKey: ArrayBuffer): Promise<string>;
|
||||
formatFingerprint(fingerprint: string): string;
|
||||
fetchPublicKeys(
|
||||
userIds: string[]
|
||||
): Promise<{ publicKeys: Record<string, ArrayBuffer> }>;
|
||||
checkFingerprints(
|
||||
userFingerprints: Record<string, string>,
|
||||
currentUserId?: string
|
||||
): Promise<{
|
||||
results: Array<{
|
||||
userId: string;
|
||||
knownFingerprint: string | null;
|
||||
providedFingerprint: string;
|
||||
status: 'trusted' | 'refused' | 'unknown';
|
||||
}>;
|
||||
}>;
|
||||
acceptFingerprint(userId: string, fingerprint: string): Promise<void>;
|
||||
refuseFingerprint(userId: string, fingerprint: string): Promise<void>;
|
||||
getKnownFingerprints(): Promise<{
|
||||
fingerprints: Record<
|
||||
string,
|
||||
{
|
||||
fingerprint: string;
|
||||
status: 'trusted' | 'refused' | 'unknown';
|
||||
}
|
||||
>;
|
||||
}>;
|
||||
openOnboarding(container: HTMLElement): void;
|
||||
openBackup(container: HTMLElement): void;
|
||||
openRestore(container: HTMLElement): void;
|
||||
openDeviceTransfer(container: HTMLElement): void;
|
||||
openSettings(container: HTMLElement): void;
|
||||
closeInterface(): void;
|
||||
on<K extends string>(event: K, listener: (data: unknown) => void): void;
|
||||
off<K extends string>(event: K, listener: (data: unknown) => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable error codes carried by `VaultError`. Sourced from the
|
||||
* encryption SDK (re-exported on `window.EncryptionClient.VaultErrorCode`)
|
||||
* — drive consumers match on these via `(err as VaultError).code` rather
|
||||
* than regexing message text. Keep in sync with the SDK definition.
|
||||
*/
|
||||
type VaultErrorCode =
|
||||
| 'MISSING_KEYS'
|
||||
| 'WRONG_SECRET_KEY'
|
||||
| 'INVALID_BACKUP'
|
||||
| 'INVALID_MNEMONIC'
|
||||
| 'NOT_INITIALIZED'
|
||||
| 'AUTH_REQUIRED'
|
||||
| 'PRIVILEGED_ORIGIN_REQUIRED'
|
||||
| 'TIMEOUT'
|
||||
| 'IFRAME_REQUIRED'
|
||||
| 'CIPHERTEXT_TOO_SHORT'
|
||||
| 'UNKNOWN';
|
||||
|
||||
interface VaultError extends Error {
|
||||
readonly code: VaultErrorCode;
|
||||
}
|
||||
type VaultClient = import('./client-sdk').VaultClient;
|
||||
type VaultError = import('./client-sdk').VaultError;
|
||||
type VaultErrorCode = import('./client-sdk').VaultErrorCode;
|
||||
type RegisteredUser = import('./client-sdk').RegisteredUser;
|
||||
type RecipientLabel = import('./client-sdk').RecipientLabel;
|
||||
|
||||
interface Window {
|
||||
EncryptionClient: {
|
||||
VaultClient: new (options: {
|
||||
vaultUrl: string;
|
||||
interfaceUrl: string;
|
||||
timeout?: number;
|
||||
theme?: string;
|
||||
lang?: string;
|
||||
}) => VaultClient;
|
||||
VaultError: new (code: VaultErrorCode, message: string) => VaultError;
|
||||
VaultErrorCode: { readonly [K in VaultErrorCode]: K };
|
||||
isVaultError: (err: unknown) => err is VaultError;
|
||||
};
|
||||
__driveVaultClient: VaultClient | null;
|
||||
EncryptionClient: typeof import('./client-sdk');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { convertFromInternal } from './x2tConverter';
|
||||
import { getPatchIndex } from './changesPipeline';
|
||||
import { acquireSaveLock, releaseSaveLock, isSaveLocked } from './locks';
|
||||
import { acquireSaveLock, releaseSaveLock } from './locks';
|
||||
import { pauseIncomingOT, resumeIncomingOT } from './incomingOtGate';
|
||||
|
||||
const CHECKPOINT_CHANGES_THRESHOLD = 50;
|
||||
@@ -22,6 +22,7 @@ const CHECKPOINT_CHANGES_THRESHOLD = 50;
|
||||
const CHECKPOINT_TIME_INTERVAL_MS = 120_000;
|
||||
|
||||
/** Reference to the OnlyOffice editor instance */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor internals
|
||||
let editorInstance: any = null;
|
||||
|
||||
/** Original file format (e.g. "docx") */
|
||||
@@ -75,6 +76,7 @@ let saveResultCallback:
|
||||
let isSaveLeader: (() => boolean) | null = null;
|
||||
|
||||
export function initCheckpointing(opts: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor internals
|
||||
editor: any;
|
||||
format: string;
|
||||
type: string;
|
||||
@@ -213,6 +215,7 @@ async function saveCheckpoint(): Promise<void> {
|
||||
// asc_nativeGetFile() is on the INNER editor object inside the OO iframe,
|
||||
// not on the DocsAPI.DocEditor wrapper. Access it via the iframe's window.
|
||||
const ooIframe = document.querySelector('iframe[name="frameEditor"]') as HTMLIFrameElement | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor inner window
|
||||
const innerWindow = ooIframe?.contentWindow as any;
|
||||
const innerEditor = innerWindow?.editor || innerWindow?.editorCell;
|
||||
|
||||
@@ -346,7 +349,7 @@ async function saveCheckpoint(): Promise<void> {
|
||||
// `converted.buffer` is detached after the await — read the
|
||||
// byte count from the local capture above.
|
||||
try {
|
||||
await uploadCallback(converted.buffer, originalFormat);
|
||||
await uploadCallback(converted.buffer as ArrayBuffer, originalFormat);
|
||||
} catch (error) {
|
||||
transientFailure =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -373,9 +373,10 @@ export class EncryptedRelay {
|
||||
private roomId: string;
|
||||
private userId: string;
|
||||
private userName: string;
|
||||
private vaultClient: any;
|
||||
private vaultClient: VaultClient;
|
||||
private encryptedSymmetricKey: ArrayBuffer;
|
||||
private encryptedKeyChain: ArrayBuffer[];
|
||||
private keyVersion: number;
|
||||
private callbacks: RelayCallbacks;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
@@ -423,9 +424,10 @@ export class EncryptedRelay {
|
||||
roomId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
vaultClient: any;
|
||||
vaultClient: VaultClient;
|
||||
encryptedSymmetricKey: ArrayBuffer;
|
||||
encryptedKeyChain: ArrayBuffer[];
|
||||
keyVersion: number;
|
||||
callbacks: RelayCallbacks;
|
||||
}) {
|
||||
this.roomId = opts.roomId;
|
||||
@@ -434,6 +436,7 @@ export class EncryptedRelay {
|
||||
this.vaultClient = opts.vaultClient;
|
||||
this.encryptedSymmetricKey = opts.encryptedSymmetricKey;
|
||||
this.encryptedKeyChain = opts.encryptedKeyChain;
|
||||
this.keyVersion = opts.keyVersion;
|
||||
this.callbacks = opts.callbacks;
|
||||
}
|
||||
|
||||
@@ -470,7 +473,7 @@ export class EncryptedRelay {
|
||||
this.processing = this.processing.then(() => this.handleMessage(data));
|
||||
};
|
||||
|
||||
this.ws.onclose = event => {
|
||||
this.ws.onclose = () => {
|
||||
this.callbacks.onConnectionChange(false);
|
||||
// Pending settlement promises become unresolvable once the socket
|
||||
// is gone — the relay on the other side of a future reconnect
|
||||
@@ -819,6 +822,7 @@ export class EncryptedRelay {
|
||||
const { data: plaintext } = await this.vaultClient.decryptWithKey(
|
||||
ciphertext,
|
||||
this.cloneKey(),
|
||||
this.keyVersion,
|
||||
this.cloneKeyChain()
|
||||
);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface MockServerOptions {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor internals
|
||||
let editorInstance: any = null;
|
||||
/**
|
||||
* Diagnostic counter — when > 0, every `sendToEditor` call is logged
|
||||
@@ -48,6 +49,7 @@ let editorInstance: any = null;
|
||||
*/
|
||||
let verboseSends = 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice editor internals
|
||||
export function setEditorInstance(editor: any): void {
|
||||
editorInstance = editor;
|
||||
}
|
||||
@@ -258,6 +260,7 @@ export function createMockServerCallbacks(
|
||||
peerLocksShownToEditor.add(key);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice message protocol
|
||||
sendToEditor({ type: 'getLock', locks } as any);
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -267,6 +270,7 @@ export function createMockServerCallbacks(
|
||||
// either have a fresh connection or have surfaced the
|
||||
// disconnect to the user.
|
||||
console.warn('[lockArbitrator] claim failed:', err);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice message protocol
|
||||
sendToEditor({ type: 'getLock', locks: {} } as any);
|
||||
});
|
||||
break;
|
||||
@@ -284,6 +288,7 @@ export function createMockServerCallbacks(
|
||||
block,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice message protocol
|
||||
sendToEditor({ type: 'getLock', locks } as any);
|
||||
break;
|
||||
}
|
||||
@@ -383,10 +388,12 @@ export function createMockServerCallbacks(
|
||||
break;
|
||||
|
||||
case 'message': {
|
||||
const payload = (msg as any).messages
|
||||
? ((msg as any).messages as unknown[])
|
||||
: (msg as any).message !== undefined
|
||||
? [(msg as any).message]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice message protocol
|
||||
const anyMsg = msg as any;
|
||||
const payload = anyMsg.messages
|
||||
? (anyMsg.messages as unknown[])
|
||||
: anyMsg.message !== undefined
|
||||
? [anyMsg.message]
|
||||
: [];
|
||||
if (payload.length > 0) {
|
||||
options.onMessageBroadcast?.(payload);
|
||||
@@ -396,6 +403,7 @@ export function createMockServerCallbacks(
|
||||
|
||||
case 'meta': {
|
||||
const payload =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OnlyOffice message protocol
|
||||
((msg as any).messages as unknown[] | undefined) ?? [];
|
||||
if (payload.length > 0) {
|
||||
options.onMetaBroadcast?.(payload);
|
||||
|
||||
@@ -28,6 +28,7 @@ interface X2TModule {
|
||||
}
|
||||
|
||||
// Store on window to survive HMR module reloads
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- x2t WASM module global
|
||||
const getWindow = (): any => (typeof window !== 'undefined' ? window : {});
|
||||
const getX2TModule = (): X2TModule | null => getWindow().__x2tModule ?? null;
|
||||
const setX2TModule = (m: X2TModule) => { getWindow().__x2tModule = m; };
|
||||
@@ -48,6 +49,7 @@ function getX2T(): Promise<X2TModule> {
|
||||
if (pending) return pending;
|
||||
|
||||
// Check if Module was loaded by a previous render
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- x2t WASM module global
|
||||
const existing = (window as any).Module;
|
||||
if (existing?.FS?.readdir) {
|
||||
setX2TModule(existing as X2TModule);
|
||||
@@ -59,6 +61,7 @@ function getX2T(): Promise<X2TModule> {
|
||||
|
||||
if (document.querySelector(`script[src="${x2tUrl}"]`)) {
|
||||
const poll = setInterval(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- x2t WASM module global
|
||||
const mod = (window as any).Module;
|
||||
if (mod?.FS?.readdir) {
|
||||
clearInterval(poll);
|
||||
@@ -74,10 +77,12 @@ function getX2T(): Promise<X2TModule> {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- x2t WASM module global
|
||||
(window as any).Module = {
|
||||
locateFile: (path: string) =>
|
||||
new URL(`/onlyoffice/x2t/${path}`, window.location.origin).href,
|
||||
onRuntimeInitialized: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- x2t WASM module global
|
||||
const mod = (window as any).Module as X2TModule;
|
||||
try { mod.FS.mkdir('/working'); } catch {}
|
||||
try { mod.FS.mkdir('/working/media'); } catch {}
|
||||
|
||||
+86
-36
@@ -5,11 +5,11 @@ import { Item, ItemType, LinkReach } from '@/features/drivers/types';
|
||||
import { getDriver } from '@/features/config/Config';
|
||||
import { APIError, errorToString } from '@/features/api/APIError';
|
||||
import { useAuth } from '@/features/auth/Auth';
|
||||
import { fetchRegisteredKeys } from '@/features/encryption/fetchRegisteredKeys';
|
||||
import { useVaultClient } from '../VaultClientProvider';
|
||||
import {
|
||||
chainForNode,
|
||||
fetchSubtree,
|
||||
filesOnly,
|
||||
foldersOnly,
|
||||
innerEncryptionRoots,
|
||||
isInsideInnerRoot,
|
||||
@@ -170,6 +170,29 @@ export type UseRecursiveEncryptionJob = {
|
||||
* PATCH commits the whole fileKeyMapping + encryptedKeysForDescendants in a
|
||||
* DB transaction. Failure pre-commit ⇒ DB untouched; rollback is `return;`.
|
||||
*/
|
||||
|
||||
type RecipientLabel = { email: string; name?: string };
|
||||
|
||||
/**
|
||||
* Build the SDK's labeled recipient map (sub → {email, name}) for a set of
|
||||
* subs, reading each label from the sourced access-row labels. The vault only
|
||||
* uses the subs (it resolves + trust-checks keys itself); the labels are shown
|
||||
* if the verify-recipients trust modal opens. The fallback placeholder guards
|
||||
* only a sub that has a key but no resolvable access row (should not happen for
|
||||
* individual accesses) and is never shown for a trusted recipient — the modal
|
||||
* surfaces only the blocked ones.
|
||||
*/
|
||||
function toRecipients(
|
||||
subs: string[],
|
||||
labels: Record<string, RecipientLabel>,
|
||||
): Record<string, RecipientLabel> {
|
||||
const recipients: Record<string, RecipientLabel> = {};
|
||||
for (const sub of subs) {
|
||||
recipients[sub] = labels[sub] ?? { email: 'unknown@unknown.invalid' };
|
||||
}
|
||||
return recipients;
|
||||
}
|
||||
|
||||
export function useRecursiveEncryptionJob({
|
||||
mode,
|
||||
item,
|
||||
@@ -188,7 +211,9 @@ export function useRecursiveEncryptionJob({
|
||||
const flatRef = useRef<FlatNode[]>([]);
|
||||
const processableIdsRef = useRef<string[]>([]);
|
||||
const publicKeysRef = useRef<Record<string, ArrayBuffer>>({});
|
||||
const versionsRef = useRef<Record<string, number>>({});
|
||||
const pendingUserIdsRef = useRef<string[]>([]);
|
||||
const recipientLabelsRef = useRef<Record<string, RecipientLabel>>({});
|
||||
// For encrypt mode, the root's per-user wrapped key map (committed).
|
||||
const rootEncryptedKeysRef = useRef<Record<string, ArrayBuffer>>({});
|
||||
// Current user's wrapped copy of the root key — entry key passed to the
|
||||
@@ -218,7 +243,9 @@ export function useRecursiveEncryptionJob({
|
||||
flatRef.current = [];
|
||||
processableIdsRef.current = [];
|
||||
publicKeysRef.current = {};
|
||||
versionsRef.current = {};
|
||||
pendingUserIdsRef.current = [];
|
||||
recipientLabelsRef.current = {};
|
||||
rootEncryptedKeysRef.current = {};
|
||||
currentUserRootWrappedRef.current = null;
|
||||
folderWrappedKeysRef.current = new Map();
|
||||
@@ -343,9 +370,27 @@ export function useRecursiveEncryptionJob({
|
||||
}
|
||||
|
||||
if (errors.length === 0 && userIds.length > 0) {
|
||||
const { publicKeys } = await vaultClient.fetchPublicKeys(userIds);
|
||||
const { publicKeys, versions } =
|
||||
await fetchRegisteredKeys(userIds);
|
||||
if (cancelled) return;
|
||||
|
||||
// Source display labels (email/name) for the recipients so the
|
||||
// SDK's verify-recipients trust modal can identify them — the job
|
||||
// otherwise carries only subs (accesses_user_ids) + pubkeys, and
|
||||
// emails live on the item's access rows.
|
||||
const accessRows = await driver.getItemAccesses(item.id);
|
||||
if (cancelled) return;
|
||||
const labels: Record<string, RecipientLabel> = {};
|
||||
for (const a of accessRows) {
|
||||
if (a.user.sub) {
|
||||
labels[a.user.sub] = {
|
||||
email: a.user.email,
|
||||
name: a.user.full_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
recipientLabelsRef.current = labels;
|
||||
|
||||
const missing = userIds.filter(uid => !publicKeys[uid]);
|
||||
const callerMissing = user?.sub && missing.includes(user.sub);
|
||||
|
||||
@@ -372,6 +417,7 @@ export function useRecursiveEncryptionJob({
|
||||
// pending on the backend and accepted later from the share
|
||||
// dialog. Informational, not blocking.
|
||||
publicKeysRef.current = publicKeys;
|
||||
versionsRef.current = versions;
|
||||
pendingUserIdsRef.current = missing;
|
||||
dispatch({
|
||||
type: 'SET_PENDING_USER_COUNT',
|
||||
@@ -498,6 +544,7 @@ export function useRecursiveEncryptionJob({
|
||||
flat: flatRef.current,
|
||||
processableIds: processableIdsRef.current,
|
||||
publicKeys: publicKeysRef.current,
|
||||
recipientLabels: recipientLabelsRef.current,
|
||||
rootEncryptedKeysRef,
|
||||
currentUserRootWrappedRef,
|
||||
folderWrappedKeysRef,
|
||||
@@ -573,38 +620,29 @@ export function useRecursiveEncryptionJob({
|
||||
encryptedSymmetricKeyPerUser[uid] = null;
|
||||
}
|
||||
|
||||
// Compute a fingerprint for each user whose public key we have
|
||||
// and null for pending users (no public key → no fingerprint).
|
||||
// Backend stores the map verbatim on the ItemAccess rows so
|
||||
// clients can later tell which key each user's wrapped key was
|
||||
// produced for — the "Fingerprint at the time it was shared
|
||||
// with you" line in the key-mismatch panel reads directly from
|
||||
// here. Mirrors the symmetric-key payload: every user on the
|
||||
// access list appears in the map exactly once.
|
||||
const encryptionPublicKeyFingerprintPerUser: Record<
|
||||
// Record the encryption public key version for each user whose
|
||||
// public key we have and null for pending users (no public key →
|
||||
// no version). Backend stores the map verbatim on the ItemAccess
|
||||
// rows so clients can later tell which key version each user's
|
||||
// wrapped key was produced for — the key-staleness check in the
|
||||
// key-mismatch panel reads directly from here. Mirrors the
|
||||
// symmetric-key payload: every user on the access list appears
|
||||
// in the map exactly once.
|
||||
const encryptionPublicKeyVersionPerUser: Record<
|
||||
string,
|
||||
string | null
|
||||
number | null
|
||||
> = {};
|
||||
for (const [uid, publicKey] of Object.entries(publicKeysRef.current)) {
|
||||
try {
|
||||
encryptionPublicKeyFingerprintPerUser[uid] =
|
||||
await vaultClient.computeKeyFingerprint(publicKey);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[encrypt] computeKeyFingerprint failed for',
|
||||
uid,
|
||||
err,
|
||||
);
|
||||
encryptionPublicKeyFingerprintPerUser[uid] = null;
|
||||
}
|
||||
for (const uid of Object.keys(publicKeysRef.current)) {
|
||||
encryptionPublicKeyVersionPerUser[uid] =
|
||||
versionsRef.current[uid] ?? null;
|
||||
}
|
||||
for (const uid of pendingUserIdsRef.current) {
|
||||
encryptionPublicKeyFingerprintPerUser[uid] = null;
|
||||
encryptionPublicKeyVersionPerUser[uid] = null;
|
||||
}
|
||||
|
||||
await driver.encryptItem(item.id, {
|
||||
encryptedSymmetricKeyPerUser,
|
||||
encryptionPublicKeyFingerprintPerUser,
|
||||
encryptionPublicKeyVersionPerUser,
|
||||
encryptedKeysForDescendants,
|
||||
fileKeyMapping,
|
||||
});
|
||||
@@ -739,6 +777,7 @@ type EncryptPipelineArgs = {
|
||||
flat: FlatNode[];
|
||||
processableIds: string[];
|
||||
publicKeys: Record<string, ArrayBuffer>;
|
||||
recipientLabels: Record<string, RecipientLabel>;
|
||||
rootEncryptedKeysRef: React.MutableRefObject<Record<string, ArrayBuffer>>;
|
||||
currentUserRootWrappedRef: React.MutableRefObject<ArrayBuffer | null>;
|
||||
folderWrappedKeysRef: React.MutableRefObject<Map<string, ArrayBuffer>>;
|
||||
@@ -754,6 +793,7 @@ async function encryptPipeline({
|
||||
flat,
|
||||
processableIds,
|
||||
publicKeys,
|
||||
recipientLabels,
|
||||
rootEncryptedKeysRef,
|
||||
currentUserRootWrappedRef,
|
||||
folderWrappedKeysRef,
|
||||
@@ -766,13 +806,12 @@ async function encryptPipeline({
|
||||
// and encryptWithoutKey is called with the actual file content below.
|
||||
if (rootItem.type === ItemType.FOLDER) {
|
||||
dispatch({ type: 'UPDATE_ROW', id: rootItem.id, state: 'running' });
|
||||
// Vault SDK is safe-by-default: public-key buffers in
|
||||
// `userPublicKeys` are structured-cloned by postMessage (never in
|
||||
// the transferList), so the same map stays valid for the
|
||||
// `computeKeyFingerprint` calls that follow at submit time.
|
||||
// Pass a labeled recipient map; the vault resolves + trust-checks each key
|
||||
// itself (binding + TOFU) and uses only the subs. `publicKeys` is keyed by
|
||||
// userId; labels come from the sourced access rows.
|
||||
const { encryptedKeys } = await vaultClient.encryptWithoutKey(
|
||||
new ArrayBuffer(0),
|
||||
publicKeys
|
||||
toRecipients(Object.keys(publicKeys), recipientLabels)
|
||||
);
|
||||
if (signal.aborted) throw abortError();
|
||||
rootEncryptedKeysRef.current = encryptedKeys;
|
||||
@@ -827,6 +866,7 @@ async function encryptPipeline({
|
||||
flat,
|
||||
targetId: id,
|
||||
publicKeys,
|
||||
recipientLabels,
|
||||
rootEncryptedKeysRef,
|
||||
currentUserRootWrappedRef,
|
||||
folderWrappedKeysRef,
|
||||
@@ -846,6 +886,7 @@ type StageOneEncryptArgs = {
|
||||
flat: FlatNode[];
|
||||
targetId: string;
|
||||
publicKeys: Record<string, ArrayBuffer>;
|
||||
recipientLabels: Record<string, RecipientLabel>;
|
||||
rootEncryptedKeysRef: React.MutableRefObject<Record<string, ArrayBuffer>>;
|
||||
currentUserRootWrappedRef: React.MutableRefObject<ArrayBuffer | null>;
|
||||
folderWrappedKeysRef: React.MutableRefObject<Map<string, ArrayBuffer>>;
|
||||
@@ -860,6 +901,7 @@ async function stageOneEncryption({
|
||||
flat,
|
||||
targetId,
|
||||
publicKeys,
|
||||
recipientLabels,
|
||||
rootEncryptedKeysRef,
|
||||
currentUserRootWrappedRef,
|
||||
folderWrappedKeysRef,
|
||||
@@ -889,12 +931,17 @@ async function stageOneEncryption({
|
||||
|
||||
if (rootItem.type === ItemType.FILE && targetId === rootItem.id) {
|
||||
// optimizeMemory: hot path — `plaintext` is the full file body
|
||||
// (often several MB) and is discarded after this call. publicKeys
|
||||
// is never in the transferList regardless of the flag.
|
||||
// (often several MB) and is discarded after this call. Pass a labeled
|
||||
// recipient map; the vault resolves + trust-checks each key (binding +
|
||||
// TOFU) and uses only the subs.
|
||||
const { encryptedContent: ct, encryptedKeys } =
|
||||
await vaultClient.encryptWithoutKey(plaintext, publicKeys, {
|
||||
optimizeMemory: true,
|
||||
});
|
||||
await vaultClient.encryptWithoutKey(
|
||||
plaintext,
|
||||
toRecipients(Object.keys(publicKeys), recipientLabels),
|
||||
{
|
||||
optimizeMemory: true,
|
||||
}
|
||||
);
|
||||
encryptedContent = ct;
|
||||
rootEncryptedKeysRef.current = encryptedKeys;
|
||||
wrappedKey = new ArrayBuffer(0);
|
||||
@@ -1244,11 +1291,14 @@ async function decryptPipeline({
|
||||
}
|
||||
const ciphertext = await resp.arrayBuffer();
|
||||
|
||||
const keyVersion = node.item.encryption_public_key_version_for_user ?? 1;
|
||||
|
||||
// optimizeMemory: hot path — file body, ciphertext discarded
|
||||
// after this call and plaintext goes straight to S3.
|
||||
const { data: plaintext } = await vaultClient.decryptWithKey(
|
||||
ciphertext,
|
||||
entryKey,
|
||||
keyVersion,
|
||||
chain.length > 0 ? chain : undefined,
|
||||
{ optimizeMemory: true }
|
||||
);
|
||||
|
||||
+9
-4
@@ -8,6 +8,7 @@ import {
|
||||
fetchSubtreeEntryKey,
|
||||
wrapSubtreeKeyForUser,
|
||||
} from './wrapKeyForUser';
|
||||
import { fetchRegisteredKeys } from '@/features/encryption/fetchRegisteredKeys';
|
||||
|
||||
interface Props {
|
||||
itemId: string;
|
||||
@@ -82,7 +83,7 @@ export const PendingEncryptionSection = ({ itemId, accesses }: Props) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { publicKeys } = await vaultClient.fetchPublicKeys(subs);
|
||||
const { publicKeys } = await fetchRegisteredKeys(subs);
|
||||
if (cancelled) return;
|
||||
const next: Record<string, boolean> = {};
|
||||
for (const sub of subs) {
|
||||
@@ -102,7 +103,7 @@ export const PendingEncryptionSection = ({ itemId, accesses }: Props) => {
|
||||
};
|
||||
// pendingSubsSignature intentionally used instead of `pending` itself
|
||||
// to avoid re-firing on unrelated Access array identity changes.
|
||||
}, [pendingSubsSignature]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [pendingSubsSignature]);
|
||||
|
||||
if (pending.length === 0) {
|
||||
return null;
|
||||
@@ -117,7 +118,11 @@ export const PendingEncryptionSection = ({ itemId, accesses }: Props) => {
|
||||
});
|
||||
try {
|
||||
const entryKey = await fetchSubtreeEntryKey(itemId);
|
||||
const wrapped = await wrapSubtreeKeyForUser(entryKey, access.user.sub);
|
||||
const wrapped = await wrapSubtreeKeyForUser(entryKey, {
|
||||
sub: access.user.sub,
|
||||
email: access.user.email,
|
||||
name: access.user.full_name,
|
||||
});
|
||||
if (!wrapped) {
|
||||
// Race: the key probe said they had one but fetching just now
|
||||
// returned nothing. Surface a concrete message and remove the
|
||||
@@ -142,7 +147,7 @@ export const PendingEncryptionSection = ({ itemId, accesses }: Props) => {
|
||||
itemId: access.item.id,
|
||||
accessId: access.id,
|
||||
encrypted_item_symmetric_key_for_user: wrapped.wrappedKeyBase64,
|
||||
encryption_public_key_fingerprint: wrapped.fingerprint,
|
||||
encryption_public_key_version: wrapped.version,
|
||||
});
|
||||
// Invalidate the currently-viewed item's access cache too when
|
||||
// it differs from where the access lives — the modal is looking
|
||||
|
||||
@@ -11,15 +11,28 @@
|
||||
*
|
||||
* Both paths need the same primitives: fetch the subtree's key chain,
|
||||
* unwrap the caller's own entry key, wrap it for the invitee, and
|
||||
* compute the fingerprint of the invitee's public key so the backend
|
||||
* can track key-change events later.
|
||||
* record the version of the invitee's public key so the backend can
|
||||
* track key-change events later.
|
||||
*/
|
||||
|
||||
import { getDriver } from '@/features/config/Config';
|
||||
import { fetchRegisteredKeys } from '@/features/encryption/fetchRegisteredKeys';
|
||||
|
||||
export interface WrappedKeyForUser {
|
||||
wrappedKeyBase64: string;
|
||||
fingerprint: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A share recipient: the OIDC sub plus a display-only label (email required,
|
||||
* name optional). The vault wraps for the sub after its own binding + TOFU
|
||||
* trust check; the label is surfaced only if the "verify recipients" trust
|
||||
* modal opens, so callers pass the user's real email/name.
|
||||
*/
|
||||
export interface ShareRecipient {
|
||||
sub: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
@@ -59,28 +72,31 @@ export async function fetchSubtreeEntryKey(
|
||||
*/
|
||||
export async function wrapSubtreeKeyForUser(
|
||||
encryptedSymmetricKey: ArrayBuffer,
|
||||
userSub: string,
|
||||
recipient: ShareRecipient,
|
||||
): Promise<WrappedKeyForUser | null> {
|
||||
const vaultClient = window.__driveVaultClient;
|
||||
if (!vaultClient) {
|
||||
throw new Error('Vault client not available');
|
||||
}
|
||||
const { publicKeys } = await vaultClient.fetchPublicKeys([userSub]);
|
||||
const userPublicKey = publicKeys[userSub];
|
||||
const { sub, email, name } = recipient;
|
||||
const { publicKeys, versions } = await fetchRegisteredKeys([sub]);
|
||||
const userPublicKey = publicKeys[sub];
|
||||
if (!userPublicKey) {
|
||||
return null;
|
||||
}
|
||||
const { encryptedKeys } = await vaultClient.shareKeys(
|
||||
encryptedSymmetricKey,
|
||||
{ [userSub]: userPublicKey },
|
||||
);
|
||||
const wrappedKey = encryptedKeys[userSub];
|
||||
// Pass a labeled recipient map (sub → {email, name}): the vault resolves +
|
||||
// trust-checks the key itself (binding + TOFU). The fetched publicKey above
|
||||
// only gates on the user being registered; the label is display-only, shown
|
||||
// if the trust modal opens.
|
||||
const { encryptedKeys } = await vaultClient.shareKeys(encryptedSymmetricKey, {
|
||||
[sub]: { email, name },
|
||||
});
|
||||
const wrappedKey = encryptedKeys[sub];
|
||||
if (!wrappedKey) {
|
||||
return null;
|
||||
}
|
||||
const fingerprint = await vaultClient.computeKeyFingerprint(userPublicKey);
|
||||
return {
|
||||
wrappedKeyBase64: arrayBufferToBase64(wrappedKey),
|
||||
fingerprint,
|
||||
version: versions[sub],
|
||||
};
|
||||
}
|
||||
|
||||
+8
-4
@@ -128,13 +128,17 @@ export const ItemShareModal = ({
|
||||
|
||||
const promises = inviteByUsername.map(async (user) => {
|
||||
let memberEncryptedSymmetricKey: string | undefined;
|
||||
let memberKeyFingerprint: string | undefined;
|
||||
let memberKeyVersion: number | undefined;
|
||||
|
||||
if (entryKey && user.sub) {
|
||||
const wrapped = await wrapSubtreeKeyForUser(entryKey, user.sub);
|
||||
const wrapped = await wrapSubtreeKeyForUser(entryKey, {
|
||||
sub: user.sub,
|
||||
email: user.email,
|
||||
name: user.full_name,
|
||||
});
|
||||
if (wrapped) {
|
||||
memberEncryptedSymmetricKey = wrapped.wrappedKeyBase64;
|
||||
memberKeyFingerprint = wrapped.fingerprint;
|
||||
memberKeyVersion = wrapped.version;
|
||||
}
|
||||
// wrapped === null → invitee has no public key yet; omit the
|
||||
// key fields → backend creates the row pending.
|
||||
@@ -145,7 +149,7 @@ export const ItemShareModal = ({
|
||||
userId: user.id,
|
||||
role: role as Role,
|
||||
encrypted_item_symmetric_key_for_user: memberEncryptedSymmetricKey,
|
||||
encryption_public_key_fingerprint: memberKeyFingerprint,
|
||||
encryption_public_key_version: memberKeyVersion,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export const useMutationAcceptEncryptionAccess = () => {
|
||||
itemId: string;
|
||||
accessId: string;
|
||||
encrypted_item_symmetric_key_for_user: string;
|
||||
encryption_public_key_fingerprint: string;
|
||||
encryption_public_key_version: number;
|
||||
},
|
||||
) => {
|
||||
return driver.acceptEncryptionAccess(
|
||||
@@ -91,8 +91,8 @@ export const useMutationAcceptEncryptionAccess = () => {
|
||||
{
|
||||
encrypted_item_symmetric_key_for_user:
|
||||
payload.encrypted_item_symmetric_key_for_user,
|
||||
encryption_public_key_fingerprint:
|
||||
payload.encryption_public_key_fingerprint,
|
||||
encryption_public_key_version:
|
||||
payload.encryption_public_key_version,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -201,8 +201,8 @@ export const itemToPreviewFile = (item: Item) => {
|
||||
is_wopi_supported: item.is_wopi_supported && !item.is_encrypted,
|
||||
is_encrypted: item.is_encrypted,
|
||||
is_pending_encryption_for_user: item.is_pending_encryption_for_user,
|
||||
encryption_public_key_fingerprint_for_user:
|
||||
item.encryption_public_key_fingerprint_for_user,
|
||||
encryption_public_key_version_for_user:
|
||||
item.encryption_public_key_version_for_user,
|
||||
size: item.size,
|
||||
abilities: item.abilities,
|
||||
} as FilePreviewType;
|
||||
|
||||
@@ -769,8 +769,8 @@
|
||||
"key_mismatch": {
|
||||
"title": "Ce fichier a été chiffré avec une autre clé",
|
||||
"body": "Le fichier a été chiffré pour vous à un moment où vous utilisiez une autre clé de chiffrement — probablement avant une réinitialisation de vos clés ou un changement d'appareil sans restauration de sauvegarde. Votre clé actuelle ne peut plus le déchiffrer. Demandez à un propriétaire ou administrateur de ce fichier de vous retirer de la liste d'accès puis de vous ajouter à nouveau afin qu'il soit rechiffré avec votre clé actuelle.",
|
||||
"share_time_fingerprint_label": "Empreinte au moment du partage avec vous :",
|
||||
"fingerprint_label": "Empreinte de votre clé actuelle :"
|
||||
"share_time_version_label": "Version de la clé au moment du partage avec vous :",
|
||||
"current_version_label": "Version actuelle de votre clé :"
|
||||
},
|
||||
"pending_self": {
|
||||
"title": "Terminez votre configuration du chiffrement pour ouvrir ce fichier",
|
||||
|
||||
@@ -54,7 +54,7 @@ export const useDecryptedContent = (item?: Item) => {
|
||||
|
||||
// 3. Decrypt via vault with key chain
|
||||
// The vault client must be available on window (loaded by VaultClientProvider)
|
||||
const vaultClient = (window as any).__driveVaultClient;
|
||||
const vaultClient = window.__driveVaultClient;
|
||||
if (!vaultClient) {
|
||||
throw new Error(
|
||||
"Vault client not initialized. Encryption keys are required.",
|
||||
@@ -79,9 +79,15 @@ export const useDecryptedContent = (item?: Item) => {
|
||||
}
|
||||
const encryptedSymmetricKey = entryKeyBytes.buffer;
|
||||
|
||||
// Version of the user's encryption key this wrap was produced against,
|
||||
// stored on the access row. Default to 1 for the current single-version
|
||||
// reality when the field is absent.
|
||||
const keyVersion = item.encryption_public_key_version_for_user ?? 1;
|
||||
|
||||
const { data: decryptedBuffer } = await vaultClient.decryptWithKey(
|
||||
encryptedBuffer,
|
||||
encryptedSymmetricKey,
|
||||
keyVersion,
|
||||
encryptedKeyChain.length > 0 ? encryptedKeyChain : undefined,
|
||||
);
|
||||
|
||||
@@ -139,7 +145,7 @@ export const downloadDecryptedFile = async (
|
||||
const encryptedBuffer = await response.arrayBuffer();
|
||||
|
||||
// 3. Decrypt via vault
|
||||
const vaultClient = (window as any).__driveVaultClient;
|
||||
const vaultClient = window.__driveVaultClient;
|
||||
if (!vaultClient) {
|
||||
throw new Error("Vault client not initialized.");
|
||||
}
|
||||
@@ -159,9 +165,15 @@ export const downloadDecryptedFile = async (
|
||||
entryKeyBytes[i] = entryKeyBinary.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Version of the user's encryption key this wrap was produced against,
|
||||
// stored on the access row. Default to 1 for the current single-version
|
||||
// reality when the field is absent.
|
||||
const keyVersion = item.encryption_public_key_version_for_user ?? 1;
|
||||
|
||||
const { data: decryptedBuffer } = await vaultClient.decryptWithKey(
|
||||
encryptedBuffer,
|
||||
entryKeyBytes.buffer,
|
||||
keyVersion,
|
||||
encryptedKeyChain.length > 0 ? encryptedKeyChain : undefined,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MimeCategory,
|
||||
} from "@/features/explorer/utils/mimeTypes";
|
||||
import { useDecryptedContent } from "@/features/items/hooks/useDecryptedContent";
|
||||
import { Item } from "@/features/drivers/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ImageViewer } from "../image-viewer/ImageViewer";
|
||||
import { VideoPlayer } from "../video-player/VideoPlayer";
|
||||
@@ -116,7 +117,7 @@ export const EncryptedFileViewer = ({
|
||||
title: file.title,
|
||||
is_encrypted: true,
|
||||
};
|
||||
return <OOEditor item={itemLike as any} />;
|
||||
return <OOEditor item={itemLike as unknown as Item} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -151,7 +152,9 @@ const NonOfficeEncryptedViewer = ({
|
||||
is_encrypted: true,
|
||||
mimetype: file.mimetype,
|
||||
};
|
||||
const { blobUrl, isDecrypting, error } = useDecryptedContent(item as any);
|
||||
const { blobUrl, isDecrypting, error } = useDecryptedContent(
|
||||
item as unknown as Item
|
||||
);
|
||||
|
||||
if (isDecrypting) {
|
||||
return (
|
||||
@@ -187,7 +190,7 @@ const NonOfficeEncryptedViewer = ({
|
||||
if (isWrongSecretKeyError(error)) {
|
||||
return (
|
||||
<KeyMismatchPanel
|
||||
shareTimeFingerprint={file.encryption_public_key_fingerprint_for_user}
|
||||
shareTimeVersion={file.encryption_public_key_version_for_user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export type FilePreviewType = {
|
||||
is_wopi_supported?: boolean;
|
||||
is_encrypted?: boolean;
|
||||
is_pending_encryption_for_user?: boolean;
|
||||
encryption_public_key_fingerprint_for_user?: string | null;
|
||||
encryption_public_key_version_for_user?: number | null;
|
||||
url_preview: string;
|
||||
url: string;
|
||||
abilities?: {
|
||||
|
||||
Reference in New Issue
Block a user