encode = lambda s: list(ord(x) - ord('a') for x in s)
decode = lambda v: ''.join(chr(c + ord('a')) for c in v)
add = lambda v1, v2: [(x + y) % 26 for x, y in zip(v1, v2)]
mul = lambda x, v: [(x * y) % 26 for y in v]
inv = lambda x: next(i for i in range(26) if (x * i) % 26 == 1)
We now encode the ciphertext and the known portion of the plaintext.
c = 'rbqdoobweruuis'
p = 'help'
ec = encode(c)
ec
[17, 1, 16, 3, 14, 14, 1, 22, 4, 17, 20, 20, 8, 18]
ep = encode(p)
ep
[7, 4, 11, 15]
Assuming that a $2 \times 2$ matrix $A$ has been used to encrypt the original plaintext, we look for its inverse $A^{-1}$ such that $A^{-1} C = P$, where $$ C = \begin{bmatrix} 17 & 16 \\ 1 & 3 \end{bmatrix} \quad \text{and} \quad P = \begin{bmatrix} 7 & 11 \\ 4 & 15 \end{bmatrix}. $$ We will first compute $C^{-1}$. To do so, we will use Gaussian elimination on $$ \begin{bmatrix} 17 & 16 & | & 1 & 0 \\ 1 & 3 & | & 0 & 1 \end{bmatrix}. $$
inv(17)
23
mul(23, [17, 16, 1, 0])
[1, 4, 23, 0]
add([1, 4, 23, 0], mul(-4, [0, 1, 23, 25]))
[1, 0, 9, 4]
Using the above computations, we derive $$ \begin{bmatrix} 17 & 16 & | & 1 & 0 \\ 1 & 3 & | & 0 & 1 \end{bmatrix} \sim \begin{bmatrix} 1 & 4 & | & 23 & 0 \\ 1 & 3 & | & 0 & 1 \end{bmatrix} \sim \begin{bmatrix} 1 & 4 & | & 23 & 0 \\ 0 & 1 & | & 23 & 25 \end{bmatrix} \sim \begin{bmatrix} 1 & 0 & | & 9 & 4 \\ 0 & 1 & | & 23 & 25 \end{bmatrix}. $$ We thus have $$ C^{-1} = \begin{bmatrix} 9 & 4 \\ 23 & 25 \end{bmatrix}. $$ We may now compute $A^{-1} = PC^{-1}$.
(7*9 + 11*23) % 26
4
(7*4 + 11*25) % 26
17
(4*9 + 15*23) % 26
17
(4*4 + 15*25) % 26
1
We obtain $$ A^{-1} = \begin{bmatrix} 4 & 17 \\ 17 & 1 \end{bmatrix}. $$ Let us now decrypt the ciphertext.
A = [[4, 17], [17, 1]]
decode((r[0]*ec[i] + r[1]*ec[i+1]) % 26 for i in range(0, len(ec), 2) for r in A)
'helpisontheway'
The index of coincidence tells us the probability that a randomly chosen pair of letters in a string will coincide.
IC = lambda s: sum(f*(f-1) for f in map(lambda c: s.count(c), set(s))) / (len(s) * (len(s) - 1))
Let us first encrypt a short string with the Vigenère cipher.
s = 'ljubljana'
k = 'fri'
from itertools import repeat
sc = decode((a+b) % 26 for a, b in zip(encode(s), (c for kk in repeat(encode(k)) for c in kk)))
sc
'qacgcrfei'
We compute the index of coincidence of the plaintext and the ciphertext.
IC(s)
0.08333333333333333
IC(sc)
0.027777777777777776
We note that the plaintext has a considerably larger index of coincidence.
Next, we try to determine the key length of the Vigenère cipher which was used to obtain the next ciphertext. We use the Kasiski method - we notice that the substrings NKA
and BYI
repeat, so let us find the indices of their occurences.
ss = 'NKASF BBYIY PWZCW TBIYK PFKUF KBJIA NKABY IYPWZ JMJ'.lower().replace(' ', '')
print([i for i in range(len(ss)) if ss[i:i+3] == 'nka'])
print([i for i in range(len(ss)) if ss[i:i+3] == 'byi'])
[0, 30] [6, 33]
We may deduce that the key length divides $\gcd(30-0, 33-6) = 3$. Let us compute the indices of coincidence of the ciphertext and its substrings containing every third letter.
IC(ss)
0.058693244739756366
[IC(ss[i::3]) for i in range(3)]
[0.0761904761904762, 0.13186813186813187, 0.10989010989010989]
We note that the indices of coincidence for the substrings are significantly higher than the value for the entire ciphertext. This supports the suspicion given by the Kasiski method that the key length is $3$.
We may now compare the letter frequencies with those of English (assuming that the plaintext really is in English) to find the key. Here, we just try using the same key as before.
decode((a-b) % 26 for a, b in zip(encode(ss), (c for kk in repeat(encode(k)) for c in kk)))
'itsnotwhatyoulookatthatmattersitswhatyousee'