I was recently reminded of a clever way to modify encrypted messages that I thought would be interesting to share. The main message is that encryption only provides the first of the following three properties:
- Confidentiality: unauthorized actors cannot read the message
- Integrity: unauthorized actors cannot secretly change the message
- Authentication: the message was sent by the authorized party
The motivation for this post is a smart contract that we audited, which uses a commit-and-reveal scheme for voting. This means that participants first submit a hash of their ballot, and only reveal the ballot (which must match the hash to be considered valid) after all hashes have been collected. This prevents participants from deciding how to vote based on the running tally. However, it does not prevent someone from copying someone else’s hash commitment, and later “revealing” the same value, allowing them to blindly duplicate another voter’s ballot:
- Alice commits to
ballotAlicewith the commitment
hash( ballotAlice )
- Bob sees this and commits to the same value
- Everyone else commits to their ballots
- Later, Alice reveals
ballotAliceand everyone can confirm it matches her commitment
- Bob sees this and also “reveals”
ballotAlice, which also matches his commitment, thereby duplicating Alice’s vote
To prevent duplication, we recommended that
msg.sender be included in the hash (and validated after the ballot is revealed), binding each commitment to the voter that made it.
Things get more interesting in a different contract within the same system, where votes are encrypted with a key that will eventually be revealed (whether or not the original voter reveals it). As with the previous mechanism, this also allows participants to duplicate other ballots but interestingly, the same mitigation does not work. At an abstract level, this is because when encrypted data (or ciphertext) is modified, it still decrypts to something. In security lingo, encryption provides confidentiality but not integrity. Whether or not this fact is exploitable depends on the details of the system.
In this case, the smart contract uses a stream cipher, where the key stream is combined with the ballot (the plaintext) using the XOR operation:
ciphertext = key ⊕ ballot
Decryption is achieved by combining the result with the same key:
decrypted = key ⊕ ciphertext = key ⊕ (key ⊕ ballot) = (key ⊕ key) ⊕ ballot = 0 ⊕ ballot = ballot
If we append the voter’s address to the ballot before encryption, we can treat the result like two separate encryption operations:
- If someone changes
ciphervoter, it will create a corresponding change in the decrypted voter value.
- This will not affect the decryption of the ballot
Consider what happens if someone replaces the voter ciphertext with
cipher*voter = ciphervoter ⊕ voter ⊕ voter*
Voter decryption becomes:
decryptedvoter = keyvoter ⊕ cipher*voter = keyvoter ⊕ ( ciphervoter ⊕ voter ⊕ voter* ) = keyvoter ⊕ keyvoter ⊕ voter ⊕ voter ⊕ voter* = ( keyvoter ⊕ keyvoter ) ⊕ ( voter ⊕ voter ) ⊕ voter* = 0 ⊕ 0 ⊕ voter* = voter*
In other words, anyone who knows any section of the original plaintext can replace that section as desired, despite not knowing the encryption key, leaving all other sections intact. They could use this to replace the voter address with their own, bypassing the duplication protection.
In this particular case (but importantly, not in every case), this attack can be mitigated by simply appending a hash of the ballot and voter address to the plaintext.