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.
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.
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)
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