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)