0xPolygonMiden / miden-vm

STARK-based virtual machine
MIT License
630 stars 161 forks source link

Encryption in the VM #1240

Open Al-Kindi-0 opened 9 months ago

Al-Kindi-0 commented 9 months ago

Encryption in the VM can be done using RPO as was described in this paper.

Encryption

Encryption can be summarized using the following figure

encryption

As can be seen from the figure, we need to initialize the state of the hash function with a key (assumed to be a word) and a nonce and call the permutation once.

To encrypt, assuming we have the plaintext we would like to encrypt laid out in a contiguous memory region, we need to:

  1. Load the plaintext in batches of two words and add it to the top 8 stack elements (i.e., the rate portion of the state) element-wise. This results in the ciphertext block corresponding to the plaintext we just loaded.
  2. Store the ciphertext block in some memory region.
  3. Call the permutation.
  4. Repeat until all plaintext blocks have been processed (i.e., encrypted).

The most costly part of the above will be the stack manipulations needed to achieve the first point and to store the ciphertext. It seems that if we want to make the cost of encryption reasonable we would need to introduce an additional VM instruction or potentially more than just one.

Option 1

The new instruction can take a pointer on the stack ptr and it can do:

  1. Fetch the double word referenced by ptr.
  2. Add the double word to the top 8 stack elements element-wise.
  3. Store the top 8 stack elements resulting from the previous point in the memory region referenced by ptr. This amounts to overwriting the double word from point $1$.
  4. Increment the pointer by $2$.

The following figures illustrate this:

encrypt_1 encrypt_2

Option 2

If we want to avoid overwriting the plaintext in memory, we can use an additional pointer on the stack to reference the memory region we would like to use for storing the resulting ciphertext. The following figure illustrates this alternative: encrypt_3 Note that in this case we will need to increment the additional pointer as well by $2$.

Decryption

Decryption can be summarized using the following figure decryption

As can be seen from the figure, we need to initialize the state of the hash function with a key (assumed to be a word) and a nonce and call the permutation once.

To decrypt, assuming we have the ciphertext we would like to decrypt laid out in a contiguous memory region, we need to:

  1. Negate (i.e., compute the additive inverse) of each of the top 8 stack elements.
  2. Load the ciphertext in batches of two words and add it to the top 8 stack elements (i.e., the rate portion of the state) element-wise. This is the plaintext block corresponding to the ciphertext we just loaded.
  3. Store the plaintext block in some memory region.
  4. Overwrite the top 8 stack elements with the same ciphertext double word from point $2$ above.
  5. Call the permutation.
  6. Repeat until all ciphertext blocks have been processed (i.e., decrypted).

As can be seen from the previous description, decryption is a more complex operation than encryption. Depending on the use cases, it might be fine to do decryption with the currently available instructions. However, and if we are willing to introduce a new instruction for encryption, we might be able to design a more generic instruction that might be useful for both encryption and decryption. One possible way to do decryption using a dedicated instruction could be to have, again, two pointers, one to reference the ciphertext and the other to reference the plaintext. Then, the new instruction could take care of points 1-3, as illustrated in the following two figures

decrypt_0 decrypt_1

Note that in the above, we have only updated the pointer referencing the plaintext. At this point, we can use the existing mem_stream instruction to accomplish step 4 as the following figure illustrates

decrypt_2

Questions

  1. Could we design an instruction that would be helpful for both encryption and decryption? Maybe a flag to switch between the two and do addition element-wise for encryption and "subtraction" element-wise for decryption?
  2. Should we just forget about decryption and design a specialized instruction to do only encryption?
  3. Can we modify mem_stream to make it more general and avoid needing to add any new instructions?