# Group (Same DHT) - With Encryption ###### tags: `Kizuna-architecture` <style> .ui-infobar, #doc.markdown-body { max-width: 1000px; } </style> #### comments - The version of this architecture with no encryption can be found [here](https://hackmd.io/@SiY_fKT2QNG8vujhpgGOyA/HJoY8IY1O) - This document describes how an agent can create a group with entry-roles pattern in a public DNA - When HoloPort allowed cloneable DNA, this will be refactored to fit the cloneable DNA pattern - If bob leaves the group, the secret is recreated. If bob is readded, he will not be able to read the message that were sent when he was not a member - If bob was offline when he was added then removed, bob will only be able to read the messages sent when he was part of the group - later on, we can allow the agents to choose whether newly added members can read past messages (by providing the secret) or not. - multi threading remote calls for fetching secret? - Messages have to be push based if we want instant update for all members (but then is taxing for the sender) - Spam protection for creating meaningless groups so many times - limiting the number of groups that can be created by an agent within a timeframe through the usage of local timestamp + the signature of the Group entry + counter - nonce currently is generated randomly with no counter. - can sender and recepient be the same in box encryption (for own data on Holo) - `leave_group` is architected minimally as there will be drastic change when this pattern shifts to cloneable DNA. - `block_group` is not yet architected and will be with cloned DNA in mind - Secrets are stored on DHT - store encrypted secret in source chain (to protect agent from HP owner) and later be retrieved by other members -> cannot be accessed without private key which only exists when browser is online -> store the secret on DHT encrypted with receiver key. ## Entry Structure ``` rust // verify the signature of this entry with Group entry "creator" #[hdk_entry(id = "group", visibility = "public")] struct Group { name: String, created: Timestamp, creator: AgentPubKey, members: HashMap<AgentPubKey, XSalsa20Poly1305EncryptedData> } Links: { agent_pubkey->group|member| } #[hdk_entry(id = "group_secret_key", visibility = "private")] struct GroupSecretKey { group_hash: EntryHash, // this is currently the actual secret bytes key_hash: XSalsa20Poly1305KeyRef } // created_timestamp can be entry hash or no? // express timestamp as a unix timestamp (decide on which denomination e.g. hourly, daily) and just perform modulo operations for more specific message filtering Path::from("all_groups.<CREATOR_PUB_KEY><CREATED_TIMESTAMP>.<unix_timestamp>") Links: { Path(all_groups)->group|all_groups| // may be removed in cloned dna pattern Path(CREATOR_PUB_KEY)->group|alice's groups| Path(year)->group_message|(year)'s message| Path(month)->group_message|(month)'s message| Path(day)->group_message|(day)'s message| Path(hour)->group_message|(hour)'s message| } pub struct XSalsa20Poly1305EncryptedData { nonce: XSalsa20Poly1305Nonce, encrypted_data: Vec<u8>, } // verify the signature of this entry with Group entry "creator" #[hdk_entry(id = "group_message", visibility = "public")] struct GroupMessage { group_hash: EntryHash, key_ref: SecretBoxKeyRef, // counter: u8, // nonce: Vec<u8>, // needs to be unique per message. If we could set the nonce's length, we can do DNA hash + agentPubKey + counter // set one nonce and counter per agent. encrypted_payload: XSalsa20Poly1305EncryptedData, // Contains the text of the message and the created timestamp, and the agent_pub_key of the sender } ``` ## Entry Relationship Diagram - group_members_A_v3's links are ommitted but works the same way with group_members_A_v2. Only shown to signify that update pattern is the same with Group entry. ```mermaid graph TD subgraph group_zome subgraph group_entry group_a==>|update by add/remove_members/update_name|group_a_v2 group_a_v2 group_a==>|update by add/remove_members/update_name|group_a_v3 end subgraph agents alice-->|member|group_a bobby-->|member|group_a clark-->|member|group_a diego-->|member|group_a group_a.->alice group_a.->bobby group_a.->clark group_a_v2.->alice group_a_v2.->bobby group_a_v2.->clark group_a_v2.->diego end subgraph group_message_entry group_message_1.->group_a group_message_2.->group_a group_message_3.->group_a group_message_1-->|read|alice group_message_1-->|read|bobby end subgraph path group_entry_hash-->unix_timestamp_1 group_entry_hash-->unix_timestamp_2 group_entry_hash-->unix_timestamp_3 unix_timestamp_1-->|tag 'text'|group_message_1 unix_timestamp_1-->|tag 'text'|group_message_2 unix_timestamp_1-->|tag 'file'|group_message_3 end end ``` ## Zome Functions ### `init` - may be required if x25519 keypair should be generated for use on encrypting shared secret - not yet added to ERD and entry structures ```mermaid sequenceDiagram participant ALD as Alice_Lobby_Cell participant LDHT as Lobby_DHT ALD-->>ALD: create_x25519_keypair() ALD-->>LDHT: commit x25519PubKey ALD-->>LDHT: create_link AgentPubKey --> x25519PubKey "pubkey" ``` ### `create_group` ```rust pub struct CreateGroupInput { name: String, // cannot be empty and must at least include 2 pubkeys // creator AgentPubKey is not included here members: Vec<AgentPubKey> } pub struct CreateGroupOutput { content: Group, group_id: EntryHash, group_revision_id: HeaderHash } fn create_group(create_group_input: CreateGroupInput) -> ExternResult<CreateGroupOutput> ``` ```rust type SecretBoxKeyRef = XSalsa20Poly1305KeyRef; // @todo don't generate these in wasm. // What we really want to be doing is have secrets generated in lair and then lair passes back an // opaque reference to the secret. // That is why the struct is is called KeyRef not Key. impl_try_from_random!( SecretBoxKeyRef, holochain_zome_types::x_salsa20_poly1305::key_ref::KEY_REF_BYTES ); ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice_Lobby_CELL participant LDHT as Lobby_DHT AUI-->>ALD: call `create_group` rect rgba(255, 0, 0, .5) ALD-->>ALD: call list_blocked() to contacts zome ALD-->>ALD: check if any members are blocked alt if even one agent is blocked ALD-->>AUI: return error("cannot create group with blocked agents") note right of ALD: dep->contacts zome end end ALD-->>ALD: call `TryFromRandom` to generate symmetric key ALD-->>ALD: store the encrypted secret key on source chain note right of ALD: generate a placeholder x25519 key for encryption if box don't work with AgentPubKey note right of ALD: encrypting with own key (with box) may not yet work. Note right of ALD: current implementation generates secret key in WASM loop for each member ALD-->>ALD: encrypt secret with agent's pubkey <br> and append to HashMap<AgentPubKey, XSalsa20Poly1305EncryptedData> note right of ALD: might need to use x25519 key instead. if so, <br> x25519 key should be retrieved from links from agent_pubkey. end ALD-->>LDHT: commit group entry LDHT-->>ALD: return header address loop for all members ALD-->>LDHT: link from each member to Group entry tagged "member" end loop for all added agents ALD-->>IM: send remote signal with Group EntryHash note right of ALD: check forum for not getting the entry just committed to DHT\ alt if online rect rgb(100,100,300,0.1) IM-->>IM: call `handshake_secret` end end end ALD-->>AUI: return Group entry ``` ### `handshake_secret` - should be called when bob comes back online - should be called when remote signal is received about a new group - works for both synchronous (`remote_signal`) and asynchrnous (bob was offline for a long time) case. ```rust pub struct HashesWrapper(Vec<EntryHash>) pub fn handshake_secrets(group_member_hashes: HashesWrapper) -> ExternResult<()> ``` ```mermaid sequenceDiagram participant BUI as Bobby_UI participant BLD as Bobby_Lobby_CELL participant ALD as Alice(Admin)_Lobby_CELL participant LDHT as Lobby_DHT participant OM as Other Members loop for all GroupMember hash BLD-->>LDHT: get() on GroupMember hash note right of BLD: GetOption: content LDHT-->>BLD: GroupMember Element BLD-->>BLD: find the <K,V> in members field <br> where K = bobby's agent pubkey and get V BLD-->>BLD: decrypt V with x_25519_x_salsa20_poly1305_decrypt() note right of BLD: need to get either x25519 of sender (admin) <br> or AgentPubKey (from author of GroupMember or <br>get Group then get the AgentPubKey from creator field??) alt decrypt return None note right of BLD: what to do here?? end BLD-->>BLD: encrypt with own private key and store in source chain note right of BLD: x_25519_x_salsa20_poly1305_encrypt should work <br> with same key for both sender and recepient field end ``` ### `get_needed_group_members_hashes` - no input/output. - this function calls the `handshake_secret` fn in the end - this function gets the GroupMember entry hashes that the agent still need to retrieve secret of - This function should be called when the conductor boots up only to get the latest secrets of all groups if not retrieved yet ```rust pub fn get_needed_group_members_hashes(_: ()) -> ExternResult<()> ``` ```mermaid sequenceDiagram participant BUI as Bobby_UI participant BLD as Bobby_Lobby_CELL participant ALD as Alice(Admin)_Lobby_CELL participant LDHT as Lobby_DHT participant OM as Other Members BLD-->>LDHT: get_links from pubkey with tag "member" loop for all groups loop until getting all updates BLD-->>LDHT: get() on all updates LDHT-->>BLD: GroupMembers Element end end note right of BLD: should be HashMap<H, Vec<K>> <br> where H is the group HeaderHash and <br> Vec<K> are all the update versions of GroupMembers entry for a group BLD-->>BLD: query source chain for all secrets bob has BLD-->>BLD: filter GroupMembers entry that Bob is part of <br> but dont have the secret yet and get their entry hashes loop for all groups rect rgb(100,100,300,0.1) BLD-->>BLD: call handshake_secrets end end ``` ### `request_secrets` - **CURRENTLY NOT IN USE** - used when agents will ask secrets from other members of the group ```rust= // This is the hash of the Secret Key. // SecretBoxKeyRef (XSalsa20Poly1305KeyRef) // This is the actual Secret encrypted with x_25519_x_salsa20_poly1305_encrypt() // XSalsa20Poly1305EncryptedData pub struct Secrets(Map<SecretBoxKeyRef, XSalsa20Poly1305EncryptedData>) pub struct HashesWrapper(Vec<EntryHash>) pub fn request_secrets(group_member_hashes: HashesWrapper) -> ExternResult<Secrets> ``` ```mermaid sequenceDiagram participant BLD as Bobby_Lobby_CELL participant ALD as Keyholder_Lobby_CELL participant LDHT as Lobby_DHT BLD-->>ALD: GroupMembers entry hashes loop for all entry hashes ALD-->>LDHT: get GroupMembers entry LDHT-->>ALD: return group entry ALD-->>ALD: check if Bobby is part of the group's members for the secret he's asking for alt Bobby is not part of some group ALD-->>BLD: return Error end ALD-->>ALD: check if you have the secret in source chain alt the secret cant be found ALD-->>BLD: return error (secret not found) end ALD-->>ALD: encrypt secret in Lair with Bobby's pubkey ALD-->>ALD: collect all encrypted secrets end ALD-->>BLD: return encrypted secrets with key handshake receipt signed by alice ``` ### `add_members` - can only be called by the admin of the group ```rust pub struct AgentPubKeys(Vec<AgentPubKey>) pub struct UpdateMembersInput { members: Vec<AgentPubKey>, // assume that original HeaderHash is given here group_id: EntryHash group_revision_id: HeaderHash } pub fn add_members(add_member_input: AddMemberInput) -> ExternResult<AgentPubKeys> ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice(Admin)_Lobby_CELL participant LDHT as Lobby_DHT participant OM as Other Members AUI-->>ALD: UpdateMembersInput ALD-->>ALD: check whether members field is empty alt if empty ALD-->>AUI: return Err(members field is empty) end note right of ALD: this is also checked in the UI rect rgba(255, 0, 0, .5) ALD-->>ALD: call list_blocked() to contacts zome ALD-->>ALD: check if any invitees are blocked <br> and dont add blocked members to the group alt if anyone is blocked ALD-->>AUI: return error("blocked agents <br> cannot be added to the group") note right of ALD: dep->contacts zome end end ALD-->>LDHT: get GroupMembers entry with HeaderHash given note right of ALD: get should have the contents() option. LDHT-->>ALD: GroupMembers ALD-->>LDHT: get_deatils the Group entry with EntryHash given note right of ALD: get should have the latest() option. LDHT-->>ALD: return Group EntryDetails ALD-->>ALD: filter the latest Header (should be element) note right of ALD: the current API only returns Vec<SignedHeaderHashed> <br> so we need to call get on the EntryHash found in the latest update header. <br> This architecture should be changed once get_details() API change. ALD-->>LDHT: call get() on most recent update of Group Entry with EntryHash note right of ALD: getOption is contents() LDHT-->>ALD: most recent Group Entry ALD-->>ALD: generate a new shared secret key ALD-->>ALD: store the encrypted secret key on source chain loop for all members ALD-->>ALD: encrypt secret with agent's pubkey and <br> append to HashMap<AgentPubKey, XSalsa20Poly1305EncryptedData> note right of ALD: might need to use x25519 key instead. if so, x25519 key <br> should be retrieved from links from agent_pubkey. end ALD-->>LDHT: update_entry the Group with new members field with original HeaderHash loop for all newly added agents ALD-->>LDHT: link from each member to original Group entry tag "member" end ALD-->>OM: send remote signal with GroupMembers <br> EntryHash to all members alt if online rect rgb(100,100,300,0.1) OM-->>OM: call `handshake_secret` end end ``` ### `add_initial_members` - only called from create_group ```rust pub struct AgentPubKeys(Vec<AgentPubKey>) pub struct AddInitialMembersInput { invitee: Vec<AgentPubKey>, group_entry_hash: EntryHash, // todo: this should be the actual secret and not key ref. secret: SecretBoxKeyRef } pub fn add_initial_members(add_member_input: AddMembersInput) -> ExternResult<HeaderHash> ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice_Lobby_CELL participant LDHT as Lobby_DHT participant IM as Initial Members loop for each member ALD-->>ALD: encrypt secret with agent's pubkey <br> and append to HashMap<AgentPubKey, XSalsa20Poly1305EncryptedData> note right of ALD: might need to use x25519 key instead. if so, <br>x25519 key should be retrieved from links from agent_pubkey. end ALD-->>ALD: initialize GroupMembers with args given ALD-->>LDHT: commit GroupMembers entry with members LDHT-->>ALD: return GroupMembers HeaderHash ALD-->>LDHT: link Group -> GroupMembers tag "members" ALD-->>LDHT: link from the GroupMembers to own pubkey with tag "keyholder" loop for all newly added agents ALD-->>LDHT: link from each member to GroupMembers entry tag "member" end loop for all added agents ALD-->>IM: send remote signal with GroupMembers EntryHash note right of ALD: check forum for not getting the entry just committed to DHT end alt if online rect rgb(100,100,300,0.1) IM-->>IM: call `handshake_secret` end end ``` ### `remove_members` - can only be called by the admin of the group - this will be drastically changed once the pattern moves to cloneable DNA ```rust pub struct AgentPubKeys(Vec<AgentPubKey>) // same with add_members input pub struct UpdateMembersInput { members: Vec<AgentPubKey>, // assume that original HeaderHash is given here group_id: EntryHash group_revision_id: HeaderHash } pub fn remove_members(member: UpdateMembersInput) -> ExternResult<AgentPubKeys> ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice(Admin)_Lobby_CELL participant LDHT as Lobby_DHT participant OM as Other Members AUI-->>ALD: UpdateMembersInput ALD-->>ALD: check whether members field is empty alt if empty ALD-->>AUI: return Err(members field is empty) end note right of ALD: this is also checked in the UI ALD-->>LDHT: get GroupMembers entry with HeaderHash given note right of ALD: get should have the contents() option. LDHT-->>ALD: GroupMembers ALD-->>LDHT: get_deatils the Group entry with EntryHash given note right of ALD: get should have the latest() option. LDHT-->>ALD: return Group EntryDetails ALD-->>ALD: filter the latest Header (should be element) note right of ALD: the current API only returns Vec<SignedHeaderHashed> <br> so we need to call get on the EntryHash found in the latest update header. <br> This architecture should be changed once get_details() API change. ALD-->>LDHT: call get() on most recent update of Group Entry with EntryHash note right of ALD: getOption is contents() LDHT-->>ALD: most recent Group Entry ALD-->>ALD: remove members from members field ALD-->>ALD: generate a new shared secret key ALD-->>ALD: store the encrypted secret key on source chain loop for all remaining members ALD-->>ALD: encrypt secret with agent's pubkey and <br> append to HashMap<AgentPubKey, XSalsa20Poly1305EncryptedData> note right of ALD: might need to use x25519 key instead. if so, <br> x25519 key should be retrieved from links from agent_pubkey. end ALD-->>LDHT: update_entry the Group with new members field with original HeaderHash loop for all removed agents ALD-->>LDHT: link from each member to original Group entry tag "member" end ALD-->>OM: send remote signal with GroupMembers EntryHash to all members alt if online rect rgb(100,100,300,0.1) OM-->>OM: call `handshake_secret` end end ``` ### `get_all_my_groups` - TODO: invited checking if the invitor is blocked (warning on ui) - probably need to fetch the messages with the group as well ```rust pub struct GroupOutput { // taken from the latest update of Group entry latest_name: String, // taken from the latest update of GroupMembers members: Vec<AgentPubKey>, // This should be the EntryHash of the very first Group Entry group_id: EntryHash, // This should be the EntryHash to which the AgentPubKey is linked to group_members_id: EntryHash, // This should be the original Header at all times. group_revision_id: HeaderHash, // None if the agent is NOT admin group_members_revision_id: Option<HeaderHash>, // If this will only be used to get the secrets then we can probably only return the relevant info for that group_members_versions: Vec<GroupMembers>, created: Timestamp, creator: AgentPubKey, } pub struct Groups(Vec<GroupOutput>) pub fn get_all_groups() -> ExternResult<Groups> ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice(Admin)_Lobby_CELL participant LDHT as Lobby_DHT AUI-->>ALD: call get_all_my_groups ALD-->>LDHT: get_links agent_pubkey->group|member| LDHT-->>ALD: return Links note right of ALD: can get group_id loop for each link ALD-->>LDHT: get_details on Target(Group EntryHash) note right of ALD: GetOption latest() LDHT-->>ALD: Group EntryDetails note right of ALD: can get group_revision_id , creator, created from here ALD-->>ALD: check if there is update or not alt there is an update for group Entry ALD-->>ALD: check which update header is the latest ALD-->>LDHT: get entry of the EntryHash in latest update header note right of ALD: GetOption contents() note right of ALD: may change once get_details() API returns elements in update field and not just header LDHT-->>ALD: latest Group entry note right of ALD: can get members and latest_name note right of ALD: need to get all versions of GroupMembers if need to know the secrets (id?) end ALD-->>ALD: create GroupOutput end ALD-->>ALD: collect GroupOutputs into Groups ALD-->>AUI: return Groups ``` ### `update_group_name` * Admin only * updating group entry may change in multi-admin pattern. * store header hashes in ui. ```rust pub struct UpdateGroupNameInput { name: String, group_hash: HeaderHash } pub fn update_group_name(group_input: UpdateGroupNameInput) -> ExternResult<HeaderHash> ``` ```mermaid sequenceDiagram participant AUI as Alice_UI participant ALD as Alice(Admin)_Lobby_CELL participant BLD as Bobby_Lobby_CELL participant LDHT as Lobby_DHT participant OM as Other Members AUI-->>ALD: call `update_group_name` send Group header hash (oldest) and new group name ALD-->>ALD: construct Group struct with new group name ALD-->>LDHT: update_entry Group entry with old hash being the given header hash LDHT-->>ALD: return HeaderHash ALD-->>AUI: return HeaderHash ``` ### `leave_group` - this function will be architected once moved to cloneable DNA pattern - this will change current validation of group_members ### `block_group` - this function will be architected once moved to cloneable DNA pattern - this will change current validation of group_members ### Validations #### Entries * `group` * create is valid if creator pubkey matches the signature * create is valid if group name is not more than 50 characters * create is valid if group name is at least one character long * delete is valid if creator pubkey matches the signature (not implemented) * update (name) is valid if creator pubkey matches the signature * update is only valid if the old_entry's header is Create * update is valid only if members > 2 * update is only valid if the old_entry's header is Create * update is only valid if old_group_name != new_group_name | old_members != new_members * `group_message` * create is only valid when the author is part of the group members given GroupMembers entry hash field in GroupMessage #### Links * `agent_pubkey->group|member|` * `Path(all_groups)->group` * `Path(CREATOR_PUB_KEY)->group` * `Path(unix_timestamp)->group_message` * create is only valid if the author of the link is part of the group members given group members being the target of the link ### Encryption Discussion Points - Requirements - Be able to encrypt messages with non-shared, per-agent secret key. - Why don't we just generate asymmetric keys per agent and encrypt