Free app, which encrypt/decrypt sensitive data in database

Hi!

I have build a small demo app, which will store sensitive data encrypted in the database.
This is done by an input field for the users password, which is then used for encrypting the
data. Data is unlocked the same way.

Try it here:
https://MVLGQXTBUHZXYPKA.anvilapp.net

User: test@test.test
Passwd: test

The app can be cloned from here:
https://anvil.works/ide#75PJPFIGTVJW3GVFLDHPITEZ
Note: After cloning, to prevent calling the original “User” database, first remove
the “User services” and then add it again. That should fix ownership problem.

1

Code

Form: Launch

from anvil import *
import anvil.server
import tables
from tables import app_tables
import anvil.users

class Launch (LaunchTemplate):
  def __init__(self, **properties):
    # You must call self.init_components() before doing anything else in this function
    self.init_components(**properties)

    # Start login. Returns None, if they press cancel.
    # We keep the login screen, as long as they have not logged in
    while not anvil.users.login_with_form():
      pass

    # Any code you write here will run when the form opens.
    # Set the user text
    self.user = anvil.server.call('get_user_info', "email")
    self.label_user_email_val.text = self.user

    # Set the dropdown
    #self.dropdown_db_type_val.items = [("postgresql", "postgresql+psycopg2"), ("mysql", "mysql")]
    self.dropdown_db_type_val.items = [("postgresql", "postgresql+psycopg2")]

    # Get the db_info write methods
    self.my_users_db_writable = anvil.server.call('get_users_db_writable')
    # Returns none, if no one is logged in. Then fill empty
    db_info_row = self.my_users_db_writable.get(user=self.user)
    if not db_info_row:
      self.my_users_db_writable.add_row(user=self.user)

    # Initial update the form not to be enabled
    self.enable_change(enabled=False)

  #def textbox_unlock_pressed_enter (self, **event_args):
    # This method is called when the user presses Enter in this text box
  def textbox_unlock_change (self, **event_args):
    # This method is called when the text in this text box is edited
    unlock = anvil.server.call('check_password', self.textbox_unlock.text)
    # Only update if different
    checkbox_unlock_status = self.checkbox_unlock.checked
    if unlock != checkbox_unlock_status:
      self.checkbox_unlock.checked = unlock
      # Make a call to the unlock change
      self.enable_change(enabled=self.checkbox_unlock.checked)

  def enable_change(self, enabled):
    # Enable or disable the boxes
    self.dropdown_db_type_val.enabled = enabled
    self.textbox_db_username_val.enabled = enabled
    self.button_update.enabled = enabled
    # Define values if enabled
    if enabled:
      # Get info
      db_info_row = self.my_users_db_writable.get(user=self.user)
      db_type = anvil.server.call('decode', self.textbox_unlock.text, db_info_row['db_type'])
      db_username = anvil.server.call('decode', self.textbox_unlock.text, db_info_row['db_username'])
    else:
      db_type = self.dropdown_db_type_val.selected_value
      db_username = ""

    # Fill data
    #self.dropdown_db_type_val.selected_value = db_type
    self.set_dropdown_selected_value(self.dropdown_db_type_val, db_type)
    self.textbox_db_username_val.text = db_username

  def set_dropdown_selected_value(self, dropmod, new_item):
    # Make sure that drop down filling is safe
    dropmod_items = getattr(dropmod, 'items')
    dropmod_selected_value = getattr(dropmod, 'selected_value')
    if new_item not in dropmod_items:
      dropmod_selected_value = dropmod_items[0]
  
  def button_update_click (self, **event_args):
    # This method is called when the button is clicked
    # First get the values
    db_type = self.dropdown_db_type_val.selected_value
    db_username = self.textbox_db_username_val.text

    # Write to database. First get row, and then replace
    db_write = self.my_users_db_writable.get(user=self.user)
    db_write["db_type"] = anvil.server.call('encode', self.textbox_unlock.text, db_type)
    db_write['db_username'] = anvil.server.call('encode', self.textbox_unlock.text, db_username)

    # We are done
    Notification("Update complete",title="Update:", style="success").show()

Server: ServerModule1

import tables
from tables import app_tables
import anvil.users
import anvil.server

# Other libraries
import bcrypt
import base64

# Test if PRO version
try:
  import Crypto
  anvil.server.session['Crypto'] = True
except ImportError:
  anvil.server.session['Crypto'] = False
  #print("'Crypto' module is not available. Semi-safe method applied.")


@anvil.server.callable
def get_user_info(row):
  # Returns none, if no one is logged in
  user_row_obj = anvil.users.get_user()
  
  # If logged in:
  if user_row_obj:
    if row in ["email", "password_hash"]:
      user_info = user_row_obj[row]
    else:
      user_info = ""
    return user_info
  
@anvil.server.callable
def get_users_db_writable():
  # Returns none, if no one is logged in
  user_row_obj = anvil.users.get_user()

  # If logged in:
  if user_row_obj:
    return app_tables.users_db.client_writable(owner=user_row_obj)
  
@anvil.server.callable
def check_password(password):
  hashed = get_user_info("password_hash")
  if bcrypt.hashpw(password, hashed) == hashed:
    return True
  else:
    return False

# Encrypt data!
# See answer from "qneill" at: 
# https://stackoverflow.com/questions/2490334/simple-way-to-encode-a-string-according-to-a-password
@anvil.server.callable
def encode(key, clear):
  enc = []
  for i in range(len(clear)):
    key_c = key[i % len(key)]
    enc_c = chr((ord(clear[i]) + ord(key_c)) % 256)
    enc.append(enc_c)
  return base64.urlsafe_b64encode("".join(enc))

@anvil.server.callable
def decode(key, enc):
  if enc == None:
    return enc

  dec = []
  enc = base64.urlsafe_b64decode(enc)
  for i in range(len(enc)):
    key_c = key[i % len(key)]
    dec_c = chr((256 + ord(enc[i]) - ord(key_c)) % 256)
    dec.append(dec_c)
  return "".join(dec)

Hi there,

This is a great thing to be doing, and makes perfect sense for your use case!

A word of caution - for sensitive data, I would recommend against using the Vignere cipher - it’s pretty weak encryption, so unless you’re just trying to prevent yourself accidentally looking at it, I’d recommend something stronger. Even the Crypto module should be used with care - Cryptography is very easy to accidentally misuse and break your security!

I would recommend using something like PyNaCl, which is designed to be difficult to get wrong. Here’s some sample code showing how to encrypt something using a secret key, and some more sample code showing how to securely generate a secret key from a password. Perhaps your suggestion of a tutorial is a good idea :smiley:

Hope that helps!

1 Like