Securing Your Forms With Braintree.js

Photo of ENIGMA Rotor Set by brewbooks

Think back to the web of fifteen years ago. Most of the web sites of the time
consisted of a few pages of content along with a contact page (and maybe even a
guestbook.) Most often that contact page was backed by a script that mailed
the results to a fixed e-mail address. Is anyone willing to admit to using
formmail.pl? In the least, we can admit that we didn’t move data around in
the most secure way.

Braintree.js is a smallish library (<41kiB minified, <14kiB minified and
gzipped) that makes it simple to encrypt your form data before submission to
the server. Today I’m going to use it to secure contact form data, but there’s
no reason you can’t use it with any HTML form.) Let’s start with a simple
contact form. There is nothing special about this form.

<!DOCTYPE html>
<html>
    <head>
        <title>Contact Us</title>
    </head>
    <body>
        <h1>Contact Us</h1>
        <form action="contact" method="post">
            <label>
                E-mail address
                <input type="email" name="email"></input>
            </label>
            <label>
                Message
                <textarea name="message"></textarea>
            </label>
            <input type="submit"></input>
        </form>
    </body>
</html>

I’ll also write a flask method that sends the message to my inbox:

@app.route('/contact', methods=['POST'])
def contact():
   text = 'From: {email}nn{message}'.format(
    email=flask.request.form['email'],
    message=flask.request.form['message'],
   )

   msg = MIMEMultipart()

   msg['From'] = gmail_user
   msg['To'] = gmail_user
   msg['Subject'] = 'Contact Form Submission'

   msg.attach(MIMEText(text))

   smtp = smtplib.SMTP('smtp.gmail.com', 587)
   smtp.ehlo()
   smtp.starttls()
   smtp.ehlo()
   smtp.login(gmail_user, gmail_pwd)
   smtp.sendmail(gmail_user, gmail_user, msg.as_string())
   smtp.quit()
   smtp.close()
   return 'Message sent!'

Note that I’ve defined gmail_user and gmail_pwd in the global scope. Let’s
add the braintree.js library and some data attributes to the form inputs.

<!DOCTYPE html>
<html>
    <head>
        <title>Contact Us</title>
        <script src="static/js/braintree-1.3.3.js"></script>
    </head>
    <body>
        <h1>Contact Us</h1>
        <form action="contact" id="contact-form" method="post">
            <label>
                E-mail address
                <input type="email" data-encrypted-name="email"></input>
            </label>
            <label>
                Message
                <textarea data-encrypted-name="message"></textarea>
            </label>
            <input type="submit"></input>
        </form>
        <script>
            var braintree = Braintree.create('PUBLIC_KEY');
            braintree.onSubmitEncryptForm('contact-form');
        </script>
    </body>
</html>

As you can see, the changes are almost trivial. Unfortunately, that won’t cut
it. We need a keypair. We can use OpenSSL to generate one:

$ openssl genrsa 4096 > private.pem
Generating RSA private key, 4096 bit long modulus
..........................................................................+
+
...........................................................................
...........................................................................
...........................................................................
....................++
e is 65537 (0x10001)
$ openssl rsa -in private.pem -pubout > public.pem
writing RSA key

Keep the private.pem file a secret (ignoring for a moment, the fact that I have
checked it in to my public git repository.) Take the Base64-encoded value from
public.pem and pass it to Braintree.create:

<script>
    var publicKey="MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv7BV
eNTu1PZk6eOnU6+2rz3MoLHpx5RODcOYS+EMFANI3F/zLRPdb6tkeZTy95RIEeQsxlQ
+Hnwcwcq3zf+goWjxshLLCMwziMNUsCSLvd9lm5aLfVMGJTbUbJiZMmbTmBfADmE8TF
PexgAWdsl3470ni2ODsKfHB02vzOV2KahqN36F8b8CeM/m0B+wLFfvujlGxVuU3q9cI
RZGvvtkuG3keBZpEAjYzWwOLu+cpvRUAAhndJqLLw0FUJ1Sa+G1SyTEr2J4yxPLCnd6
Hoill60wDa1ZFmc13RnPmvVbud5TwwaQ4j4KAXYiu3p2uP29mfQnQq8+qeGgcOKM4Vu
dtatwd4z7H/TK/tJyfx6gUW1uWQ+bedmzp/5ntIAgyFIETMy9J/FmHpN42i2plciGHW
iZDRUCDeyOFygaBTExQWszj5JAa8imPCvHYmz+bhK7CVga7Ej/bHiLtu2nhr/BS9rz7
9ZqZQEFZS6XyuX9Xms6+tBB7nV4l4hywHAfJrrU8Kom+ysDMgncsT4GoyRFTMYPVkZ4
46pHEdOoVFj2ZwqMNZUQ1pL3eVK5qpQvbMjUJ8mCg78GO4rQlZ6F7TsLCdGuK30A/Z+
h3+xQlpVwbACv6I3cIqA1dgWiYHk/3wbjshGj47Aqt8MVOL3MWFJVFqct535iWxiY02
ZDQzNgz+MCAwEAAQ==";
    var braintree = Braintree.create(publicKey);
    braintree.onSubmitEncryptForm('contact-form');
</script>

With this in place, submitting the form results in strings like these on the
server:

$bt4|javascript_1_3_3$nRaLNbS35mpDaK33mMLaePwIXmk2J6I3CnNvSucjqGwzD5I2akEbp
vWPAftp8/sIgpxxTgQzHRnje/SslcBA9w2WevYirURZMTNjMbkoQ66M6XQTR3LAvTMgfr1z1394
PZRr6MXn+jjou8luydaweBw7O/0ledkOk375M4FhHeCW46LLFx+490K+VJuRGt94PICTb9r/Zje
7FsPsAuzy/squ444N+a4WDrB/fmCWWMY2oQnWyHk+VTklLyenisqfJTW0gW7fFiq/AhVFJ4/29p
CDAOQ6CPGtKNv8cjABCpdrom7FIscB7qoEbPb5yCSfNjwITj4os2bMJN8wDlhDLGlnSvrH5pfxX
VRi7l2jE4+ogQMr/5WeH9EL/c4b6MpPcwuL07oCQscv6+FMvtJ7uSgKHo66+dMAFKRs03S4N3M5
eBSOiQhu/PWywd/CwTqZGfouIwLbKbBcwC7LaiA65jCS5jGHuaSLl2UceJXZURdyQTDYwRYjhAw
Cf3NDk3LFgn+LG0ZRWWHZuXSmoPoVNzHTKq8elE+WeDQPuNWlRBcLPynNSCtTLawDGpNykv0mW9
mk9N77vHPxp0fObwL5hjgu6dQL6zCV55Lg4iJeKrmAJqxbuvF+3dPpi01D7bHbu8UAzx1TdLazq
ODazZkOuJcDmVT0lhoBMj0pjWHm7rs=$LAxoEa0qlBCMjJObpi4XYd0kA/nmXCERqfJHKvrSdZ0
=$GFtjx84PXmyHZsM2Xxbqtluu/RCzdX1F1J3NvTaIwa8=

This string contains several values, separated by dollar signs. (You can
follow along in the source code.) The first part, bt4|javascript_1_3_3
suggests that this is the fourth revision of Braintree’s encryption format.
Next is the Base64-encoded, RSA-encrypted, Base64-encoded (again) concatenation
of the AES and HMAC(SHA-256) keys. After that, you’ll find the Base64-encoded,
AES-encrypted value of the input, followed by the HMAC.

Great. So we have encrypted form values. Now what? It seems we should figure
out how to decrypt them. Braintree doesn’t provide that part of the code
(unless I missed it,) so I’ll have to figure it out. Since we need to keep the
private key private, I’ll write this as a Python script that works from the
command line. Here’s the meat of the script:

def Decrypt(rsaCipher, encryptedValue):
    """
    Decrypt Braintree.js-encrypted values

    rsaCipher - a PKCS#1 v1.5 RSA cipher object from PyCrypto (which contains
                the necessary private key information)
    encryptedValue - the encrypted string from braintree.js
    """

    # Get the version, keys, ciphertext and hmac from the user's input.
    parts = filter(None, encryptedValue.split('$'))
    version, keys, ciphertext, hmac = parts

    # The goal is to decrypt the ciphertext.  In order to do that, we need to
    # verify the HMAC, because it is best practice to authenticate ciphertext
    # before decrypting it.  To verify the HMAC, we need the HMAC key, which
    # is encrypted with our public key in the ``keys`` variable (along with the
    # AES key.)  So we have these steps:
    #
    # 1) Decrypt ``keys`` with our private key, and extract the AES and HMAC
    #    keys from it.
    # 2) Verify the HMAC(ciphertext, HMAC key) matches.  If not, report the
    #    error and discontinue the process.
    # 3) Decrypt the ciphertext and return it.

    # Step one: decrypt the keys with our private key.
    keysBytes = base64.b64decode(keys)
    keysDecrypted = rsaCipher.decrypt(keysBytes, None)
    if keysDecrypted is None:
        raise RsaDecryptionFailedError()
    keysDecoded = base64.b64decode(keysDecrypted)
    aesKey, hmacKey = keysDecoded[:32], keysDecoded[32:]

    # Step two: verify the HMAC.
    hmacCruncher = HMAC(hmacKey, base64.b64decode(ciphertext), SHA256)
    decodedHmac = base64.b64decode(hmac)
    if hmacCruncher.digest() != decodedHmac:
        raise HmacIsInvalidError()

    # Step three: decrypt the ciphertext.
    decodedCiphertext = base64.b64decode(ciphertext)
    initializationVector = decodedCiphertext[:16]
    ciphertextWithoutIV = decodedCiphertext[16:]
    aesCipher = AES.new(key=aesKey, mode=AES.MODE_CBC, IV=initializationVector)
    plaintext = aesCipher.decrypt(ciphertextWithoutIV)
    return plaintext

You can find the full source code in this git repository. Here’s what it looks
like to use the script:

$ python decrypt.py ../keys/private.pem
Encrypted value: $bt4|javascript_1_3_3$Swq3PPQTG1WyU+slkniGQNJMvt9z53t1JfeW
j3FacUl1ZdiDVtj/DCWqglbvhxHF5/wLDp0M+N824I2vt4CZPA04DFyYVeep3yOObviubA/FOHw
fUKhK8Kcy0axHEyIKwcN9eAglM1xO2kFGWTtZdJf1PFQZOarOgK4gLu60PwDzwt8aSmqu08Pv5z
QJkOin7Lyz6LsilxGQCKt2j6TtLv2jidI5NatZ+sJFSGPk1LVSgq8z9JloEZCCERxoD8KcIDYD+
t/9Xcyq1m/lellI1+/HmTWol9m96rHdG4GvfuG9edn4p41Qqfq239ShaemVyqRemA44u9g8UpMD
z4SDtzswSteL+mPSytxFNtnf/1WPVySC0fpBiriSJLAisX2Oev0NOe6doQbOrTbvHepssJoCrEX
894ZRzlR3tOanobPRbBQryemHgh/b5c+tFjFh3pAzu0v3tdEsLP3bAami32OInJI9znR+p+bH31
bi130b+sZYisaYrnyqb8exCyEgtCh5HjNCQfiKsRBw3qQACc10zRyXwiNV4iryOjOXkjJP6O4p9
3OTcnm8blvC9CJ4sUWg4WserbZKZuNL/14lqRd0P2gya/eH56QjnuCuXXvYuNDtQPQdMhMY58oA
oahLubYoUkhdylyOcOTW1c3cE5x3GuUy92Gj8PWQfoEM7mc=$4b20RSdQhrMjot6aC/X4j2dOQt
JnCJDz5yTX8poEmGH6G5lZg/LhHEtuBLfCX5Bb$Mw4UN5kfxuHRqU/6LTmflEny0fRmg5oqeQMn
c8DxbUc=
tfarrell@artandlogic.com
Encrypted value: $bt4|javascript_1_3_3$hpIVTaJlFQ0DQ7NcazBCDrZQ5oq+/tbHuOhd
bq8bYQrWO9AfUXjfNlx87qMBfvDqIXSZG1HCFWE6qIWcb/SILj51rdiwb2DnlXvGZxO1r0bfKgv
YHLNddWF+Kd7jAWu98Ht7NHhHSE3o8kYnDxN9x6On1seiAcAs75u5KZPe2Nz8pY0y3gPe3JHq/d
vZ8Q2OLVXE9ezfWGLjUJSnO7HtuU3M95pRZzQUEdAIh8ylBfmreh+uUKG+yg4f1vot5wB3yfovE
CQnZwgvMnEliDe//Aa6+DiwKHtp5DUjftwQL4UgORVvKMyZZ8aI+nmXr5xfQCMHPr4Xwpth3p+I
jh7OjtoneqBnpkHkjaY7pVVwrsZWvHMujtqHXyRc7YvfBUV1MInlOy4MZUK8XACnZ8xhyuDFUBh
ap4eD0us+0LkX8HAjDabrorvjZNkq0fRXtTcQYR1vMbLIxupJCBiA8/+J4V6csbo7UGgsN7XhT2
Gu1GM1b1wyzt6lrg/ibiVRlDA2z0A0Pkw7cbn+sL6OUFO8xCAfKM5UFUAvZInY5J/tWVV+ASheK
eIYw0AhpqgVizPIbVxPyY9nf2EORsx0PE5qvUHz65IN7v+2EvAL1Cp4ehTm286zq1dW+0QnUwSy
BdWnEJlG6qLpRKiPTNBdWdUn7Zknsy0FF2TlH125xjk0Evs=$dnHrp6HGOIFyxu4nysLF0eJfNb
pYSECXbEvhhX7S87XwTfSh6lkS+vDen3WcEkdvtzzvxgb7t8zfiTpZRJaMfQ==$7Hwsh8VYeVIa
Hoi1haNm2MV7CdUoMdTXNhUOw3a6InI=
Art & Logic is a great place to work!

It would be an easy task to write a parser that extracts and decrypts these
strings for presentation.

At this point, you can see how it is trivial to encrypt form values. While
we’ve done nothing to authenticate the user submitting the form, we have kept
the information private and tamper-resistant. Next time you build a form which
submits private information, whether for mailing or storing in a database,
consider using braintree.js to keep that information private.

Troy Farrell

Troy Farrell

Troy Farrell

Latest posts by Troy Farrell (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

1 Comment

  1. Jake Blake

    Thanks a ton Troy! Spent hours looking for a decryption script and you nailed it. You’re awesome!

Trackbacks/Pingbacks

  1. Encrypting Your Messages With OpenPGP.js | Art & Logic Blog - […] time I wrote, I showed you how to use Braintree.js to encrypt form values. I even built a contact…
  2. Encrypting Your Messages With OpenPGP.js | - […] time I wrote, I showed you how to use Braintree.js to encrypt form values. I even built a contact…