ZKSAFE Password Docking


Required Node.js v16,install snarkjs

npm install -g snarkjs

install ethers, you need to know how to use ethers, all the code examples bellow assumed you know how to use ethers

npm install ethers

Contract source code

Testing code

Note: The test environment is hardhat. ethers is used slightly differently than the formal environment. The following code is based on the test environment

We suggested don't enter password outside ZKPass and ZKSAFE, to prevent password leakage. ZKPass (short of ZKSAFE Password) contracts are open to partner contracts, such as ZKSAFE

resetPassword() reset password

Initializing password and changing password are the same interface. Let's start with the util function getProof() that all ZK use

Util Function

async function getProof(pwd, address, nonce, datahash) {
    let expiration = parseInt(Date.now() / 1000 + 600)
    let chainId = (await provider.getNetwork()).chainId
    let fullhash = utils.solidityKeccak256(['uint256','uint256','uint256','uint256'], [expiration, chainId, nonce, datahash])
    fullhash = s(b(fullhash).div(8)) //fullhash must 254b, solidityKeccak256 is 256b, so it need convert

    let input = [stringToHex(pwd), address, fullhash]
    let data = await snarkjs.groth16.fullProve({in:input}, "./zk/v1/circuit_js/circuit.wasm", "./zk/v1/circuit_final.zkey")

    const vKey = JSON.parse(fs.readFileSync("./zk/v1/verification_key.json"))
    const res = await snarkjs.groth16.verify(vKey, data.publicSignals, data.proof)

    if (res === true) {
        console.log("Verification OK")

        let pwdhash = data.publicSignals[0]
        let fullhash = data.publicSignals[1]
        let allhash = data.publicSignals[2]

        let proof = [

        return {proof, pwdhash, address, expiration, chainId, nonce, datahash, fullhash, allhash}

    } else {
        console.log("Invalid proof")

For the convenience, we wrote a util function getProof(), wraps all of our ZK algorithms. Note that circuit.wasm, circuit_final.zkey, verification_key.json are fixed values that can be found in ZK source code

getProof() is the ZK Circuit in the diagram

getProof() has 4 params:

  • pwd: your password, string type

  • address: your wallet address, string type

  • nonce: obtain your nonce value from ZKPass contarct, string type

  • datahash: the hash of the data you would like to sign, string type

Return all data related to ZK algorithm:

  • proof: proof of ZK-SNARK, array of 8 uint256

  • pwdhash: pwdhash needed in ZKPass contract, uint256 type

  • address: address from params, string type

  • expiration: password signing expiration seconds, int type

  • chainId: chain id, int type

  • nonce: nonce from params, string type

  • datahash: datahash from params, string type

  • fullhash: dosen’t need to upload to contract, 254 bits

  • allhash: hash of all above, uint256 type

Initialize Password

let pwd = 'abc123' //your password
let nonce = '1' //Initialize password, nonce=1
let datahash = '0' //for resetPassword, datahash=0
let p = await getProof(pwd, accounts[0].address, nonce, datahash)

let gasLimit = await zkPass.estimateGas.resetPassword(p.proof, 0, 0, p.proof, p.pwdhash, p.expiration, p.allhash)
await zkPass.resetPassword(p.proof, 0, 0, p.proof, p.pwdhash, p.expiration, p.allhash, {gasLimit})
console.log('initPassword done')

resetPassword() has 7 params:

  • proof1: proof generated by the old password, array of 8 uint256

  • expiration1: old password signing expiry seconds, uint256 type

  • allhash1: allhash generated by the old password, uint256 type

  • proof2: proof generated by the new password, array of 8 uint 256

  • pwdhash2: pwdhash of the new password generated by ZK, uint256

  • expiration2: new password signing expiry seconds, uint256 type

  • allhash2: allhash generated by the new password, uint256 type

Since there’s no old password for initial password, the first 3 parameters related to the old password are not required in the contract. However, they were all required to the contract (parameter as 0) or take proof2 of the new password as proof1 (as in the example)

Upon success, the password for the caller's address (msg.sender) is pwd

Reset Password

let oldpwd = 'abc123' //old password
let nonce = await zkPass.nonceOf(accounts[0].address) //current nonce
let datahash = '0' //for resetPassword, datahash=0
let oldZkp = await getProof(oldpwd, accounts[0].address, s(nonce), datahash) //old password proof

let newpwd = '123123' //new password
let newZkp = await getProof(newpwd, accounts[0].address, s(nonce.add(1)/**new password nonce+1*/), datahash) //new password proof

await zkPass.resetPassword(oldZkp.proof, oldZkp.expiration, oldZkp.allhash, newZkp.proof, newZkp.pwdhash, newZkp.expiration, newZkp.allhash)
console.log('resetPassword done')

Still resetPassword() function, old password is required for resetting password, so the first 3 params were generated by the old password

Upon success, the password for the caller's address (msg.sender) is newpwd, and the oldpwd is invalid

verify() verify password

Password can be verified off chain by obtaining pwdhash, or onchain with the partner contract. The partner contract calls ZKPAss.verify(), if the password is incorrect, it throws an error. If no errors, the password is correct, and the signature is valid

Unsuggested to enter passwords outside ZKPass and ZKSAFE, to prevent password leakage. Partners can use ZKPass for on-chain verification

verify() has 5 params:

  • user: the password owner, address type

  • proof: from getProof(), array of 8 uint256

  • datahash: the data what user signing, this is the hash of the data, uint256 type

  • expiration: from getProof(), uint256 type

  • allhash:from getProof(),uint256 type

The contract will use the user's pwdhash to verify the password and convert the datahash to 254 bits fullhash... In summary, the getProof() tool will process all ZK validation parameters

ZKSAFE as a partner contract to call ZKPass

function withdrawERC20(
    uint[8] memory proof,
    address tokenAddr,
    uint amount,
    uint expiration,
    uint allhash
) external onlyOwner {
    uint datahash = uint(keccak256(abi.encodePacked(tokenAddr, amount))); //calculate datahash
    eps.verify(owner(), proof, datahash, expiration, allhash); //verify password and signing

    IERC20(tokenAddr).safeTransfer(owner(), amount); //verified!

    emit WithdrawERC20(tokenAddr, amount);

In this example, user wants to withdaw the token from ZKSAFE, so the tokenAddr and token amount needs to be signed with password

ZKSAFE off-chain code

let pwd = 'abc123' //user’s password 
let nonce = s(await eps.nonceOf(accounts[0].address)) //user's current nonce
let tokenAddr = usdt.address //token to withdraw
let amount = s(m(40, 18)) //amount of the token to withdraw
let datahash = utils.solidityKeccak256(['address', 'uint256'], [tokenAddr, amount]) //calculate datahash
datahash = s(b(datahash)) //convert to string type
let p = await getProof(pwd, accounts[0].address, nonce, datahash) //calculate ZK Proof

await safebox.withdrawERC20(p.proof, tokenAddr, amount, p.expiration, p.allhash) //call the contract, withdraw
console.log('withdrawERC20 done')

await print()

datahash is defined by the partner, uint256 type, which is usually a hash value. There are exceptions, such as address for the signed code which is uint160 type, it fits datahash without Keccak256

The datahash calculated off chain should be consistent with the one in the partner contract

Last updated