docs: def crypt(header, body_bytes, secret):
      """
      TACACS+ uses a shared secret key (known to both the client and server)
      to obfuscate the body of sent packets.  Only the packet body (not
      the header) is obfuscated.
  
      https://datatracker.ietf.org/doc/draft-ietf-opsawg-tacacs/?include_text=1#section-3.7
  
      ENCRYPTED {data} == data ^ pseudo_pad
  
      The pad is generated by concatenating a series of MD5 hashes (each 16
      bytes long) and truncating it to the length of the input data.
  
      pseudo_pad = {MD5_1 [,MD5_2 [ ... ,MD5_n]]} truncated to len(data)
  
      The first MD5 hash is generated by concatenating the session_id, the
      secret key, the version number and the sequence number and then
      running MD5 over that stream.  All of those input values are
      available in the packet header, except for the secret key which is a
      shared secret between the TACACS+ client and server.
  
      Subsequent hashes are generated by using the same input stream, but
      concatenating the previous hash value at the end of the input stream.
  
      MD5_1 = MD5{session_id, key, version, seq_no} MD5_2 = MD5{session_id,
      key, version, seq_no, MD5_1} ....  MD5_n = MD5{session_id, key,
      version, seq_no, MD5_n-1}
  
      :param header:     a TACACSHeader object
      :param body_bytes: packed bytes, i.e., `struct.pack(...)`
      :param secret:     a key used to encrypt/obfuscate packets according
                         to the TACACS+ spec
  
      :return:           packed bytes, i.e., `struct.pack(...)` representing the
                         obfuscated packet body
      """  # noqa
  
      # B = unsigned char
      # !I = network-order (big-endian) unsigned int
      body_length = len(body_bytes)
      unhashed = (
          struct.pack('!I', header.session_id) +
          six.b(secret) +
          struct.pack('B', header.version) +
          struct.pack('B', header.seq_no)
      )
      pad = hashed = md5(unhashed).digest()
  
      if (len(pad) < body_length):
          # remake hash, appending it to pad until pad >= header.length
          while True:
              hashed = md5(unhashed + hashed).digest()
              pad += hashed
              if len(pad) >= body_length:
                  break
  
      pad = pad[0:(body_length)]
      pad = list(struct.unpack('B' * len(pad), pad))
  
      packet_body = []
      for x in struct.unpack('B' * body_length, body_bytes):
          packet_body.append(x ^ pad.pop(0))
  
      return struct.pack('B' * len(packet_body), *packet_body)