Writeup by sin-infosec for Trappy Skippy

crypto symmetric

November 22, 2023

TLDR

Cryptanalysis of the Skipjack cipher with modified Sbox and key schedule.

Recon

Disclaimer: I didn’t have much prior knowledge on the cryptanalysis of block ciphers, so I will describe how I solved the challenge, but there is some big chance that some claims make no sense theoretically.

For this challenge, we are given the implementation of a cipher inside skippy.py.

Additionally, we get 14 pairs of plaintexts/ciphertexts in skippy.txt and skippy.txt.enc, and the ciphertext of the flag in flag.txt.enc. So most probably the goal is to recover the key that was used to encrypt the 14 plaintexts we have and the flag, to eventually decrypt the flag.

The cipher

First things first, let’s study the cipher. After looking for an hour at the source, I eventually googled “skippy cipher” and found out that this cipher was a slightly modified version of the Skipjack cipher, a symmetric cipher developped by the NSA based on a 32-round unbalanced feistel network with a block size of 64 bits, and with an interesting myth around it as it was initially classified and supposed to be used to secure phone communications. You can read more about the story on wikipedia : https://en.wikipedia.org/wiki/Skipjack_(cipher) and from the 1998 analysis of Schneier : https://www.schneier.com/crypto-gram/archives/1998/0715.html#skip

The declassified specification from the NIST can be found here: https://csrc.nist.gov/csrc/media/projects/cryptographic-algorithm-validation-program/documents/skipjack/skipjack.pdf

After going through the implementation we realize that the cipher is correctly implemented except for two points that differ:

This seems like very small modifications to impact a cipher described as “made greatly to resist all known attacks”.

Let’s have a look at the Sbox in Sage:

from sage.crypto.sbox import SBox
Sbox = [
        0x81, 0x3f, 0xab, 0x3d, 0xa4, 0xb4, 0x31, 0x9e,
        0xba, 0xee, 0x90, 0xec, 0x9f, 0x50, 0x85, 0x62,
        0xb8, 0xde, 0xa2, 0xf4, 0x08, 0x78, 0x0a, 0xc5,
        0xb3, 0x15, 0xa9, 0x27, 0x96, 0xac, 0x33, 0x11,
        0xa0, 0xdc, 0x05, 0x7b, 0xaf, 0xdd, 0xad, 0x7a,
        0x14, 0x9a, 0xb1, 0xaa, 0x29, 0x3b, 0x03, 0x23,
        0x99, 0x82, 0x3c, 0x98, 0x2b, 0x13, 0xa6, 0x21,
        0x2d, 0x63, 0x88, 0x51, 0x10, 0xc7, 0x9d, 0xf5,
        0x4c, 0x58, 0xf3, 0xd5, 0x65, 0x06, 0xc0, 0x91,
        0x5c, 0xd0, 0x76, 0xfa, 0xe6, 0x1a, 0xfc, 0x2a,
        0x5e, 0x6f, 0xd3, 0xf8, 0x6b, 0x97, 0x59, 0x18,
        0xf1, 0x68, 0xc3, 0x6a, 0xe8, 0x2e, 0x4d, 0x1c,
        0xd1, 0x5f, 0x44, 0x47, 0xcc, 0x00, 0xe4, 0xbf,
        0x7e, 0xcf, 0xe9, 0xcd, 0x7f, 0x04, 0x55, 0x89,
        0x7c, 0x40, 0xdb, 0x5a, 0x7d, 0x34, 0x67, 0x2c,
        0xe3, 0x5d, 0x46, 0xe2, 0xfe, 0x02, 0x69, 0x32,
        0x52, 0x87, 0xf7, 0x20, 0x79, 0x1d, 0x4b, 0x1f,
        0x09, 0x8e, 0x39, 0x1b, 0xa8, 0x0e, 0x0f, 0x0c,
        0xae, 0xbe, 0x0b, 0x19, 0x0d, 0x83, 0xb0, 0x26,
        0x48, 0x22, 0xef, 0x12, 0x49, 0x37, 0xf6, 0x92,
        0xb6, 0x94, 0x86, 0x01, 0x25, 0x3e, 0x17, 0x24,
        0xed, 0xb7, 0x60, 0xb5, 0x61, 0x35, 0xc6, 0x2f,
        0xdf, 0x3a, 0x4a, 0x38, 0x53, 0x8a, 0xc4, 0x07,
        0x84, 0xbc, 0x9c, 0x8c, 0xb2, 0x16, 0x80, 0x9b,
        0xbd, 0x71, 0x30, 0x73, 0xca, 0xe1, 0x75, 0x74,
        0xbb, 0xf0, 0x36, 0xea, 0xfd, 0x4e, 0xff, 0x64,
        0x8b, 0x4f, 0x1e, 0xc2, 0x70, 0x56, 0x72, 0x54,
        0xa5, 0xd6, 0xa7, 0xd4, 0x6d, 0xc9, 0x45, 0xf9,
        0xa3, 0xd8, 0xa1, 0xda, 0xd7, 0xeb, 0xe7, 0xd9,
        0x28, 0x41, 0x8d, 0x5b, 0xe0, 0xfb, 0xc8, 0x6e,
        0x95, 0xce, 0x8f, 0x43, 0xd2, 0xcb, 0x77, 0x6c,
        0xb9, 0xf2, 0x93, 0x57, 0xe5, 0xc1, 0x42, 0x66,
    ]

FTABLE = [0xa3, 0xd7, 0x09, 0x83, 0xf8, 0x48, 0xf6, 0xf4,
          0xb3, 0x21, 0x15, 0x78, 0x99, 0xb1, 0xaf, 0xf9,
          0xe7, 0x2d, 0x4d, 0x8a, 0xce, 0x4c, 0xca, 0x2e,
          0x52, 0x95, 0xd9, 0x1e, 0x4e, 0x38, 0x44, 0x28,
          0x0a, 0xdf, 0x02, 0xa0, 0x17, 0xf1, 0x60, 0x68,
          0x12, 0xb7, 0x7a, 0xc3, 0xe9, 0xfa, 0x3d, 0x53,
          0x96, 0x84, 0x6b, 0xba, 0xf2, 0x63, 0x9a, 0x19,
          0x7c, 0xae, 0xe5, 0xf5, 0xf7, 0x16, 0x6a, 0xa2,
          0x39, 0xb6, 0x7b, 0x0f, 0xc1, 0x93, 0x81, 0x1b,
          0xee, 0xb4, 0x1a, 0xea, 0xd0, 0x91, 0x2f, 0xb8,
          0x55, 0xb9, 0xda, 0x85, 0x3f, 0x41, 0xbf, 0xe0,
          0x5a, 0x58, 0x80, 0x5f, 0x66, 0x0b, 0xd8, 0x90,
          0x35, 0xd5, 0xc0, 0xa7, 0x33, 0x06, 0x65, 0x69,
          0x45, 0x00, 0x94, 0x56, 0x6d, 0x98, 0x9b, 0x76,
          0x97, 0xfc, 0xb2, 0xc2, 0xb0, 0xfe, 0xdb, 0x20,
          0xe1, 0xeb, 0xd6, 0xe4, 0xdd, 0x47, 0x4a, 0x1d,
          0x42, 0xed, 0x9e, 0x6e, 0x49, 0x3c, 0xcd, 0x43,
          0x27, 0xd2, 0x07, 0xd4, 0xde, 0xc7, 0x67, 0x18,
          0x89, 0xcb, 0x30, 0x1f, 0x8d, 0xc6, 0x8f, 0xaa,
          0xc8, 0x74, 0xdc, 0xc9, 0x5d, 0x5c, 0x31, 0xa4,
          0x70, 0x88, 0x61, 0x2c, 0x9f, 0x0d, 0x2b, 0x87,
          0x50, 0x82, 0x54, 0x64, 0x26, 0x7d, 0x03, 0x40,
          0x34, 0x4b, 0x1c, 0x73, 0xd1, 0xc4, 0xfd, 0x3b,
          0xcc, 0xfb, 0x7f, 0xab, 0xe6, 0x3e, 0x5b, 0xa5,
          0xad, 0x04, 0x23, 0x9c, 0x14, 0x51, 0x22, 0xf0,
          0x29, 0x79, 0x71, 0x7e, 0xff, 0x8c, 0x0e, 0xe2,
          0x0c, 0xef, 0xbc, 0x72, 0x75, 0x6f, 0x37, 0xa1,
          0xec, 0xd3, 0x8e, 0x62, 0x8b, 0x86, 0x10, 0xe8,
          0x08, 0x77, 0x11, 0xbe, 0x92, 0x4f, 0x24, 0xc5,
          0x32, 0x36, 0x9d, 0xcf, 0xf3, 0xa6, 0xbb, 0xac,
          0x5e, 0x6c, 0xa9, 0x13, 0x57, 0x25, 0xb5, 0xe3,
0xbd, 0xa8, 0x3a, 0x01, 0x05, 0x59, 0x2a, 0x46]

s = SBox(Sbox)
s_original = SBox(FTABLE)

I took both the original and the modified Sbox and tried to find differences in their structure.

Useful ressources:

print(s.fixed_points())
print(s_original.fixed_points())
[]
[]

So this is quite obvious, but there no fixed point in any of them. Let’s check linearity now:

print(s.has_linear_structure())
print(s_original.has_linear_structure())
True
False

Ah! So there is a linear structure in the new Sbox where there wasn’t any in the previous one…

print(s.linear_structures())
print(s_original.linear_structures())
[(5, 2, 0), (5, 24, 0), (5, 26, 0), (5, 40, 0), (5, 42, 0), (5, 48, 0), (5, 50, 0), (5, 141, 0), (5, 143, 0), (5, 149, 0), (5, 151, 0), (5, 165, 0), (5, 167, 0), (5, 189, 0), (5, 191, 0), (57, 2, 0), (57, 24, 0), (57, 26, 0), (57, 40, 0), (57, 42, 0), (57, 48, 0), (57, 50, 0), (57, 141, 0), (57, 143, 0), (57, 149, 0), (57, 151, 0), (57, 165, 0), (57, 167, 0), (57, 189, 0), (57, 191, 0), (60, 2, 0), (60, 24, 0), (60, 26, 0), (60, 40, 0), (60, 42, 0), (60, 48, 0), (60, 50, 0), (60, 141, 0), (60, 143, 0), (60, 149, 0), (60, 151, 0), (60, 165, 0), (60, 167, 0), (60, 189, 0), (60, 191, 0), (64, 2, 0), (64, 24, 0), (64, 26, 0), (64, 40, 0), (64, 42, 0), (64, 48, 0), (64, 50, 0), (64, 141, 0), (64, 143, 0), (64, 149, 0), (64, 151, 0), (64, 165, 0), (64, 167, 0), (64, 189, 0), (64, 191, 0), (69, 2, 0), (69, 24, 0), (69, 26, 0), (69, 40, 0), (69, 42, 0), (69, 48, 0), (69, 50, 0), (69, 141, 0), (69, 143, 0), (69, 149, 0), (69, 151, 0), (69, 165, 0), (69, 167, 0), (69, 189, 0), (69, 191, 0), (121, 2, 0), (121, 24, 0), (121, 26, 0), (121, 40, 0), (121, 42, 0), (121, 48, 0), (121, 50, 0), (121, 141, 0), (121, 143, 0), (121, 149, 0), (121, 151, 0), (121, 165, 0), (121, 167, 0), (121, 189, 0), (121, 191, 0), (124, 2, 0), (124, 24, 0), (124, 26, 0), (124, 40, 0), (124, 42, 0), (124, 48, 0), (124, 50, 0), (124, 141, 0), (124, 143, 0), (124, 149, 0), (124, 151, 0), (124, 165, 0), (124, 167, 0), (124, 189, 0), (124, 191, 0), (129, 2, 0), (129, 24, 0), (129, 26, 0), (129, 40, 0), (129, 42, 0), (129, 48, 0), (129, 50, 0), (129, 141, 0), (129, 143, 0), (129, 149, 0), (129, 151, 0), (129, 165, 0), (129, 167, 0), (129, 189, 0), (129, 191, 0), (132, 2, 0), (132, 24, 0), (132, 26, 0), (132, 40, 0), (132, 42, 0), (132, 48, 0), (132, 50, 0), (132, 141, 0), (132, 143, 0), (132, 149, 0), (132, 151, 0), (132, 165, 0), (132, 167, 0), (132, 189, 0), (132, 191, 0), (184, 2, 0), (184, 24, 0), (184, 26, 0), (184, 40, 0), (184, 42, 0), (184, 48, 0), (184, 50, 0), (184, 141, 0), (184, 143, 0), (184, 149, 0), (184, 151, 0), (184, 165, 0), (184, 167, 0), (184, 189, 0), (184, 191, 0), (189, 2, 0), (189, 24, 0), (189, 26, 0), (189, 40, 0), (189, 42, 0), (189, 48, 0), (189, 50, 0), (189, 141, 0), (189, 143, 0), (189, 149, 0), (189, 151, 0), (189, 165, 0), (189, 167, 0), (189, 189, 0), (189, 191, 0), (193, 2, 0), (193, 24, 0), (193, 26, 0), (193, 40, 0), (193, 42, 0), (193, 48, 0), (193, 50, 0), (193, 141, 0), (193, 143, 0), (193, 149, 0), (193, 151, 0), (193, 165, 0), (193, 167, 0), (193, 189, 0), (193, 191, 0), (196, 2, 0), (196, 24, 0), (196, 26, 0), (196, 40, 0), (196, 42, 0), (196, 48, 0), (196, 50, 0), (196, 141, 0), (196, 143, 0), (196, 149, 0), (196, 151, 0), (196, 165, 0), (196, 167, 0), (196, 189, 0), (196, 191, 0), (248, 2, 0), (248, 24, 0), (248, 26, 0), (248, 40, 0), (248, 42, 0), (248, 48, 0), (248, 50, 0), (248, 141, 0), (248, 143, 0), (248, 149, 0), (248, 151, 0), (248, 165, 0), (248, 167, 0), (248, 189, 0), (248, 191, 0), (253, 2, 0), (253, 24, 0), (253, 26, 0), (253, 40, 0), (253, 42, 0), (253, 48, 0), (253, 50, 0), (253, 141, 0), (253, 143, 0), (253, 149, 0), (253, 151, 0), (253, 165, 0), (253, 167, 0), (253, 189, 0), (253, 191, 0)]
[]

As we saw before, the linear structure array of the original Sbox is empty, but the new one has many of them!

After seeing this, I wrote a small script to check how often the linear approximations hold (as I struggled to understand how to interpret the linear structures printed by Sage). Here is the result:

bit5 ^ bit7 = Sbit0 ^ Sbit7 with probability 0.75
bit5 ^ bit7 = Sbit0 ^ Sbit1 ^ Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5 ^ Sbit7 with probability 0.75
bit2 ^ bit3 ^ bit4 ^ bit7 = Sbit1 ^ Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5   with probability 0.75
bit2 ^ bit3 ^ bit4 ^ bit7 = Sbit0 ^ Sbit1 ^ Sbit2 ^ Sbit3 ^ Sbit4   with probability 0.75
bit2 ^ bit3 ^ bit4 ^ bit5 = Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5   with probability 0.75
bit2 ^ bit3 ^ bit4 ^ bit5 = Sbit0 ^ Sbit7 with probability 0.75
bit1   = Sbit1   with probability 0.75
bit1 ^ bit5 ^ bit7 = Sbit0 ^ Sbit1 ^ Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5 ^ Sbit7 with probability 0.75
bit0 ^ bit5 ^  = Sbit0 ^ Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5 ^ Sbit7 with probability 0.75
bit0 ^ bit2 ^ bit3 ^ bit4  = Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5   with probability 0.75
bit0 ^ bit2 ^ bit3 ^ bit4 ^ bit5 ^ bit7 = Sbit0 ^ Sbit1  Sbit7 with probability 0.75
bit0 ^ bit1 ^ bit7 = Sbit1 with probability 0.75
bit0 ^ bit1 ^ bit7 = Sbit0 ^ Sbit2 ^ Sbit3 ^ Sbit4   with probability 0.75
bit0 ^ bit1 ^ bit5   = Sbit0 ^ Sbit1 ^ Sbit7 with probability 0.75
bit0 ^ bit1 ^ bit2 ^ bit3 ^ bit4   = Sbit1 ^ Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit5   with probability 0.75
bit0 ^ bit1 ^ bit2 ^ bit3 ^ bit4 ^ bit5 ^ bit7 = Sbit2 ^ Sbit3 ^ Sbit4 ^ Sbit7 with probability 0.75

And we have quite a lot of approximations that hold for 75% of the input bytes! That’s definitely too much and it could be used to set up a linear cryptanalysis attack, but we only have 14 pairs, and a lot of Sbox applied over the 32 rounds…

This is where I started reading a lot trying to understand how I could exploit this weak Sbox to recover the key. I tried deriving linear equations, studied the key schedule that could have been weakened by the fact that the two last bytes are the same as the two first ones.

Some useful and very interesting ressources I found along the way are:

But still theoretically I did not find any way to exploit anything with only a dozens of pairs…

So then I thought : “Ok I cannot find any major vulnerability, let’s just try to discover correlations between key bytes and ciphertexts by flipping every bits of the key for 1000 keys and checking what bits of the ciphertext get impacted”.

To avoid pasting hundreds of dirty lines of testing, I’ll go straight to the point:

First I discovered that applying the mask 00000010 to any byte of the key (so doing int(mask,2) ^ key_byte) never impacts bits at index 1,9,17,25,33,41,49,57 in the ciphertext. What this means is that we can now bruteforce one bit less for every byte of key as the ciphertexts bits at these indexes should not be impacted whatever the value of this one bit per key byte is. That is still $2^{56}$ though… But maybe some more masks involving more than one bit flip in the key bytes also have correlations with the ciphertexts!

Eventually by trying them all I found that 16 masks on any key byte do not impact the previous indexes of the ciphertext bits (actually 15 masks and the 0 mask which doesn’t change the key). Here there are:

masks = ['00000000','00000010','00011000','00011010','00101000','00101010','00110000','00110010','10001101','10001111','10010101','10010111','10100101','10100111','10111101','10111111']

So the keys that won’t impact the previous ciphertext bits are any key with bytes equal to the original key bytes xored with one of these masks. That makes 16 valid ones for every byte, and $ 2^{32} \text{ out of the }2^{64}$ possible keys that won’t impact these bits. Can you see it coming? That’s a $P = 2^{-32}$ probability to pick one of these keys by randomly trying one, so after $2^{32}$ we have a non-negligeable probability to find a valid one. If we manage to find one of these keys related by the masks above to the original key, then we can bruteforce all possible masks to recover the original key!

To be sure not to get any false positive, I checked the candidate keys on the 14 pairs of plaintexts/ciphertexts, so that means 14 Skipjack encryptions for every single key trial. That makes roughly:

$$ 2^{32} \times 14 \approx 2^{36} $$

necessary encryptions to find a good key candidate. That was approximated to around 40 hours on my computer using pypy, so I ran the script on 13 different cores over 2 computers to lower the bruteforce time to around 3 hours. This is the script I used:

def check_difference(cts1,cts2):
  indexes = [1,9,17,25,33,41,49,57]
  somme = 0
  ct1 = [int.from_bytes(x, "big") for x in cts1]
  ct2 = [int.from_bytes(x, "big") for x in cts2]
  for i in range(len(cts1)):
    for index in indexes:
      if (ct1[i] >> (63 -index)) & 1 == (ct2[i] >> (63-index)) & 1:
        somme +=1
  return somme/(len(indexes) * len(cts1))


def bruteforce():
    m = open("skippy.txt", "rb").read()
    c1_raw = open("skippy.txt.enc","rb").read()
    c1 = [c1_raw[i:i+8] for i in range(0,len(c1_raw),8)]
    while True:
        key_trial = int(random.getrandbits(64)).to_bytes(8, byteorder="big")
        gourou = Skippy(key_trial)
        c2 = gourou.encrypt(m)
        diff = check_difference(c1,c2)
        if diff == 1:
            return key_trial
    return 0

result = bruteforce()
print("found candidate:")
print(result)
print(result.hex())

And indeed, after around 3 hours I got lucky and got one valid candidate! Then hoping this is not a false positive, all we need to do is bruteforce every mask for every byte until we find the right key! This is how I did it, and ran it on 4 cores:


def brute(k,s,e):
    masks = ['00000000','00000010','00011000','00011010','00101000','00101010','00110000','00110010','10001101','10001111','10010101','10010111','10100101','10100111','10111101','10111111']
    mask_key_list = [int(x,2) for x in masks]
    m = open("skippy.txt", "rb").read()[:8]
    c1 = [open("skippy.txt.enc","rb").read()[:8]]

    for m0 in mask_key_list[s:e]:
        for m1 in mask_key_list:
            for m2 in mask_key_list:
                for m3 in mask_key_list:
                    for m4 in mask_key_list:
                        for m4 in mask_key_list:
                            for m5 in mask_key_list:
                                for m6 in mask_key_list:
                                    for m7 in mask_key_list:
                                        key = bytes([k[0] ^ m0,k[1] ^ m1,k[2] ^ m2,k[3] ^ m3,k[4] ^ m4,k[5] ^ m5,k[6] ^ m6,k[7] ^ m7])
                                        gourou = Skippy(key)
                                        c2 = gourou.encrypt(m)
                                        if c2 == c1:
                                            return key



candidates_hex = ["94c1388f720bf155"]
candidates = [bytes.fromhex(x) for x in candidates_hex]

k = candidates[0]
start = int(sys.argv[1])
end = int(sys.argv[2])


key_final = brute(k,start,end)
print("key recovered:")
print(key_final)
print(key_final.hex())

And after around 30mins we get the key: "94db3aa768236655" and we can finally decrypt the flag! (we just need to write the decryption routine that you can find below)

import os

class Skippy:
    Sbox = [
        0x81, 0x3f, 0xab, 0x3d, 0xa4, 0xb4, 0x31, 0x9e,
        0xba, 0xee, 0x90, 0xec, 0x9f, 0x50, 0x85, 0x62,
        0xb8, 0xde, 0xa2, 0xf4, 0x08, 0x78, 0x0a, 0xc5,
        0xb3, 0x15, 0xa9, 0x27, 0x96, 0xac, 0x33, 0x11,
        0xa0, 0xdc, 0x05, 0x7b, 0xaf, 0xdd, 0xad, 0x7a,
        0x14, 0x9a, 0xb1, 0xaa, 0x29, 0x3b, 0x03, 0x23,
        0x99, 0x82, 0x3c, 0x98, 0x2b, 0x13, 0xa6, 0x21,
        0x2d, 0x63, 0x88, 0x51, 0x10, 0xc7, 0x9d, 0xf5,
        0x4c, 0x58, 0xf3, 0xd5, 0x65, 0x06, 0xc0, 0x91,
        0x5c, 0xd0, 0x76, 0xfa, 0xe6, 0x1a, 0xfc, 0x2a,
        0x5e, 0x6f, 0xd3, 0xf8, 0x6b, 0x97, 0x59, 0x18,
        0xf1, 0x68, 0xc3, 0x6a, 0xe8, 0x2e, 0x4d, 0x1c,
        0xd1, 0x5f, 0x44, 0x47, 0xcc, 0x00, 0xe4, 0xbf,
        0x7e, 0xcf, 0xe9, 0xcd, 0x7f, 0x04, 0x55, 0x89,
        0x7c, 0x40, 0xdb, 0x5a, 0x7d, 0x34, 0x67, 0x2c,
        0xe3, 0x5d, 0x46, 0xe2, 0xfe, 0x02, 0x69, 0x32,
        0x52, 0x87, 0xf7, 0x20, 0x79, 0x1d, 0x4b, 0x1f,
        0x09, 0x8e, 0x39, 0x1b, 0xa8, 0x0e, 0x0f, 0x0c,
        0xae, 0xbe, 0x0b, 0x19, 0x0d, 0x83, 0xb0, 0x26,
        0x48, 0x22, 0xef, 0x12, 0x49, 0x37, 0xf6, 0x92,
        0xb6, 0x94, 0x86, 0x01, 0x25, 0x3e, 0x17, 0x24,
        0xed, 0xb7, 0x60, 0xb5, 0x61, 0x35, 0xc6, 0x2f,
        0xdf, 0x3a, 0x4a, 0x38, 0x53, 0x8a, 0xc4, 0x07,
        0x84, 0xbc, 0x9c, 0x8c, 0xb2, 0x16, 0x80, 0x9b,
        0xbd, 0x71, 0x30, 0x73, 0xca, 0xe1, 0x75, 0x74,
        0xbb, 0xf0, 0x36, 0xea, 0xfd, 0x4e, 0xff, 0x64,
        0x8b, 0x4f, 0x1e, 0xc2, 0x70, 0x56, 0x72, 0x54,
        0xa5, 0xd6, 0xa7, 0xd4, 0x6d, 0xc9, 0x45, 0xf9,
        0xa3, 0xd8, 0xa1, 0xda, 0xd7, 0xeb, 0xe7, 0xd9,
        0x28, 0x41, 0x8d, 0x5b, 0xe0, 0xfb, 0xc8, 0x6e,
        0x95, 0xce, 0x8f, 0x43, 0xd2, 0xcb, 0x77, 0x6c,
        0xb9, 0xf2, 0x93, 0x57, 0xe5, 0xc1, 0x42, 0x66,
    ]

    def __init__(self, key):
        self._key = key + key[:2]

    def _g(self, k, w):
        g1 = w >> 8
        g2 = w & 0xff
        g3 = self.Sbox[g2 ^ self._key[(4 * k + 0) % 10]] ^ g1
        g4 = self.Sbox[g3 ^ self._key[(4 * k + 1) % 10]] ^ g2
        g5 = self.Sbox[g4 ^ self._key[(4 * k + 2) % 10]] ^ g3
        g6 = self.Sbox[g5 ^ self._key[(4 * k + 3) % 10]] ^ g4
        return (g5 << 8) ^ g6


    def g_inv(self,k,w):
        g5 = w >> 8
        g6 = w & 0xff
        g4 = self.Sbox[g5 ^ self._key[(4 * k + 3) % 10]] ^ g6
        g3 = self.Sbox[g4 ^ self._key[(4 * k + 2) % 10]] ^ g5
        g2 = self.Sbox[g3 ^ self._key[(4 * k + 1) % 10]] ^ g4
        g1 = self.Sbox[g2 ^ self._key[(4 * k + 0) % 10]] ^ g3
        return (g1 << 8) ^ g2


    def _skip(self, b):
        w1 = (b[0] << 8) ^ b[1]
        w2 = (b[2] << 8) ^ b[3]
        w3 = (b[4] << 8) ^ b[5]
        w4 = (b[6] << 8) ^ b[7]

        k = 0
        for t in range(2):
            for i in range(8):
                gw1 = self._g(k, w1)
                w1, w2, w3, w4 = gw1 ^ w4 ^ (k + 1), gw1, w2, w3
                k += 1
            for i in range(8):
                gw1 = self._g(k, w1)
                w1, w2, w3, w4 = w4, gw1, w1 ^ w2 ^ (k + 1), w3
                k += 1

        return bytes([
            w1 >> 8, w1 & 0xff,
            w2 >> 8, w2 & 0xff,
            w3 >> 8, w3 & 0xff,
            w4 >> 8, w4 & 0xff,
        ])

    def unskip(self, b):
        w1 = (b[0] << 8) ^ b[1]
        w2 = (b[2] << 8) ^ b[3]
        w3 = (b[4] << 8) ^ b[5]
        w4 = (b[6] << 8) ^ b[7]

        k = 32
        for t in range(2):
            for i in range(8):
                gw2 = self.g_inv(k-1, w2)
                w1, w2, w3, w4 = gw2, gw2^w3^k, w4, w1
                k -= 1
            for i in range(8):
                w1, w2, w3, w4 = self.g_inv(k-1, w2), w3, w4, w1^w2^k
                k -= 1

        return bytes([
            w1 >> 8, w1 & 0xff,
            w2 >> 8, w2 & 0xff,
            w3 >> 8, w3 & 0xff,
            w4 >> 8, w4 & 0xff,
        ])


    def encrypt(self, m):
        if len(m) % 8:
            m += b"\x00" * (8 - len(m) % 8)
        return b"".join(self._skip(m[i:i+8]) for i in range(0, len(m), 8))

    def decrypt(self, c):
        return b"".join(self.unskip(c[i:i+8]) for i in range(0, len(c), 8))

$in