diff --git a/server/src/uds/core/managers/crypto.py b/server/src/uds/core/managers/crypto.py index e216fddc..1b8b75da 100644 --- a/server/src/uds/core/managers/crypto.py +++ b/server/src/uds/core/managers/crypto.py @@ -173,6 +173,9 @@ class CryptoManager: return toDecode[4 : 4 + struct.unpack('>i', toDecode[:4])[0]] def xor(self, s1: typing.Union[str, bytes], s2: typing.Union[str, bytes]) -> bytes: + if len(s2) == 0: + return b'' # Protect against division by cero + if isinstance(s1, str): s1 = s1.encode('utf-8') if isinstance(s2, str): @@ -202,12 +205,12 @@ class CryptoManager: if isinstance(key, str): key = key.encode() - if not cryptText: + if not cryptText or not key: return '' try: return self.AESDecrypt(cryptText, key).decode('utf-8') - except Exception: # Error decoding stored crypted password, return empty one + except Exception: # Error decoding crypted element, return empty one return '' def loadPrivateKey(self, rsaKey: str): diff --git a/server/src/uds/models/ticket_store.py b/server/src/uds/models/ticket_store.py index 01df404e..f0079889 100644 --- a/server/src/uds/models/ticket_store.py +++ b/server/src/uds/models/ticket_store.py @@ -45,6 +45,7 @@ logger = logging.getLogger(__name__) ValidatorType = typing.Callable[[typing.Any], bool] +SECURED = '#SECURE#' # Just a "different" owner. If used anywhere, it's not important (will not fail), but class TicketStore(UUIDModel): """ @@ -99,9 +100,14 @@ class TicketStore(UUIDModel): validity is in seconds """ validator = pickle.dumps(validatorFnc) if validatorFnc else None + data = pickle.dumps(data) + if secure: - pass + if not owner: + raise ValueError('Tried to use a secure ticket without owner') + data = cryptoManager().AESCrypt(data, owner.encode()) + owner = SECURED # So data is REALLY encrypted return TicketStore.objects.create( stamp=getSqlDatetime(), @@ -111,40 +117,6 @@ class TicketStore(UUIDModel): owner=owner, ).uuid - @staticmethod - def store( - uuid: str, - data: str, - validatorFnc: typing.Optional[ValidatorType] = None, - validity: int = DEFAULT_VALIDITY, - owner: typing.Optional[str] = None, - secure: bool = False, - ) -> None: - """ - Stores an ticketstore. If one with this uuid already exists, replaces it. Else, creates a new one - validity is in seconds - """ - validator = pickle.dumps(validatorFnc) if validatorFnc else None - - if secure: # TODO: maybe in the future? what will mean "secure?" :) - pass - - try: - t = TicketStore.objects.get(uuid=uuid) - t.data = pickle.dumps(data) - t.stamp = getSqlDatetime() - t.validity = validity - t.owner = owner - t.save() - except TicketStore.DoesNotExist: - TicketStore.objects.create( - uuid=uuid, - stamp=getSqlDatetime(), - data=pickle.dumps(data), - validator=validator, - validity=validity, - ) - @staticmethod def get( uuid: str, @@ -153,7 +125,13 @@ class TicketStore(UUIDModel): secure: bool = False, ) -> typing.Any: try: - t = TicketStore.objects.get(uuid=uuid, owner=owner) + dbOwner = owner + if secure: + if not owner: + raise ValueError('Tried to use a secure ticket without owner') + dbOwner = SECURED + + t = TicketStore.objects.get(uuid=uuid, owner=dbOwner) validity = datetime.timedelta(seconds=t.validity) now = getSqlDatetime() @@ -161,8 +139,12 @@ class TicketStore(UUIDModel): if t.stamp + validity < now: raise TicketStore.InvalidTicket('Not valid anymore') - # if secure: TODO - data = pickle.loads(t.data) + data: bytes = t.data + + if secure: # Owner has already been tested and it's not emtpy + data = cryptoManager().AESDecrypt(data, typing.cast(str, owner).encode()) + + data = pickle.loads(data) # If has validator, execute it if t.validator: @@ -173,7 +155,7 @@ class TicketStore(UUIDModel): if invalidate is True: t.stamp = now - validity - datetime.timedelta(seconds=1) - t.save() + t.save(update_fields=['stamp']) return data except TicketStore.DoesNotExist: @@ -207,16 +189,12 @@ class TicketStore(UUIDModel): TicketStore.objects.filter(stamp__lt=cleanSince).delete() def __str__(self) -> str: - if self.validator: - validator = pickle.loads(self.validator) - else: - validator = None + data = pickle.loads(self.data) if self.owner != SECURED else '{Secure Ticket}' - return 'Ticket id: {}, Secure: {}, Stamp: {}, Validity: {}, Validator: {}, Data: {}'.format( + return 'Ticket id: {}, Owner: {}, Stamp: {}, Validity: {}, Data: {}'.format( self.uuid, self.owner, self.stamp, self.validity, - validator, - pickle.loads(self.data), + data, ) diff --git a/server/src/uds/transports/RDP/rdp_base.py b/server/src/uds/transports/RDP/rdp_base.py index 8651bdfe..78ca08ef 100644 --- a/server/src/uds/transports/RDP/rdp_base.py +++ b/server/src/uds/transports/RDP/rdp_base.py @@ -372,7 +372,7 @@ class BaseRDPTransport(transports.Transport): user: 'models.User', password: str, ) -> typing.Dict[str, str]: - return self.processUserPassword(userService, user, password) + return self.processUserPassword(typing.cast('models.UserService', userService), user, password) def getScript( self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any] diff --git a/server/src/uds/web/views/service.py b/server/src/uds/web/views/service.py index 6f4f3199..e93127ca 100644 --- a/server/src/uds/web/views/service.py +++ b/server/src/uds/web/views/service.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2019 Virtual Cable S.L. +# Copyright (c) 2012-2020 Virtual Cable S.L.U. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -44,7 +44,11 @@ from uds.core.ui.images import DEFAULT_IMAGE from uds.core.util.model import processUuid from uds.models import Transport, Image from uds.core.util import html, log -from uds.core.services.exceptions import ServiceNotReadyError, MaxServicesReachedError, ServiceAccessDeniedByCalendar +from uds.core.services.exceptions import ( + ServiceNotReadyError, + MaxServicesReachedError, + ServiceAccessDeniedByCalendar, +) from uds.web.util import errors from uds.web.util import services @@ -57,32 +61,37 @@ logger = logging.getLogger(__name__) @webLoginRequired(admin=False) -def transportOwnLink(request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str): +def transportOwnLink( + request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str +): response: typing.MutableMapping[str, typing.Any] = {} # For type checkers to "be happy" try: - res = userServiceManager().getService(request.user, request.os, request.ip, idService, idTransport) + res = userServiceManager().getService( + request.user, request.os, request.ip, idService, idTransport + ) ip, userService, iads, trans, itrans = res # pylint: disable=unused-variable # This returns a response object in fact if itrans and ip: response = { - 'url': itrans.getLink(userService, trans, ip, request.os, request.user, webPassword(request), request) + 'url': itrans.getLink( + userService, + trans, + ip, + request.os, + request.user, + webPassword(request), + request, + ) } except ServiceNotReadyError as e: - response = { - 'running': e.code * 25 - } + response = {'running': e.code * 25} except Exception as e: logger.exception("Exception") - response = { - 'error': str(e) - } + response = {'error': str(e)} - return HttpResponse( - content=json.dumps(response), - content_type='application/json' - ) + return HttpResponse(content=json.dumps(response), content_type='application/json') # Will never reach this return errors.errorView(request, errors.UNKNOWN_ERROR) @@ -114,7 +123,9 @@ def serviceImage(request: 'ExtendedHttpRequest', idImage: str) -> HttpResponse: @webLoginRequired(admin=False) @never_cache -def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str) -> HttpResponse: +def userServiceEnabler( + request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str +) -> HttpResponse: # Maybe we could even protect this even more by limiting referer to own server /? (just a meditation..) logger.debug('idService: %s, idTransport: %s', idService, idTransport) url = '' @@ -123,7 +134,9 @@ def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, i # If meta service, process and rebuild idService & idTransport try: - res = userServiceManager().getService(request.user, request.os, request.ip, idService, idTransport, doTest=False) + res = userServiceManager().getService( + request.user, request.os, request.ip, idService, idTransport, doTest=False + ) scrambler = cryptoManager().randomString(32) password = cryptoManager().symCrypt(webPassword(request), scrambler) @@ -140,7 +153,7 @@ def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, i 'service': 'A' + userService.uuid, 'transport': trans.uuid, 'user': request.user.uuid, - 'password': password + 'password': password, } ticket = TicketStore.create(data) @@ -149,7 +162,9 @@ def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, i logger.debug('Service not ready') # Not ready, show message and return to this page in a while # error += ' (code {0:04X})'.format(e.code) - error = _('Your service is being created, please, wait for a few seconds while we complete it.)') + '({}%)'.format(int(e.code * 25)) + error = _( + 'Your service is being created, please, wait for a few seconds while we complete it.)' + ) + '({}%)'.format(int(e.code * 25)) except MaxServicesReachedError: logger.info('Number of service reached MAX for service pool "%s"', idService) error = errors.errorString(errors.MAX_SERVICES_REACHED) @@ -161,42 +176,54 @@ def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, i error = str(e) return HttpResponse( - json.dumps({ - 'url': str(url), - 'error': str(error) - }), - content_type='application/json' + json.dumps({'url': str(url), 'error': str(error)}), + content_type='application/json', ) + def closer(request: 'ExtendedHttpRequest') -> HttpResponse: return HttpResponse('') + @webLoginRequired(admin=False) @never_cache -def action(request: 'ExtendedHttpRequestWithUser', idService: str, actionString: str) -> HttpResponse: - userService = userServiceManager().locateUserService(request.user, idService, create=False) +def action( + request: 'ExtendedHttpRequestWithUser', idService: str, actionString: str +) -> HttpResponse: + userService = userServiceManager().locateUserService( + request.user, idService, create=False + ) response: typing.Any = None rebuild: bool = False if userService: - if actionString == 'release' and userService.deployed_service.allow_users_remove: + if ( + actionString == 'release' + and userService.deployed_service.allow_users_remove + ): rebuild = True log.doLog( userService.deployed_service, log.INFO, - "Removing User Service {} as requested by {} from {}".format(userService.friendly_name, request.user.pretty_name, request.ip), - log.WEB + "Removing User Service {} as requested by {} from {}".format( + userService.friendly_name, request.user.pretty_name, request.ip + ), + log.WEB, ) userServiceManager().requestLogoff(userService) userService.release() - elif (actionString == 'reset' - and userService.deployed_service.allow_users_reset - and userService.deployed_service.service.getType().canReset): + elif ( + actionString == 'reset' + and userService.deployed_service.allow_users_reset + and userService.deployed_service.service.getType().canReset + ): rebuild = True log.doLog( userService.deployed_service, log.INFO, - "Reseting User Service {} as requested by {} from {}".format(userService.friendly_name, request.user.pretty_name, request.ip), - log.WEB + "Reseting User Service {} as requested by {} from {}".format( + userService.friendly_name, request.user.pretty_name, request.ip + ), + log.WEB, ) # userServiceManager().requestLogoff(userService) userServiceManager().reset(userService)