Skip to content

Commit 8333f98

Browse files
committed
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.
1 parent 68f0cfe commit 8333f98

File tree

5 files changed

+50
-15
lines changed

5 files changed

+50
-15
lines changed

docs/authentication.rst

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Community authentication 2.0
2-
============================
1+
Community authentication 2.0-4.0
2+
================================
33
While the old community authentication system was simply having the
44
clients call a PostgreSQL function on the main website server, version
55
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:
6363
#. This dictionary of information is then URL-encoded.
6464
#. The resulting URL-encoded string is padded with spaces to an even
6565
16 bytes, and is then AES-SIV encrypted with a shared key and a 16
66-
byte nonce. This key is stored in the main website system and
66+
byte nonce (v4 uses ChaCha20_Poly1305 with standard size key and nonce,
67+
but v3 is the preferred version). This key is stored in the main website system and
6768
indexed by the site id, and it is stored in the settings of the
6869
community website somewhere. Since this key is what protects the
6970
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:
7778
#. The community website detects that this is a redirected authentication
7879
response, and starts processing it specifically.
7980
#. Using the shared key, the data is decrypted (while first being base64
80-
decoded, of course). Since authenticated encryption using AES-SIV
81+
decoded, of course). Since authenticated encryption using AES-SIV or ChaCha20_Poly1305
8182
is used, this step will fail if there has been any tampering with the
8283
data.
8384
#. The resulting string is urldecoded - and if any errors occur in the
@@ -115,6 +116,17 @@ The flow for a logout request is trivial:
115116
at the URL <redirection_url>?s=logout (where redirection_url is the
116117
same URL as when logging in)
117118

119+
Versions
120+
--------
121+
The different versions are primarily different in that they use different
122+
encryption algorithms.
123+
124+
v2 uses standard AES without authentication. This version is *deprecated*.
125+
v3 uses AES-SIV authenticated encryption. This is the *recommended* vcersion.
126+
v4 uses ChaCha20_Poly1305 authenticated encryption, for platforms that don't
127+
support AES-SIV.
128+
129+
118130
Searching
119131
---------
120132
The community authentication system also supports an API for searching for

pgweb/account/migrations/0010_communityauthsite_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
1313
migrations.AddField(
1414
model_name='communityauthsite',
1515
name='version',
16-
field=models.IntegerField(choices=[(2, 2), (3, 3)], default=2),
16+
field=models.IntegerField(choices=[(2, 2), (3, 3), (4, 4)], default=2),
1717
),
1818
]

pgweb/account/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class CommunityAuthSite(models.Model):
1818
apiurl = models.URLField(max_length=200, null=False, blank=True)
1919
cryptkey = models.CharField(max_length=100, null=False, blank=False,
2020
help_text="Use tools/communityauth/generate_cryptkey.py to create a key")
21-
version = models.IntegerField(choices=((2, 2), (3, 3)), default=2)
21+
version = models.IntegerField(choices=((2, 2), (3, 3), (4, 4)), default=2)
2222
comment = models.TextField(null=False, blank=True)
2323
org = models.ForeignKey(CommunityAuthOrg, null=False, blank=False, on_delete=models.CASCADE)
2424
cooloff_hours = models.PositiveIntegerField(null=False, blank=False, default=0,

pgweb/account/views.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import base64
1818
import urllib.parse
1919
from Cryptodome.Cipher import AES
20+
from Cryptodome.Cipher import ChaCha20_Poly1305
2021
from Cryptodome import Random
2122
import time
2223
import json
@@ -721,11 +722,14 @@ def communityauth(request, siteid):
721722
# the first block more random..
722723
s = "t=%s&%s" % (int(time.time()), urllib.parse.urlencode(info))
723724

724-
if site.version == 3:
725-
# v3 = authenticated encryption
725+
if site.version in (3, 4):
726+
# v3 = authenticated encryption, v4 = authenticated encryption with XChaCha20-Poly1305
726727
r = Random.new()
727-
nonce = r.read(16)
728-
encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce)
728+
nonce = r.read(16 if site.version == 3 else 24)
729+
if site.version == 3:
730+
encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce)
731+
else:
732+
encryptor = ChaCha20_Poly1305.new(key=base64.b64decode(site.cryptkey), nonce=nonce)
729733
cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii'))
730734
redirparams = {
731735
'd': base64.urlsafe_b64encode(cipher),
@@ -785,11 +789,14 @@ def communityauth_consent(request, siteid):
785789

786790

787791
def _encrypt_site_response(site, s, version):
788-
if version == 3:
789-
# Use authenticated encryption
792+
if version in (3, 4):
793+
# Use authenticated encryption (v3 = SIV, v4 = ChaCha20_Poly1305
790794
r = Random.new()
791-
nonce = r.read(16)
792-
encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce)
795+
nonce = r.read(16 if site.version == 3 else 24)
796+
if site.version == 3:
797+
encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_SIV, nonce=nonce)
798+
else:
799+
encryptor = ChaCha20_Poly1305.new(key=site.cryptkey, nonce=nonce)
793800
cipher, tag = encryptor.encrypt_and_digest(s.encode('ascii'))
794801

795802
return "&".join((

tools/communityauth/generate_cryptkey.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,29 @@
77

88
from Cryptodome import Random
99
import base64
10+
import sys
11+
12+
13+
def usage():
14+
print("Usage: generate_cryptkey.py <version>")
15+
print("")
16+
print("Version must be 3 or 4, representing the version of community authentication encryption to use")
17+
sys.exit(0)
18+
1019

1120
if __name__ == "__main__":
21+
if len(sys.argv) != 2:
22+
usage()
23+
if sys.argv[1] not in ("3", "4"):
24+
usage()
25+
26+
version = int(sys.argv[1])
27+
1228
print("The next row contains a 64-byte (512-bit) symmetric crypto key.")
1329
print("This key should be used to integrate a community auth site.")
1430
print("Note that each site should have it's own key!!")
1531
print("")
1632

1733
r = Random.new()
18-
key = r.read(64)
34+
key = r.read(64 if version == 3 else 32)
1935
print(base64.b64encode(key).decode('ascii'))

0 commit comments

Comments
 (0)