From: Magnus Hagander Date: Tue, 12 Aug 2025 14:41:28 +0000 (+0200) Subject: Implement community auth using ChaCha20_Poly1305 encryption X-Git-Url: http://git.postgresql.org/gitweb/delmail?a=commitdiff_plain;h=8333f98e373e0c6d8da1978ba7fcd96eca604cce;p=pgweb.git Implement community auth using ChaCha20_Poly1305 encryption This is called version 4 (though version 3 is the preferred version so they are not strictly in increasing number order), and is intended to be used for platforms that don't support AES-SIV encryption. --- diff --git a/docs/authentication.rst b/docs/authentication.rst index b63e250c..181c189e 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -1,5 +1,5 @@ -Community authentication 2.0 -============================ +Community authentication 2.0-4.0 +================================ While the old community authentication system was simply having the clients call a PostgreSQL function on the main website server, version 2.0 of the system uses browser redirects to perform this. This allows @@ -63,7 +63,8 @@ The flow of an authentication in the 2.0 system is fairly simple: #. This dictionary of information is then URL-encoded. #. The resulting URL-encoded string is padded with spaces to an even 16 bytes, and is then AES-SIV encrypted with a shared key and a 16 - byte nonce. This key is stored in the main website system and + byte nonce (v4 uses ChaCha20_Poly1305 with standard size key and nonce, + but v3 is the preferred version). This key is stored in the main website system and indexed by the site id, and it is stored in the settings of the community website somewhere. Since this key is what protects the authentication, it should be treated as very valuable. @@ -77,7 +78,7 @@ The flow of an authentication in the 2.0 system is fairly simple: #. The community website detects that this is a redirected authentication response, and starts processing it specifically. #. Using the shared key, the data is decrypted (while first being base64 - decoded, of course). Since authenticated encryption using AES-SIV + decoded, of course). Since authenticated encryption using AES-SIV or ChaCha20_Poly1305 is used, this step will fail if there has been any tampering with the data. #. The resulting string is urldecoded - and if any errors occur in the @@ -115,6 +116,17 @@ The flow for a logout request is trivial: at the URL ?s=logout (where redirection_url is the same URL as when logging in) +Versions +-------- +The different versions are primarily different in that they use different +encryption algorithms. + +v2 uses standard AES without authentication. This version is *deprecated*. +v3 uses AES-SIV authenticated encryption. This is the *recommended* vcersion. +v4 uses ChaCha20_Poly1305 authenticated encryption, for platforms that don't + support AES-SIV. + + Searching --------- The community authentication system also supports an API for searching for diff --git a/pgweb/account/migrations/0010_communityauthsite_version.py b/pgweb/account/migrations/0010_communityauthsite_version.py index 1b4ab2b8..00574d23 100644 --- a/pgweb/account/migrations/0010_communityauthsite_version.py +++ b/pgweb/account/migrations/0010_communityauthsite_version.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='communityauthsite', name='version', - field=models.IntegerField(choices=[(2, 2), (3, 3)], default=2), + field=models.IntegerField(choices=[(2, 2), (3, 3), (4, 4)], default=2), ), ] diff --git a/pgweb/account/models.py b/pgweb/account/models.py index 55459b9a..6ee715b2 100644 --- a/pgweb/account/models.py +++ b/pgweb/account/models.py @@ -18,7 +18,7 @@ class CommunityAuthSite(models.Model): apiurl = models.URLField(max_length=200, null=False, blank=True) cryptkey = models.CharField(max_length=100, null=False, blank=False, help_text="Use tools/communityauth/generate_cryptkey.py to create a key") - version = models.IntegerField(choices=((2, 2), (3, 3)), default=2) + version = models.IntegerField(choices=((2, 2), (3, 3), (4, 4)), default=2) comment = models.TextField(null=False, blank=True) org = models.ForeignKey(CommunityAuthOrg, null=False, blank=False, on_delete=models.CASCADE) cooloff_hours = models.PositiveIntegerField(null=False, blank=False, default=0, diff --git a/pgweb/account/views.py b/pgweb/account/views.py index 809d4bea..d674f3fc 100644 --- a/pgweb/account/views.py +++ b/pgweb/account/views.py @@ -17,6 +17,7 @@ from django.db.models import Q, Prefetch import base64 import urllib.parse from Cryptodome.Cipher import AES +from Cryptodome.Cipher import ChaCha20_Poly1305 from Cryptodome import Random import time import json @@ -721,11 +722,14 @@ def communityauth(request, siteid): # the first block more random.. s = "t=%s&%s" % (int(time.time()), urllib.parse.urlencode(info)) - if site.version == 3: - # v3 = authenticated encryption + if site.version in (3, 4): + # v3 = authenticated encryption, v4 = authenticated encryption with XChaCha20-Poly1305 r = Random.new() - nonce = r.read(16) - encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce) + nonce = r.read(16 if site.version == 3 else 24) + if site.version == 3: + encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce) + else: + encryptor = ChaCha20_Poly1305.new(key=base64.b64decode(site.cryptkey), nonce=nonce) cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii')) redirparams = { 'd': base64.urlsafe_b64encode(cipher), @@ -785,11 +789,14 @@ def communityauth_consent(request, siteid): def _encrypt_site_response(site, s, version): - if version == 3: - # Use authenticated encryption + if version in (3, 4): + # Use authenticated encryption (v3 = SIV, v4 = ChaCha20_Poly1305 r = Random.new() - nonce = r.read(16) - encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce) + nonce = r.read(16 if site.version == 3 else 24) + if site.version == 3: + encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce) + else: + encryptor = ChaCha20_Poly1305.new(key=site.cryptkey, nonce=nonce) cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii')) return "&".join(( diff --git a/tools/communityauth/generate_cryptkey.py b/tools/communityauth/generate_cryptkey.py index ee70d7f0..ba84399a 100755 --- a/tools/communityauth/generate_cryptkey.py +++ b/tools/communityauth/generate_cryptkey.py @@ -7,13 +7,29 @@ from Cryptodome import Random import base64 +import sys + + +def usage(): + print("Usage: generate_cryptkey.py ") + print("") + print("Version must be 3 or 4, representing the version of community authentication encryption to use") + sys.exit(0) + if __name__ == "__main__": + if len(sys.argv) != 2: + usage() + if sys.argv[1] not in ("3", "4"): + usage() + + version = int(sys.argv[1]) + 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(64) + key = r.read(64 if version == 3 else 32) print(base64.b64encode(key).decode('ascii'))