A password protected remittance contract

Recently I have to make a DApp for remittances. The requirements go like this

  • There are three people: Alice (UserA), Bob (UserB) & Carol (UserC).
  • Alice wants to send funds to Bob, but she only has ether & Bob wants to be paid in local currency.
  • Luckily, Carol runs an exchange shop that converts ether to local currency.
  • Therefore, to get the funds to Bob, Alice will allow the funds to be transferred through Carol’s Exchange Shop. Carol will convert the ether from Alice into local currency for Bob (possibly minus commission).

To make it simple, let’s make this contract super barebone, and one-time-use only.

Attempt 1: A naive approach

Here is a naive and crazily insecure contract. It’s pretty common for beginners to make this mistake. Let’s jump straight into code.

pragma solidity 0.4.15;

// WARNING: THIS IS A NEGATIVE EXAMPLE.
// THE CONTRACT HAS SERIOUS VULNERABILITIES!!
contract PasswordedRemittance {
    bytes32 combinedPassword; // sha3(sha3(clearPassword1), sha3(clearPassword2))

    // constructor
    // owner creates a remittance with sha3(clearPassword1) and sha3(clearPassword2)
    PasswordedRemittance(bytes32 hashedPassword1, bytes32 hashedPassword2) payable {
        combinedPassword = sha3(hashedPassword1, hashedPassword2);
    }

    // hashedPassword is sha3(clearPassword)
    claim(bytes32 hashedPassword1, bytes32 hashedPassword2) {
        require(combinedPassword == sha3(hashedPassword1, hashedPassword2));
        suicide(msg.sender);
    }
}

Here’s the way it’s supposed to work.

  • Contract creator creates the remittance, with 2 passwords.
  • Contract creator gives clearPassword1 to UserB, clearPassword2 to UserC
  • The two parties do their exchange off-chain
  • When the off-chain exchange is done, UserB and UserC meet up in person, find a computer, and type in the passwords, hash them, and claim the remittance

No one is supposed to be able to claim the remittance until they get both passwords.

Here is the problem: leaked password hashes

Remember that all the transactions on the blockchain are visible to everyone. And remember that the CALLDATA is also public (Just go to etherscan.io and see some transactions yourself to get the impression).

So when the funder creates this remittance, he/she creates a txn with to set to 0 (i.e. a contract creation transaction), with all the arguments to the constructor as CALLDATA, placed in the txn. It means the password hashes are already leaked by the time contract is created on the blockchain.

So what happens then? Well, nothing is preventing someone from reading the txn that created the contract, getting the password hashes, and claiming the remittance with the password hashes.

Attempt 2: Let’s fix the password hash leak

// WARNING: THIS IS A NEGATIVE EXAMPLE.
// THE CONTRACT STILL HAS SERIOUS VULNERABILITIES!!
contract PasswordedRemittance {
    bytes32 combinedPassword; // sha3(sha3(clearPassword1), sha3(clearPassword2))

    // constructor
    // owner creates a remittance with sha3(sha3(clearPassword1), sha3(clearPassword2))
    PasswordedRemittance(bytes32 _combinedPassword) payable {
        combinedPassword = _combinedPassword;
    }

    // hashedPassword is sha3(clearPassword)
    claim(bytes32 hashedPassword1, bytes32 hashedPassword2) {
        require(combinedPassword == sha3(hashedPassword1, hashedPassword2));
        suicide(msg.sender);
    }
}

Here is how it’s supposed to fix the password hash leak

  • Remittance creator comes up with 2 passwords, stores it off-chain
  • Remittance creator creates the contract with the combinedPassword, which is sha3(sha3(clearPass1), sha3(clearPass2))
  • The creator gives the clearPass1 to UserB, clearPass2 to UserC
  • B and C do the off-chain exchange
  • UserB and UserC meet up in person, sit down in front of a computer, type in the passwords, hash them, and claim the remittance.

Here is another problem: leaked proofs to malicious miners

Even though no one can see the two password hashes on the blockchain, the claimer still has to call claim with the proofs that he/she knows the secret, to claim the remittance.

But here’s the problem: we know that the blockchain is only eventually-consistent, and the claim doesn’t happen instantly. It isn’t your typical SQL database. When the claimer calls claim, he/she creates a txn with the proofs (in this case, hashedPassword1 and hashedPassword2) in the CALLDATA. The transaction gets submitted to the transaction pool. Miners are able to see the txns in the transaction pool.

So what can a malicious miner do?

  • The miner can first read the password hashes off the txn
  • The miner can then create a transaction of their own to call the claim, with the password hashes
  • Finally he can choose to not include the legit txn in the block at all

If he’s lucky (and have a lot of hashpower), he gets to claim the remittance, leaving the legit claimer nothing.

Final Attempt: What if we know the claimer’s address beforehand?

We have other options. Let’s take a step back.

What if we know the claimer’s address beforehand? Then we only need 1 password, for UserB. And we can use a common trick: hashing the password with an address

// This one is pretty secure
contract PasswordedRemittance {
    address claimer;
    bytes32 combinedPassword; // sha3(sha3(clearPassword), claimer)

    // constructor
    // owner creates a remittance with sha3(sha3(clearPassword), claimer)
    PasswordedRemittance(address _claimer, bytes32 _combinedPassword) payable {
        claimer = _claimer;
        combinedPassword = _combinedPassword;
    }

    // hashedPassword is sha3(clearPassword)
    claim(bytes32 hashedPassword) {
        require(claimer == msg.sender);
        require(combinedPassword == sha3(hashedPassword, msg.sender));
        suicide(msg.sender);
    }
}

No more vulnerabilities

  • When the contract is created, everyone knows the claimer’s address, but not the password.
  • The claimer can’t claim the remittance without the password hash
  • When the claimer calls claim, miners see the password hash. But they are unable to steal the funds, as they can’t spoof msg.sender to be the claimer.

It’s also provably fair!

The final implementation is also fair to everyone. No one can bullshit no one.

  • UserB can verify that UserA (the creator of the contract) didn’t give him a fake password
    • Pretty easy. All he/she has to do is to calculate sha3(sha3(clearPassword), claimerAddress) off-chain, and check if that matches the combinedPassword
  • UserC can verify that he/she is indeed able to claim the remittance
    • Just check if his/her address is the claimer

Is there another way?

Yes, I suppose you can write your own public key crypto implementation in Solidity, generate 2 private keys, store their public keys on contract. Then you have UserB and UserC sign a message saying what address they wish the funds to be released to. But I think that’s overkill, and may not be necessarily secure / cheap.

See related EIP

Moral of the story?

Don’t just rely on passwords. Rely on addresses (i.e. private keys) and signed messages.