Category: Cryptography
Flag: texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}
Challenge Description
There’s a spy amongst us! We found one of their messages, but can’t seem to crack it. For some reason, they wrote the message down twice.
Analysis
This challenge came as a single ASCII text file, and the important clue was structural rather than binary. The solve log showed two ciphertext sections with matching punctuation, spacing, and line lengths, plus two different flag-like strings at the top. That strongly suggested the same plaintext had been encrypted twice instead of two unrelated messages being present in the file.
To verify that, I compared the two prose blocks line by line and checked whether their word-length patterns matched exactly. The short Python snippet below reads the ciphertext file, splits it into the two blocks, and prints the word lengths for each aligned pair of lines.
# compare_structure.py
from pathlib import Path
text = Path('/home/rei/Downloads/TheImitationGame/ciphertext.txt').read_text().splitlines()
block1 = [l for l in text[2:10] if l]
block2 = [l for l in text[13:21] if l]
print('block1 lines', len(block1), 'block2', len(block2))
for a, b in zip(block1, block2):
wa = a.split()
wb = b.split()
print('line lengths', len(a), len(b), 'words', [len(x) for x in wa], [len(x) for x in wb])
print('same pattern?', [len(x) for x in wa] == [len(x) for x in wb])python compare_structure.pyblock1 lines 7 block2 7
line lengths 79 79 words [4, 4, 2, 5, 3, 3, 4, 1, 6, 2, 4, 6, 4, 4, 3, 3, 1, 3] [4, 4, 2, 5, 3, 3, 4, 1, 6, 2, 4, 6, 4, 4, 3, 3, 1, 3]
same pattern? True
line lengths 54 54 words [4, 3, 7, 4, 3, 4, 2, 2, 4, 2, 9] [4, 3, 7, 4, 3, 4, 2, 2, 4, 2, 9]
same pattern? True
line lengths 54 54 words [4, 3, 7, 6, 4, 4, 3, 5, 2, 7] [4, 3, 7, 6, 4, 4, 3, 5, 2, 7]
same pattern? True
line lengths 83 83 words [2, 3, 4, 5, 4, 4, 3, 2, 3, 5, 7, 8, 8, 2, 9] [2, 3, 4, 5, 4, 4, 3, 2, 3, 5, 7, 8, 8, 2, 9]
same pattern? True
line lengths 29 29 words [4, 4, 6, 3, 8] [4, 4, 6, 3, 8]
same pattern? True
line lengths 79 79 words [4, 3, 5, 10, 5, 2, 4, 5, 3, 6, 5, 2, 4, 3, 4] [4, 3, 5, 10, 5, 2, 4, 5, 3, 6, 5, 2, 4, 3, 4]
same pattern? True
line lengths 17 17 words [1, 4, 10] [1, 4, 10]
same pattern? TrueThat was enough to rule in “same plaintext twice.” A simple monoalphabetic relation between the two ciphertexts did not hold, so the next useful anchor was the known flag prefix texsaw{}. Using the first six letters of the two brace-wrapped ciphertext strings against the known plaintext texsaw, I recovered two short key fragments.
# derive_prefix_key.py
import string
A = {c: i for i, c in enumerate(string.ascii_lowercase)}
flag1 = 'twhsnz'
flag2 = 'brassg'
pt = 'texsaw'
for name, ct in [('k1', flag1), ('k2', flag2)]:
key = ''.join(string.ascii_lowercase[(A[c] - A[p]) % 26] for c, p in zip(ct, pt))
print(name, key)python derive_prefix_key.pyk1 askand
k2 indaskThose fragments were the real giveaway. They look like cyclic slices of the same Vigenere key rather than two unrelated keys, which fits the prompt hint that the message was written down twice. From there, the solve modeled the two ciphertext streams as the same plaintext encrypted with the same repeating key but with different starting offsets into that key. By stripping both blocks down to letters only and comparing the per-position differences modulo a candidate key length of 41, it was possible to recover a recurrence for the full key and test each possible shift until both prefix fragments lined up.
# recover_key.py
from pathlib import Path
import string
A = {c: i for i, c in enumerate(string.ascii_lowercase)}
alpha = string.ascii_lowercase
text = Path('/home/rei/Downloads/TheImitationGame/ciphertext.txt').read_text().splitlines()
s1 = ''.join(ch for l in text[:10] for ch in l.lower() if ch.isalpha())
s2 = ''.join(ch for l in text[11:] for ch in l.lower() if ch.isalpha())
L = 41
D = []
for j in range(L):
vals = {(A[b] - A[a]) % 26 for i, (a, b) in enumerate(zip(s1, s2)) if i % L == j}
print(j, vals)
if len(vals) != 1:
raise SystemExit('not single-valued')
D.append(vals.pop())
print('D letters', ''.join(alpha[x] for x in D))
known = {0: A['a'], 1: A['s'], 2: A['k'], 3: A['a'], 4: A['n'], 5: A['d']}
known2 = 'indask'
for s in range(1, L):
K = [None] * L
K[0] = 0
stack = [0]
ok = True
while stack and ok:
j = stack.pop()
nj = (j + s) % L
val = (K[j] + D[j]) % 26
if K[nj] is None:
K[nj] = val
stack.append(nj)
elif K[nj] != val:
ok = False
if not ok or any(v is None for v in K):
continue
poss = None
for idx, v in known.items():
t = (v - K[idx]) % 26
if poss is None:
poss = t
elif poss != t:
ok = False
break
if not ok:
continue
KK = [(x + poss) % 26 for x in K]
kstr = ''.join(alpha[x] for x in KK)
second = ''.join(alpha[KK[(s + i) % L]] for i in range(6))
print('shift', s, 'key', kstr, 'secondseg', second, 'ok2', second == known2)python recover_key.pyD letters ivtafhsulbthwzhftjcvxqtgkqierhcjlrehwvdyc
shift 38 key askanditshallbegivenyouseekandyeshallfind secondseg indask ok2 TrueWith the key askanditshallbegivenyouseekandyeshallfind and second-block offset 38 recovered, the rest was just a straightforward Vigenere decryption. Decrypting both blocks produced the same plaintext, which confirmed the model completely and exposed the real flag on the first line of each decrypted copy.
# decrypt_blocks.py
from pathlib import Path
import string
alpha = string.ascii_lowercase
A = {c: i for i, c in enumerate(alpha)}
key = 'askanditshallbegivenyouseekandyeshallfind'
shift = 38
def dec_lines(lines, start=0):
out = []
j = 0
L = len(key)
for line in lines:
row = []
for ch in line:
if ch.isalpha():
k = A[key[(start + j) % L]]
row.append(alpha[(A[ch] - k) % 26])
j += 1
else:
row.append(ch)
out.append(''.join(row))
return out
lines = Path('/home/rei/Downloads/TheImitationGame/ciphertext.txt').read_text().splitlines()
p1 = dec_lines(lines[:10], 0)
p2 = dec_lines(lines[11:], shift)
print('---BLOCK1---')
print('\n'.join(p1))
print('---BLOCK2---')
print('\n'.join(p2))python decrypt_blocks.py---BLOCK1---
texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}
they know im here, and its only a matter of time before they find out who i am.
tell the general what the flag is as soon as possible.
tell the general before they find out where im hiding!
if all goes well, i'll meet you at our first meeting location tomorrow at midnight.
make sure you're not followed
p.s. the movie "imitation game" is very good. you should watch it when you can.
- john cairncross
---BLOCK2---
texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}Solution
The solve used two short Python scripts in sequence: one to recover the repeated Vigenere key and the offset between the two ciphertext copies, and one to decrypt both blocks with those recovered values.
# recover_key.py
from pathlib import Path
import string
A = {c: i for i, c in enumerate(string.ascii_lowercase)}
alpha = string.ascii_lowercase
text = Path('/home/rei/Downloads/TheImitationGame/ciphertext.txt').read_text().splitlines()
s1 = ''.join(ch for l in text[:10] for ch in l.lower() if ch.isalpha())
s2 = ''.join(ch for l in text[11:] for ch in l.lower() if ch.isalpha())
L = 41
D = []
for j in range(L):
vals = {(A[b] - A[a]) % 26 for i, (a, b) in enumerate(zip(s1, s2)) if i % L == j}
if len(vals) != 1:
raise SystemExit('not single-valued')
D.append(vals.pop())
known = {0: A['a'], 1: A['s'], 2: A['k'], 3: A['a'], 4: A['n'], 5: A['d']}
known2 = 'indask'
for s in range(1, L):
K = [None] * L
K[0] = 0
stack = [0]
ok = True
while stack and ok:
j = stack.pop()
nj = (j + s) % L
val = (K[j] + D[j]) % 26
if K[nj] is None:
K[nj] = val
stack.append(nj)
elif K[nj] != val:
ok = False
if not ok or any(v is None for v in K):
continue
poss = None
for idx, v in known.items():
t = (v - K[idx]) % 26
if poss is None:
poss = t
elif poss != t:
ok = False
break
if not ok:
continue
KK = [(x + poss) % 26 for x in K]
kstr = ''.join(alpha[x] for x in KK)
second = ''.join(alpha[KK[(s + i) % L]] for i in range(6))
if second == known2:
print('shift', s, 'key', kstr)
breakpython recover_key.pyshift 38 key askanditshallbegivenyouseekandyeshallfind# decrypt_blocks.py
from pathlib import Path
import string
alpha = string.ascii_lowercase
A = {c: i for i, c in enumerate(alpha)}
key = 'askanditshallbegivenyouseekandyeshallfind'
shift = 38
def dec_lines(lines, start=0):
out = []
j = 0
L = len(key)
for line in lines:
row = []
for ch in line:
if ch.isalpha():
k = A[key[(start + j) % L]]
row.append(alpha[(A[ch] - k) % 26])
j += 1
else:
row.append(ch)
out.append(''.join(row))
return out
lines = Path('/home/rei/Downloads/TheImitationGame/ciphertext.txt').read_text().splitlines()
p1 = dec_lines(lines[:10], 0)
p2 = dec_lines(lines[11:], shift)
print('\n'.join(p1))
print()
print('\n'.join(p2))python decrypt_blocks.pytexsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}
they know im here, and its only a matter of time before they find out who i am.
tell the general what the flag is as soon as possible.
tell the general before they find out where im hiding!
if all goes well, i'll meet you at our first meeting location tomorrow at midnight.
make sure you're not followed
p.s. the movie "imitation game" is very good. you should watch it when you can.
- john cairncross
texsaw{luojmfsgmkqltenaemdqlxgtyrfdlzxdmqmxysvdettsxpatcq}