Comment on page


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
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