diff options
| author | Magnus Hagander | 2025-04-01 11:41:08 +0000 |
|---|---|---|
| committer | Magnus Hagander | 2025-06-11 18:26:21 +0000 |
| commit | de76f82f62c0818f041c28a7a5f4ccdcb2e3a1ab (patch) | |
| tree | 1ad83270e4be3e1285833f9d4d494ffcc089211a /tools | |
| parent | 7a42e2a5f595466d0632cd2f8cd1893b88866619 (diff) | |
Implement authenticated encryption in community auth
This creates a community auth version 3 (previous one being 2, and 1 is
long gone) trhat uses AES_SIV as the encryption method instead of
regular AES_CBC, and validates the digests on all accounts.
As this gets deployed on servers incrementall, the version has to be
specified in the database record for the site. We could have the site
indicate this itself, but doing it this way seems safer as it will then
just break for any app that accidentally reverts the plugin.
Reviewed by Jacob Champion
Diffstat (limited to 'tools')
| -rwxr-xr-x | tools/communityauth/generate_cryptkey.py | 4 | ||||
| -rw-r--r-- | tools/communityauth/sample/django/auth.py | 75 | ||||
| -rwxr-xr-x | tools/communityauth/test_auth.py | 21 |
3 files changed, 65 insertions, 35 deletions
diff --git a/tools/communityauth/generate_cryptkey.py b/tools/communityauth/generate_cryptkey.py index c0ca505c..ee70d7f0 100755 --- a/tools/communityauth/generate_cryptkey.py +++ b/tools/communityauth/generate_cryptkey.py @@ -9,11 +9,11 @@ from Cryptodome import Random import base64 if __name__ == "__main__": - print("The next row contains a 32-byte (256-bit) symmetric crypto key.") + print("The next row contains a 64-byte (512-bit) symmetric crypto key.") print("This key should be used to integrate a community auth site.") print("Note that each site should have it's own key!!") print("") r = Random.new() - key = r.read(32) + key = r.read(64) print(base64.b64encode(key).decode('ascii')) diff --git a/tools/communityauth/sample/django/auth.py b/tools/communityauth/sample/django/auth.py index 9343fc0f..dc5c1fb6 100644 --- a/tools/communityauth/sample/django/auth.py +++ b/tools/communityauth/sample/django/auth.py @@ -41,7 +41,7 @@ import hmac from urllib.parse import urlencode, parse_qs import requests from Cryptodome.Cipher import AES -from Cryptodome.Hash import SHA +from Cryptodome.Hash import SHA256 from Cryptodome import Random import time @@ -75,15 +75,19 @@ def login(request): s = "t=%s&%s" % (int(time.time()), urlencode({'r': request.GET['next']})) # Now encrypt it r = Random.new() - iv = r.read(16) - encryptor = AES.new(SHA.new(settings.SECRET_KEY.encode('ascii')).digest()[:16], AES.MODE_CBC, iv) - cipher = encryptor.encrypt(s.encode('ascii') + b' ' * (16 - (len(s) % 16))) # pad to 16 bytes - - return HttpResponseRedirect("%s?d=%s$%s" % ( - settings.PGAUTH_REDIRECT, - base64.b64encode(iv, b"-_").decode('utf8'), - base64.b64encode(cipher, b"-_").decode('utf8'), - )) + nonce = r.read(16) + encryptor = AES.new( + SHA256.new(settings.SECRET_KEY.encode('ascii')).digest()[:32], AES.MODE_SIV, nonce=nonce + ) + cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii')) + + return HttpResponseRedirect("%s?%s" % (settings.PGAUTH_REDIRECT, urlencode({ + 'd': '$'.join(( + base64.b64encode(nonce, b"-_").decode('utf8'), + base64.b64encode(cipher, b"-_").decode('utf8'), + base64.b64encode(tag, b"-_").decode('utf8'), + )), + }))) else: return HttpResponseRedirect(settings.PGAUTH_REDIRECT) @@ -103,17 +107,24 @@ def auth_receive(request): # This was a logout request return HttpResponseRedirect('/') - if 'i' not in request.GET: - return HttpResponse("Missing IV in url!", status=400) + if 'n' not in request.GET: + return HttpResponse("Missing nonce in url!", status=400) if 'd' not in request.GET: return HttpResponse("Missing data in url!", status=400) + if 't' not in request.GET: + return HttpResponse("Missing tag in url!", status=400) # Set up an AES object and decrypt the data we received try: - decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), - AES.MODE_CBC, - base64.b64decode(str(request.GET['i']), "-_")) - s = decryptor.decrypt(base64.b64decode(str(request.GET['d']), "-_")).rstrip(b' ').decode('utf8') + decryptor = AES.new( + base64.b64decode(settings.PGAUTH_KEY), + AES.MODE_SIV, + nonce=base64.b64decode(str(request.GET['n']), "-_"), + ) + s = decryptor.decrypt_and_verify( + base64.b64decode(str(request.GET['d']), "-_"), + base64.b64decode(str(request.GET['t']), "-_"), + ).rstrip(b' ').decode('utf8') except UnicodeDecodeError: return HttpResponse("Badly encoded data found", 400) except Exception: @@ -200,11 +211,16 @@ We apologize for the inconvenience. # Finally, check of we have a data package that tells us where to # redirect the user. if 'd' in data: - (ivs, datas) = data['d'][0].split('$') - decryptor = AES.new(SHA.new(settings.SECRET_KEY.encode('ascii')).digest()[:16], - AES.MODE_CBC, - base64.b64decode(ivs, b"-_")) - s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(b' ').decode('utf8') + (nonces, datas, tags) = data['d'][0].split('$') + decryptor = AES.new( + SHA256.new(settings.SECRET_KEY.encode('ascii')).digest()[:32], + AES.MODE_SIV, + nonce=base64.b64decode(nonces, b"-_"), + ) + s = decryptor.decrypt_and_verify( + base64.b64decode(datas, "-_"), + base64.b64decode(tags, "-_"), + ).rstrip(b' ').decode('utf8') try: rdata = parse_qs(s, strict_parsing=True) except ValueError: @@ -304,17 +320,24 @@ def user_search(searchterm=None, userid=None): r = requests.get( '{0}search/'.format(settings.PGAUTH_REDIRECT), params=q, + timeout=10, ) if r.status_code != 200: return [] - (ivs, datas) = r.text.encode('utf8').split(b'&') + (nonces, datas, tags) = r.text.encode('utf8').split(b'&') # Decryption time - decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), - AES.MODE_CBC, - base64.b64decode(ivs, "-_")) - s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(b' ').decode('utf8') + decryptor = AES.new( + base64.b64decode(settings.PGAUTH_KEY), + AES.MODE_SIV, + nonce=base64.b64decode(nonces, "-_") + ) + s = decryptor.decrypt_and_verify( + base64.b64decode(datas, "-_"), + base64.b64decode(tags, "-_"), + ).rstrip(b' ').decode('utf8') + j = json.loads(s) return j diff --git a/tools/communityauth/test_auth.py b/tools/communityauth/test_auth.py index ea6d1fa1..db16f288 100755 --- a/tools/communityauth/test_auth.py +++ b/tools/communityauth/test_auth.py @@ -54,12 +54,19 @@ if __name__ == "__main__": s = "t=%s&%s" % (int(time.time() + 300), urllib.parse.urlencode(info)) r = Random.new() - iv = r.read(16) - encryptor = AES.new(base64.b64decode(options.key), AES.MODE_CBC, iv) - cipher = encryptor.encrypt(s.encode('ascii') + b' ' * (16 - (len(s) % 16))) + nonce = r.read(16) + encryptor = AES.new( + base64.b64decode(options.key), + AES.MODE_SIV, + nonce=nonce, + ) + cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii')) + + redirparams = { + 'd': base64.b64encode(cipher, b"-_").decode('ascii'), + 'n': base64.b64encode(nonce, b"-_").decode('ascii'), + 't': base64.b64encode(tag, b"-_").decode('ascii'), + } print("Paste the following after the receiving url:") - print("?i=%s&d=%s" % ( - base64.b64encode(iv, b"-_").decode('ascii'), - base64.b64encode(cipher, b"-_").decode('ascii'), - )) + print("?" + urllib.parse.urlencode(redirparams)) |
