OpenE2EE

Deletion and device revocation

What deletion can and cannot mean in an encrypted system, and the lifecycle operations that actually revoke access.

Status
pre-1.0
Applies to
0.1.0
Platforms
Expo · Browser · Node
Prereqs
A working client from Start → Quickstart
Reading time
12 min

Start with the boundary that controls every deletion claim. Your product copy and legal review must use the same boundary.

Cryptographic confidentiality creates no remote deletion guarantee. After another device decrypts a message, its user controls that plaintext. Copies can exist in device storage, backups, and screenshots. Deletion is a request to software that you do not operate. It is not an enforcement mechanism.

E2EE strengthens the case that you, the operator, cannot produce the content. It says nothing about what recipients retain. A "delete for everyone" feature is a cooperative convention among well-behaved clients. A modified client ignores it and nothing in the protocol detects that. Write your product copy to match. "Deleted for everyone" is defensible if it describes a request to participating clients. "Permanently erased everywhere" is not.

What each operation actually removes

OperationRemovesDoes not remove
deleteSession(remoteAddress)Local session state for one peerAnything at the relay; peer's copy
archiveSession(remoteAddress)Nothing — moves to inactive listAbility to decrypt delayed messages
deleteGroupSenderKey(groupId)Local sender key for a groupOther members' keys or history
deleteRemoteAttachment(attachment, options?)The encrypted remote objectLocal caches; recipients' copies
client.media.cleanup(input, options?)Local media staging and cachesRemote objects; message rows
clearAllData()All local keys and sessionsAnything remote; anything on a peer

archiveSession(remoteAddress) is not deletion. It moves the current session to the inactive list. This preserves decryption for messages that are already in flight. Per SESAME §3.2, the "previously active session is moved to the head of the inactive sessions list." Using deleteSession where archiveSession is correct silently destroys in-flight messages.

deleteRemoteAttachment(attachment, options?) "only touches remote object storage. App-owned local message rows and local media caches must be deleted by the application." Pair it with client.media.cleanup(...) and with syncMediaAttachmentDeleteToLinkedDevices(...) so the user's own other devices converge, rather than one device showing a file the user believes they deleted.

clearAllData() permanently deletes all local keys and sessions. It is the local half of an account reset and it is not, on its own, an account reset.

Blocking

/blocking exports SignalProtocolBlockingManager, constructed as new SignalProtocolBlockingManager({ store, mirror }) over a SignalProtocolBlockingStore you supply.

import { SignalProtocolBlockingManager } from '@open-e2ee/signal-protocol-sdk/blocking';

const blocking = new SignalProtocolBlockingManager({ store, mirror });

await blocking.blockRecipient(recipientId);
const blocked = await blocking.listBlockedRecipients();
await client.syncBlockedRecipientsToLinkedDevices(blocked);

if (await blocking.isBlocked(senderId)) return; // drop before surfacing

unblockRecipient(recipientId) reverses it. The local store is authoritative for the current device. The optional mirror projects the snapshot to your backend.

Blocking is a delivery and presentation policy, not a cryptographic control. A blocked sender's envelope can still reach your relay. Your application decides not to surface it. Do not describe blocking as preventing someone from sending to a user, because it does not.

Device revocation

Revoking a device is a backend operation with a local counterpart, and both halves must happen.

The backend half. "Backend authentication must bind registration, provisioning, unlink, and removal to the owning account." A relay must not accept an unlink request based only on a client claim. Such a request could disconnect another account's device. The relay owns linked-device slot allocation. Primary is 1, and the backend allocates linked devices from 2 through 5. The contract states that "clients must not choose linked deviceIds." Revocation frees a slot, and the backend controls that decision.

The local half. "Account reset must clear the device-ID cache, platform secret storage, and protocol store as one product-level lifecycle." Three stores, one operation:

  • Device-ID cache: /device/device-id (getDeviceId(), preloadDeviceId(), getDeviceIdSync()).
  • Platform secret storage: the vault, e.g. ExpoSecureStoreSignalProtocolSecretVault from /local/vault/expo-secure-store, holding the small bootstrap secret such as "signal-store-wrapping-key". Use deleteSecret(key).
  • Protocol store: clearAllData() on the client.

DeviceLifecycleManager from /device/lifecycle is the coordination point for this sequence.

Revocation and rotation have different boundaries. Unlinking a device does not rotate the account identity. It also cannot remove messages that the device already decrypted and stored. If an attacker controls the device, unlinking stops future delivery. It does not remove existing copies. See key rotation for the effects of identity rotation.

Group member removal: the security-critical one

Removing a member from a member list is a UI change. Rotating the sender key is the security control. Without rotation, the removed member still holds the sender key. They can decrypt each later group message that uses it.

removeGroupMemberV2(groupId, editorAci, targetAci) triggers sender-key rotation. leaveGroupV2(groupId, userAci) handles the departing member's own side. Pair it with deleteGroupSenderKey(groupId) locally. handleGroupMembershipChange(groupId, change) returns { rotated, distributionMessage? }. rotateGroupSenderKey(groupId) explicitly rotates the key and returns { senderKeyId, distributionMessage }. Pass that result through distributeGroupSenderKey(groupId, memberUserIds).

Assert the rotation rather than trusting it. getGroupSenderKeyStats(groupId, senderId, senderDeviceId) returns generation. A removal that did not advance generation did not revoke anything. Make that a test, not a code review comment. See groups and testing.

Rotation is forward-looking. It does not remove the departed member's ability to read messages sent before the rotation, which they already received. There is no protocol operation that does.

The deletion sequence for one account

Ordered so that a failure part-way through leaves the least usable residue:

  1. Revoke at the backend first, authenticated against the owning account. Delivery stops.
  2. Rotate sender keys for every group the account was in to protect the remaining members regardless of what happens next.
  3. Delete remote objects with deleteRemoteAttachment(...) for attachments the account owned.
  4. Clear local media with client.media.cleanup(...) and delete app-owned message rows: your responsibility, not the SDK's.
  5. clearAllData().
  6. Clear the vault secret and the device-ID cache.

Complete steps 1 and 2 first. Steps 3 through 6 destroy the credentials that those earlier steps need.

Opacity ledger

ArtifactStays on deviceSent to relayIn object storeRemoved by
Session record (version: 4)yesnonodeleteSession / clearAllData
identityKeyPair private halfyesnonoclearAllData
identityKeyPair public halfyesyesnobackend removal, not the SDK
Sender key (group)yesnonodeleteGroupSenderKey; revoked by rotation
Encrypted attachment objectstaged locallynoyesdeleteRemoteAttachment
Local media cacheyesnonoclient.media.cleanup
Decrypted message rowsyesnonoyour application
BlockedRecipientEntry listyesmirror is optionalnounblockRecipient
Vault bootstrap secretyesnonodeleteSecret(key)
A recipient's decrypted copytheir devicenononothing

Next

On this page