Skip to content

Commit

Permalink
Encrypt access tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
micbar committed Oct 5, 2022
1 parent 7b3b699 commit 8d535d8
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 13 deletions.
54 changes: 45 additions & 9 deletions src/core/wopiutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
from urllib.parse import quote_plus as url_quote_plus
import http.client
import flask
import jwt
from werkzeug.utils import secure_filename
import core.commoniface as common
from jwcrypto import jwk, jwe, jwt

# this is the xattr key used for conflicts resolution on the remote storage
LASTSAVETIMEKEY = 'iop.wopi.lastwritetime'
Expand Down Expand Up @@ -99,10 +99,10 @@ def validateAndLogHeaders(op):
srv.refreshconfig()
# validate the access token
try:
acctok = jwt.decode(flask.request.args['access_token'], srv.wopisecret, algorithms=['HS256'])
acctok = decodeAccessToken(flask.request.args['access_token'])
if acctok['exp'] < time.time() or 'cs3org:wopiserver' not in acctok['iss']:
raise jwt.exceptions.ExpiredSignatureError
except (jwt.exceptions.DecodeError, jwt.exceptions.ExpiredSignatureError, KeyError) as e:
raise jwt.JWTExpired
except (jwt.JWTMissingKey, jwt.JWTExpired, jwe.InvalidJWEData, ValueError) as e:
log.info('msg="Expired or malformed token" client="%s" requestedUrl="%s" error="%s" token="%s"' %
(flask.request.remote_addr, flask.request.base_url, str(type(e)) + ': ' + str(e), flask.request.args['access_token']))
return 'Invalid access token', http.client.UNAUTHORIZED
Expand Down Expand Up @@ -202,11 +202,22 @@ def generateAccessToken(userid, fileid, viewmode, user, folderurl, endpoint, app
# does not set appname when the app is not proxied, so we optimistically assume it's Collabora and let it go)
log.info('msg="Forcing read-only access to ODF file" filename="%s"' % statinfo['filepath'])
viewmode = ViewMode.READ_ONLY
acctok = jwt.encode({'userid': userid, 'wopiuser': wopiuser, 'filename': statinfo['filepath'], 'username': username,
'viewmode': viewmode.value, 'folderurl': folderurl, 'endpoint': endpoint,
'appname': appname, 'appediturl': appediturl, 'appviewurl': appviewurl,
'exp': exptime, 'iss': 'cs3org:wopiserver:%s' % WOPIVER}, # standard claims
srv.wopisecret, algorithm='HS256')

acctokValues = {
'userid': userid,
'wopiuser': wopiuser,
'filename': statinfo['filepath'],
'username': username,
'viewmode': viewmode.value,
'folderurl': folderurl,
'endpoint': endpoint,
'appname': appname,
'appediturl': appediturl,
'appviewurl': appviewurl,
'exp': exptime,
'iss': 'cs3org:wopiserver:%s' % WOPIVER
}
acctok = encodeAccessToken(acctokValues)
log.info('msg="Access token generated" userid="%s" wopiuser="%s" mode="%s" endpoint="%s" filename="%s" inode="%s" '
'mtime="%s" folderurl="%s" appname="%s" expiration="%d" token="%s"' %
(userid[-20:], wopiuser if wopiuser != userid else username, viewmode, endpoint,
Expand All @@ -216,6 +227,31 @@ def generateAccessToken(userid, fileid, viewmode, user, folderurl, endpoint, app
return statinfo['inode'], acctok, viewmode


def encodeAccessToken(acctok):
# Create a signed JWT
token = jwt.JWT(header={"alg": "HS256"}, claims=acctok)
key = jwk.JWK.from_password(srv.wopisecret)
token.make_signed_token(key)

# if enabled, create an encrypted JWT (JWE)
if srv.config.getboolean('security', "encrcyptjwetoken"):
token = jwt.JWT(header={"alg": "A256KW", "enc": "A256CBC-HS512"}, claims=token.serialize())
token.make_encrypted_token(srv.tokensecret)

return token.serialize()


def decodeAccessToken(token):
# if enabled, decrypt the JWE
if srv.config.getboolean('security', "encrcyptjwetoken"):
token = jwt.JWT(key=srv.tokensecret, jwt=token, expected_type="JWE").claims

# decode the signed JWT
token = jwt.JWT(key=jwk.JWK.from_password(srv.wopisecret), jwt=token, expected_type="JWS")

return json.loads(token.claims)


def encodeLock(lock):
'''Generates the lock payload for the storage given the raw metadata'''
if lock:
Expand Down
25 changes: 21 additions & 4 deletions src/wopiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import flask # Flask app server
from werkzeug.exceptions import NotFound as Flask_NotFound
from werkzeug.exceptions import MethodNotAllowed as Flask_MethodNotAllowed
import jwt # JSON Web Tokens support
from jwcrypto import jwk, jwe, jwt # JSON Web Tokens support
from prometheus_flask_exporter import PrometheusMetrics # Prometheus support

except ImportError:
Expand Down Expand Up @@ -110,6 +110,8 @@ def init(cls):
cls.codetypes = cls.config.get('general', 'codeofficetypes', fallback='.odt .ods .odp').split()
with open(cls.config.get('security', 'wopisecretfile')) as s:
cls.wopisecret = s.read().strip('\n')
if cls.config.getboolean('security', "encrcyptjwetoken"):
initEncryptionKey(cls)
with open(cls.config.get('security', 'iopsecretfile')) as s:
cls.iopsecret = s.read().strip('\n')
cls.tokenvalidity = cls.config.getint('general', 'tokenvalidity')
Expand Down Expand Up @@ -358,11 +360,11 @@ def iopDownload():
'''Returns the file's content for a given valid access token. Used as a download URL,
so that the path and possibly the x-access-token are never explicitly visible.'''
try:
acctok = jwt.decode(flask.request.args['access_token'], Wopi.wopisecret, algorithms=['HS256'])
acctok = utils.decodeAccessToken(flask.request.args['access_token'])
if acctok['exp'] < time.time():
raise jwt.exceptions.ExpiredSignatureError
raise jwt.JWTExpired
return core.wopi.getFile(0, acctok) # note that here we exploit the non-dependency from fileid
except (jwt.exceptions.DecodeError, jwt.exceptions.ExpiredSignatureError, KeyError) as e:
except (jwt.JWTMissingKey, jwt.JWTExpired, jwe.InvalidJWEData, ValueError) as e:
Wopi.log.info('msg="Expired or malformed token" client="%s" requestedUrl="%s" error="%s" token="%s"' %
(flask.request.remote_addr, flask.request.base_url, e, flask.request.args['access_token']))
return 'Invalid access token', http.client.UNAUTHORIZED
Expand Down Expand Up @@ -619,6 +621,21 @@ def cboxDownload_deprecated():
return iopDownload()


def initEncryptionKey(cls):
try:
with open(cls.config.get('security', 'jwesecretfile'), 'r') as kf:
key_data = kf.read()
key = jwk.JWK.from_json(key_data)
except Exception as e:
Wopi.log.info('msg="No encryption key file found: %s"' % str(e))
try:
key = jwk.JWK.generate(kty='oct', size=256)
Wopi.log.info('msg="Generating new encryption key"')
with open(cls.config.get('security', 'jwesecretfile'), 'w') as kwf:
kwf.write(key.export())
except Exception as ge:
Wopi.log.critical('msg="Error during encryption key generation: %s"' % str(ge))
cls.tokensecret = key
#
# Start the Flask endless listening loop
#
Expand Down

0 comments on commit 8d535d8

Please sign in to comment.