# 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