Tags: qiskit crypto 

Rating:

# `kvant.py`
The first function in `kvant.py` is basically the application of the ket notation on a binary number.

```py
def get_state(x):
l = [0] * 2**len(x)
l[int(x, 2)] = 1
return l
```
In computer science terms, it maps a binary number string $b$ of length $n$ (corresponding to a decimal number $x$) to an array of size $2^n$ whose entries are all $0$ except for the $x^ \text{th}$ one, which is $1$. An example:
$$\verb|get_state('01')| = |{01}> = \begin{pmatrix}0\\\\1\\\\0\\\\0\end{pmatrix} = \verb|[0, 1, 0, 0]|$$

The next two functions should be self explanatory:

```py
def str2bin(s):
return ''.join(bin(ord(i))[2:].zfill(8) for i in s)

def bin2str(a):
return ''.join(chr(int(a[i:i+8], 2)) for i in range(0, len(a), 8))
```

## Encoding functions
---

### `encode_1()`

Now comes the interesting part. The following function expects an initial state vector and creates a single qubit quantum circuit. Thus the initial state must be an array of size $2^1$.

```py
def encode_1(initial_state):
qc = QuantumCircuit(1)
qc.initialize(initial_state)
qc.x(0)
qc.save_statevector()
qobj = assemble(qc)
state = sim.run(qobj).result()
return list(state.get_statevector().real.astype(int))
```
It is to be noted that the above circuit performs the Qiskit `.x()` gate ( `NOT` ) on the $0^\text{th}$ qubit of the circuit, returning the resultant state vector. Given our definitions of $|{0}>$ and $|{1}>$, the possible results can be as follows:

$$\verb|NOT| \lvert{0}\rangle = \verb|NOT| \begin{pmatrix} 1\\\\0\end{pmatrix} = \begin{pmatrix} 0\\\\1\end{pmatrix} = \lvert{1}\rangle$$

$$\verb|NOT| \lvert{1}\rangle = \verb|NOT| \begin{pmatrix} 0\\\\1\end{pmatrix} = \begin{pmatrix} 1\\\\0\end{pmatrix} = \lvert{0}\rangle$$

### `encode_2()`

The second encoding function happens to be a 2-qubit circuit, hence expecting a state vector of size $2^2$ as an input.

```py
def encode_2(initial_state):
qc = QuantumCircuit(2)
qc.initialize(initial_state)
qc.cx(0, 1)
qc.save_statevector()
qobj = assemble(qc)
state = sim.run(qobj).result()
return list(state.get_statevector().real.astype(int))
```
This time however, a `.cx()` gate ( `CNOT` ) is being applied to the 2 qubits – the $0^\text{th}$ and $1^\text{st}$ bit being the target and control bits respectively (*damn qiskit for inverting their order*). The `CNOT` gate simply flips the target bit, only if the control bit is set to 1 and does nothing otherwise. Fortunately, mathematics comes to the rescue with a handy matrix definition for the `CNOT` gate:

$$\verb|CNOT| := \begin{pmatrix} 1&0&0&0\\\\0&0&0&1\\\\0&0&1&0\\\\0&1&0&0\\\\\end{pmatrix}$$

Its effect on a 2-qubit state vector $M$ is replicable by swapping the $1^\text{st}$ and $3^\text{rd}$ entries of $M$ *(starting from 0)*, i.e.

$$\verb|CNOT| \begin{pmatrix}a\\\\b\\\\c\\\\d\end{pmatrix} = \begin{pmatrix} 1&0&0&0\\\\0&0&0&1\\\\0&0&1&0\\\\0&1&0&0\\\\\end{pmatrix} \begin{pmatrix}a\\\\b\\\\c\\\\d\end{pmatrix}= \begin{pmatrix}a\\\\d\\\\c\\\\b\end{pmatrix}$$

An example with a prepared state $\lvert{11}$:

$$\verb|CNOT| \lvert{11} = \verb|CNOT| \begin{pmatrix}0\\\\0\\\\0\\\\1\end{pmatrix} = \begin{pmatrix}0\\\\1\\\\0\\\\0\end{pmatrix} = \verb|[0,1,0,0]|$$

### `encode_3()`

The third encoding function is a 3-qubit circuit, hence expecting a state vector of size $2^3$ as an input.

```
def encode_3(initial_state):
qc = QuantumCircuit(3)
qc.initialize(initial_state)
qc.ccx(0, 1, 2)
qc.save_statevector()
qobj = assemble(qc)
state = sim.run(qobj).result()
return list(state.get_statevector().real.astype(int))
```

The last encoding function is a `.ccx()` gate ( `CCNOT` ), also known as the Toffoli gate, which acts on 3 qubits. The `CCNOT` gate is a controlled version of the `CNOT` gate, i.e. it flips the target bit only if **both control bits** are set to 1 and does nothing otherwise. The `CCNOT` gate is defined as follows:

$$\verb|CCNOT| := \begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\\\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\\\\end{pmatrix}$$

Again, it can be seen as swapping the $6^\text{th}$ and $7^{th}$ entries *(starting from 0)* of a 3-qubit state vector $M$ like so:

$$\verb|CCNOT| \begin{pmatrix}a\\\\b\\\\c\\\\d\\\\e\\\\f\\\\g\\\\h\end{pmatrix} = \begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\\\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\\\\end{pmatrix} \begin{pmatrix}a\\\\b\\\\c\\\\d\\\\e\\\\f\\\\g\\\\h\end{pmatrix}= \begin{pmatrix}a\\\\b\\\\c\\\\d\\\\e\\\\f\\\\h\\\\g\end{pmatrix}$$

---

## Encrypting functions

### encrypt_1/2/3()

The `encrypt_1/2/3()` functions are the ones that actually perform the *'encryption'*. The first one iterates over every bit, applying the `encode_1()` to it and concatenating all the entries of the resulting state vectors. The second and third functions do the same, but with the `encode_2()` and `encode_3()` functions over every pairs and triples of bits respectively.

**Note:** After every encrypting function, the length of the encrypted message doubles!

### Dry run using `'a'`
Consider the encryption with the following string – `'a'`

```text
'a' = 97
```

↓ str2bin()
```text
01100001
```

↓ encode_1()

```text
not |0> not |1> not |1> not |0> not |0> not |0> not |0> not |1>
```

↓ computing!
```text
|1> |0> |0> |1> |1> |1> |1> |0>
```

↓ expanding the states!
```text
01 10 10 01 01 01 01 10
```

↓ encode_2() `remember, first bit is target and second bit is control`
```text
cnot |01> cnot |10> cnot |10> ...
```

↓ computing!
```text
|11> |10> |10> ...
```

↓ expanding the states!
```text
0001 0010 0010 0001 0001 0001 0001 0010
```

↓ group every 3 bits *(last one is wrong due to the short length of the message — length of message should be a multiple of 3)*
```text
000 100 100 010 000 100 010 001 000 100 10
```

↓ encode_3()
```text
ccnot |000> ccnot |100> ccnot |100> ...
```

↓ computing!
```text
|000> |100> |100> ...
```

↓ expanding the states!
```text
10000000 00001000 0000100 ...
```

Now that we've understood how the encrpytion occurs, it's just a matter of reversing each function, by doing the right swaps and collapsing the array to the previous, shorter, bit string.

---

## My Solution

```py
def str2bin(s):
return ''.join(bin(ord(i))[2:].zfill(8) for i in s)

def bin2str(a):
return ''.join(chr(int(a[i:i+8], 2)) for i in range(0, len(a), 8))

def decrypt_1(enc_1):
dec_bin = ''
for i in range(0, len(enc_1), 2):
state = [int(x) for x in enc_1[i:i+2]]
dec_bin += str(bin(state.index(0))[2:]) # swap 0 and 1
return dec_bin

def decrypt_2(enc_2):
enc_1 = ''
for i in range(0, len(enc_2), 4):
state = [int(x) for x in enc_2[i:i+4]]
state[1], state[3] = state[3], state[1] # CNOT swap
enc_1 += str(bin(state.index(1))[2:].zfill(2))
return enc_1

def decrypt_3(enc_3):
enc_2 = ''
for i in range(0, len(enc_3), 8):
state = [int(x) for x in enc_3[i:i+8]]
state[3], state[7] = state[7], state[3] # CCNOT swap
enc_2 += str(bin(state.index(1))[2:].zfill(3))
return enc_2

with open('enc.txt', 'r') as f:
enc = str2bin(f.read()) # read file and convert to binary

flag = bin2str(decrypt_1(decrypt_2(decrypt_3(enc)))) # full decrypt and convert to string

print(flag)
```

Flag = `CyberErudites{W3lc0m3_7o_th3_w0rld_0f_Qu4ntum_C0mput1ng!}`