Build an Encrypted Voicemail system with Python and Twilio Programmable Voice

March 18, 2021
Written by
Carlos Mucuho
Contributor
Opinions expressed by Twilio contributors are their own

Build an Encrypted Voicemail system with Python and Twilio Programmable Voice

In this tutorial, we are going to write an application that uses the Twilio Programmable Voice API to build a voicemail system that records and encrypts messages left in our Twilio phone number. We will also implement a dashboard that will allow us to see, decrypt and listen to all the encrypted voicemails.

By the end of this tutorial we will have an application that looks like the following:

Project demo

Tutorial requirements

To follow this tutorial you are going to need the following components:

  • A free or paid Twilio account. If you are new to Twilio create a free account now. If you create your account using this link and later upgrade to a paid account, you will receive $10 in credit.
  • A Twilio phone number capable of receiving phone calls. Get one now if you don’t have it.
  • OpenSSL installed.
  • Python 3.6+ installed.
  • ngrok installed. ngrok is a reverse proxy service that creates a secure tunnel from a public endpoint to a locally running web service. We will need to use ngrok to create a secure URL that allows Twilio to connect to our application.
  • A cell phone or telephone capable of making phone calls, to test the project.

Creating the project structure

In this section, we will create our project’s directory structure. Then we will create and initialize a Python virtual environment. Lastly, we will install the packages needed to build our application.

Open a terminal window and write the following command:

$ mkdir twilio-voicemail-encryption
$ cd twilio-voicemail-encryption

In the block of code above, we created our working directory named twilio-voicemail-encryption and we navigated into it.

Create a directory named static and inside of this directory, create another named recordings.

If you are using a Unix or Mac OS system, run the following commands to do what described above:

$ mkdir static
$ mkdir static/recordings

If you are on Windows, run the following commands:

$ mkdir static
$ mkdir static\recordings

When using Flask, static is the directory where we store the files that we need to send to a client. The recordings subdirectory is where we will store the encrypted and decrypted recordings.

Create a directory named templates:

$ mkdir templates 

The templates directory is where we will store the HTML templates of our Flask application.

Inside our working directory create a virtual environment and activate it. If you are using a Unix or Mac OS system, run the following commands to do it:

$ python3 -m venv venv
$ source venv/bin/activate

If you are following the tutorial on Windows, run the following commands instead:

$ python -m venv venv
$ venv\Scripts\activate

Now that we created and activated our virtual environment we can install the libraries that we need to create our application:

$ pip3 install twilio flask python-dotenv cryptography

In the command above we used pip the Python package installer, to install the following packages that we are going to use in this project:

  • twilio-python is a Python package for communicating with the Twilio API.
  • Flask is a Python micro-framework for building web applications. We will use to create a webhook to interact with Twilio and to create a dashboard to manage our voicemails.
  • python-dotenv is a library that reads the key-value pairs from a .env file and adds them as environment variables. We will use this module to retrieve our Twilio credentials stored in the .env file.
  • cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. We will use this module to decrypt our voicemails.

We will also use Bootstrap to build this application but we can’t install it using pip. Bootstrap is a potent front-end framework used to create modern websites and web apps. We will use it to style our application.

For your reference, at the time this tutorial was released these were the versions of the above packages and their dependencies tested:

certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
click==7.1.2
cryptography==3.4.6
Flask==1.1.2
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.3
MarkupSafe==1.1.1
pkg-resources==0.0.0
pycparser==2.20
PyJWT==1.7.1
python-dotenv==0.15.0
pytz==2021.1
requests==2.25.1
six==1.15.0
twilio==6.52.0
urllib3==1.26.3
Werkzeug==1.0.1

Creating a Twilio public key resource

In this section, we are going to create a Twilio public key resource and then we are going to use this key to enable voice recording encryption.

First, we will use openssl to generate an RSA public/private key pair. We can generate a 2048 bit long private key by running the following command:

$ openssl genrsa -out private_key.pem 2048

The contents of this file should look similar to this:

-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAxkzBjVbdqK6O6nlU+vEm0kObxmjdhSlIH7Mnw+RohA+6e1Bn
p+VZxUKslRdvoV0yjvKJuIFWmYog4n2cgxBfb8dsxp5Bj6i4xT5ByuB7sr3V6hNn
wEpUO/MeqN7Qbp3MCjPh65RPCsPwioKNzkeKeoxlQSk/xWOgYEt/F+k00Xg4YoMc
xawOxNgzi5n49HPNLaQltxNl/FF7RHDCi9vvdCNQtTWTVvCfmNyhJOyrslw2NMmu
Kh3Db6DRWg+ZjNWefp9Ysgx//apwUXro7DuCUXwacguYJ78ZcbTyQhFasqM0pFDx
FBDa/azRnQhx6Hd/J6/OgrY4pI6uMhmjir/NNwIDAQABAoIBAF7BRquXGowD/V6l
Y6oVmYtXqxP///olY8ViAlpkxlx0d5N9NErSGTddSMJlVH4y3nnYA18azprHmjcf
9q3aIQB0ttGhxYo0ATafLSkYY4NhmtojM6x62A1dewUOk2KIHHuzlvzO/YYTYtmM
0N4E6XLtq2SbpVFY7cWVzcFLOmXsyNIm4CWXyqy4ZaY1U5bwmwJagUeXV2orif5O
RNvb4TKYo3ZghmPuS/+OMtYgkuLapzyy9I5Yzzv4HaA4jmnBMftO9jUIlr9FFW2G
T9/lT1AZKfjZMKA/yuv3RsYf/8au3q5EQcMw/B4HSrI4mxCDLPKX+uOLKla/KCbq
zYZDnAECgYEA7FYXta4WWV6aumC8iivMCQGPwv8EIj5/Ewx2ZxmewJe74qnBhorg
0RZlMMp6hWx9rRTkkE3XbP7OADRZNtt12C5SAfyLolNk8srOrg6peQMcE6sSrWxl
uxt5O3JAa0oOGH/O0hoXBkqPzZeSs+zR8ReAmqV/YxK18YLQdGxjGbcCgYEA1sx+
31rhNVA9x+r6yB15i/ihvwLN6FXpyRctxEtamnxRzI1/QmtuRkVNNHqYAa//WSkZ
IWGcIHLKbtn/Hn8m2RQpOgg1dsNBO9hjuZarYm6UZKwRZXjNpxgN+oUTj2HsRi8k
5WXs40I9zovTWULYv+HG6d7MtfeHMeS5d/aO6IECgYBpD8SugVtyEzpZjFOEYP/t
KQKNvuxJhNrczvd70cne+BUQKELd2rMigAAv9nMNCTO9U8Jf3BreW0+ci6j1WA5F
MiJGu8wfN09zF3FVszLnlthObgh5i+yVhxsXsCyvBVbK0VZR1ENUCqVu0ejnj2ms
8bO8C8Jbep/jYzHj76MGdQKBgByDsSP9cIAfUtRDQV9nakdGjlJJEQSSwyJKzWyN
hE2Vy1YYQpiSomT5tjINRDiVIJS5e/iOeKdmFbF5hwCJaKLQplhp1o8ZhINpSnPM
qJu8ij6DCRwrWUGhU9m56MrT+QWoJIG/ch8JICNXNItY8GUol7tcNFjDr1LURjrC
pF4BAoGAdozqIAGiBVuVkL4kPEzF4M7HqaxA5m0OgZiIyXzXpKLMB6ijDf2MHg88
wt+L1WxukREQtZ185RcCjaOcYEw1tWa9fP4bXGXDIKNipRmQGiCQjDjqIckrOe2i
w+UhhvPYSI1uYcrWbLsW6kEU/vtOMuGX5JcNS2MMBigS2t3wtgs=
-----END RSA PRIVATE KEY-----

Since this is a private key, it is very important that you keep this file secure. In particular, do not commit it to source control.

Generate the corresponding public key by running the following command:

$ openssl rsa -in private_key.pem -pubout -out public_key.pem

The content of the public key file should look similar to the this:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxkzBjVbdqK6O6nlU+vEm
0kObxmjdhSlIH7Mnw+RohA+6e1Bnp+VZxUKslRdvoV0yjvKJuIFWmYog4n2cgxBf
b8dsxp5Bj6i4xT5ByuB7sr3V6hNnwEpUO/MeqN7Qbp3MCjPh65RPCsPwioKNzkeK
eoxlQSk/xWOgYEt/F+k00Xg4YoMcxawOxNgzi5n49HPNLaQltxNl/FF7RHDCi9vv
dCNQtTWTVvCfmNyhJOyrslw2NMmuKh3Db6DRWg+ZjNWefp9Ysgx//apwUXro7DuC
UXwacguYJ78ZcbTyQhFasqM0pFDxFBDa/azRnQhx6Hd/J6/OgrY4pI6uMhmjir/N
NwIDAQAB
-----END PUBLIC KEY-----

Go to Twilio account Console > Project > Settings > Credentials > Public keys , then click the “Create new Credential” button (or the red “+” icon) to create a new credential. In the “FRIENDLY NAME” field enter the name for your key such as my-public-key. In the “TYPE” field select “Public Key”. Paste the contents of the public_key.pem file in the “PUBLIC KEY” field. Click the “Create” button to store this new key.

Create new credential

Click on the “Done” button and you should see something similar to this:

Credentials

Copy the “SID” value to the clipboard and then create a file named .env. After that, Take the “SID” you copied from the console and paste it in the .env file as PUBLIC_KEY_SID.

Your .env should look similar to this:

PUBLIC_KEY_SID="CR660ba10ca8a2fecb0a57c96531141f2b"

Go to the home page of your Twilio Console and copy the Twilio Account SID and Auth Token values to the .env file as follows:

PUBLIC_KEY_SID="CR660ba10ca8a2fecb0a57c96531141f2b"
TWILIO_ACCOUNT_SID="ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
TWILIO_AUTH_TOKEN="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Enabling voice recording encryption

Navigate to Twilio Console  > Voice > Settings > General and enable voice recording encryption. After activating this option, a “Public Key” dropdown field will appear. Select the credential you created in the previous step.

Enable voice recording encryption

Receiving and recording incoming calls

In this section, we will create an application capable of receiving and recording incoming calls to your Twilio phone number.

Create a file named main.py. Open it using your favorite text editor and then add the following code to it:

from flask import Flask, render_template, redirect
from flask import request

from twilio.twiml.voice_response import VoiceResponse
from twilio.rest import Client

from dotenv import load_dotenv

import urllib.request

from pprint import pprint

import glob
import os

import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

In the block of code above, we imported all the packages that we are going to need in order to build our application:

  • Flask will be used to define a webhook that Twilio Voice API will use no notify our application when there is an incoming call to our Twilio phone number. We will also use Flask to serve pages that allow us to interact with the voicemails left by the callers.
  • The twilio package will be used to interact with the Twilio API, allowing us to record voice messages made by people who called our phone number; then fetch and delete said recordings.
  • python-dotenv will be used to import our Twilio account credentials from the .env file.
  • urllib  will be used  to download the encrypted voicemails stored in Twilio's servers.
  • pprint will be used to format and print the data received when Twilio finishes encrypting and storing a voicemail left by a caller.
  • glob and os will be used to manage the voicemails that we are going to download and then store locally.
  • base64 and cryptography will be used to decrypt the voicemails that we will download from Twilio.

Add the following code to the bottom of the main.py file:

load_dotenv()

account_sid = os.environ['TWILIO_ACCOUNT_SID']
auth_token = os.environ['TWILIO_AUTH_TOKEN']

app = Flask(__name__)


@app.route('/')
def home():
    return "<h1>Hello world</h1>"


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=3000, debug=True)

In the block of code above, we first initialized the python-dotenv module, and then we retrieved the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKENcredentials.

We created a Flask application instance in a variable named app and with that, we created the / endpoint of our application.

At the bottom of the script, if we find that the script was invoked as the main script, we launch the Flask application on port 3000.

Open a second terminal window in our project directory, activate the Python virtual environment, and start the application by running the following command:

$ python main.py

Now, open your browser and type http://localhost:3000/ in the address bar. You should see something similar to this:

Hello world application

Go back to your main.py file and add the following code below the home() function:

@app.route("/handle_incoming_call", methods=['GET', 'POST'])
def handle_incoming_call():
    print('call received')
    response = VoiceResponse()
    response.say("Hi, I can't come to the phone right now, please leave a message after the beep")
    response.pause(length=3)

    response.record(
        recording_status_callback='/recording_status_callback',
        recording_status_callback_event='completed')
    
    response.hangup()
    return str(response)

In the block of code above, first, we created an endpoint named /handle_incoming_call. As the name suggests this endpoint will be triggered whenever our Twilio phone number receives a phone call.

When this endpoint is triggered, we initialize a TwiML response object. Then we call the say() method in the response to add a greeting. The text passed to this method will be spoken to the caller using text-to-speech. The pause() method then adds a short pause.

The record() method then instructs Twilio to record the message left by the caller. We also set the endpoint that will be triggered when Twilio finishes recording the message to /recording_status_callback.

After that, we use the hangup() method to end the call.

The response object with all the instructions is then returned back to Twilio.

Add the following code bellow the /handle_incoming_call route :

@app.route("/recording_status_callback", methods=['GET', 'POST'])
def recording_status_callback():
    pprint(request.form)
    return ''

In the block of code above, we created the /recording_status_callback endpoint. This endpoint will be triggered when Twilio finishes encrypting and storing the voicemail left by the caller. In this first implementation we are printing the details of the encrypted voicemail that Twilio sent to us in the request.form.

Open another terminal window and start ngrok on it:

$ ngrok http 3000

After running the command above you should see something similar to this:

Ngrok screenshot

Copy the https ngrok URL to the clipboard. Then go to your Twilio console > Phone Numbers > Manage Numbers > Active Numbers dashboard and select the number you purchased for this tutorial.

Locate the “Voice” section of the phone number configuration and paste the https:// URL provided by ngrok followed by /handle_incoming_call in the “A call comes in” field. This will create a webhook that connects your application to your Twilio number. In this example, the ngrok URL is https://db62511f4311.ngrok.io/handle_incoming_call. The first section of the URL will be different every time ngrok is launched.

Twilio voice webhook configuration

To test the voicemail application, call your Twilio number and you should see something similar to this in the terminal where you started the Flask application:

call received
127.0.0.1 - - [01/Mar/2021 19:21:44] "POST /handle_incoming_call HTTP/1.1" 200 -
call received
127.0.0.1 - - [01/Mar/2021 19:21:57] "POST /handle_incoming_call HTTP/1.1" 200 -
{'AccountSid': 'ACxxxxx',
 'CallSid': 'CAxxxxx',
 'EncryptionDetails': '{"type":"rsa-aes","public_key_sid":"CRxxxxx","encrypted_cek":"xxxxx","iv":"xxxxx"}',
 'ErrorCode': '0',
 'RecordingChannels': '1',
 'RecordingDuration': '3',
 'RecordingSid': 'RExxxxx',
 'RecordingSource': 'RecordVerb',
 'RecordingStartTime': 'Mon, 15 Mar 2021 17:07:27 +0000',
 'RecordingStatus': 'completed',
 'RecordingUrl': 'https://api.twilio.com/2010-04-01/Accounts/ACxxxxx/Recordings/RExxxxx'}
127.0.0.1 - - [01/Mar/2021 19:21:59] "POST /recording_status_callback HTTP/1.1" 200 -

We will need the information in the EncryptionDetails and the RecordingSid fields in order to decrypt our recordings. Have a look at this resource provided by Twilio if you wish to understand what is a SID.

Please note that the value of the public_key_sid field is the same as the one we stored in our .env file. This means that Twilio successfully used the public key that we created in the previous section to encrypt this recording.

Here is what happened on Twilio's side when we made a phone call to our Twilio number:

  1. Twilio generated a random Content Encryption Key (CEK) for this recording.
  2. Twilio encrypted the recording content with the generated CEK using the AES256-GCM cipher.
  3. Twilio encrypted the CEK with our public key using the RSAES-OAEP-SHA256-MGF1 cipher.

Retrieving and decrypting the recordings

In this section, we are going to create the endpoints that need to retrieve and decrypt our voicemails. We will only need two endpoints, an endpoint named / and another named /recording/<sid>:

  1. - The / endpoint is where we will retrieve and show all the voicemails that are stored in Twilio's servers.
  2. - The /recording/<sid> is where we will decrypt and listen to a specific voicemail. The sid you see in the endpoint is the unique string used to identify a recording, it is the RecordingSid field that you saw when the /recording_status_callback was triggered in the previous section.

Go back to the main.py file, add replace the contents of the home() function endpoint to the following:

@app.route('/')
def home():
    recordings = get_recordings()
    return render_template(
        'home.html',
        title="My encrypted voicemails",
        recordings=recordings
    )

In the block of code above, we are calling a function named get_recordings() and then storing the value returned in a variable named recordings, which is then passed on to a template named home.html along with a page title.

Add the following code bellow the home() function :

def get_recordings():
    client = Client(account_sid, auth_token)
    recordings = client.recordings.list()
    all_recordings = []
    for record in recordings:
        if record.encryption_details is not None:
            rec = {
                'date_created': record.date_created,
                'sid': record.sid,
                'duration': record.duration,
                'status': record.status,
                'price': record.price,
                'path': "/recording/{}".format(record.sid)
            }
            all_recordings.append(rec)
    return all_recordings

The get_recordings() function creates a Twilio client object, which allows us to retrieve all the recordings stored in our Twilio account. The function returns a list containing one entry per recording, each with the information that we need to render the home.html template in the / endpoint. If your account has recordings that were generated before you enabled encryption, those are skipped.

Note the path field in the rec dictionary. This is the URL path to navigate to a specific recording page.

Create a file named home.html inside the templates directory. Open this file using your favorite text editor and then add the following code to it

Add the following code :

<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{title}}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<div class="container" style="max-width: 800px;
        padding-top: 100px;">
    <h2 style="text-align: center;">My encrypted voicemails</h2>
    <table class="table table-striped table-hover">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Date</th>
            <th scope="col">SID</th>
            <th scope="col">Duration</th>
            <th scope="col">Status</th>
            <th scope="col">Price</th>
        </tr>
        </thead>
        <tbody>
        {% for n in recordings %}
        <tr style=" cursor: pointer;" onclick="window.location='{{n.path}}';">
            <td>{{loop.index}}</td>
            <td>{{n.date_created}}</td>
            <td>{{n.sid}}</td>
            <td>{{n.duration}}</td>
            <td>{{n.status}}</td>
            <td>{{n.price}}</td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
        integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
        crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
        integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
        crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
        integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
        crossorigin="anonymous"></script>
</body>
</html>

This template implements a page that shows a table containing all the recordings stored in your Twilio account. Each row in the table has an onClick event listener that will redirect you to a page for the recording. This is done by setting the URL of the recording page in window.location variable.

Navigate to http://localhost:3000 in your browser to see the new home page. Alternatively, you can use the https:// URL provided by ngrok. Your application you should look as follows:

List of voicemails

Depending on the number of phone calls you’ve made to your Twilio phone number, the number of recordings listed may be different.

Open the main.py file using your favorite text editor and then add the following code bellow home() function:

@app.route('/recording/<sid>', methods=['GET', 'POST'])
def recording_by_sid(sid):
    if request.method == 'GET':
        client = Client(account_sid, auth_token)
        recording = client.recordings(sid).fetch()
        print('recording found', recording.date_created)
        recording_url = "https://api.twilio.com{}".format(recording.uri.replace('.json', ''))
        decrypted_recording_name = recording.sid + '_decrypted.wav'

        if not decrypted_recording_exists(decrypted_recording_name):
            decrypt_recording(recording.encryption_details, recording_url, recording.sid)

        return render_template(
            'recording.html',
            title="Voicemail {} page".format(recording.sid),
            recording=decrypted_recording_name,
            sid=recording.sid,
            path="/recording/{}".format(recording.sid)
        )
    else:
        print('delete file')
        delete_recording(sid)
        return redirect('/')

In this block of code we are adding the /recording/<sid> endpoint. In this endpoint first, we check if the request received was a GET request or a POST request.

If the request received is a GET request, we create a Twilio client object and use it to retrieve the recording given by the sid value that is passed as part of the endpoint URL. After finding the recording, we use the uri field to generate the recording_url field, which will allow us to download the recording in .wav format. For more information about downloading call recordings consult the documentation.

Next the function checks if a decrypted file exists. The decrypted_recording_name variable is constructed from the recording sid value, and is passed to the decrypted_recording_exists() function as an argument. Note that we are yet to define this function.

If a decrypted recording isn’t found, then we call the decrypt_recording() function and pass the encryption details returned by Twilio, the download URL for the encrypted audio file, and the sid. The decrypt_recording() function will download the audio, decrypt it, and save the decrypted audio file to disk so that it can be used later on by the application.

The GET request ends by rendering the recording.html template.

The browser will send a POST request to this endpoint when the user clicks on a “Delete” button for the recording. In this case we call a delete_recording() auxiliary function and pass the recording sid to it.

Let’s now add the auxiliary functions described above. Add the following code bellow the get_recordings() function:

def decrypted_recording_exists(recording_name):
    files = glob.glob('static/recordings/*.wav')
    for f in files:
        if recording_name in f:
            print('File exists', f)
            return True

    print('No files found')
    return False

The logic to check if a decrypted recording file exists or not is based on the glob() function from the Python standard library, which returns all the files in a directory that match a given pattern.

Add the following code below the decrypted_recording_exists() function:

def decrypt_recording(encryption_details, url, recording_sid):
    urllib.request.urlretrieve(url, 'static/recordings/{}.wav'.format(recording_sid))

    encrypted_cek = encryption_details['encrypted_cek']
    iv = encryption_details['iv']

    private_key = open("private_key.pem", mode="rb")
    key = serialization.load_pem_private_key(private_key.read(), password=None, backend=default_backend())
    private_key.close()

    decrypted_cek = key.decrypt(
        base64.b64decode(encrypted_cek),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )

    decryptor = Cipher(
        algorithms.AES(decrypted_cek),
        modes.GCM(base64.b64decode(iv)),
        backend=default_backend()
    ).decryptor()

    encrypted_recording_file_path = 'static/recordings/{}.wav'.format(recording_sid)
    decrypted_recording_file_path = 'static/recordings/{}_decrypted.wav'.format(recording_sid)
    decrypted_recording_file = open(decrypted_recording_file_path, "wb")
    encrypted_recording_file = open(encrypted_recording_file_path, "rb")

    for chunk in iter(lambda: encrypted_recording_file.read(4 * 1024), b''):
        decrypted_chunk = decryptor.update(chunk)
        decrypted_recording_file.write(decrypted_chunk)

    decrypted_recording_file.close()
    encrypted_recording_file.close()
    print("Recording decrypted Successfully. You can play the recording from " + decrypted_recording_file_path)

The decrypt_recording() function is a modified version of the Twilio sample code to decrypt recordings using Python. The function first downloads the encrypted audio file, and then uses the information provided in the encryption_details argument to generate a decrypted version of the file in .wav format. Both the encrypted and decrypted files are left in the static/recordings directory.

Add the following code bellow the decrypt_recording() function:

def delete_recording(sid):
    client = Client(account_sid, auth_token)
    client.recordings(sid).delete()

    files = glob.glob('static/recordings/*.wav')
    for f in files:
        try:
            if sid in f:
                os.remove(f)
                print(f, ' file deleted')
        except OSError as e:
            print("Error: %s : %s" % (f, e.strerror))

The delete_recording() function is called when the user requests to delete a recording. The function receives the recording sid as an argument and uses it to delete the corresponding recording, both locally and on Twilio's servers.

Create a file named recording.html in the templates directory, open this file using your favorite text editor and then add the following code to inside it:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{title}}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<div class="container" style="margin-top: 100px">
    <div class="card " style="width: 400px; margin: auto;">
        <div class="card-body">
            <h4 style="text-align: center;">Voicemail</h4>
            <h6 style="text-align: center;">{{sid}}</h6>
            <canvas id="canvas"></canvas>
            <!-- <source src="/static/recordings/{{recording}}" type="audio/mpeg">-->
            <audio
                    id="audio"
                    src="/static/recordings/{{recording}}"
                    style="margin: auto; width: 360px;"
                    controls></audio>
            <form action="{{path}}" method="POST">
                <button
                        style="width: 360px "
                        class="btn btn-danger"
                        type="submit"
                        name="delete"
                        value="{{sid}}">Delete
                </button>
            </form>
        </div>
    </div>
</div>

<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
        integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
        crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
        integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
        crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
        integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
        crossorigin="anonymous"></script>
<script src="/static/visualizer.js"></script>
</body>
</html>

The recording.html template renders a page for a given recording. It allows us to listen to the decrypted voicemail and also has a “Delete” button.

The template uses a modified version of the code found in https://codepen.io/nfj525/pen/rVBaab to build an audio visualizer in a canvas element. Inside the static directory create a file named visualizer.js. Open this file using your text editor and then add the following code inside it:

window.onload = function () {

    var file = document.getElementById("thefile");
    var audio = document.getElementById("audio");

    audio.load();
    var context = new AudioContext();
    var src = context.createMediaElementSource(audio);
    var analyser = context.createAnalyser();

    var canvas = document.getElementById("canvas");
    canvas.width = 360
    canvas.height = 210;
    var ctx = canvas.getContext("2d");

    src.connect(analyser);
    analyser.connect(context.destination);

    analyser.fftSize = 256;

    var bufferLength = analyser.frequencyBinCount;
    console.log(bufferLength);

    var dataArray = new Uint8Array(bufferLength);

    var WIDTH = canvas.width;
    var HEIGHT = canvas.height;

    var barWidth = (WIDTH / bufferLength) * 2.5;
    var barHeight;
    var x = 0;

    function renderFrame() {
        requestAnimationFrame(renderFrame);

        x = 0;

        analyser.getByteFrequencyData(dataArray);

        ctx.fillStyle = "#000";
        ctx.fillRect(0, 0, WIDTH, HEIGHT);

        for (var i = 0; i < bufferLength; i++) {
            barHeight = dataArray[i];

            var r = barHeight + (25 * (i / bufferLength));
            var g = 250 * (i / bufferLength);
            var b = 50;

            ctx.fillStyle = "rgb(" + r + "," + g + "," + b + ")";
            ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);

            x += barWidth + 1;
        }
    }

    renderFrame();
};

Testing the final application

Go back to the browser, refresh the page, and click one of your recordings. You should see a page with an audio player for it:

Voicemail page

Click in triangle to play the decrypted recording, and you should see the following:

Voicemail player

Now, click on the “Delete” button and the recording that is stored in Twilio's server and in the recordings directory will be deleted. After this you will be redirected to the home page of the application.

Conclusion

In this tutorial we learned how to use the Twilio Voice API to create an encrypted voicemail system. We learned how to use the Flask framework to build a dashboard where these voicemails can be decrypted and played. Lastly, we learned how to delete those voicemails.

The code for the entire application is available in the following repository https://github.com/CSFM93/twilio-voicemail-encryption.

Carlos Mucuho is a Mozambican geologist turned developer who enjoys using programming to bring ideas into reality. https://github.com/CSFM93