Solidity, Cryptography, Encryption – How to Decrypt a Message On-Chain

cryptographyencryptionsolidity

I want to store some data in smart contract and then decrypt it on chain. Pseudocode:

contract EncryptedData {
   bytes data;
   string public decryptedData;

   constructor(bytes data_) public {
      data = data_;
   }

   function decrypt(bytes key) public {
      decryptedData = decryptDataSomehow(data, key);
   }
}

I know that when contract gets called the entire network gets the key and I'm fine with it.

The question is only "how can I decrypt data on chain"? I googled a lot but I didn't find any cryptographic primitives available. Only some EIPs like EIP-213 and so on, but nothing specific.

I'm not cryptographic PhD to implement cryptography myself (and it probably would burn bazillions of gas) so I need some builtins tools or at least already tested contracts.

What are my options here? I can't do it offchain because the entire idea of the whole thing is trust that only smart contract could have. I can't call oracles or something, the entire thing should be completely on-chain.


Edit: I actually have a little bit different situation. My contract is more like

contract EncryptedData {
   bytes[] data;
   string public decryptedData[];

   function addData(bytes data_) { 
       data.push(data_);
   }

   function decrypt(bytes key) public {
       for (uint i = 0; i < data.length; i++) {  
           decryptedData.push(decryptDataSomehow(data[i], key));
       }
   }
}

Best Answer

You may use the following simple function to encrypt/decrypt arbitrary data on-chain:

function encryptDecrypt (bytes memory data, bytes memory key)
  public pure returns (bytes memory result) {
  // Store data length on stack for later use
  uint256 length = data.length;

  assembly {
    // Set result to free memory pointer
    result := mload (0x40)
    // Increase free memory pointer by lenght + 32
    mstore (0x40, add (add (result, length), 32))
    // Set result length
    mstore (result, length)
  }

  // Iterate over the data stepping by 32 bytes
  for (uint i = 0; i < length; i += 32) {
    // Generate hash of the key and offset
    bytes32 hash = keccak256 (abi.encodePacked (key, i));

    bytes32 chunk;
    assembly {
      // Read 32-bytes data chunk
      chunk := mload (add (data, add (i, 32)))
    }
    // XOR the chunk with hash
    chunk ^= hash;
    assembly {
      // Write 32-byte encrypted chunk
      mstore (add (result, add (i, 32)), chunk)
    }
  }
}

It is symmetric, i.e. encryptDecrypt (encryptDecrypt (x, key), key) == x for any x and key. It is also secure enough.

Algorithm explanation: This function just XORs data with the following sequence of 32-byte hashes:

hash0 = keccak256 (key, 0)
hash1 = keccak256 (key, 32)
hash2 = keccak256 (key, 64)
...
hashN = keccak256 (key, N * 32)

This approach is known as Counter (CTR) encryption mode.

You may easily implement the same logic on any programming language that is able to calculate keccak256 on binary data.

Security Note: This schema assumes that each key is used only once and is hard to guess, preferably random. If you want to use one key several times, or use weak keys, you need to use initial vector like this:

For encryption:

  1. Generate random sequence of, say, 16 bytes, lets call it IV (initial vector)
  2. Append IV to the key (key = key + IV)
  3. Encrypt your data with this enforced key
  4. Prepend encrypted data with IV (encData = IV + encData)

For decryption:

  1. Cut first 16 bytes from encrypted data (these bytes are actually IV) and append them to the key
  2. Decrypt data using this enforced key
Related Topic