TryQuiet / quiet

A private, p2p alternative to Slack and Discord built on Tor & IPFS
https://www.tryquiet.org
GNU General Public License v3.0
1.96k stars 85 forks source link

Add communityMetadata store validation #1891

Closed leblowl closed 9 months ago

leblowl commented 1 year ago

Only the owner should be able to update the community metadata. Currently anyone can modify the community metadata. This allows anyone to change the owner certificate (and nickname) and community name.

As an example, we replicate community metadata here: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/backend/src/nest/storage/storage.service.ts#L244-L248

We save the metadata to the Redux store: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts#L263-L271

We get the owner nickname from ownerCertficate: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/state-manager/src/sagas/communities/communities.selectors.ts#L78

Which is used to send a channel message: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/state-manager/src/sagas/publicChannels/createGeneralChannel/sendInitialChannelMessage.saga.ts#L20

Another example, we set the community name from communityMetadata.rootCa: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/state-manager/src/sagas/communities/updateCommunity/updateCommunity.saga.ts#L6

which is displayed to the user in the left-side panel: https://github.com/TryQuiet/quiet/blob/277f96631805071654ad15f2bdf7f03567d3fadc/packages/desktop/src/renderer/components/Sidebar/IdentityPanel/IdentityPanel.tsx#L49

holmesworcester commented 1 year ago

Whoever tackles this should dig into it a bit and write up a document proposing how to solve the problem and get feedback before they start writing code.

Part of the complexity is that you need to know the owner's OrbitDB ID or something about the owner to have some sense of truth here.

leblowl commented 1 year ago

In order to only allow the community owner to edit community metadata, we need to know if an entry was written by the community owner. To do this we can use the OrbitDB identity ID as OrbitDB uses to verify write permissions in OrbitDB's IPFSAccessController: https://github.com/orbitdb/orbit-db-access-controllers/blob/3741eb318e9c7efea5af15a54110cf6de8ae1fe3/src/access-controllers/ipfs.js#L20. Since each member of the community will need to know this ID in order to validate community metadata entries, we can simply include the community owner's OrbitDB identity ID in the invite link.

After the owner initializes their OrbitDB databases for the first time, we can save their identity ID (orbitdb.identity.id) in LevelDB under a key like ownerOrbitDbIdentityId. We can create a new file like src/nest/storage/identity.ts with functions for storing and retrieving the owner identity ID (putOwnerOrbitDbIdentityId, getOwnerOrbitDbIdentityId).

The owner's identity ID should be sent to the frontend for incorporating it into the invite link. In order to include the owner's OrbitDB identity ID and a PSK (#1897) in the link, we will need to reduce the number of peers in the invite link by 1. So PSK + owner OrbitDB identity ID + 3 peers.

When a new user clicks that link and joins, the owner's identity ID should be parsed from the invite link and sent to the backend where it's stored in LevelDB (via the same interface defined in src/nest/storage/identity.ts). This should happen before initializing any OrbitDB databases. Once the owner ID is stored, proceed to first initialize the community metadata DB.

The community metadata DB stores the owner's public key, which we use for authentication in some instances (e.g. when checking cert validity). So we should wait until community metadata has been replicated before initializing other OrbitDB databases (or at least those that depend on the owner's cert).

As for community metadata validation, it looks like access controllers only validate heads and not each entry. So instead of using an access controller, we can simply validate each entry ourselves. We can create a new validation function in the backend validateCommunityMetadataEntry that, given an entry, validates the entry by checking that the entry is valid, its identity signature is valid and the entry identity matches the owner and then also checking that the public key included in the community metadata payload is used to sign the community metadata payload.

In order to validate entries in an EventStore, it's fairly straightforward as discussed in https://github.com/TryQuiet/quiet/issues/1893. However, communityMetadata is currently a key/value store. Since we don't support multiple communities yet, I'm not sure we benefit from using a key/value store in this case, but we can continue with that approach to reduce refactoring.

For KeyValueStore databases, the interface does not include an iterator method, so we can create our own KeyValueIndex (https://github.com/orbitdb/orbit-db-kvstore/blob/main/src/KeyValueIndex.js) class with a custom updateIndex method to filter log values with validateCommunityMetadataEntry before they are indexed. We can provide this modified Index class to the KeyValue store during instantiation. For an example, see: https://github.com/TryQuiet/quiet/pull/1923

Then we can create a new function, getCommunityMetadata that simply retrieves the community metadata for a given community ID.

I think it would be nice to encapsulate this logic in a CommunityMetadataStore class.

EmiM commented 10 months ago

https://github.com/TryQuiet/quiet/pull/2073

kingalg commented 9 months ago

2.0.3-alpha.15

Done