Recognizing and decrypting custom cipher with known plaintext and ciphertext
I figured it out.

keys = [k1, k2, ... , k20]
def encrypt(offset, message):
    ctext = []
    for i in len(message):
        ctext[i] = keys[(offset + i) % len(keys)] ^ message[i]


The "rotation" is caused by the "offset" parameter. The keys are stored in a rotation buffer and xored with plaintext bytes. The keys can be obtained by xoring a plaintext with a ciphertext.