Key commitment. One of our agents has infiltrated Roseacrucis’ inner circle, but all secret keys are required to be surrendered to the counterintelligence authority. Help her send AES-GCM ciphertexts back to the Library that decrypt to confidential information under one key, but innocuous banter under another.
You can test your ciphertext with Go. Run the following in a shell,
then open /tmp/polyglot-first.jpg
and /tmp/polyglot-second.bmp
in an image viewer. You may need to alter the path of polyglot.enc
to reflect
your download directory.
curl -L -o /tmp/decrypt-aes-gcm.go https://cyfraeviolae.org/forbidden-salamanders/static/decrypt-aes-gcm.go go build -o /tmp/decrypt-aes-gcm /tmp/decrypt-aes-gcm.go < polyglot.enc /tmp/decrypt-aes-gcm 8007941455b5af579bb12fff92ef31a3 4a4f5247454c424f52474553 > /tmp/polyglot-first.jpg < polyglot.enc /tmp/decrypt-aes-gcm 14ef746e8b1792e52b1d22ef124fae97 4a4f5247454c424f52474553 > /tmp/polyglot-second.bmp
This attack was shown by Yevgeniy Dodis, Paul Grubbs, Thomas Ristenpart, and Joanne Woodage in Fast Message Franking: From Invisible Salamanders to Encryptment.
First, we will describe a general strategy to create a ciphertext that yields the same MAC with two different keys. Then we will show how to construct a ciphertext that yields meaningful results when decrypted with those two keys.
Consider arbitrary keys \(k_1, k_2\), nonce \(n\), and ciphertext \(c\) (additional associated data can be accounted for in a straightforward manner). \(k_1, k_2\) are associated with authentication keys \(h_1, h_2\) and blinds \(s_1, s_2\), respectively.
Given a ciphertext of three blocks, we will attempt to add a fourth ciphertext block \(c_3\), set the MACs equal to each other, and solve for \(c_3\). Remember that in \(\mathbb{F}_{2^{128}}\), addition is the same as subtraction.
For the resulting ciphertext of four blocks, the MACs for each key are computed as \[ mac_{1} = s_1 + \vert c\vert{}h_1 + c_3h_1^2 + c_2h_1^3 + c_1h_1^4 + c_0h_1^5 \] \[ mac_{2} = s_2 + \vert c\vert{}h_2 + c_3h_2^2 + c_2h_2^3 + c_1h_2^4 + c_0h_2^5 \] where \(s\) is a constant depending on the AES-GCM key and the nonce, and \(h\) is the authentication key depending only on the AES-GCM key. We can now set the MACs equal to each other and solve for \(c_3\). \[ s_1 + \vert c\vert{}h_1 + c_3h_1^2 + c_2h_1^3 + c_1h_1^4 + c_0h_1^5 = s_2 + \vert c\vert{}h_2 + c_3h_2^2 + c_2h_2^3 + c_1h_2^4 + c_0h_2^5 \] \[ c_3(h_1^2 + h_2^2) = (s_1 + s_2) + \vert c \vert (h_1 + h_2) + c_2(h_1^3+h_2^3) + c_1(h_1^4 + h_2^4) + c_0(h_1^5 + h_2^5) \] \[ c_3 = \frac{(s_1 + s_2) + \vert c \vert (h_1 + h_2) + c_2(h_1^3+h_2^3) + c_1(h_1^4 + h_2^4) + c_0(h_1^5 + h_2^5)}{h_1^2 + h_2^2} \]
Note that the choice to place the extra block in the final position was arbitrary. For the attack below we will instead need to change the penultimate block rather than adding a block; the computation is similar.
For the next phase, we construct a ciphertext that decrypts to a valid JPEG under one key and a valid BMP under another. Recall that the ciphertext of AES-GCM, as in AES-CTR, is computed by taking the XOR of the keystream and the message. The keystream is computed from the cipher key and the nonce.
The basic strategy is to place the JPEG bytes and BMP bytes at different locations, carefully arranging it so each parser will ignore the other data for the file. JPEG files can include comments, in which we will include the BMP data. The BMP parser will stop reading as soon as the indicated length of the BMP has been read, after which we will include the JPEG data. In each decrypted file, the data for the other image will be scrambled as we are using a different key, but it will not matter as the junk data will be in a location that is ignored by the image parser.
All JPEG files start with the magic bytes \(\mathtt{ffd8}\) and end with \(\mathtt{ffd9}\). We will place a JPEG comment immediately after the initial magic bytes, which is indicated by \(\mathtt{fffe}\) and is followed by a 2-byte big-endian encoding of the comment length \(J\). Let \(J_i\) indicate the \(i\)th byte of \(J\); \(J_0\) being the least significant byte.
All BMP files start with the magic bytes \(\mathtt{424d}\) followed by a 4-byte little-endian encoding of the file length. Because we need the BMP file to fit inside the JPEG comment, we set \[ \begin{array}{|c|c|}\hline & 0 & 1 & 2 & 3 & 4 & 5 & \ldots & -2 & -1 \\\hline \mathsf{JPEG} & \mathtt{ff} & \mathtt{d8} & \mathtt{ff} & \mathtt{fe} & J_1 & J_0 & \ldots & \mathtt{ff} & \mathtt{d9} \\ \mathsf{BMP} & \mathtt{42} & \mathtt{4d} & J_0 & J_1 & \mathtt{00} & \mathtt{00} & \ldots & & \\\hline \end{array} \]
In addition to the file length at the beginning, BMP files also include the size of the color array (the pixels of the image) in the initial metadata. BMP parsers ignore any data after the color array is supposed to be over, even if the file length has not been exhausted yet. That means we can set \(J=\mathtt{ffff}=65536\), and the resulting header will be valid for any BMP file less than \(J\) bytes.
Since these headers must be in the same location at the start of the file, we search for two keys \(k_1, k_2\) and a nonce \(n\) such that \[ \operatorname{AES-GCTR}(k_1, n, \mathtt{ffd8fffeffff}) = \operatorname{AES-GCTR}(k_2, n, \mathtt{424dffff0000}), \] where \(\operatorname{AES-GCTR}\) returns the ciphertext portion of \(\operatorname{AES-GCM}\) but not the MAC.
The easiest way to do this is via a birthday attack: fix an arbitrary nonce, then generate random keys for both the JPEG header and the BMP header. Encrypt each and store the ciphertext in a lookup table. Repeat until two keys are found that encrypt their respective headers to the same ciphertext bytes. The search takes less than a minute on a desktop computer.
We have now computed the ciphertext header \(C_{H}\) and two keys which will decrypt it to the correct header bytes for both files. Note that \(C_{H}\) only depends on the maximum size of the BMP file, and thus can be precomputed. The remainder of the attack that depends on the specific images is very fast.
As explained before, we place the BMP bytes in the JPEG comment, add padding to finish the comment, and add the JPEG bytes after the comment is over. Below we show the structure of the ciphertext. For the blank cells in the ciphertext row, use either the encryption of the JPEG cell under \(k_1\) or the BMP cell under \(k_2\) as indicated. \[ \begin{array}{|c|c|}\hline \mathsf{JPEG} && & & \textrm{JPEG} & \mathtt{ffd9} \\ C & C_H & & \mathtt{00}^{J-\vert \textrm{BMP}\vert}& & \\ \mathsf{BMP} && \textrm{BMP} & & & \\\hline \end{array} \]
Here, \(\textrm{JPEG}\) is the bytes of the JPEG file without the initial and final magic bytes, and similarly \(\textrm{BMP}\) is the bytes of the BMP file without the initial magic bytes.
These ciphertexts do not have the same MAC yet. If we tried to use the strategy outlined at the beginning where we add an extra block at the end, the JPEG file would no longer end in \(\mathtt{ffd9}\) and would be invalid. Instead, we modify it to change the penultimate block. The collision algorithm will result in a ciphertext block \(X\).
However, we don't want any data from the penultimate block to corrupt our JPEG image. After \(\textrm{JPEG}\) ends, we start another comment that will include the penultimate block, hiding it from the parser. Care must be taken to ensure the penultimate block is really on a block boundary. For AES-GCM, the block size is 16 bytes.
Below is the final structure of the polyglot ciphertext. \[ J' = 16 - (6 + J + \vert \textrm{JPEG} \vert + 4) \pmod{16} \] \[ \begin{array}{|c|c|}\hline \mathsf{JPEG} && & & \mathrm{JPEG} & \mathtt{fffe} & J' & & & & \mathtt{ffd9} \\ C & C_{H} & & \mathtt{00}^{J-\vert \textrm{BMP}\vert}& & & &\mathtt{00}^{J'-30} & X & \mathtt{00}^{14}& \\ \mathsf{BMP} && \textrm{BMP} & & \\\hline \end{array} \]
from aesgcmanalysis import att_merge_jpg_bmp with open('first.jpg', 'rb') as h: jpg = h.read() with open('second.bmp', 'rb') as h: bmp = h.read() c, mac = att_merge_jpg_bmp(jpg, bmp, aad=b"")