05/29 Fixed a shared item isn't auto updated when it's modified by owner

This commit is contained in:
2026-05-29 14:58:11 -04:00
parent 5f8ce4b5de
commit 977805f5e7
3 changed files with 44 additions and 10 deletions
+12 -1
View File
@@ -444,7 +444,17 @@ The sharing key uses raw `SubtleCrypto` calls (not `SharingCrypto.decryptPrivate
### Shared item name encryption
When creating a share, the client encrypts `item.name` with `SharingCrypto.encryptName(sharedKey, name)``enc_name`/`iv_name`. The server receives `item_name = item.item_type` (non-sensitive type label) for the `NOT NULL` column. On the recipient's inbox, `enc_name`/`iv_name` are stored in data attributes on the View button and decrypted client-side with `SharingCrypto.decryptName()` when the user clicks View. Legacy shares (pre-migration, no `enc_name`) fall back to showing `item_name` (the type string).
When creating a share, the client encrypts `item.name` with `SharingCrypto.encryptName(sharedKey, name)``enc_name`/`iv_name`. The server receives `item_name = item.item_type` (non-sensitive type label) for the `NOT NULL` column. On the recipient's inbox, `enc_name`/`iv_name` are decrypted client-side with `SharingCrypto.decryptName()` when the user clicks View. Legacy shares (pre-migration, no `enc_name`) fall back to showing `item_name` (the type string).
### Live sync of owner edits to accepted shares
When the owner edits a vault item, `buildSharedUpdates(itemId, plainData, name)` re-encrypts the item for every accepted share using each recipient's ECDH public key, then POSTs all re-encrypted blobs in `shared_updates[]` inside `PUT /api/vault/<id>`. The server updates `enc_data`, `iv`, `enc_name`, `iv_name` on each matching `SharedItem` row and logs a `shared_item.update` audit entry.
On the grantee side there are two paths:
- **Vault list (main view):** `openFreshSharedDetail()` calls `loadSharedItemsForVault()` → re-fetches `/api/sharing/inbox` on every View click — always current.
- **Sharing tab "View" button:** previously baked `enc_data`/`iv`/`enc_name`/`iv_name` into HTML `data-*` attributes at render time. Fixed to fetch `/api/sharing/inbox` fresh on every click, look up the share by ID, then decrypt the server's latest ciphertext. This prevents stale data if the sharing tab was already open when the owner edited.
**Gotcha:** never read `data-enc` / `data-iv` / `data-owner-key` from the View button for decryption — those attrs may be stale. Always re-fetch from `/api/sharing/inbox`.
### Vault item tags
@@ -598,6 +608,7 @@ Audit log details **never** contain plaintext item names, shared item names, or
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
| `vault.py` | `vault_item.create/update/delete` | CRUD (detail: type + id only) |
| `vault.py` | `shared_item.update` | Re-encrypted N accepted share copies when owner edits item |
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
| `folders.py` | `folder.create/update/delete` | Folder CRUD |
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup |
+11
View File
@@ -145,6 +145,7 @@ def update_item(item_id):
# Re-encrypt accepted shared copies if the owner provided updated ciphertext.
# Each entry: { share_id, enc_data, iv, enc_name?, iv_name? }
from app.models.shared_item import SharedItem
updated_share_ids = []
for upd in (data.get('shared_updates') or []):
share = SharedItem.query.filter_by(
id=upd.get('share_id'),
@@ -157,6 +158,7 @@ def update_item(item_id):
if upd.get('enc_name') is not None:
share.enc_name = upd['enc_name']
share.iv_name = upd.get('iv_name')
updated_share_ids.append(share.id)
try:
db.session.flush()
@@ -168,6 +170,15 @@ def update_item(item_id):
detail=f'Updated {item.item_type} item (id={item.id})',
ip_address=client_ip(),
)
if updated_share_ids:
AuditLog.log(
user_id=g.current_user_id,
action='shared_item.update',
resource_type='shared_item',
resource_id=item.id,
detail=f'Re-encrypted {len(updated_share_ids)} shared copy(ies) for vault item (id={item.id}), share_ids={updated_share_ids}',
ip_address=client_ip(),
)
db.session.commit()
except Exception:
db.session.rollback()
+20 -8
View File
@@ -2212,8 +2212,20 @@ const Vault = (() => {
return;
}
try {
// Always fetch fresh share data from the server so the grantee sees
// the owner's latest version, not the ciphertext that was baked into
// the button's data-* attributes when the tab was last rendered.
const shareId = parseInt(btn.dataset.viewShare);
const inboxRes = await apiFetch("/api/sharing/inbox");
if (!inboxRes) return;
const inbox = await inboxRes.json();
const freshShare = inbox.find((s) => s.id === shareId);
if (!freshShare || !freshShare.owner_public_key) {
showToast("Share not found or owner key unavailable.", "error");
return;
}
const ownerPubKey = await SharingCrypto.importPublicKey(
btn.dataset.ownerKey,
freshShare.owner_public_key,
);
const sharedKey = await SharingCrypto.deriveSharedKey(
SharingSession.getKey(),
@@ -2221,21 +2233,21 @@ const Vault = (() => {
);
const plain = await SharingCrypto.decryptShare(
sharedKey,
btn.dataset.enc,
btn.dataset.iv,
freshShare.enc_data,
freshShare.iv,
);
// Decrypt the item name if an encrypted version is available.
// Falls back to the non-sensitive label for legacy shares.
let displayName = btn.dataset.name;
if (btn.dataset.encName && btn.dataset.ivName) {
let displayName = freshShare.item_name;
if (freshShare.enc_name && freshShare.iv_name) {
const decrypted = await SharingCrypto.decryptName(
sharedKey,
btn.dataset.encName,
btn.dataset.ivName,
freshShare.enc_name,
freshShare.iv_name,
);
if (decrypted) displayName = decrypted;
}
showSharedItemDetails(displayName, btn.dataset.type, plain);
showSharedItemDetails(displayName, freshShare.item_type, plain);
} catch (err) {
showToast("Could not decrypt: " + err.message, "error");
}