Encryption and decryption in Python Kivy for Android
Just recently I developed an Android app using Python
and Kivy
, which required some tight security measures because it involved sensitive patient data. Moreover, the app allowed users to make requests to a server, for which a token was needed. Since I couldn't find any tutorial explaining encryption and decription for the Android OS
based on Python Kivy
, I decided to write a short tutorial on how to implement encryption and decryption on an Android
device using Python Kivy
.
Here is what you will learn in this tutorial:
- Generate an AES key and store the key in the Android Keystore
- Encrypt a string that will be stored in a config file
- Decrypt the stored string
Basic remarks
First of all, I strongly advice you to carefully read about encryption methods to get a sound understanding of how encryption works. It's one of the areas in programming, in which you as a developer are supposed to really know what you are doing. In addition to this, the following tutorial just provides a basic overview on how to implement encryption on an Android device using Python kivy and does not include security measures other than encryption and decryption.
Moreover, you should keep in mind that there are different encryption methods such as symmetric or asymmetric encryption methods. In the following tutorial I will show how to implement a symmetric encryption method. Also, be aware that there is no perfect solution and that there are always trade-offs between security and practicability. Let's go!
Generate a key
Before we can encrypt and decrypt data we need to generate a key that will be used to encrypt and decrypt the data. Within the context of symmetric encryption we use the same key for both encryption and decryption. Here, one of the most critical questions is, where do we store the key? Depending on the operating system there are different locations feasible for key storage (Windows Data Protection API (DPAPI)
on Windows, Keychain service
on Mac or GNOME Keyring
on Linux). Don't store the key in a plain file or as environment variable, which is a common mistake.
On Android devices we can use the Android Keystore
, which is a special security container that can be only accessed by the app. For detailed information on how the Android Keystore reduces the security risk, check out the Android documentation.
First, we write a function that generates a key and stores the key in the Android Keystore. Note that I use pyjnius
to access the java
classes.
def generate_android_key(alias="my_key_alias"):
'''
Generates key for configuration file when user logs
in for first time
'''
KeyProperties = autoclass('android.security.keystore.KeyProperties')
KeyGenerator = autoclass('javax.crypto.KeyGenerator')
Builder = autoclass('android.security.keystore.KeyGenParameterSpec$Builder')
key_gen_spec = Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM) \
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) \
.setKeySize(256) \
.build()
key_generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, 'AndroidKeyStore')
key_generator.init(key_gen_spec)
key_generator.generateKey()
print("Key generated in KeyStore.")
In order to generate a key I used the Android key generator
, which allows us to specify the key properties such as the key size, the block mode as well as the padding settings. Make sure that the key size and the other properties are compatible with the requirements of the Android keystore.
Function to encrypt a string
After the key is generated we can access the key in the Android Keystore
and encrypt data. The following steps are executed by the following function and consists of four high level processes: (1) define the java classes, (2) get the key from the keystore, (3) check whether the key exists, (4) concatenate the encrypted string and the cipher parameters.
def encrypt_string(plain_string, alias="my_key_alias"):
Cipher = autoclass('javax.crypto.Cipher')
KeyStore = autoclass('java.security.KeyStore')
Base64 = autoclass('android.util.Base64')
SecretKeyEntry = autoclass('java.security.KeyStore$SecretKeyEntry')
keystore = KeyStore.getInstance('AndroidKeyStore')
keystore.load(None)
entry = keystore.getEntry(alias, None)
if not isinstance(entry, SecretKeyEntry):
print(f"Error: The key with alias '{alias}' is not a SecretKeyEntry.")
print(f"Actual type of the entry: {entry.getClass().getName()}")
return None
# Try retrieving the SecretKey directly from the SecretKeyEntry
secret_key = None
try:
secret_key_entry = cast('java.security.KeyStore$SecretKeyEntry', entry)
secret_key = secret_key_entry.getSecretKey()
except Exception as e:
print(f"Error retrieving the secret key: {e}")
return None
if not secret_key:
print("Failed to retrieve the secret key!")
return None
# Set up the Cipher for encryption
cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, cast('java.security.Key', secret_key))
cipher_text_bytes = cipher.doFinal(plain_string.encode('utf-8'))
print(cipher_text_bytes)
iv = cipher.getIV()
bundle_encrypted_text_iv = concatenate_byte_arrays(iv, cipher_text_bytes)
encrypted_data = Base64.encodeToString(bundle_encrypted_text_iv, Base64.DEFAULT)
return encrypted_data
To concatenate the encrypted string and the cipher parameters we can use the following function:
def concatenate_byte_arrays(iv, cipher_text_bytes):
'''
Function prepares byte array concatenating string and cipher parameters
'''
iv_py = bytes(iv)
cipher_text_bytes_py = bytes(cipher_text_bytes)
combined = iv_py + cipher_text_bytes_py
return combined
Function to decrypt a string
In the next step, we write a function to use the key for decryption. Similar to the encryption function we first define the java classes, get the key from the keystore, retrieve the cipher parameter from the concatenated string and finally decrypt the data.
def decrypt_string(encrypted_string, alias="my_key_alias"):
"""
Function decrypts token using the private key from the KeyStore
"""
Cipher = autoclass('javax.crypto.Cipher')
KeyStore = autoclass('java.security.KeyStore')
Base64 = autoclass('android.util.Base64')
GCMParameterSpec = autoclass('javax.crypto.spec.GCMParameterSpec')
SecretKeyEntry = autoclass('java.security.KeyStore$SecretKeyEntry')
# Load the key from the keystore
keystore = KeyStore.getInstance('AndroidKeyStore')
keystore.load(None)
entry = keystore.getEntry(alias, None)
secret_key_entry = cast('java.security.KeyStore$SecretKeyEntry', entry)
secret_key = secret_key_entry.getSecretKey()
# Check if the entry is an instance of SecretKeyEntry
if not isinstance(entry, SecretKeyEntry):
print(f"Error: The key with alias '{alias}' is not a SecretKeyEntry.")
return None
# Decode the base64 encrypted text
decoded_data = Base64.decode(encrypted_string, Base64.DEFAULT)
# Extract the IV and encrypted data
iv = decoded_data[:12]
cipher_text_bytes = decoded_data[12:]
# Set up the Cipher for decryption
cipher = Cipher.getInstance("AES/GCM/NoPadding")
# Specify the GCM tag length
gcm_spec = GCMParameterSpec(128, iv) # Tag length in bits, and the IV
cipher.init(Cipher.DECRYPT_MODE, cast('java.security.Key', secret_key), gcm_spec)
plain_text_bytes = cipher.doFinal(cipher_text_bytes)
plain_text_bytes_python = bytes(plain_text_bytes)
plain_text = plain_text_bytes_python.decode('utf-8')
return plain_text
Concluding remarks
Using these functions allows us to generate a key, save it in the Android keystore and to encrypt and decrypt data on an Android device. I tested the code on multiple Android devices and did not observe any issues. If this method doesn't work, you might want to implement a plan B and adjust your error handling in a way that your data is save.