Client-Side Encryption to At Rest Storage and Back (AES 256)

Hello all.

A while back I asked in the Q&A about implementing client-side encryption and at the time there wasn’t an out of the box answer.

With a few hours to sit down and after learning to hack my way through JS to make it do what I want in Anvil, here’s a proof of concept (I’ve also run the code through Snyk, but always check out your own stuff before implementing, of course!)

Step 1:

Create a new asset in your theme and call it clientcrypto.js, then paste in this code:

  if (typeof KEYUTIL === 'undefined') {
    const KEYUTIL = window.KEYUTIL;
  }

  const enc = new TextEncoder();
  
  const base64UrlEncode = (buffer) => {
    const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
    return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
    };
  
  const pemToBuffer = (pemString) => {
    const lines = pemString.split('\n');
    const encoded = lines.slice(1, -1).join('');
    const decoded = window.atob(encoded);
    return new Uint8Array([...decoded].map(char => char.charCodeAt(0)));
    };
  async function getPublicKey(publicPEM) {
    const publicKeyBuffer = pemToBuffer(publicPEM);
    const publicKeyObj = await window.crypto.subtle.importKey(
      'spki',
      publicKeyBuffer,
      {
        name: 'RSA-OAEP',
        hash: 'SHA-256',
        },
      false,
      ['encrypt']
      );
    return publicKeyObj;
    }

  async function encryptData(secretData, publicPEM) {
    const rsaKey = await getPublicKey(publicPEM);
    const encryptedContent = await window.crypto.subtle.encrypt(
      {
        name: 'RSA-OAEP',
        },
      rsaKey,
      enc.encode(secretData)
      );
    const base64EncryptedContent = base64UrlEncode(encryptedContent);
    return base64EncryptedContent;
    }
  
  async function encrypt(data, publicPEM) {
    const encryptedData = await encryptData(data, publicPEM);
    return encryptedData;
    }
  
  const publicPEMPEM = publicPEM; // replace with your PEM-formatted public key
  
  const pem = publicPEMPEM;
  
  console.log(pem);

This script uses the Web Crypto API.

Step 2:

Open up your Native Libraries and add this code:

<script src="_/theme/clientcrypto.js"></script>
<script src="https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js"></script>

Step 3:

Create a server module and bring in this code:

import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.secrets
import anvil.server
import secrets
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.Cipher import AES
import json

@anvil.server.callable
def sessionstart():
  key = RSA.generate(2048)  # Generate a 2048-bit RSA key pair
  public_key = key.publickey()
  publicpem = public_key.export_key()  # Export the public key
  public_pem = publicpem.decode('utf-8')
  private_key = key.export_key()  # Export the private key
  private_key_pem = key.export_key(format='PEM').decode('utf-8')
  anvil.server.session['private_key_pem'] = private_key_pem
  return public_pem

@anvil.server.callable
def passdata(ciphertext):
  private_key_fetch = anvil.server.session['private_key_pem']
  private_key_encode = private_key_fetch.encode('utf-8')
  private_key_import = RSA.import_key(private_key_encode)
  cipher = PKCS1_OAEP.new(private_key_import, hashAlgo=SHA256.new())
  ciphertext_bytes = base64.urlsafe_b64decode(ciphertext.encode('utf-8') + b'=' * (-len(ciphertext) % 4))
  decrypted = cipher.decrypt(ciphertext_bytes)
  decrypted_decode = decrypted.decode('utf-8')
  key = anvil.secrets.get_secret('storagesecret')
  padded_secret_key = key.ljust(32, '\0').encode('utf-8')
  cipher = AES.new(padded_secret_key, AES.MODE_EAX)
  storage_data, tag = cipher.encrypt_and_digest(decrypted_decode.encode())
  storage_data_b64 = base64.b64encode(storage_data).decode()
  tag_b64 = base64.b64encode(tag).decode()
  nonce = cipher.nonce
  nonce_b64 = base64.b64encode(nonce).decode()
  app_tables.storage.add_row(storage_data_b64=storage_data_b64,tag_b64=tag_b64,ref="Test1",nonce=nonce_b64)

@anvil.server.callable
def getdata(reference):
  key = anvil.secrets.get_secret('storagesecret')
  padded_secret_key = key.ljust(32, '\0').encode('utf-8')
  storage_data_b64 = app_tables.storage.get(ref=reference)['storage_data_b64']
  tag_64 = app_tables.storage.get(ref=reference)['tag_b64']
  nonce = app_tables.storage.get(ref=reference)['nonce']
  storage_data_bytes = base64.b64decode(storage_data_b64)
  tag_bytes = base64.b64decode(tag_64)
  nonce_bytes = base64.b64decode(nonce.encode())
  cipher = AES.new(padded_secret_key, AES.MODE_EAX, nonce=nonce_bytes)
  decrypted_data = cipher.decrypt_and_verify(storage_data_bytes, tag_bytes)
  decrypted_str = decrypted_data.decode('utf-8')
  return decrypted_str

Step 4:

Open your default form (Form1) and add a label, a text box, and two more labels. Change the text for label_1 to: “Say something:”

Use this as your client code:

from ._anvil_designer import Form1Template
from anvil import *
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.server
import base64
from anvil.js.window import encrypt
import anvil.js

class Form1(Form1Template):
  def __init__(self, **properties):
  
    self.init_components(**properties)

  def form_show(self, **event_args):
    pass

  def text_box_1_pressed_enter(self,**event_args):
    publicPEM = anvil.server.call('sessionstart')
    data = self.text_box_1.text
    ciphertext = encrypt(data,publicPEM)
    self.label_2.text = ciphertext
    anvil.server.call('passdata',ciphertext)
    reference = "Test1"
    self.label_3.text = anvil.server.call('getdata',reference)
    pass

Make sure your text box has the pressed enter event registered in its properties.

Step 6:

Add the secrets module and create a secret called ‘storagesecret’.

Step 7:

Add a datatable called ‘Storage’.

Add four text columns: storage_data_b64, tag_b64, ref, and nonce

Step 8:

In settings, change your python version to 3.10 (Beta) and add the package ‘pycryptodome’.

Run your app:

When you enter some text in the text box and press enter:

  • Your server creates a new RSA key pair and sends the public key to the client side and stores your private key in the server session.
  • Your client uses the key in the JS script to encrypt your input and show you it in label_2, the sends it to the server (over HTTPS).
  • The server receives the encrypted data, decrypts it with the private key from the session (so you could do some secure data processing, for example), then, in this example, encrypts the data 256 AES for storage at rest using the key stored as an anvil secret.
  • The client then asks for the data back, using the line reference “Test1” to get the right row. (This is POC, so delete your row every you time your run again).
  • The server then retrieves the row, decrypts with the anvil secret, and returns the plain text to the client where it is displayed in label_3.

For completeness, you can add:

anvil.server.call('deletestorage',reference)

to your pressed enter event in the form and

row = app_tables.storage.get(ref=reference)
  row.delete()

to your server code.

With this, there’s a new key pair every session. (You can find out more about Sessions, here.

That’s it!

Hopefully that’s helpful to someone. Certainly is to me - though I want to add the standard caveat: cryptography is much harder to get right than to get wrong, and I’m not saying this is right, just that it proves a concept!

3 Likes