# Case Study: Cryptanalysis of Vigenere Ciper

## Introduction

Vigenere Cipher是一个polyalphabetic cipher，多表替换密码。

## Encryption & Decryption

Vigenere Cipher通过使用多个偏移量向右移位字母来加密。

e.g.

keyword：flamingo

P || t h e r a i n i | n s p a i n s t | a y s m a i n l | y i n t h e p l | a i n
K || f l a m i n g o | f l a m i n g o | f l a m i n g o | f l a m i n g o | f l a
C || y s e d i v t w | s d p m q a y h | f j s y i v t z | d t n f p r v z | f t n


Note：Vigenere Cipher中，虽然同一个明文字母可以被加密成不同的密文字母，但是也有可能一段相同的明文部分，会被加密成相同的密文部分。例如，在rainmainly中的ain都被ing加密成ivt。但也有被加密成不同的密文部分，明文末尾的ain就被加密为ftn

keyword长度问题：

Efficiency (and ease of use) <————– versus ————–> Security.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34  from itertools import cycle from string import ascii_lowercase as alphabet def Vigenere_enc(pt, k): ''' Encryption of Vigenere Cipher. :param str pt: The plaintext to be encrypted. :param str k: The keyword to encrypt the plaintext. :return: The ciphertext after encryption. :rtype: str ''' shifts = cycle(alphabet.index(c) for c in k) ct = '' for c in pt.lower(): if c not in alphabet: continue ct += alphabet[(alphabet.index(c) + next(shifts)) % len(alphabet)] return ct def Vigenere_dec(ct, k): ''' Decryption of Vigenere Cipher. :param str ct: The ciphertext to be decrypted. :param str k: The keyword to decrypt the ciphertext. :return: The plaintext after decryption. :rtype: str ''' shifts = cycle(alphabet.index(c) for c in k) pt = '' for c in ct.lower(): if c not in alphabet: continue pt += alphabet[(alphabet.index(c) - next(shifts)) % len(alphabet)] return pt 

## Cryptanalysis of the Vigenere Cipher: Theory

### Determine the Key Length

• Kasiski Test
• The Index of Coincidence Test

#### Kasiski Test

Note：由于birthday paradox，在密文序列中寻找到重复片段的概率实际上要比我们想象的要高很多。

#### The Index of Coincidence Test

e.g.

Definition. Let $s = c_1c_2c_3…c_n$ be a string of $n$ alphabetic characters. The index of coincidence of $s$, denoted by $\text{IndCo}(s)$, is the probability that two randomly chosen characters in the string $s$ are identical.

$\text{IndCo}(s)$越大：越像是被某种简单替换加密的英文文本；

$\text{IndCo}(s)$越小：越像是随机字符串。

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  from string import ascii_lowercase as alphabet def IndCo(s): ''' Index of Coincidence. :param str s: The Substring. :return: The index of coincidence of the substring. :rtype: float ''' N = len(s) frequency = [s.count(c) for c in alphabet] return sum(i**2 - i for i in frequency) / (N**2 - N) def CalKeyLength(s): ''' Calculate the probable key lengths using the index of coincidence method. :param str s: The character string to be analysed. :return: All the probable key lengths. :rtype: list ''' res = [] for kl in range(2, 100): # Key length range can be adjusted accordingly. subs = [s[i::kl] for i in range(kl)] # Group into substrings. if sum(IndCo(si) for si in subs) / kl > 0.06: if all(map(lambda x: kl % x, res)): # Avoid multiples. res.append(kl) return res 

### Recover keyword

#### Trivial Method

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  def RecoverKeyword_1(ct, kl): ''' Recover the keyword according to the most frequent letters. :param str ct: The ciphertext. :param int kl: The key length. :return: The recovered keyword. :rtype: str ''' keyword = '' subs = [ct[i::kl] for i in range(kl)] for s in subs: frequency = [s.count(c) for c in alphabet] most_fqc = max(frequency) keyword += alphabet[(frequency.index(most_fqc) - 4) % len(alphabet)] return keyword 

#### Nontrivial Method

Chi-squared Statistic是一个用来衡量两类概率分布的相似程度的统计方法。如果这两类分布完全相同，那么chi-squared statistic为0，如果这两类分布十分不同，那么就会得到比较高的结果。

Chi-squared Statistic定义如下： $$\chi^2(C, E) = \sum_{i=0}^{i=25} \frac{(C_i - E_i)^2}{E_i}$$ 其中 $C_i$ 是数字 $i$ 对应字母的出现个数，$E_i$ 是对应字母期望的出现个数。

e.g.

aoljhlzhyjpwolypzvulvmaollhysplzaruvduhukzptwslzajpwoly
zpapzhafwlvmzbizapabapvujpwolypudopjolhjoslaalypuaolwsh
pualeapzzopmalkhjlyahpuubtilyvmwshjlzkvduaolhswohila


qebzxbpxozfmebofplkblcqebbxoifbpqhkltkxkapfjmibpqzfmebo
pfqfpxqvmblcprypqfqrqflkzfmebofktefzebxzeibqqbofkqebmix
fkqbuqfppefcqbaxzboqxfkkrjybolcmixzbpaltkqebximexybq


shift     plaintext               chi-squared
-----------------------------------------------------
0  aoljhlzhyjpwolypzvul...     1631.14
1  znkigkygxiovnkxoyutk...     3435.58
2  ymjhfjxfwhnumjwnxtsj...     2970.75
3  xligeiwevgmtlivmwsri...     1546.41
4  wkhfdhvduflskhulvrqh...     1194.14
5  vjgecguctekrjgtkuqpg...     1461.07
6  uifdbftbsdjqifsjtpof...     1818.22
7  thecaesarcipherisone...       30.71
8  sgdbzdrzqbhogdqhrnmd...     1759.97
9  rfcaycqypagnfcpgqmlc...     1383.66
10  qebzxbpxozfmebofplkb...     3557.93
11  pdaywaownyeldaneokja...      808.44
12  oczxvznvmxdkczmdnjiz...     4641.41
13  nbywuymulwcjbylcmihy...      760.08
14  maxvtxltkvbiaxkblhgx...     2237.19
15  lzwuswksjuahzwjakgfw...     1781.72
16  kyvtrvjritzgyvizjfev...     3521.91
17  jxusquiqhsyfxuhyiedu...     2972.49
18  iwtrpthpgrxewtgxhdct...     1364.30
19  hvsqosgofqwdvsfwgcbs...      958.50
20  gurpnrfnepvcurevfbar...      469.77
21  ftqomqemdoubtqdueazq...     4408.58
22  espnlpdlcntaspctdzyp...      697.88
23  dromkockbmszrobscyxo...     1239.69
24  cqnljnbjalryqnarbxwn...     1853.82
25  bpmkimaizkqxpmzqawvm...     3027.01


  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62  from string import ascii_lowercase as alphabet LETTER_FREQUENCY = { # From https://en.wikipedia.org/wiki/Letter_frequency. 'e': 0.12702, 't': 0.09056, 'a': 0.08167, 'o': 0.07507, 'i': 0.06966, 'n': 0.06749, 's': 0.06327, 'h': 0.06094, 'r': 0.05987, 'd': 0.04253, 'l': 0.04025, 'c': 0.02782, 'u': 0.02758, 'm': 0.02406, 'w': 0.02360, 'f': 0.02228, 'g': 0.02015, 'y': 0.01974, 'p': 0.01929, 'b': 0.01492, 'v': 0.00978, 'k': 0.00772, 'j': 0.00153, 'x': 0.00150, 'q': 0.00095, 'z': 0.00074 } def ChiSquared(s): ''' Calculate the Chi-squared Statistic. :param str s: The string to be analysed. :return: The Chi-squared Statistic of the string. :rtype: float ''' f = lambda c: LETTER_FREQUENCY[c] * len(s) return sum((s.count(c) - f(c))**2 / f(c) for c in alphabet) def RecoverKeyword_2(ct, kl): ''' Recover the keyword according to the Chi-squared Statistic. :param str ct: The ciphertext. :param int kl: The key length. :return: The recovered keyword. :rtype: str ''' keyword = '' subs = [ct[i::kl] for i in range(kl)] for s in subs: chi_squareds = [] for shift in range(len(alphabet)): # Try all possible shifts. shifted_s = ''.join(\ alphabet[(alphabet.index(c) - shift) % len(alphabet)]\ for c in s) chi_squareds.append((shift, ChiSquared(shifted_s))) keyword += alphabet[min(chi_squareds, key=lambda x: x[1])[0]] return keyword 

## Cryptanalysis of the Vigenere Cipher: Practice

• 2019 西湖论剑线下赛 VVVV
• 2019 De1CTF xorz

### VVVV

encrypt.py文件主要内容如下：

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  # ... cton = lambda x: ord(x) - ord('a') ntoc = lambda x: chr(x + ord('a')) ret = "" for k, v in enumerate(content): v = string.lower(v) if v in "{}":ret += v if not v in string.ascii_lowercase: continue s = secret[k % len(secret)] ret += ntoc( (cton(v) + cton(s)) % 26) open("cipher", "w").write(ret) 

cipher文件中有大概25W+个字母，在文件的结尾能看到：

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  from string import ascii_lowercase as alphabet def IndCo(s): N = len(s) frequency = [s.count(c) for c in alphabet] return sum(i**2 - i for i in frequency) / (N**2 - N) def CalKeyLength(s): res = [] for kl in range(2, 100): subs = [s[i::kl] for i in range(kl)] if sum(IndCo(si) for si in subs) / kl > 0.06: if all(map(lambda x: kl % x, res)): res.append(kl) return res with open('cipher', 'r') as f: cipher = f.read() print(CalKeyLength(cipher)) # output: [21] 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  # continue... keyLength = 21 def RecoverKeyword_1(ct, kl): keyword = '' subs = [ct[i::kl] for i in range(kl)] for s in subs: frequency = [s.count(c) for c in alphabet] most_fqc = max(frequency) keyword += alphabet[(frequency.index(most_fqc) - 4) % len(alphabet)] return keyword print(RecoverKeyword_1(cipher, keyLength)) # output: fnxtldpznxpzprdlppdzn 

Chi-squared Statistic，也能得到同样的结果，只不过慢了一点。

Note：cipher末尾有'{''}'，用Encryption & Decryption里的解密代码会有问题，需要进行一些修改才行。

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  # continue... from itertools import cycle keyword = 'fnxtldpznxpzprdlppdzn' def Vigenere_dec(ct, k): shifts = cycle(alphabet.index(c) for c in k) pt = '' for c in ct.lower(): if c not in alphabet: next(shifts) pt += c continue pt += alphabet[(alphabet.index(c) - next(shifts)) % len(alphabet)] return pt plain = Vigenere_dec(cipher, keyword) print(plain[-50:]) # output: hewouldnotlethiminthedoorflag{thisistheflagtakeit} 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52  from itertools import cycle from string import ascii_lowercase as alphabet def IndCo(s): N = len(s) frequency = [s.count(c) for c in alphabet] return sum(i**2 - i for i in frequency) / (N**2 - N) def CalKeyLength(s): res = [] for kl in range(2, 100): subs = [s[i::kl] for i in range(kl)] if sum(IndCo(si) for si in subs) / kl > 0.06: if all(map(lambda x: kl % x, res)): res.append(kl) return res def RecoverKeyword_1(ct, kl): keyword = '' subs = [ct[i::kl] for i in range(kl)] for s in subs: frequency = [s.count(c) for c in alphabet] most_fqc = max(frequency) keyword += alphabet[(frequency.index(most_fqc) - 4) % len(alphabet)] return keyword def Vigenere_dec(ct, k): shifts = cycle(alphabet.index(c) for c in k) pt = '' for c in ct.lower(): if c not in alphabet: next(shifts) pt += c continue pt += alphabet[(alphabet.index(c) - next(shifts)) % len(alphabet)] return pt def main(): with open('cipher', 'r') as f: cipher = f.read() keyLengths = CalKeyLength(cipher) print(f"All the probable key lengths: {keyLengths}") for kl in keyLengths: keyword = RecoverKeyword_1(cipher, kl) print(f"keyword: {keyword}") plain = Vigenere_dec(cipher, keyword) print(plain[-50:]) if __name__ == '__main__': main() 

### xorz

crypto_xorz.py文件内容如下：

  1 2 3 4 5 6 7 8 9 10 11 12  from itertools import * from data import flag,plain key=flag.strip("de1ctf{").strip("}") assert(len(key)<38) salt="WeAreDe1taTeam" ki=cycle(key) si=cycle(salt) cipher = ''.join([hex(ord(p) ^ ord(next(ki)) ^ ord(next(si)))[2:].zfill(2) for p in plain]) print cipher # output: # 49380d773440222d1b421b3060380c3f403c3844791b202651306721135b6229294a3c3222357e766b2f15561b35305e3c3b670e49382c295c6c170553577d3a2b791470406318315d753f03637f2b614a4f2e1c4f21027e227a4122757b446037786a7b0e37635024246d60136f7802543e4d36265c3e035a725c6322700d626b345d1d6464283a016f35714d434124281b607d315f66212d671428026a4f4f79657e34153f3467097e4e135f187a21767f02125b375563517a3742597b6c394e78742c4a725069606576777c314429264f6e330d7530453f22537f5e3034560d22146831456b1b72725f30676d0d5c71617d48753e26667e2f7a334c731c22630a242c7140457a42324629064441036c7e646208630e745531436b7c51743a36674c4f352a5575407b767a5c747176016c0676386e403a2b42356a727a04662b4446375f36265f3f124b724c6e346544706277641025063420016629225b43432428036f29341a2338627c47650b264c477c653a67043e6766152a485c7f33617264780656537e5468143f305f4537722352303c3d4379043d69797e6f3922527b24536e310d653d4c33696c635474637d0326516f745e610d773340306621105a7361654e3e392970687c2e335f3015677d4b3a724a4659767c2f5b7c16055a126820306c14315d6b59224a27311f747f336f4d5974321a22507b22705a226c6d446a37375761423a2b5c29247163046d7e47032244377508300751727126326f117f7a38670c2b23203d4f27046a5c5e1532601126292f577776606f0c6d0126474b2a73737a41316362146e581d7c1228717664091c 

 1 2 3 4 5 6 7  from itertools import * salt="WeAreDe1taTeam" si=cycle(salt) cipher = bytes.fromhex('49380d773440222d1b421b3060380c3f403c3844791b202651306721135b6229294a3c3222357e766b2f15561b35305e3c3b670e49382c295c6c170553577d3a2b791470406318315d753f03637f2b614a4f2e1c4f21027e227a4122757b446037786a7b0e37635024246d60136f7802543e4d36265c3e035a725c6322700d626b345d1d6464283a016f35714d434124281b607d315f66212d671428026a4f4f79657e34153f3467097e4e135f187a21767f02125b375563517a3742597b6c394e78742c4a725069606576777c314429264f6e330d7530453f22537f5e3034560d22146831456b1b72725f30676d0d5c71617d48753e26667e2f7a334c731c22630a242c7140457a42324629064441036c7e646208630e745531436b7c51743a36674c4f352a5575407b767a5c747176016c0676386e403a2b42356a727a04662b4446375f36265f3f124b724c6e346544706277641025063420016629225b43432428036f29341a2338627c47650b264c477c653a67043e6766152a485c7f33617264780656537e5468143f305f4537722352303c3d4379043d69797e6f3922527b24536e310d653d4c33696c635474637d0326516f745e610d773340306621105a7361654e3e392970687c2e335f3015677d4b3a724a4659767c2f5b7c16055a126820306c14315d6b59224a27311f747f336f4d5974321a22507b22705a226c6d446a37375761423a2b5c29247163046d7e47032244377508300751727126326f117f7a38670c2b23203d4f27046a5c5e1532601126292f577776606f0c6d0126474b2a73737a41316362146e581d7c1228717664091c') cipher = b''.join(bytes([c ^ ord(next(si))]) for c in cipher) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  alphabet = list(range(256)) def IndCo(s): N = len(s) frequency = [s.count(c) for c in alphabet] return sum(i**2 - i for i in frequency) / (N**2 - N) def CalKeyLength(s): res = [] for kl in range(2, 38): subs = [s[i::kl] for i in range(kl)] if sum(IndCo(si) for si in subs) / kl > 0.06: if all(map(lambda x: kl % x, res)): res.append(kl) return res print(CalKeyLength(cipher)) # output: [30] 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35  kl = 30 subs = [cipher[i::kl] for i in range(kl)] for s in subs: print(s) # output: # b'\x1e#6$.2:90$9?w292??%#' # b'][\x13\x13\x13DZV\x13GE\x13]\x13T\x13V\x13\x13\x13' # b'LL\x18\x01\x08L\x02L\x18\t\x05\x18\x03\x03L\x00\r\x18%\x01' # b'\x05\x0e\x0b\x1a\x06\n\x06\x07\x0cO\x17\x0b\x11\r\x17\n\x11\x0cC\x02' # b'QY_\x10CC\x10U\x10\x10UU\x10UX[D\x10S[' # b'\x04\x03\x18\x05\x1dM\x08\x01\x0f\x03\t\x08\x00M\x08\x08\r\x0f\x02\x08' # b'GV@VZCRZR\\g\x13JUV]@VF@' # b'\x1cT\x15\x15\x07\x18\x06\x13\x07\x06\x1b\x15T\x1bX\x11TZ\x1aT' # b'o*!=**<\'*oo#) \x18<<\x00;"' # b'#\x13\x0e\x1eF\x0bJ\x1eJ\x19\x0b\x05\x03\x06\x02\x19\x06\x04J\x0f' # b'O\nOO8\x1c\x18\n\x1b\x02\x01\x01\x19\x06\x00O\x0e\x03\x02O' # b'UBTEYTXU^THTTB\x11^GHHB' # b'\x01B\x1c\x06\x01\n\x1aU\x1b\x02N@N\x06\x02\x08\x0bNN\x07' # b'U3\x07\x14UU\x1d;\x16\x19\x067\x06U\x10UU\x18\x12\x1b' # b'[ZZA\\A\x15Z]\x19P@P]TTTLT\x15' # b'ZGG\x15[ZAGP\x15[A[PC\x15[\x15\\T' # b'\x01U\x06\x19UU\x1dU\x06\x11\x06U\x06\x14\x10\x18\x11\x05\x1b\x02' # b'N\x1aN\x01\n\n\x17\x1aN\x0b\x1b\x03\x0b\x1c\x1d\x0fN\x02B\x0f' # b']Y_GT^\x11TABPHBE\x11_GPeC' # b'\x00\n\x00\n\x1c\x1b\x1b\x01\x1d\x06\x03OOO\x1aC\x0e\x08\x07\x0b' # b'\x1c\x13\x1e\x19\x1a\x0f\x05\x0e\x05\x18J\x0c\t\x0c\x04>\x19\x1f\x0b\x19' # b"*o*o&a!*!*)&.=<'<*;o" # b'T\x1dO\x03\x00:\x13\x06\x11T\x11\x02\x1a\x1b\x03\r\x15TT\x19' # b'G]q[V\\F\x13\x1fGRVw^R\x13_G@V' # b'\x05M\x18\x0cM\x1f\x08\x0b#\x02\x1eM\x04M\x14\x1dM\x05\x05M' # b'UDDD_\x10PU_\x10DGCCUBGEU@' # b'\x06\x0bCC\x05\x02\x10\x06\x11\x01C\n\x10\x06\x07\x0c\x11\x10C\x02' # b'L\t\x0c\x18L\x1eL\x00L\t\x1b\x18\x19\x1eL\x19\tL\x18\x05' # b'DVG[EVGZG\x13Z@REGWGU[]' # b'>w>2>w"96>#{3>?w466y' 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29  # continue... LETTER_FREQUENCY = {...} def score(s): score = 0 for c in s.lower(): if c in LETTER_FREQUENCY: score += LETTER_FREQUENCY[c] return score def RecoverKey(ct, kl): key = b'' subs = [ct[i::kl] for i in range(kl)] for s in subs: scores = [] for xor in range(256): # Try all possible xor values. xored_s = ''.join(chr(c ^ xor) for c in s) scores.append((xor, score(xored_s))) key += bytes([max(scores, key=lambda x: x[1])[0]]) return key kl = 30 key = RecoverKey(cipher, kl) print(key) print(Vigenere_dec(cipher, key)) # output: # b'Wvlc0m3tOjo1nu55un1ojOt3q0cl3W' # I+ faith I do not love ttee wit- mine eyes,For they in

In faith I do not love thee with mine eyes,For they in thee a thousand errors note;But tis my heart that loves what they despise,Who in despite of view is pleased to dote.Nor are mine ears with thy tongues tune delighted;Nor tender feeling to base touches prone,Nor taste, nor smell, desire to be invitedTo any sensual feast with thee alone.But my five wits, nor my five senses canDissuade one foolish heart from serving thee,Who leaves unswayed the likeness of a man,Thy proud hearts slave and vassal wretch to be.Only my plague thus far I count my gain,That she that makes me sin awards me pain.


  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92  from itertools import * from string import printable alphabet = list(range(256)) LETTER_FREQUENCY = { ' ': 0.25000, 'e': 0.12702, 't': 0.09056, 'a': 0.08167, 'o': 0.07507, 'i': 0.06966, 'n': 0.06749, 's': 0.06327, 'h': 0.06094, 'r': 0.05987, 'd': 0.04253, 'l': 0.04025, 'c': 0.02782, 'u': 0.02758, 'm': 0.02406, 'w': 0.02360, 'f': 0.02228, 'g': 0.02015, 'y': 0.01974, 'p': 0.01929, 'b': 0.01492, 'v': 0.00978, 'k': 0.00772, 'j': 0.00153, 'x': 0.00150, 'q': 0.00095, 'z': 0.00074 } def IndCo(s): N = len(s) frequency = [s.count(c) for c in alphabet] return sum(i**2 - i for i in frequency) / (N**2 - N) def CalKeyLength(s): res = [] for kl in range(2, 38): subs = [s[i::kl] for i in range(kl)] if sum(IndCo(si) for si in subs) / kl > 0.06: if all(map(lambda x: kl % x, res)): res.append(kl) return res def score(s): score = 0 for c in s.lower(): if c in LETTER_FREQUENCY: score += LETTER_FREQUENCY[c] return score def RecoverKey(ct, kl): key = b'' subs = [ct[i::kl] for i in range(kl)] for s in subs: scores = [] for xor in range(256): xored_s = ''.join(chr(c ^ xor) for c in s) if all(c in printable for c in xored_s): scores.append((xor, score(xored_s))) key += bytes([max(scores, key=lambda x: x[1])[0]]) return key def Vigenere_dec(cipher, key): keyCircle = cycle(key) pt = '' for c in cipher: pt += chr(c ^ next(keyCircle)) return pt def main(): salt="WeAreDe1taTeam" si=cycle(salt) cipher = bytes.fromhex('49380d773440222d1b421b3060380c3f403c3844791b202651306721135b6229294a3c3222357e766b2f15561b35305e3c3b670e49382c295c6c170553577d3a2b791470406318315d753f03637f2b614a4f2e1c4f21027e227a4122757b446037786a7b0e37635024246d60136f7802543e4d36265c3e035a725c6322700d626b345d1d6464283a016f35714d434124281b607d315f66212d671428026a4f4f79657e34153f3467097e4e135f187a21767f02125b375563517a3742597b6c394e78742c4a725069606576777c314429264f6e330d7530453f22537f5e3034560d22146831456b1b72725f30676d0d5c71617d48753e26667e2f7a334c731c22630a242c7140457a42324629064441036c7e646208630e745531436b7c51743a36674c4f352a5575407b767a5c747176016c0676386e403a2b42356a727a04662b4446375f36265f3f124b724c6e346544706277641025063420016629225b43432428036f29341a2338627c47650b264c477c653a67043e6766152a485c7f33617264780656537e5468143f305f4537722352303c3d4379043d69797e6f3922527b24536e310d653d4c33696c635474637d0326516f745e610d773340306621105a7361654e3e392970687c2e335f3015677d4b3a724a4659767c2f5b7c16055a126820306c14315d6b59224a27311f747f336f4d5974321a22507b22705a226c6d446a37375761423a2b5c29247163046d7e47032244377508300751727126326f117f7a38670c2b23203d4f27046a5c5e1532601126292f577776606f0c6d0126474b2a73737a41316362146e581d7c1228717664091c') cipher = b''.join(bytes([c ^ ord(next(si))]) for c in cipher) kls = CalKeyLength(cipher) print(f"All probable key length: {kls}") for kl in kls: key = RecoverKey(cipher, kl) print(f"Key: {key}") print(Vigenere_dec(cipher, key)) if __name__ == '__main__': main() 

flag：de1ctf{W3lc0m3tOjo1nu55un1ojOt3m0cl3W}