Deep neural networks as a cryptographic engine? — build your AI solution in 30 minutes.

In this tutorial I will show how easy it is to build a cryptographic engine using a deep neural network.

You may ask why would you need that? — there are plenty of open source solutions to be used, yet.

The answer is simple: all of them are public, hence may be cracked (interesting articles here and here) — and secondly, it’s a fun building an AI :)

The strength of having its own engine lies in:

  • this is not public — so to crack it hacker would have to possess all resources
  • a key to the system is actually in your hands as a neural networks weights and model architecture
  • it’s flexible — you may change model architecture whenever you want to

Let’s start!

Note: the below solution is just a baseline, that should be fine-tuned. I used very low dimensions on purpose to let everyone (even with very old PCs) to train and use a model.

Requirements

Data

To train an AI model we need tons of labelled data. Fortunately, we may simply generate a dataset based on some randomness. This will be essential, especially for labels, where we should not allow for duplicates (you will see in a moment).

As for this tutorial, we’re going to build a neural network to encode and decode just a single string. This means a very quick training and inference time along with low memory requirements. Nevertheless, the code may be easily extended to take as an input any kind of string (including web page source codes, word documents or even images) — but you will need a lot of RAM to train such a model.

Let’s go to the code:

# Data configuration
letters = '_' + string.ascii_lowercase
digits = string.digits
RANGE = range(20)

X = []
Y = []

for _ in RANGE:
if _ not in X:
X.append(''.join(random.choice(letters) for i in range(2)))

Y = list(range(1, 500, 24))
Y = Y[:len(X)]

assert len(set(Y)) == len(X)

In the for loop statement we generate a random combination of three characters from string.ascii_lowercase:

‘abcdefghijklmnopqrstuvwxyz’

For example, we can have:

'ab'
'gt'
'tt'

For labels we’re going to generate a range of a unique combination like:

[24, 48, ..., 2400]

This makes that each string from the first loop will have assigned a unique digit combination. This is because we need to make sure to decrypt the digits after all.

Models

There will be two models:

  • for encryption
  • for decryption

This is a model architecture for encryption:

Note: the model for decryption is almost the same, but a reverse. The complete code is avaliable at the bottom of the page.

class Encryptor(nn.Module):
def __init__(self):
super(Encryptor, self).__init__()
self.linn1 = nn.Linear(2, 2000)
self.linn11 = nn.Linear(2000, 600)
self.linn2 = nn.Linear(600, 1200)
self.linn22 = nn.Linear(1200, 2400)
self.linn3 = nn.Linear(2400, 3000)
self.linn33 = nn.Linear(3000, 2600)
self.linn4 = nn.Linear(2600, 2600)

def forward(self, x):
x = self.linn1(x)
x = F.relu(x)
x = self.linn11(x)
x = F.relu(x)
x = self.linn2(x)
x = F.relu(x)
x = self.linn22(x)
x = F.relu(x)
x = self.linn3(x)
x = F.relu(x)
x = self.linn33(x)
x = F.relu(x)
output = self.linn4(x)
return output

As you can see I used a very, very simple architecture of linear layers and relu for activations. For bigger input we should have more layers and neurons in general.

Training and testing encryption

To train and test a model we’re going to use two methods that contain all needed code:

def train(interval, model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)

optimizer.zero_grad()
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
optimizer.step()
if batch_idx % interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))


def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
pred_list = []
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.cross_entropy(output, target)
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
pred_list.append(pred)

test_loss /= len(test_loader.dataset)

print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))

return pred_list

Summary

As you can see building a model prototype is quite simple. Nevertheless, to extend the model for any bigger input you would have to:

  1. enlarge model dimension
  2. have a lot of computing power!

If you are interested in building AI models you can check out my post on how to build MusicAI model that predicts whether a song will be a world-class success. (with python code).

Appendix: complete code for both encryption and decryption model

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR


import random
import string

# Data configuration
letters = '_' + string.ascii_lowercase
digits = string.digits
RANGE = range(20)

X = []
Y = []

for _ in RANGE:
if _ not in X:
X.append(''.join(random.choice(letters) for i in range(2)))

Y = list(range(1, 500, 24))
Y = Y[:len(X)]

assert len(set(Y)) == len(X)


class Encryptor(nn.Module):
def __init__(self):
super(Encryptor, self).__init__()
self.linn1 = nn.Linear(2, 2000)
self.linn11 = nn.Linear(2000, 600)
self.linn2 = nn.Linear(600, 1200)
self.linn22 = nn.Linear(1200, 2400)
self.linn3 = nn.Linear(2400, 3000)
self.linn33 = nn.Linear(3000, 2600)
self.linn4 = nn.Linear(2600, 2600)

def forward(self, x):
x = self.linn1(x)
x = F.relu(x)
x = self.linn11(x)
x = F.relu(x)
x = self.linn2(x)
x = F.relu(x)
x = self.linn22(x)
x = F.relu(x)
x = self.linn3(x)
x = F.relu(x)
x = self.linn33(x)
x = F.relu(x)
x = self.linn4(x)
output = x
return output


class Decryptor(nn.Module):
def __init__(self):
super(Decryptor, self).__init__()
self.linn1 = nn.Linear(1, 10)
self.linn11 = nn.Linear(10, 100)
self.linn2 = nn.Linear(100, 100)
self.linn22 = nn.Linear(100, 1000)
self.linn3 = nn.Linear(1000, 1000)
self.linn4 = nn.Linear(1000, 1000)

def forward(self, x):
x = self.linn1(x)
x = F.relu(x)
x = self.linn11(x)
x = F.relu(x)
x = self.linn2(x)
x = F.relu(x)
x = self.linn22(x)
x = F.relu(x)
x = self.linn3(x)
x = F.relu(x)
x = self.linn4(x)
output = x
return output


def train(interval, model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)

optimizer.zero_grad()
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
optimizer.step()
if batch_idx % interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))


def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
pred_list = []
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.cross_entropy(output, target)
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
pred_list.append(pred)

test_loss /= len(test_loader.dataset)

print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))

return pred_list


class DatasetEncryptor(torch.utils.data.Dataset):

def __init__(self, list_IDs, labels, x):
self.list_IDs = list_IDs
self.labels = labels
self.x = x
self.letters = string.ascii_lowercase

def __len__(self):
return len(self.x)

def __getitem__(self, index):
ID = self.list_IDs[index]
_x = self.x[index]
x = []
for char in _x:
_int = self.letters.find(char)
x.append(_int)

y = self.labels[ID]

return torch.as_tensor(torch.from_numpy(np.array(x)), dtype=torch.float), int(y)


class DatasetDecryptor(torch.utils.data.Dataset):

def __init__(self, list_IDs, labels, x):
self.list_IDs = list_IDs
self.labels = labels
self.x = x
self.letters = string.ascii_lowercase

def __len__(self):
return len(self.x)

def __getitem__(self, index):
ID = self.list_IDs[index]
x = [self.x[index]]
y = self.labels[ID]

return torch.as_tensor(torch.from_numpy(np.array(x)), dtype=torch.float), int(y)


if __name__ == '__main__':

labels_train = {}
for _ in range(18):
labels_train[_] = Y[_]

labels_test = {}
for _ in range(18, 20):
labels_test[_] = Y[_]

X_train = X[:18]
X_test = X[18:]

dataset_train = DatasetEncryptor(list(range(18)), labels_train, X_train)
dataset_test = DatasetEncryptor(list(range(18, 20)), labels_test, X_test)

train_loader = torch.utils.data.DataLoader(dataset_train)
test_loader = torch.utils.data.DataLoader(dataset_test)

for item in train_loader:
print(item)

for item in test_loader:
print(item)

model = Encryptor()
optimizer = optim.Adadelta(model.parameters(), lr=0.3)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

scheduler = StepLR(optimizer, step_size=1, gamma=0.5)
for epoch in range(1, 500):
train(1, model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
scheduler.step()

preds = test(model, device, test_loader)
decryptor_string = X[0]
preds_encryptor = [int(x) for x in preds]

lenght_preds = len(preds_encryptor)
lenght_unqique = len(set(preds_encryptor))

assert lenght_preds == lenght_unqique, 'there are duplicates which will cause problems in decryption'

X_decryptor = preds_encryptor.copy()

labels_decryptor = {}
for _ in range(10):
labels_decryptor[_] = _

labels_decryptor_test = {}
for _ in range(1):
labels_decryptor_test[_] = _

X_decryptor_test = [X_decryptor[0]]

dataset_train_dec = DatasetDecryptor(list(range(10)), labels_decryptor, X_decryptor)
train_loader_dec = torch.utils.data.DataLoader(dataset_train_dec)

dataset_test_dec = DatasetDecryptor(list(range(1)), labels_decryptor_test, X_decryptor_test)
test_loader_dec = torch.utils.data.DataLoader(dataset_test_dec)

for item in train_loader_dec:
print(item)

for item in test_loader_dec:
print(item)

model_dec = Decryptor()
optimizer = optim.Adadelta(model_dec.parameters(), lr=0.3)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

scheduler = StepLR(optimizer, step_size=1, gamma=0.7)
for epoch in range(1, 500):
train(1, model_dec, device, train_loader_dec, optimizer, epoch)
preds = test(model_dec, device, test_loader_dec)
scheduler.step()

print(decryptor_string)
print(X_test[0])

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store