Protocol & Modules
zkShare DKMS

ZKShare Module (DKMS)

ZK-MPC encrypted secret sharing module enabling the most private decentralized file sharing with no one in the middle, not event idSign; ONLY the wallets added to document can ever reconstruct and decrypt the documents shared via IPFS


TL;DR

Imagine you want to send a secret message or file to someone or to a group of members such as a DAO. With zkShare, you can encrypt the data on the frontend with a unique generated key and this key gets stored on 3 nodes on an MPC blockchain (partisiablockchain.com) or any Advanced MPC blockchain. The other members can then request the secret key, and decrypt the data that was stored publically but encrypted.


Objective:

Securely share a secret among a predefined set of users/wallets, authorized via their wallet addresses, using zero-knowledge proofs to maintain privacy.

Components:

  • Smart Contract or Decentralized Storage: For Storing The resourceID, encryptedSecret, and its ACLs (Access Control Lists). These values can be stored publicly leveraging the “no single point of failure” nature of this technology but at the same time ensuring privacy of the stored secret limiting it to a set of authorized users
  • zkShare module: For generating a new share, granting access of a share and modifying ACLs of an existing share. This should be implemented through programmable MPC actions leveraging computational rules done by multiple on-chain nodes (zkSmartContract)

Use Cases:

  • End-to-end Encrypted on-chain messaging
  • Decentralized file-sharing (sharing the decryption key of an encrypted file stored on IPFS)
  • Sharing information privately between DAO members
  • Token-Gating without relying on a server to “Gate” the resource or the key to the resource
  • Many more…

Implementation Flow:

  • User1 wants to share data D with User2 and User3 ACLs

  • On Frontend, Generate a unique hash D-H from the data we want to encrypt and a hash ACL-H from the ACLs

  • Call ZKshare.set(D-H+ACL-H) this function will:

    1. Do a Blockchain call: This will create a new multi-party wallet bound only to this new share and have role-based signatures authorizing only User1, User2 and User3 to request signatures. Unlike multi-sig wallet, this multi-party wallet allows one user to generate signatures based on if he is included in the ACLs and also based on if they satisfy the rules defined in the ACLs (such as being a DAO member, or holds an NFT, or just have a valid signature)
    2. Blockchain Response: This new share-bound wallet will request the MPC nodes to generate a TSS signature S. Each node will return part of the signature to the frontend
    3. The frontend combines the signature parts into one signature and encrypts D with that signature. (The signature will be the encryption/decryption key)
    4. Finally the function will return encryptedData
  • User1 now can safely store ACLs, encryptedData and D-H publicly on a smart contract or IPFS or database

  • User2 now wants to access the file. They are logged in with any EOA

  • User2 queries the API or smart contract that returns the ACLs, encryptedData and D-H of the resources he is part of.

  • User2 calls ZKshare.get(D-H, ACLs, personal_sign) this function will:

    • request a TSS signature from the same share-bound wallet created by user1
    • Before creating the signature, the blockchain node must verify that this user is in the ACLs and satisfies the rules set by these ACLs
    • If this is true, then the MPC nodes creates the same signature and sends back the parts to user2
  • User2 can combine the signature parts and decrypt encryptedData to get the original data D

The programmable MPC wallet should have 1/n of a signature extracting the public key and checking it to the ACLs and then hash the ACLs to compare with ACL-H

Example Implementation:

// This is a sample and should not be used in production!!
extern crate pbc_contract_codegen;
extern crate pbc_contract_common;
 
use pbc_contract_common::context::ContractContext;
use pbc_contract_common::zk::{ZkInputDef, ZkState};
use pbc_contract_common::crypto::secp256k1::Secp256k1;
use pbc_contract_common::crypto::aes::{Aes256, NewCipher, generic_array::GenericArray};
use std::collections::HashMap;
use pbc_zk::{Sbi64, PublicKey, SecretBinary};
use serde::{Serialize, Deserialize};
use pbc_zk::{Sbi64, PublicKey, SecretBinary, store_sbi, store_metadata};
use crypto::sign; // Placeholder for actual cryptographic signing library
 
/// Structure for storing encrypted secrets and the list of authorized addresses
#[derive(Serialize, Deserialize)]
pub struct Vault {
    owner: PublicKey,
    members: Vec<PublicKey>,
}
 
#[state]
struct ContractState {
    vaults: HashMap<u32, Vault>,
}
 
/// Initialize the contract state
#[init]
fn initialize(ctx: ContractContext) -> ContractState {
    ContractState {
      vaults: HashMap::new(),
    }
}
 
 
 
/// Sets up a new vault with a secret-shared AES key and stores it securely.
///
/// ### Arguments:
///
/// * `unique_id` - Unique identifier for the vault.
/// * `members` - A vector of public keys representing the members who can access the vault.
///
/// ### Returns:
///
/// A cryptographic signature of the vault ID.
#[zk_on_secret_input]
pub fn set(unique_id: i32, members: Vec<PublicKey>) -> String {
    let aes_key_parts = split_aes_key(); // Simulate splitting AES key into three parts
    let owner = get_public_key(); // Assume this retrieves the public key of the caller
 
    // Store the parts of the vault as secret-shared data and metadata
    let id_base = generate_id_base(unique_id); // Generate a base ID for storage
    
    store_metadata::<PublicKey>(id_base, owner);
    store_sbi::<Sbi64>(id_base + 1, aes_key_parts.0);
    store_sbi::<Sbi64>(id_base + 2, aes_key_parts.1);
    store_sbi::<Sbi64>(id_base + 3, aes_key_parts.2);
    store_metadata::<Vec<PublicKey>>(id_base + 4, members);
 
    // Generate a cryptographic signature for the vault ID
    let combined_aes_key = combine_secret_parts(vault.secret_part_1, vault.secret_part_2, vault.secret_part_3);
    Ok(combined_aes_key)
}
 
#[zk_on_compute_complete]
pub fn get(vault_id: i32, siwe_signature: String) -> Result<Sbi64, &'static str> {
    let address = verify_siwe_signature(siwe_signature)?;
    let salt = request_salt_from_server(address)?;
 
    let vault = load_vault(vault_id);
    if vault.members.contains(&address) {
        let combined_aes_key = combine_secret_parts(vault.secret_part_1, vault.secret_part_2, vault.secret_part_3);
        Ok(combined_aes_key)
    } else {
        Err("Access Denied")
    }
}
 
pub fn load_vault(vault_id: i32) -> Result<Vault, &'static str> {
    // Search for vault matching vault_id
    let ids = secret_variable_ids();
    for id in ids {
        let current_id = load_metadata::<i32>(id);
        if current_id == Ok(vault_id) {
            // Load the secret parts and public data
            let owner = load_metadata::<PublicKey>(id);
            let secret_part_1 = load_sbi::<Sbi64>(id);
            let secret_part_2 = load_sbi::<Sbi64>(id + 1);
            let secret_part_3 = load_sbi::<Sbi64>(id + 2);
            let members = load_metadata::<Vec<PublicKey>>(id + 3);
            
            return Ok(Vault {
                owner,
                secret_part_1,
                secret_part_2,
                secret_part_3,
                members,
            });
        }
    }
    Err("Vault not found")
}
 
 
fn split_aes_key() -> (Sbi64, Sbi64, Sbi64) {
    // Placeholder: logic to split AES key into three parts
    (Sbi64::from(123), Sbi64::from(456), Sbi64::from(789))
}
 
 
fn verify_siwe_signature(signature: String) -> Result<PublicKey, &'static str> {
    // Placeholder: logic to verify SIWE signature
    Ok(PublicKey::new())
}
 
fn request_salt_from_server(wallet_address: PublicKey) -> Result<i64, &'static str> {
    // Placeholder: logic to request salt
    Ok(123456)
}
 
fn combine_secret_parts(part1: Sbi64, part2: Sbi64, part3: Sbi64) -> Sbi64 {
    part1 + part2 + part3 // Simplified, should be SSS
}
 

Example Usage:

idSign.com (opens in a new tab) is a decentralized eSignature protocol that allows users to sign agreements(pdfs) on-chain and get a SoulBound NFT (proof-of-signature). Anyone can verify that the parties signed an agreement but ONLY the parties can access the pdf agreement shared for signature. No one in the middle, mot even idSign, can ever decrypt the document. The parties can choose the add/remove mediators or lawyers to their agreement

NB: idSign is a DAPP, not a wallet, nor blockchain and does not have access to a user’s private key

  1. Document/Agreement creation:

    1. User1 uploads a pdf, sets the parties’s wallets that should sign
    2. The frontend encrypts the pdf file and stores the encrypted file on IPFS
    3. The encryptionKey (generated on the frontend by AES) should be shared by the parties
    const provider = '<Any wallet provider>'
    const idSignSmartContract = '<our smart contract>'
     
    const agreement = {
    	name: String,
    	file: BASE64,
    	parties: [] 0x111
    }
     
    const Key = '<generate AES symm key>'
    const encFile = AES.encrypt(agreement.file, Key)
    const acls = isArrayBindingElement.parties
     
    const mpcSignature = zkShare.set(Hash(Key), Hash(acls))
    const encKey = AES.encrypt(Key, mpcSignature)
     
     
    const ipfsCid = await uploadIPFS(encFile)
     
    const res = await idSignSmartContract.mint(
    								agreement.name,
    								agreement.parties,
    								ipfsCid,
    								encKey,
                                    Hash(Key)
                        ) // this mints ProofOfSignature soul-bound NFT on any EVM chain
  2. Document/Agreement access & sign:

    1. User2 scans our smart contract for RFSs (Request-For-Signature NFTs)
    2. The frontend downloads the encrypted pdf file from IPFS
    3. User 2 requests zkShare for the decryption key
    4. User2 can now decrypt and read the contents of the pdf and signs it
    const provider = '<Any wallet provider>'
    const idSignSmartContract = '<our smart contract>'
     
    const NFT = {
    	id: Number,
    	name: String,
    	parties: [] 0x111,
    	ipfsCid: String,
    	encKey: String,
        keyHash: String
    }
     
    const personalSig = provider.signMessage(session)
    const mpcSignature = zkShare.get(NFT.keyHash, NodeFilter.parties, authSignature)
    const Key = AES.decrypt(NodeFilter.encKey, mpcSignature)
     
    const encFile = await downloadIPFS(NFT.ipfsCid)
    const file = AES.decrypt(encFile, Key)
     
    const signature = provider.signMessage(
    					"I agree to the agreement "+NFT.id + "Date"+Date.now()
    			)
     
    const res = await idSignSmartContract.sign(NFT.id, signature) // this will record the signature on the NFT in an EVM chain