Repos affected: deso-protocol/core, deso-protocol/backend, deso-protocol/identity
Severity: Medium — silent privacy expectation gap. No funds at risk; no protocol invariant violated. Affects every app built on access-group messaging (deso-chat, ChatOn, any third-party messenger).
Reporter context: Surfaced while documenting group-chat behavior in a third-party DeSo messenger. Filing here because the root cause sits below the application layer — no app-side patch can fix it.
Summary
When the owner of an access group calls removeAccessGroupMembers, the removed member loses their on-chain AccessGroupMember entry and disappears from the members list. However, they retain the cryptographic ability to decrypt every future message posted to that group. This is the combined effect of three independent design choices, each reasonable in isolation:
- Access-group keypairs are deterministically derived from the group's
AccessGroupKeyName, so the key cannot be rotated without changing the group's identity.
removeAccessGroupMembers deletes the member's encrypted-key entry but does not regenerate the underlying access-group keypair or re-key the remaining members.
GetPaginatedMessagesForGroupChatThread (and the equivalent UtxoView lookup for self-hosted nodes) does not check whether the caller is a current group member — it returns ciphertext for any access group ID the caller can name.
Most users — and most apps building on this — assume "remove member" implies "remove access." It does not. It only revokes UI-level discoverability.
Steps to reproduce
-
User A creates an access group myGroup and adds users B, C as members.
-
B opens any DeSo messenger that exposes the access-group key (or simply re-derives it via identity.accessGroupStandardDerivation('myGroup') — the derivation is deterministic).
-
A removes B from the group via removeAccessGroupMembers.
-
A and C continue posting NewMessage transactions to the group.
-
B (now removed) calls GetPaginatedMessagesForGroupChatThread against any DeSo node:
await getPaginatedGroupChatThread({
UserPublicKeyBase58Check: A_OWNER_PUBKEY, // public
AccessGroupKeyName: 'myGroup', // public
MaxMessagesToFetch: 1000,
});
-
B decrypts the returned ciphertext using the same shared key they had as a member. All messages sent after the removal are readable.
No special tools needed. No custom node. Standard deso-protocol SDK calls against a public node.
Root cause: code references
1. Deterministic key derivation — deso-protocol/identity
accessGroupStandardDerivation(groupKeyName) returns the same keypair for the same name across all calls. There is no nonce, no epoch, no per-instantiation salt. Two consequences:
- A removed member who knows the group name can re-derive the access-group private key without ever having exfiltrated it.
- An owner who wanted to "rotate" the key has no way to do so within the same group identity.
2. No re-keying on member removal — deso-protocol/backend (and on-chain in core)
removeAccessGroupMembers accepts the list of AccessGroupMember entries to delete. It removes those entries from state. It does not:
- Generate a new access-group keypair
- Re-encrypt and re-publish the new key for remaining members
- Mark the old key as superseded for downstream consumers
This is consistent with how the transaction is documented (it's billed as a member-management operation, not a key-rotation operation), so I'm flagging it as a design gap rather than a bug in the handler.
3. Public read access to group ciphertext — deso-protocol/backend/routes/new_message.go:689
func (fes *APIServer) GetPaginatedMessagesForGroupChatThread(ww, req) {
// Decode JSON body
// Validate UserPublicKeyBase58Check + AccessGroupKeyName parse
// Build accessGroupId
// Return utxoView.GetPaginatedMessageEntriesForGroupChatThread(...)
}
No signature verification. No membership check against the current AccessGroupMember set for the requested AccessGroupId. The UserPublicKeyBase58Check parameter is the group owner's key, not a credential identifying the caller. Self-hosted nodes inherit the same property at the UtxoView level.
This is reasonable as a default — it matches the rest of DeSo's "everything is public, encryption is the gate" philosophy — but it leaves no defense-in-depth when the encryption gate is itself stuck open.
Why this matters
End-user expectation, drawn from every centralized messenger: "kicking" someone from a group prevents them from reading future messages. Apps built on access groups inherit DeSo's behavior whether they realize it or not. The mismatch shows up in the worst possible way: the user trusts the kick, the app shows the kick succeeded, and the removed party silently keeps reading.
A few specific impact paths:
- Personal: ex-partner / former friend removed from a family or friend group continues reading
- Workplace: terminated employee removed from a project group continues reading post-termination discussions
- Community/moderation: banned user continues reading mod-only or appeals discussions
- Compliance: any "right to remove access" guarantee an app makes is unenforceable at the protocol level
Possible mitigations (sketch — happy to discuss)
These are not equally good; listing in increasing protocol-invasiveness:
1. Documentation-only. Add a clear note to the access-group section of the docs and the deso-js SDK: "Removing a member does not prevent them from decrypting future messages. To revoke decryption ability, create a new access group with a different AccessGroupKeyName and migrate remaining members." Apps can surface this in their UI.
2. Convention for app-level rotation. Establish a convention where AccessGroupKeyName includes a versioned suffix (e.g., myGroup-v2), and removal triggers app-side migration to the next version. Painful UX but possible without protocol changes. Would benefit from a helper in deso-js.
3. Optional access control on GetPaginatedMessagesForGroupChatThread. Allow an opt-in mode where the endpoint requires a signed request from a current AccessGroupMember. Doesn't help against self-hosted nodes (the data is still on-chain) but materially raises the bar for casual abuse. Would need careful thought about how it interacts with backup/restore and cross-app reads.
4. Protocol-level rekey transaction. A new transaction type that updates the access-group's underlying keypair (decoupling group identity from key derivation), with re-encrypted key copies for current members. This is the only option that delivers the user expectation faithfully, but it's a significant protocol surface change.
I'd love to know which (if any) of these the team thinks is on the table, and whether there's existing thinking I missed.
Environment
- DeSo backend:
main as of 2026-04-16
- deso-js: latest
- Reproduced against
node.deso.org (public node)
- Originally surfaced via
deso-chat and getchaton.com source review
Repos affected:
deso-protocol/core,deso-protocol/backend,deso-protocol/identitySeverity: Medium — silent privacy expectation gap. No funds at risk; no protocol invariant violated. Affects every app built on access-group messaging (deso-chat, ChatOn, any third-party messenger).
Reporter context: Surfaced while documenting group-chat behavior in a third-party DeSo messenger. Filing here because the root cause sits below the application layer — no app-side patch can fix it.
Summary
When the owner of an access group calls
removeAccessGroupMembers, the removed member loses their on-chainAccessGroupMemberentry and disappears from the members list. However, they retain the cryptographic ability to decrypt every future message posted to that group. This is the combined effect of three independent design choices, each reasonable in isolation:AccessGroupKeyName, so the key cannot be rotated without changing the group's identity.removeAccessGroupMembersdeletes the member's encrypted-key entry but does not regenerate the underlying access-group keypair or re-key the remaining members.GetPaginatedMessagesForGroupChatThread(and the equivalent UtxoView lookup for self-hosted nodes) does not check whether the caller is a current group member — it returns ciphertext for any access group ID the caller can name.Most users — and most apps building on this — assume "remove member" implies "remove access." It does not. It only revokes UI-level discoverability.
Steps to reproduce
User A creates an access group
myGroupand adds users B, C as members.B opens any DeSo messenger that exposes the access-group key (or simply re-derives it via
identity.accessGroupStandardDerivation('myGroup')— the derivation is deterministic).A removes B from the group via
removeAccessGroupMembers.A and C continue posting
NewMessagetransactions to the group.B (now removed) calls
GetPaginatedMessagesForGroupChatThreadagainst any DeSo node:B decrypts the returned ciphertext using the same shared key they had as a member. All messages sent after the removal are readable.
No special tools needed. No custom node. Standard
deso-protocolSDK calls against a public node.Root cause: code references
1. Deterministic key derivation —
deso-protocol/identityaccessGroupStandardDerivation(groupKeyName)returns the same keypair for the same name across all calls. There is no nonce, no epoch, no per-instantiation salt. Two consequences:2. No re-keying on member removal —
deso-protocol/backend(and on-chain incore)removeAccessGroupMembersaccepts the list ofAccessGroupMemberentries to delete. It removes those entries from state. It does not:This is consistent with how the transaction is documented (it's billed as a member-management operation, not a key-rotation operation), so I'm flagging it as a design gap rather than a bug in the handler.
3. Public read access to group ciphertext —
deso-protocol/backend/routes/new_message.go:689No signature verification. No membership check against the current
AccessGroupMemberset for the requestedAccessGroupId. TheUserPublicKeyBase58Checkparameter is the group owner's key, not a credential identifying the caller. Self-hosted nodes inherit the same property at the UtxoView level.This is reasonable as a default — it matches the rest of DeSo's "everything is public, encryption is the gate" philosophy — but it leaves no defense-in-depth when the encryption gate is itself stuck open.
Why this matters
End-user expectation, drawn from every centralized messenger: "kicking" someone from a group prevents them from reading future messages. Apps built on access groups inherit DeSo's behavior whether they realize it or not. The mismatch shows up in the worst possible way: the user trusts the kick, the app shows the kick succeeded, and the removed party silently keeps reading.
A few specific impact paths:
Possible mitigations (sketch — happy to discuss)
These are not equally good; listing in increasing protocol-invasiveness:
1. Documentation-only. Add a clear note to the access-group section of the docs and the deso-js SDK: "Removing a member does not prevent them from decrypting future messages. To revoke decryption ability, create a new access group with a different
AccessGroupKeyNameand migrate remaining members." Apps can surface this in their UI.2. Convention for app-level rotation. Establish a convention where
AccessGroupKeyNameincludes a versioned suffix (e.g.,myGroup-v2), and removal triggers app-side migration to the next version. Painful UX but possible without protocol changes. Would benefit from a helper indeso-js.3. Optional access control on
GetPaginatedMessagesForGroupChatThread. Allow an opt-in mode where the endpoint requires a signed request from a currentAccessGroupMember. Doesn't help against self-hosted nodes (the data is still on-chain) but materially raises the bar for casual abuse. Would need careful thought about how it interacts with backup/restore and cross-app reads.4. Protocol-level rekey transaction. A new transaction type that updates the access-group's underlying keypair (decoupling group identity from key derivation), with re-encrypted key copies for current members. This is the only option that delivers the user expectation faithfully, but it's a significant protocol surface change.
I'd love to know which (if any) of these the team thinks is on the table, and whether there's existing thinking I missed.
Environment
mainas of 2026-04-16node.deso.org(public node)deso-chatandgetchaton.comsource review