Arya
Arya

Reputation: 1469

How can I validate a Fernet token without using the key?

I'm using the cryptography.fernet library to encrypt some small text, and when I receive that text on my server, I'd like to validate that it is actually valid cipher-text before making an RPC to decrypt it, so that my server can just return that the data is invalid. Is there some cheap way to do that, just to avoid cases of spam?

For example, if this is my cyphertext:

>>> k = Fernet.generate_key()
>>> f = Fernet(k)
>>> c = f.encrypt("[email protected]")
>>> c
'gAAAAABdCC4Z8fqgRu7fCv2e7cvPm46rMwTVmSJK6guR5vrnvjaCICXKI1cI-_qr3Cs_z602a4tS-sMYm_smSOzgwOJ8biVQDqlyyyt-iLcxQNCOmjBywwM='

Is there any cheap way to validate that the string beginning with gAAAA is valid? Or that the string 'abcde' would be invalid?

Thanks!

Upvotes: 1

Views: 2234

Answers (1)

Marco Bonelli
Marco Bonelli

Reputation: 69276

Well, take a look at the Fernet specification. A Fernet token (this is how the string you are talking about is called) is structured like this (where means concatenation):

token = urlsafe_b64encode(Version ‖ Timestamp ‖ IV ‖ Ciphertext ‖ HMAC)

The HMAC, which is the last part of the token, is computed from the initial part (Version ‖ Timestamp ‖ IV ‖ Ciphertext) using the signing key (which is the first half of the key), as documented by the specification.

The problem is that you don't have the key, therefore the only thing you can do is to check the initial fields of the token after decoding it from Base64:

  • The Version should be 0x80 which means Version 1 (again as documented in the spec).
  • The Timestamp is the timestamp associated with the token: you can check if this number is in a specific range to discard any expired or malformed token.

So, this what you can do to "reduce the spam":

from base64 import urlsafe_b64decode
from struct import unpack
from datetime import datetime, timedelta

bin_token = urlsafe_b64decode(c) # <-- c is the Fernet token you received
version, timestamp = unpack('>BQ', bin_token[:9])

tok_age = datetime.now() - datetime.fromtimestamp(timestamp)
max_age = timedelta(7) # 7 days

if version != 0x80:
    print 'Invalid token version!'

if tok_age > max_age:
    print 'Token expired!'
elif tok_age < timedelta(0):
    print 'Token timestamp in the future! Invalid token!'

BONUS: as I said, you cannot verify the validity of the token if you don't have at least the signing key (which is the first half of the key). So the following of course does not apply to your scenario, but let's suppose you have the signing key. In such case, in addition to the above check which you should still do, you could do the following to verify the validity of the token:

from base64 import urlsafe_b64decode
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.hmac import HMAC
from cryptography.hazmat.backends import default_backend

bin_data = urlsafe_b64decode(c)

# Assuming you have this:
signing_key = "???" # should be urlsafe_b64decode(k)[:16]

client_data = bin_data[:-32]
client_hmac = bin_data[-32:]

print 'Client HMAC:', client_hmac.encode('hex')

real_hmac = HMAC(signing_key, hashes.SHA256(), default_backend())
real_hmac.update(client_data)
real_hmac = real_hmac.finalize()

print 'Real HMAC  :', real_hmac.encode('hex')

if client_hmac == real_hmac:
    print 'Token seems valid!'
else:
    print 'Token does NOT seem valid!'

Upvotes: 3

Related Questions