A cryptographically verifyable chain of events to determine a list team members and encrypted data e.g. usernames only accessible to the members.
The goal of this project is to allow a group of participants (organization) to exchange data without the content being revealed to anyone except the organization members.
The project is inspired by web of trust and blockchains to aim for the following behaviour:
- Enable asynchronous exchange of data (participants don't have to be online at the same time)
- Only one verification is necessary to establish trust between everyone in the organization
- Organization access to a member can be revoked instantly
- An organization can't be manipulated in hindsight
- "Efficiently" be able download the current state by a client (e.g. not running a full blockchain node)
To achieve the defined goals this project relies on a central service in combination with asymmetric cryptography.
The current state of an organization can be constructed from a series of events (chain) and multiple encrypted state entries (encrypted state).
The purpose of the chain is determine who is part of the organization and what permissions does this member have.
The central service as well as the members must have full access to this information.
The purpose of the encrypted-state is contain information like usernames to hide away from the central service. Only members have access to the content of the encrypted state.
In decentralized systems there are two issues that didn't align with the goals because:
- A change e.g. adding or removing a participant from a group needs to propagate to all participants before the take effect and therefor state is only eventual consistent.
- Afaik asynchronous exchange and strong consistency is conflicting. For example blockchains require a lot of online nodes to verify the chain and to achieve a certain gurantee that the current version is the one that will be used and not another fork. There are some ideas and proposal how to tackle it though e.g. local-first-web/auth discussion
The three main objectives for the server are:
- Be an always online instance for members to asynchronously exchange data
- Prevent members to submit updates based on outdated state which ensures a correct data integrity (for the unencrypted state).
- Instantly revoke access to removed members.
For example the encrypted state has a logical clock in the public, but authenticated data submitted by a user. This way the server can throw an error in case the user didn't have the latest state. The user's client can then fetch the latest information and retry.
Note: Depending on the UX the user might want to re-review their changes. This might vary from case to case.
- Member
- Admin
- Server
- Member collaborating with the central service
- Admin collaborating with the central service
Since the chain is public all the meta data about who has access to the group and all their permissions are visible to the central service. This is a known trade-off and possibly an evolution of the protocol using zero knowledge proofs (like the Signal Private Group System) could reduce the meta data visible to the server while keep the functionality.
- Forward Secrecy is currently not supported. A future evolution of the project ideally uses a ratchet to enable forward secrecy. Inspirations could be DCGKA.
- Post-compromise security is currently not supported. Only when a member gets removed the synchronous encryption key and related lockboxes are replaced which leads to PCS in this case.
For authentication with the server the client has to request a challenge from the server. A nonce is returned. The client verifies that the nonce is prefixed with the text "server-auth-"
followed by a UUID. The UUID is only verified by the length. Once this is confirmed the client will sign the challenge and return the signature to sign in.
The server in the response sets a HTTP only session cookie to initialize a session.
The purpose of the nonce prefix is to avoid the server sending any other message that the client would sign accidentally (kind of a chosen-plaintext attack).
It might make sense to also include an encryption challenge to verify that the client also has access to the lockbox private key.
This though needs caution to prevent to prevent a chosen-plaintext attack as described here.
The demo is available at https://www.serenity.li/. Keep in mind the data is regularily wiped.
- The private and public keys are currently storred in the localstorage. This is not as planned in the production ready implementation where they should only storred encrypted secured by a password or WebAuthn.
- Some buttons are shown as active e.g. "Demote to member", but the actual action will fail in certain cases e.g. when the current user is the last admin. In this case the error will only be visible in the console.
- Remove member (based on a event proposal does not work)
- Missing checks if the publicKeys are valid for createUser
- Missing checks if the publicKeys match an existing user when adding a member to an organisation
- Chain/Encryped State related
- when promoting someone to an admin then set the name in the encrypted state
- when removing a member, take over the encrypted state updates from them
- when removing permissions from a member, take over the encrypted state updates
Note: The whole project is prototype style code e.g. some functions of the trust chain package are mutating the input object.
-
create-chain
- Usually created by one author, but can be more as well.
-
add-member
- Members can only add another member if they have the permission
canAddMember
. A member can not add another member as admin. - Admins can add members and set the permissions
canAddMember
andcanRemoveMember
. If an admin wants to add another member the event must be signed by >50% of the admins.
- Members can only add another member if they have the permission
-
remove-member
- Members can only remove another member if they have the permission
canAddMember
. A member can not remove another member as admin. - Admins can add members and set the permissions
canAddMember
andcanRemoveMember
. If an admin wants to add another member the event must be signed by >50% of the admins.
- Members can only remove another member if they have the permission
-
update-member
- Only admins can update the authorization info
isAdmin
,canAddMember
andcanRemoveMember
of other members. If an admin promote a member to an admin or demote an admin to a member the event must be signed by >50% of the admins.
- Only admins can update the authorization info
-
end-chain
TODO (not implemented yet)- The purpose is to close the chain so no one else can attach more events to it. The event must be signed by >50% of the admins.
Example structure of an event:
{
// the purpose of authors being an array is so that multiple users can sign an event and declare themselves as authors
"authors": [
{
// public signing key of creator of the event or others that signed it
"publicKey": "pkcVysaH_mC-TpXzZEAAeB47rIqsWwubaM4stZQu-B4",
// signature of the hash of the prev transaction + the hash of the current transaction
"signature": "q5BHaR8Wu1CCA6mt-XCwmxuYpVU1-6J5E_FmxgWu_C63yfCJ9IwFh9bBMX3WPAdMN_yMFMAK3Ygapjd96qKHCg"
}
],
// hash of the prev transaction
"prevHash": "M-LtpoR7cMzADedkf0TXAPVIXQR5kj7T-gCtcgNaFwu1IShI84B5PaXULQjQHMVANiMTyRyCcsne389jHIRvng",
// transaction is the actual event content
"transaction": {
"type": "update-member",
"isAdmin": true,
"canAddMembers": true,
"canRemoveMembers": true,
"memberSigningPublicKey": "EfOSyGYcwLdVjRSJDimpEFB2_XuXd2oCCh7f8I4VlwY"
}
}
Example structure of the encrypted state:
[
{
// identified for symmetric key that's used
"keyId": "43b89b1c-6c7b-48a6-839c-20cc495d3f97",
"ciphertext": "yYV1_uUtoFPRBRBCUtyv-jwPLfQI2UFYMovnUUFJh5UXALMkq69KM73YI3WaqZGrcclwTE7jMYAsYKR5rU0d-Q7yprXOS_1hXZ8TeKyQ-vSxpgZmgJVUK5zP_HDyFB4ackgBRdws6IyOMxc1Kz_HB-SCqC8Vk26H7YhGaIQWs4MXNpqgTg17cIo7Q2TL2shYN_VHSHmAeojByOfyDpUP3kpXK3zduIZZEKG3tuwjnzN0JG83BCPel8Iyrh91_UnBpctnXQUEhkoQ_ZJ_6YXpGYTnhTFur40NhX5nDjpIImbCiMoVAVIU_KZEcQ4bzGZ1_eM8AeRKTiie",
"nonce": "YOnlwPnEtwnfEQa0nEQR41RvQ7Dxt2NB",
// global logical clock to order the encrypted states (to ensure deterministic data resolving)
"publicData": "{\"clock\":2}",
"author": {
"publicKey": "pkcVysaH_mC-TpXzZEAAeB47rIqsWwubaM4stZQu-B4",
"signature": "XTNuX2mqcpRF4nLnhHURAYblpE68ftPsaNW3ZsdZY1Se7BlLTjJSnADrV9Rn4AhM4MIjAsU8mj003vQCJUtjAA"
},
"lockbox": {
"keyId": "43b89b1c-6c7b-48a6-839c-20cc495d3f97",
"receiverSigningPublicKey": "pkcVysaH_mC-TpXzZEAAeB47rIqsWwubaM4stZQu-B4",
"senderLockboxPublicKey": "7CVtpWbqKFJvZO7hso3dOmrrwva_3uDgm3erquuTFRQ",
"ciphertext": "f3_AVc5xIxC8ejhBflv_qhAjtZoTveMroA9H4uATn51yKKpToegWn_dB-P-oYG8IK_CKDt4mfmGD-43M5KVSeAykPDiPIxAIBAgNZfjVJMwhiYUFMt4U26WTlHX4aBFa75-VztyF2TcYmkhEbFw-Gf0fVVs",
"nonce": "HLDYyAc4GMEcCqHSE2a_N-l1oaRkWfCL"
}
}
]
addAuthorToEvent
createInvitation
TODOacceptInvitation
TODOrevokeInvitation
TODO
- create invitation (to be storred in the encrypted part)
- unique string id
- author
- accept invitation
- id
- signature of the unique string (proof) by invitee
The chain will not accept any invalid input. It will not ignore them, but rather throw an Error. When creating an event there is no validation.
Here an example for clarification:
const event = createChain(…); // does not throw errors
const state = resolveState([event]); // throws errors (internally uses applyEvent)
const event2 = addMember(…); // does not throw errors
const newState = applyEvent(state, event2); // throws errors
Admin actions need to be in sync, meaning I can't vote on two admin interaction at the same time. Any additional chain event will invalidate them. See future improvements for a possible solution.
- Exhaustive TS Matching
- Functions should not mutate incoming parameters
- Implement a state machine
- Add functionality to sign multiple events in multiple orders and pick from the right one to prevent the known UX issue. It doesn't scale, but with a limit of 5 it probably covers lots of cases.