Mercurial > repos > cathywise > truststore_import
view PythonTrustStore-0.2.0/py_ts/TrustStoreClient.py @ 1:ff126718bdc5
Uploaded
author | cathywise |
---|---|
date | Wed, 11 Dec 2013 21:05:12 -0500 |
parents | |
children |
line wrap: on
line source
import requests from requests_oauth2 import OAuth2 import simplejson as json import simplejson.scanner from urllib import quote from urlparse import parse_qs, urljoin from functools import wraps from tempfile import NamedTemporaryFile # from subprocess import call, check_output, Popen import subprocess import os from urlparse import urlparse from datetime import datetime import xmltodict import uuid import webbrowser import base64 import binascii import hashlib import textwrap import re import boto.exception import boto.s3.connection as botoConn from boto.s3.key import Key as botoKey from distutils.version import LooseVersion from parts import * # import threading from multiprocessing.pool import ThreadPool # import Queue from passlib.hash import pbkdf2_sha256 from passlib.utils import ab64_decode from itertools import izip_longest import sys # import cProfile #, pstats DASHES = "-----" BEGIN = "BEGIN" END = "END" PUBLIC = "PUBLIC" PRIVATE = "PRIVATE" KEY = "KEY" CERTIFICATE = "CERTIFICATE" class TrustStoreClient(object): """A client for connecting to a TrustStore service, and probably a storage provider. Currently supports S3 and S3-like storage providers (such as Nectar) which are supported by the Python boto library. Requires a version of OpenSSL greater than 1.0, i.e. Mac will have to install using homebrew. """ brewOpenSSL = '/usr/local/Cellar/openssl/' jsonHeaders = {'Content-type': 'application/json', 'Accept': 'application/json'} storesPrefix = "/store" storesList = "/all" filesPrefix = "/file" keysPrefix = "/keys" publicKeysPrefix = "/public_key" privateKeysPrefix = "/private_key" usernamesPrefix = "/username" saltPrefix = "/loginsalt" passwordResetPrefix = "/credentials" providers = [] pieceSize = 209715 openSSL = "openssl" kmsUrl = u'http://localhost:8080/TSSKeyManagementService-Collaboration' imsUrl = u'http://localhost:8080/TSSIntegrityManagementService/services/IMS' client_key = u'my-trusted-client-with-secret' client_secret = u'somesecret' headless = False redirect_uri = "oob" keyFile = None auth = None kmsClient = None username = None def requiresAuth(func): """Wraps a function which requires the client be already sucesfully authenticated with the Key Management Service.""" @wraps(func) def withAuth(self, *args, **kwargs): if self.kmsClient: return func(self, *args, **kwargs) else: raise TrustStoreClientAuthenticationException("Not authenticated!") return withAuth def __init__(self, headless, config=None, accessToken=None): """Set up the client, checking OpenSSL :param headless: Are we running somewhere where opening a browser window would be a bad idea? If we're on a server, this should always be True :type headless: boolean :param config: Optional collection of configuration variables, must have values kmsUrl, imsUrl, client_key and client_secret. :param accessToken: If being re-run, an accessToken can be supplied and autentication skipped. Note that if you're doing this you're expected to have set the username and password properties manually. In which case you may as well just authenticate. """ if os.path.exists(self.brewOpenSSL): versions = os.listdir(self.brewOpenSSL) versions.sort(key=LooseVersion) self.openSSL = self.brewOpenSSL + versions[-1] + "/bin/openssl" sslVersion = subprocess.check_output([self.openSSL, 'version']) m = re.search('([0-9]+.[0-9]+.[0-9]+[a-z]*)', sslVersion) if LooseVersion(m.group(0)) < LooseVersion("1.0.0"): raise OpenSSLVersionException(m.group(0)) elif not headless: print "Working with " + sslVersion if config: self.kmsUrl = config.kmsUrl self.imsUrl = config.imsUrl self.client_key = config.client_key self.client_secret = config.client_secret self.headless = headless self.auth = OAuth2(self.client_key, self.client_secret, self.kmsUrl, self.redirect_uri) if accessToken: self.kmsClient = requests.session(params={'access_token': accessToken}) self.username = self._getUsername() def authenticate(self, username=None, password=None): """Talk to KMS and get the required tokens etc. If the username and password are not supplied OAuth-2 token authentication is attempted. In headless mode thiis will cause a url to be returned where the user will have to login and authorize this application. Will throw exceptions if credentials are rejected or KMS cannot be reached. :param username: The username already registered with KMS. :type username: string/unicode :param password: The user's KMS password. :type password: string/unicode :rtype: None or string (authorisation url) """ if not self.kmsClient: response = None if not username or not password: authorization_url = self.auth.authorize_url(scope='read trust write', response_type='code') if authorization_url: if self.headless: return authorization_url else: webbrowser.open(authorization_url) code = raw_input('Code:') response = self.auth.get_token(code, grant_type='authorization_code') else: self.username = username self.password = password ans = requests.get("%s%s/%s" %(self.kmsUrl, self.saltPrefix, username)) salt = base64.b64decode(ans.text) # Passlib uses a *custom* base64 encoding because it is arse, so fix to normal. sendPassword = base64.b64encode(ab64_decode(pbkdf2_sha256.encrypt(username + password, rounds=1000, salt=salt).split("$")[-1])) login_form = {'username': username, 'password': sendPassword, 'client_id': self.client_key, 'client_secret': self.client_secret, 'grant_type': 'password'} response = requests.post("%s%s" % (self.auth.site, quote(self.auth.token_url)), data=login_form, allow_redirects=True) if isinstance(response.content, basestring): try: response = json.loads(response.content) except ValueError: response = parse_qs(response.content) else: response = response.content if response: try: self.kmsClient = requests.session(params={'access_token': response['access_token']}) if not self.username: self.username = self._getUsername() except KeyError: raise TrustStoreClientAuthenticationException("Credentials not accepted. Response: " + str(response)) else: raise TrustStoreClientAuthenticationException("Communication error with TrustStore server. Is it running?") def twoStageAuth(self, code): """If using oAuth2, this method will need to be called after :func:`authenticate` :param code: The oAuth2 token. """ response = self.auth.get_token(code) self.kmsClient = requests.session(params={'access_token': response['access_token']}) self.username = self._getUsername() def clearAuth(self): """Clear any authentication (username/password) but preserve other configuration (server locations etc)""" self.auth = None self.kmsClient = None self.username = None def addProvider(self, prov): """Tell the client about a new provider to use (in addition) :param prov: The new provider :type prov: `Provider` """ self.providers.append(prov) @requiresAuth def listStores(self): """List all the stores this user has access to :rtype: list of Store objects """ stores = [] r = self.kmsClient.get(self.kmsUrl + self.storesPrefix) # + self.storesList) if 'error' not in r.json: for store in r.json: stores.append(Store(store)) return stores else: print r.text print r.json @requiresAuth def getPrivateKey(self, filename): """Fetch the user's private key from KMS.""" getr = self.kmsClient.get(self.kmsUrl + self.privateKeysPrefix + "/" + self.username) if getr.json and 'error' not in getr.json: key = UserPrivateKey(getr.json) if key.certificate: with open(filename, 'w+') as f: f.write(key.certificate) self.keyFile = filename else: self.setNewKey(filename) return filename else: print "Couldn't find private key?" print getr.text @requiresAuth def setNewKey(self, filename): """Generate a new private key, either because the user doesn't have one yet, or they've requested a new one.""" print "No private key found!" self._generateKeypair(filename) keydata = None if filename: with open(filename, 'r') as f: keydata = f.read() key = UserPrivateKey(None, self.username, keydata) postr = self.kmsClient.post(self.kmsUrl + self.privateKeysPrefix, data=json.dumps(key.dict()), headers=self.jsonHeaders) if postr.status_code != requests.codes.ok: print "Server refused to save new private key." return return filename @requiresAuth def getStore(self, name): """Get the first store (that the user has access to) matching the specified name.""" for store in self.listStores(): if store.friendly_name == name: return store @requiresAuth def createStore(self, store=None, name=None): """Create a new store. :param store: Pre-prepared store to save to KMS. :type store: Store :param name: If no pre-prepared store, create new store with this name. If not supplied, store name will be "default". :type name: string/unicode :rtype: Store """ if not store: if not name: name = "default" store = Store(owner=self._getUsername(), friendly_name=name) store.administrators.append(self._getUsername()) storejson = json.dumps(store.dict()) # print storejson postr = self.kmsClient.post(self.kmsUrl + self.storesPrefix, data=storejson, headers=self.jsonHeaders) if postr.status_code != requests.codes.ok: print "Server refused request to save store description." print postr.status_code print postr.text return store = Store(postr.json) storeFile = StoreProperties() username = str(uuid.uuid4()) password = binascii.b2a_hex(os.urandom(20)) storeFile.ims_url = self.imsUrl storeFile.kms_url = self.kmsUrl storeFile.ims_user = {"ident": username, "secret": password} storeFile.providers = self.providers self._createNewBucket(storeFile) self._createAndStoreEmptyDirectory(store, storeFile) privateKeyFile = self._generatePKCS1Keypair() if privateKeyFile: publicKeyFile = self.__publicKeyFromPrivate(privateKeyFile) storeFile.private_key_bytes = self.__readPrivateKeyFromFile(privateKeyFile) storeFile.public_key_bytes = self.__readPublicKeyFromFile(publicKeyFile) # print storeFile.private_key_bytes # print storeFile.public_key_bytes imsPublicKey = self._imsRegister(username, password, self.__readPublicKeyFromFile(publicKeyFile)) storeFile.ims_public_key_bytes = imsPublicKey # print imsPublicKey _json = postr.json if self._putStoreFile(store, storeFile): putr = self.kmsClient.put(self.kmsUrl + self.storesPrefix, data=json.dumps(store.dict()), headers=self.jsonHeaders) if putr.status_code != requests.codes.ok: print "Sever refused request to save store file." print putr.status_code print putr.text else: _json = putr.json self.__remove(publicKeyFile) self.__remove(privateKeyFile) return Store(_json) @requiresAuth def updateStore(self, store): """Update a store (where root file or permissions have changed, pressumably).""" putr = self.kmsClient.put(self.kmsUrl + self.storesPrefix, data=json.dumps(store.dict()), headers=self.jsonHeaders) if putr.status_code != requests.codes.ok: print putr.status_code print putr.text else: # update access storeFile = self._getStoreFile(store) self._putStoreFile(store, storeFile) return Store(putr.json) @requiresAuth def deleteStore(self, store): """Delete a store (with the same id as this store, anyway). This is not recoverable!!""" url = self.kmsUrl + self.storesPrefix + "/" + str(store.id) delr = self.kmsClient.delete(url) if delr.status_code != requests.codes.ok: print delr.status_code print url print delr.text @requiresAuth def changePassword(self, newPassword): """Change the user's password to a new one. This requires the old password be known!""" newKeyFile = self._changeKeyPassword(self.password, newPassword) key = None with open(newKeyFile, 'rb') as f: key = f.read() ans = requests.get("%s%s/%s" %(self.kmsUrl, self.saltPrefix, self.username)) salt = base64.b64decode(ans.text) sendNewPassword = base64.b64encode(ab64_decode(pbkdf2_sha256.encrypt(self.username + newPassword, rounds=1000, salt=salt).split("$")[-1])) sendOldPassword = base64.b64encode(ab64_decode(pbkdf2_sha256.encrypt(self.username + self.password, rounds=1000, salt=salt).split("$")[-1])) reset = {"password":sendNewPassword, "oldPassword":sendOldPassword, "key":{"username":self.username, "key": key}} postr = self.kmsClient.post(self.kmsUrl + self.passwordResetPrefix, data=json.dumps(reset), headers=self.jsonHeaders) if postr.status_code != requests.codes.ok: print postr.status_code print postr.text @requiresAuth def listDirectory(self, store): """List all the files in the given store. :rtype: Directory """ directory = None makeDirectory = False storeFile = self._getStoreFile(store) if storeFile and storeFile.bucket: provider = self._getCloudService(storeFile) tmpRoot = self._getPartFromCloud(store.index_codename, provider, storeFile.bucket) if tmpRoot: key = self._getKeyForFragment(store.index_codename, store.id) if key: tmpRoot = self._decryptPart(tmpRoot, key, store.iv) if tmpRoot: jsonText = "" with open(tmpRoot) as f: jsonText = f.read() directory = Directory(jsonText=jsonText) self.__remove(tmpRoot) else: makeDirectory = True else: makeDirectory = True self.__remove(tmpRoot) else: makeDirectory = True elif storeFile and len(storeFile.providers) > 0: self._createNewBucket(storeFile) makeDirectory = True else: print "has no bucket" print storeFile if makeDirectory: print "creating empty directory" directory = self._createAndStoreEmptyDirectory(store, storeFile) self.updateStore(store) return directory @requiresAuth def updateDirectory(self, directory, store): # Check for conflicts!! newName = unicode(uuid.uuid4()) storeFile = self._getStoreFile(store) provider = self._getCloudService(storeFile) tmpText = json.dumps(directory.dict()) tmpRoot = self.__temporaryFileWithBytes(tmpText) key = self._generateKey() store.iv = self._generateIV() tmpRoot = self._encryptPart(tmpRoot, key, store.iv) self._putPartInCloud(newName, provider, storeFile.bucket, tmpRoot) self._setKeyForFragment(newName, key, store.id) store.index_codename = newName self.updateStore(store) self.__remove(tmpRoot) pass @requiresAuth def getFile(self, store, files, threads=10): storeFile = self._getStoreFile(store) provider = self._getCloudService(storeFile) if files: keySets = self._getKeysForFragments(files.fragments, store.id) if keySets: clearFile = self.__temporaryFile() with open(clearFile, 'r+b') as f: junk = b'\x00' * (files.remote_size) f.write(junk) # with open(clearFile, 'wb') as f: promises = []; pool = ThreadPool(processes=threads) for fragmentName in keySets: fragment = None for frag in files.fragments: if frag.name == fragmentName: fragment = frag promise = pool.apply_async(self._doFragmentDownload, [clearFile, fragmentName, provider, storeFile, keySets, fragment]) promises.append(promise) for order, promise in enumerate(promises): self.__graphPrinter(order, len(promises)) success = promise.get() self.__graphPrinter(order + 1, len(promises)) if not success: print "Error on part!" break files.local_path = clearFile sys.stdout.write("\n") sys.stdout.flush() return clearFile def __graphPrinter(self, done, total): completed = int(((done) * 100.0) / total) sys.stdout.write(" |" + "=" * completed + "-" * (100 - completed) + "| \r") sys.stdout.flush() @requiresAuth def getBytes(self, start, end, store, files): storeFile = self._getStoreFile(store) provider = self._getCloudService(storeFile) fragments = files.fragments allData = None sizeSoFar = 0 for fragment, idx in enumerate(fragments): if sizeSoFar + fragment.length >= start and sizeSoFar <= end: # Get this fragment key = self._getKeyForFragment(fragment, store.id) isOkay = False attempts = 0 while not isOkay and attempts < 10: tmpPart = self._getPartFromCloud(fragment.name, provider, storeFile.bucket) if tmpPart: isOkay = self._getVerifyPart(tmpPart, fragment.name, storeFile) attempts += 1 else: break if isOkay: tmpPart2 = self._decryptPart(tmpPart, key, fragment.iv) self.__remove(tmpPart) if tmpPart2: with open(tmpPart2, 'rb') as t: if sizeSoFar <= start: t.seek(start - sizeSoFar) thisMuch = fragment.length if sizeSoFar + fragment.length < end: thisMuch = (sizeSoFar + fragment.length) - end allData += t.read(thisMuch) self.__remove(tmpPart2) else: print "File corrupt." break sizeSoFar += fragment.length return allData @requiresAuth def updateFile(self, store, file_, path, directory): """ Update a file on TrustStore. :type store: Store :param store: a store to upload the file to :type file_: File :param file_: the File object to upload. Must have local_path :type path: Directory :param path: the child folder to upload the File to. May be same as directory :type directory: Directory :param directory: root folder """ # pr = cProfile.Profile() # pr.enable() print file_.name if file_.remote_size <= 0: file_.remote_size = os.path.getsize(file_.local_path) if store and file_ and path and directory: storeFile = self._getStoreFile(store) if storeFile: provider = self._getCloudService(storeFile) fileSize = os.path.getsize(file_.local_path) readFragments = 0 displacement = 0 optimalSize = self.__optimalPieceSize(fileSize) promises = []; pool = ThreadPool(processes=10) if len(file_.fragments) > 0: self._deleteKeysForFragments(file_.fragments, store.id) count404 = 0 for fragment in file_.fragments: if self._deletePartFromCloud(fragment.name, provider, storeFile.bucket) == "404": count404 += 1 sys.stdout.write(" Trying to delete old file. Parts not found in cloud: " + str(count404) + " of " + str(len(file_.fragments)) + "\r") sys.stdout.flush() file_.fragments = [] while fileSize > readFragments * optimalSize: sys.stdout.write(" " + str(readFragments) + " optimal: " + str(optimalSize) + " file size: " + str(fileSize) + "\r") sys.stdout.flush() promise = pool.apply_async(self._doFragmentUpload, [file_.local_path, optimalSize, readFragments, displacement, storeFile, store.id, provider]) readFragments += 1 # ) # displacement += optimalSize promises.append(promise) for order, promise in enumerate(promises): self.__graphPrinter(order, len(promises)) part = promise.get() if part: file_.fragments.append(part) else: print "Part broken?" break self.__graphPrinter(order + 1, len(promises)) isUpdate = False for child in path.children: if file_.name == child.name: # Assume update. child = file_ isUpdate = True if not isUpdate: path.children.append(file_) # print directory self.updateDirectory(directory, store) sys.stdout.write("\n") sys.stdout.flush() else: print "Hey, you can't upload that! : " + file_.local_path # pr.disable() # pr.print_stats(1) @requiresAuth def delFile(self, store, fileName, path, directory): if store and fileName and path and directory: for child in path.children: print child.name path.children[:] = [child for child in path.children if self._deleteChildren(fileName, child, store)] self.updateDirectory(directory, store) elif store and path and directory: for child in path.children: print child.name self._deleteChildren(None, child, store) path.children = [] self.updateDirectory(direcroty, store) else: print "Not enough information to delete!" @requiresAuth def addFile(self, store, file_, path, directory): if store and file_ and path and directory and os.path.exists(file_.local_path): self.updateFile(store, file_, path, directory) else: print "Hey, you can't upload that! " + file_.local_path ## Past here should no user go. def _createAndStoreEmptyDirectory(self, store, storeFile): directory = Directory() newName = unicode(uuid.uuid4()) store.iv = self._generateIV() provider = self._getCloudService(storeFile) tmpText = json.dumps(directory.dict()) tmpRoot = self.__temporaryFileWithString(tmpText) key = self._generateKey() tmpRoot = self._encryptPart(tmpRoot, key, store.iv) self._putPartInCloud(newName, provider, storeFile.bucket, tmpRoot) self._setKeyForFragment(newName, key, store.id) store.index_codename = newName self.__remove(tmpRoot) return directory def _createNewBucket(self, storeFile): storeFile.bucket = "truststore-" + str(uuid.uuid4()) provider = self._getCloudService(storeFile) provider.create_bucket(storeFile.bucket) def _deleteChildren(self, fileName, child, store): if fileName == child.name or fileName == None: storeFile = self._getStoreFile(store) if storeFile: try: self._deleteKeysForFragments(child.fragments, store.id) provider = self._getCloudService(storeFile) count404 = 0 for fragment in child.fragments: if self._deletePartFromCloud(fragment.name, provider, storeFile.bucket) == "404": count404 += 1 sys.stdout.write(" Trying to delete old file. Parts not found in cloud: " + str(count404) + " of " + str(len(child.fragments)) + "\r") sys.stdout.flush() child.fragments = [] print "\n" print "Deleted file " + fileName except AttributeError: for subs in child.children: print subs.name self._deleteChildren(None, subs, store) return False else: return True def _doFragmentDownload(self, filename, fragmentName, provider, storeFile, keySets, fragment): isOkay = False tries = 0 sucess = False tmpPart = None while not isOkay and tries < 10: tmpPart = self._getPartFromCloud(fragmentName, provider, storeFile.bucket) if tmpPart: isOkay = self._getVerifyPart(tmpPart, fragmentName, storeFile) tries += 1 else: break if isOkay: tmpPart2 = self._decryptPart(tmpPart, keySets[fragmentName], fragment.iv) if tmpPart2: with open(tmpPart2, 'rb') as t: currentSize = os.path.getsize(filename) data = t.read() if currentSize <= fragment.offset: with open(filename, 'a+b') as f: if currentSize < fragment.offset: sys.stdout.write(" !! :(") junk = b'\x00' * (fragment.offset - currentSize) f.write(junk) f.write(data) sys.stdout.write(" Piece " + str(fragment.order) + " \t\t\r") sys.stdout.flush() sucess = True else: with open(filename, 'r+b') as f: f.seek(fragment.offset, 0) sys.stdout.write(" Piece " + str(fragment.order) + " \t\t\r") # + ": " + str(f.tell()) + ": " + str(len(data)) + "\r") sys.stdout.flush() f.write(data) sucess = True self.__remove(tmpPart2) else: print "Part broken!!" self.__remove(tmpPart) return sucess def _doFragmentUpload(self, localPath, optimalSize, readFragments, displacement, storeFile, storeId, provider): fragment = Fragment() with open(localPath, 'rb') as f: fragment.length = optimalSize fragment.order = readFragments fragment.providers = storeFile.providers fragment.offset = displacement f.seek(displacement) data = f.read(fragment.length) # sys.stdout.write(" " + str(fragment.order) + " displacement: " + str(displacement) + " length: " + str(fragment.length) # + "\r") # sys.stdout.flush() if len(data) < fragment.length: fragment.length = len(data) # sys.stdout.write(" " + str(fragment.order) + " true length: " + str(len(data)) + " \r") sys.stdout.flush() tmpDataFile = self.__temporaryFileWithBytes(data) key = self._generateKey() fragment.name = unicode(uuid.uuid4()) fragment.iv = self._generateIV() encryptedPart = self._encryptPart(tmpDataFile, key, fragment.iv) self._putPartInCloud(fragment.name, provider, storeFile.bucket, encryptedPart) self._setKeyForFragment(fragment.name, key, storeId) self._storeVerifyPart(encryptedPart, fragment.name, storeFile) # sys.stdout.write(" Finished fragment " + unicode(readFragments) + " \r") sys.stdout.flush() self.__remove(tmpDataFile) self.__remove(encryptedPart) return fragment def _getStoreFile(self, store): getr = self.kmsClient.get(self.kmsUrl + self.filesPrefix + "/" + unicode(store.id)) # + "/" + unicode(store.filename)) message = getr.content if message: clear = self._decryptStoreFileUsingPrivateKeyfile(message, self.keyFile) if clear: # print clear try: storefile = json.loads(clear) storeProps = StoreProperties(eDict=storefile) # print json.dumps(storeProps.dict()) return storeProps except simplejson.scanner.JSONDecodeError: print "This profile is probably XML." else: print "Failed to decrypt profile, download status code: " + str(getr.status_code) def _putStoreFile(self, store, storeFile): success = False userList = [] userList += store.readers userList += store.writers userList += store.administrators userList.append(store.owner) userList = list(set(userList)) storeDict = storeFile.dict() messageFile = self._encryptStoreFileForUsers(json.dumps(storeDict), userList) # print json.dumps(storeDict) if messageFile: with open(messageFile, 'rb') as f: message = {'filename': ("file", f)} # this filename "file" is never read but it needs to be there to be valid so it's just whatever. url = self.kmsUrl + self.filesPrefix + "/" + unicode(store.id) postr = self.kmsClient.post(url, files=message) # print postr.request.headers # print postr.request.data self.__remove(messageFile) if postr.status_code == requests.codes.ok: success = True else: print postr.status_code print postr.text print "Failed to put Store File" print url print store.id return success def _getCloudService(self, storeFile): for provider in storeFile.providers: api = provider['api'] if api == "nectar" or api == "s3": calling = botoConn.SubdomainCallingFormat() if api == "nectar": calling = botoConn.OrdinaryCallingFormat() user = provider['userCredentials'] endpoint = provider['endpoint'] connection = botoConn.S3Connection( aws_access_key_id=user['ident'], aws_secret_access_key=user['secret'], port=urlparse(endpoint).port, host=urlparse(endpoint).hostname, is_secure=True, validate_certs=False, calling_format=calling ) return connection def _changeKeyPassword(self, old, new): newKey = self.keyFile + ".new" openssl = [self.openSSL, 'rsa', '-aes128', '-in', self.keyFile, '-out', newKey, '-passin', 'pass:' + old, '-passout', 'pass:' + new] okay = subprocess.call(openssl) if (okay == 0): self.keyFile = newKey return newKey else: return False def _getPublicCertFor(self, username): getr = self.kmsClient.get(self.kmsUrl + self.publicKeysPrefix + "/" + username) return UserPublicCertificate(getr.json) def _setPublicCert(self, username): publicCertFile = self.__makeRSACertificateFromPrivate(self.keyFile) if publicCertFile: certificate = self.__readCertificateFromFile(publicCertFile) cert = UserPublicCertificate(None, username, certificate) headers = {'Content-type': 'application/json', 'Accept': 'application/json'} self.kmsClient.post(self.kmsUrl + self.publicKeysPrefix, data=json.dumps(cert.dict()), headers=headers) self.__remove(publicCertFile) def _checkPublicCert(self, cert): print "Checking your certificate..." certFile = self.__writeCertificateToFile(cert.certificate) openssl = [self.openSSL, 'x509', '-in', certFile, '-modulus', '-noout'] # certModulus = subprocess.check_output(openssl) p = subprocess.Popen(openssl, stdout=subprocess.PIPE) certModulus = p.communicate()[0] openssl = [self.openSSL, 'rsa', '-in', self.keyFile, '-modulus', '-noout'] if self.password: openssl.append("-passin") openssl.append("pass:" + self.password) keyModulus = subprocess.check_output(openssl) if (certModulus == keyModulus): self.__remove(certFile) return True else: print certFile print certModulus print keyModulus return False def _getKeyForFragment(self, fragmentName, storeID): getr = self.kmsClient.get(self.kmsUrl + self.keysPrefix + "/" + unicode(storeID), params={'codenames': fragmentName}) if getr.status_code != requests.codes.ok or not getr.json: print getr.status_code print "Failed to get key for fragment" if not getr.json: print "No key for that fragment known." return return base64.b64decode(getr.json[0]['key']) def _getKeysForFragments(self, fragments, storeID): codenames = [fragment.name for fragment in fragments] args = [iter(codenames)] * 20 batches = izip_longest(fillvalue=None, *args) sets = {} for batch in batches: getr = self.kmsClient.get(self.kmsUrl + self.keysPrefix + "/" + unicode(storeID), params={'codenames': batch}) if getr.json: sets.update(dict([(key['codename'], base64.b64decode(key['key'])) for key in getr.json])) elif getr.status_code != requests.codes.ok: print "Failed to get keys for " + str(len(batch)) + " fragments." print getr print getr.text return sets def _setKeysForFragments(self, storeID, keySets): data = json.dumps([{'codename': fragmentName, 'key': base64.b64encode(keySets[fragmentName]), 'expiryDate': None} for fragmentName in keySets]) headers = {'Content-type': 'application/json', 'Accept': 'application/json'} postr = self.kmsClient.post(self.kmsUrl + self.keysPrefix + "/" + unicode(storeID), data=data, headers=headers) if postr.status_code != requests.codes.ok: print postr.status_code print "Failed to set keys for fragments" def _deleteKeysForFragments(self, fragments, storeID): codenames = [fragment.name for fragment in fragments] url = self.kmsUrl + self.keysPrefix + "/" + unicode(storeID) args = [iter(codenames)] * 20 batches = izip_longest(fillvalue=None, *args) for batch in batches: try: delr = self.kmsClient.delete(url, params={'codenames': batch}) if delr.status_code != requests.codes.ok: print delr.status_code print delr.text print delr.content print "Failed to delete keys." except requets.exceptions.ConnectionError as e: print (e) print url print batch def _setKeyForFragment(self, fragmentName, key, storeID): data = json.dumps([{'codename': fragmentName, 'key': base64.b64encode(key), 'expiryDate': None}]) headers = {'Content-type': 'application/json', 'Accept': 'application/json'} postr = self.kmsClient.post(self.kmsUrl + self.keysPrefix + "/" + unicode(storeID), data=data, headers=headers) if postr.status_code != requests.codes.ok: print postr.status_code print postr.text print "Failed to set key for fragment" def _decryptStoreFileUsingPrivateKeyfile(self, message, keyFile): publicCertificate = self._getPublicCertFor(self.username).certificate clearMessage = "" messageFileName = self.__temporaryFileWithBytes(message) decryptedFileName = self.__temporaryFile() certFileName = self.__writeCertificateToFile(publicCertificate) openssl = [self.openSSL, 'cms', '-decrypt', '-in', messageFileName, '-out', decryptedFileName, '-recip', certFileName, '-inkey', keyFile, '-inform', 'DER'] if self.password: openssl.append("-passin") openssl.append("pass:" + self.password) # decrypt # openssl smime -decrypt -in encrypted -out decrypted -recip public_cert -inkey private_key try: okay = subprocess.call(openssl) if (okay == 0): with open(decryptedFileName) as decryptFile: clearMessage = decryptFile.read() else: print okay print "Failed to decrypt store file " # + messageFileName except TypeError: print "Failed to call OpenSSL properly:" print openssl # print certFileName self.__remove(certFileName) # print messageFileName self.__remove(messageFileName) self.__remove(decryptedFileName) return clearMessage def _encryptStoreFileForUsers(self, plain, userlist): certificateList = [self._getPublicCertFor(username).certificate for username in userlist] certificateFileList = [self.__writeCertificateToFile(cert) for cert in certificateList] decryptedFileName = self.__temporaryFileWithString(plain) encryptFileName = self.__temporaryFile() argList = [self.openSSL, 'cms', '-encrypt', '-aes128', '-in', decryptedFileName, '-out', encryptFileName, '-outform', 'DER'] argList += certificateFileList # print argList okay = subprocess.call(argList) self.__remove(decryptedFileName) for cert in certificateFileList: self.__remove(cert) if okay != 0: print okay print "Failed to encrypt store file" print argList return False else: for cert in certificateFileList: self.__remove(cert) return encryptFileName def _getPartFromCloud(self, codename, provider, bucketName): attempts = 0 while attempts < 20: try: bucket = provider.get_bucket(bucketName) k = botoKey(bucket) k.key = codename tmpPart = self.__temporaryFile() k.get_contents_to_filename(tmpPart) return tmpPart except (boto.exception.S3ResponseError, boto.exception.BotoServerError) as e: if e.status == 403 or e.status == "403": sys.stdout.write(" Error talking to " + str(provider) + ", attempt: " + str(attempts + 1) + " (403) \r") else: sys.stdout.write(" Error talking to " + str(provider) + ", attempt: " + str(attempts + 1) + ": " + str(e) + " \r") sys.stdout.flush() attempts += 1 def _putPartInCloud(self, codename, provider, bucketName, part): attempts = 0 while attempts < 20: try: bucket = provider.get_bucket(bucketName) k = botoKey(bucket) k.key = codename k.set_contents_from_filename(part) return except boto.exception.S3ResponseError: sys.stdout.write(" Encountered a storing error talking to " + str(provider) + ", attempt: " + str(attempts + 1) + "\r") sys.stdout.flush() attempts += 1 def _deletePartFromCloud(self, codename, provider, bucketName): attempts = 0 while attempts < 10: try: bucket = provider.get_bucket(bucketName) k = botoKey(bucket) k.key = codename bucket.delete_key(k) return except boto.exception.S3ResponseError as e: if "404" not in str(e): sys.stdout.write(" Encountered a delete error talking to " + str(provider) + ", attempt: " + str(attempts + 1) + "\r") sys.stdout.flush() attempts += 1 else: return "404" def _decryptPart(self, part, key, iv): decryptedPart = self.__temporaryFile() mode = '-aes-128-cbc' if not iv: mode = '-aes-128-ecb' iv = b'\x00' argList = [self.openSSL, 'enc', mode, '-d', '-in', part, '-out', decryptedPart, '-K', binascii.b2a_hex(key), '-iv', binascii.b2a_hex(iv)] okay = subprocess.call(argList) # print argList if okay != 0: print okay print "Failed to decrypt part" print argList self.__remove(decryptedPart) return return decryptedPart def _encryptPart(self, part, key, iv): encryptedPart = self.__temporaryFile() mode = '-aes-128-cbc' if not iv: mode = '-aes-128-ecb' iv = b'\x00' argList = [self.openSSL, 'enc', mode, '-in', part, '-out', encryptedPart, '-K', binascii.b2a_hex(key), '-iv', binascii.b2a_hex(iv)] okay = subprocess.call(argList) # print argList if okay != 0: print okay print "Failed to encrypt part" print argList return encryptedPart def _imsRegister(self, username, password, publicKey): url = urljoin(self.imsUrl, "rest/user/" + username) headers = {'passwd': password} # postr = requests.post(url, headers=headers, data=base64.b64encode(publicKey)) postr = requests.post(url, headers=headers, data={'passwd': password, 'publicKey': base64.b64encode(publicKey)}) if postr.text and postr.status_code == 200: text = self.__cleanString(postr.text) try: # print "IMS Public Key" # print text return base64.b64decode(text) except (UnicodeError, TypeError): print "Error getting IMS Public certificate." return postr.content else: print unicode(postr.status_code) + " received while trying to register with IMS." print postr.text def _getVerifyPart(self, part, partName, storeFile): isOkay = False url = urljoin(storeFile.ims_url, "rest/hash/" + storeFile.ims_user['ident']) headers = {'passwd': storeFile.ims_user['secret']} params = {"codename": partName} getr = requests.get(url, headers=headers, params=params) if getr.text and getr.json: # try: # resp = xmltodict.parse(getr.text) resp = getr.json # if "html" not in resp and "hashInfo" in resp: if "doubleSignature" in resp: # resp = resp["hashInfo"] keyFile = self.__writePublicKeyToFile(storeFile.public_key_bytes) signature = base64.b64decode(resp["doubleSignature"]) sigFile = self.__temporaryFileWithBytes(signature) meta = resp["signableMetaInfo"] plainfile = self.__temporaryFileWithString(meta) argList = [self.openSSL, 'dgst', '-sha1', '-verify', keyFile, '-signature', sigFile, plainfile] okay = subprocess.check_output(argList) verified = (okay and okay == "Verified OK\n") self.__remove(sigFile) self.__remove(keyFile) self.__remove(plainfile) if not verified: print "Unable to verify part." return isOkay sums, imsSum = "", "" with open(part, 'rb') as f: sums = hashlib.md5(f.read()).hexdigest() imsSum = meta[:meta.find("$")] if sums == imsSum: isOkay = True else: print imsSum print sums else: print resp # except Exception as e: # print url # print getr.text # print e return isOkay def _storeVerifyPart(self, part, partName, storeFile): isOkay = False headers = {'passwd': storeFile.ims_user['secret']} sums = "" with open(part, 'rb') as f: sums = hashlib.md5(f.read()).hexdigest() imsKeyfile = self.__writePublicKeyToFile(storeFile.ims_public_key_bytes) signable = sums + "$" + partName + "$" + datetime.utcnow().isoformat() signableFile = self.__temporaryFileWithString(signable) signable64 = base64.b64encode(signable) url = urljoin(storeFile.ims_url, "rest/sign/" + signable64) getr = requests.get(url, headers=headers) if getr.text: # and "string" in getr.text: resp = getr.text # xmltodict.parse(getr.text) resp = resp.replace("\\r\\n", "\n") signature = base64.b64decode(resp) #["string"]) # print resp # print len(signature) # print len(storeFile.ims_public_key_bytes) imsSignatureFile = self.__temporaryFileWithBytes(signature) argList = [self.openSSL, 'dgst', '-sha1', '-verify', imsKeyfile, '-signature', imsSignatureFile, signableFile] okay = subprocess.check_output(argList) verified = (okay == "Verified OK\n") self.__remove(imsSignatureFile) if not verified: print "Unable to make digest." print argList else: nonce = os.urandom(4) nonceFile = self.__temporaryFileWithBytes(nonce) encryptedNonceFile = self.__temporaryFile() argList = [self.openSSL, 'rsautl', '-encrypt', '-pubin', '-inkey', imsKeyfile, '-in', nonceFile, '-out', encryptedNonceFile] okay = subprocess.call(argList) if okay != 0: print "Unable to encrypt test." print argList else: encryptedNonce = "" with open(encryptedNonceFile, 'rb') as f: encryptedNonce = f.read() signatureFile = self.__temporaryFile() privKeyFile = self.__writePrivateKeyToFile(storeFile.private_key_bytes) argList = [self.openSSL, 'dgst', '-sha1', '-sign', privKeyFile, '-out', signatureFile, signableFile] okay = subprocess.call(argList) if okay == 0: signature = "" with open(signatureFile, 'rb') as f: signature = f.read() url = urljoin(storeFile.ims_url, "rest/check/" + storeFile.ims_user['ident']) params = { "nonce": base64.b64encode(encryptedNonce), "doubleSig": base64.b64encode(signature), "xml": signable } getr = requests.get(url, headers=headers, params=params) # print url # print headers # print params if getr.text: # and "string" in getr.text and xmltodict.parse(getr.text)["string"]: resp = self.__cleanString(getr.text) resp = base64.b64decode(resp) #xmltodict.parse(getr.text)["string"]) imsEncryptedNonceFile = self.__temporaryFileWithBytes(resp) imsNonceFile = self.__temporaryFile() argList = [self.openSSL, 'rsautl', '-decrypt', '-inkey', privKeyFile, '-out', imsNonceFile, '-in', imsEncryptedNonceFile] okay = subprocess.call(argList) if okay != 0: return isOkay imsNonce = "" with open(imsNonceFile, 'r') as f: imsNonce = f.read() if imsNonce != nonce: return isOkay url = urljoin(storeFile.ims_url, "rest/storehash/" + storeFile.ims_user['ident']) payload = {"xml": signable, "doublesign": base64.b64encode(signature), "codename": partName} postr = requests.post(url, headers=headers, params=params, data=payload) if postr.text and "OK" in postr.text: # and "string" in postr.text isOkay = True else: print postr.text self.__remove(imsEncryptedNonceFile) self.__remove(imsNonceFile) else: print getr.text else: print "Unable to sign digest." self.__remove(signatureFile) self.__remove(privKeyFile) self.__remove(nonceFile) self.__remove(encryptedNonceFile) self.__remove(imsSignatureFile) else: print getr.text self.__remove(imsKeyfile) self.__remove(signableFile) return isOkay def __temporaryFile(self): fileName = "" with NamedTemporaryFile(delete=False) as aFile: fileName = aFile.name return fileName def __temporaryFileWithString(self, string): fileName = self.__temporaryFile() with open(fileName, 'w') as f: if string: f.write(string) return fileName def __temporaryFileWithBytes(self, bytes): fileName = self.__temporaryFile() with open(fileName, 'wb') as f: if bytes: f.write(bytes) return fileName def __writeCertificateToFile(self, cert): certFile = self.__temporaryFile() header = DASHES + BEGIN + " " + CERTIFICATE + DASHES footer = DASHES + END + " " + CERTIFICATE + DASHES with open(certFile, 'w') as f: if not cert.startswith(header): f.write(header + "\n") formatted = "\n".join([line for line in cert.splitlines() if line]) f.write(formatted) if not cert.endswith(footer): f.write("\n" + footer) return certFile def __readCertificateFromFile(self, file_): cert = None with open(file_, 'r') as f: first = f.readline() if first == DASHES + BEGIN + " " + CERTIFICATE + DASHES + "\n": cert = f.readlines() cert = [line for line in cert if line] cert = cert[:-1] cert = "\n".join(cert) else: cert = first + f.read() return cert def __writePublicKeyToFile(self, key): return self.__writeKeyToFile(key, PUBLIC) def __readPublicKeyFromFile(self, keyFile): return self.__readKeyFromFile(keyFile, PUBLIC) def __readPrivateKeyFromFile(self, keyFile): return self.__readKeyFromFile(keyFile, PRIVATE) def __writePrivateKeyToFile(self, key): return self.__writeKeyToFile(key, PRIVATE) def __writeKeyToFile(self, key, keyname): keyFile = self.__temporaryFile() if not self.__probablyBase64(key): key = base64.b64encode(key) publicKey = textwrap.fill(key, 63) # print "KEY" # print publicKey with open(keyFile, 'w') as f: f.write(DASHES + BEGIN + " " + keyname + " " + KEY + DASHES + "\n") f.write(publicKey) f.write("\n" + DASHES + END + " " + keyname + " " + KEY + DASHES) return keyFile def __readKeyFromFile(self, keyFile, keyname): key = None with open(keyFile) as f: key = f.read() if key.startswith(DASHES + BEGIN + " " + keyname + " " + KEY + DASHES + "\n"): key = "".join(key.splitlines()[1:-1]) if "\\r\\n" in key: key = key.replace("\\r\\n", "") if self.__probablyBase64(key): key = base64.b64decode(key) return key def _getUsername(self): getr = self.kmsClient.get(self.kmsUrl + self.usernamesPrefix) username = None if getr.json: try: username = getr.json[0] except KeyError: print getr.json if username: myCert = self._getPublicCertFor(username) if not myCert.certificate: self._setPublicCert(username) elif not self._checkPublicCert(myCert): print "Woah, your certificate isn't valid!" if self.headless: sys.exit(1) else: reset = raw_input("A new certificate can be created, but all other users will need to re-add you to any shared stores, and you will lose access to any stores you own. Do you want to make a new certificate? (Y for yes, all other input means no): ") if reset == 'Y' or reset == 'y' or reset == 'yes' or reset == "YES": self._setPublicCert(username) else: sys.exit(0) return username def _generateKey(self): """128-bit Pseudo-Random Key""" return os.urandom(16) def _generateIV(self): """128-bit IV""" return os.urandom(16) def _generateKeypair(self, filename=None, encrypt=True): if not filename: filename = self.__temporaryFile() unencrypted = self.__temporaryFile() if self.password or not encrypt: # argList = [self.openSSL, 'genrsa', '-aes128', '-out', filename, '-passout', 'pass:' + self.password, '4096'] argList = [self.openSSL, 'genrsa', '-out', unencrypted, '4096'] try: okay = subprocess.call(argList) if okay != 0: self.__remove(unencrypted) unencrypted = False filename = False elif encrypt: argList = [self.openSSL, 'pkcs8', '-topk8', '-v2', 'aes128', '-in', unencrypted, '-out', filename, '-passout', 'pass:' + self.password] try: okay = subprocess.call(argList) if okay != 0: filename = False except TypeError: filename = False else: self.__remove(filename) filename = unencrypted except TypeError: self.__remove(filename) filename = False else: self.__remove(filename) filename = False if encrypt: self.__remove(unencrypted) return filename def _generatePKCS1Keypair(self): filename = self.__temporaryFile() certFile = self.__temporaryFile() confFile = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'certs.conf') days = 365*20 argList = [self.openSSL, 'req', '-x509', '-out', certFile, '-newkey', 'rsa:4096', '-keyout', filename, '-days', unicode(days), '-nodes', '-config', confFile] try: okay = subprocess.call(argList) if okay != 0: self.__remove(filename) filename = False except TypeError: self.__remove(filename) filename = False self.__remove(certFile) return filename def __makeRSACertificateFromPrivate(self, privateKeyFile): certificateFile = self.__temporaryFile() confFile = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'certs.conf') days = 365*20 argList = [self.openSSL, 'req', '-new', '-x509', '-key', privateKeyFile, '-out', certificateFile, '-days', unicode(days), '-nodes', '-config', confFile] if self.password: argList.append("-passin") argList.append("pass:" + self.password) try: okay = subprocess.call(argList) if okay != 0: self.__remove(certificateFile) return False return certificateFile except TypeError: print "Error making certficate." print argList self.__remove(certificateFile) def __publicKeyFromPrivate(self, privateKeyFile): publicKeyFile = self.__temporaryFile() argList = [self.openSSL, 'rsa', '-pubout', '-outform', 'DER', '-in', privateKeyFile, '-out', publicKeyFile] if self.password: argList.append("-passin") argList.append("pass:" + self.password) okay = subprocess.call(argList) if okay == 0: return publicKeyFile self.__remove(publicKeyFile) return False def __probablyBase64(self, s): return (len(''.join(s.split())) % 4 == 0) and re.match('^[A-Za-z0-9+/]+[=]{0,2}$', s) def __cleanString(self, s): openTag = "<string>" closeTag = "</string>" if s.startswith(openTag): return s[len(openTag):-len(closeTag)] s = s.replace("\\r\\n", "\n") return s def __optimalPieceSize(self, totalSize): Dpieces = totalSize / 100 if Dpieces < self.pieceSize: return self.pieceSize return Dpieces def __remove(self, path): try: os.remove(path) except OSError: pass class Store(object): id = None index_codename = None friendly_name = None owner = None iv = None # readers = [] # writers = [] # administrators = [] def __init__(self, props=None, index_codename="", friendly_name="", filename="", owner="", readers=None, writers=None, administrators=None, iv=None): self.readers = [] self.writers = [] self.administrators = [] if props: self.id = props['id'] self.index_codename = props['indexCodename'] self.friendly_name = props['friendly_name'] self.owner = props['owner'] self.readers = props['readers'] self.writers = props['writers'] self.administrators = props['administrators'] if props['iv']: self.iv = base64.b64decode(props['iv']) else: self.index_codename = index_codename self.friendly_name = friendly_name # self.filename = filename self.owner = owner if readers: self.readers = readers if writers: self.writers = writers if administrators: self.administrators = administrators self.iv = iv def __str__(self): message = u'\n' + unicode(self.friendly_name) message += u'\n\tOwner: ' + unicode(self.owner) message += u'\n\tReaders: ' + unicode(self.readers) message += u'\n\tWriters: ' + unicode(self.writers) message += u'\n\tAdmins: ' + unicode(self.administrators) return message def __repr__(self): return str(self) def dict(self): this = { "id": self.id, "indexCodename": self.index_codename, "friendly_name": self.friendly_name, # "fileName": self.filename, "owner": self.owner, "readers": self.readers, "writers": self.writers, "administrators": self.administrators } if self.iv: this["iv"] = base64.b64encode(self.iv) return this class StoreProperties(object): sas_url = "" providers = [] kms_url = "" kms_user = None ims_url = "" ims_user = None bucket = None public_key_bytes = None private_key_bytes = None ims_public_key_bytes = None def __init__(self, eDict=None): self.providers = [] if eDict: self.kms_url = eDict['kmsServiceUrl'] self.private_key_bytes = base64.b64decode(eDict['privateKeyBytes']) self.sas_url = eDict['sasServiceUrl'] self.ims_user = eDict['imsUser'] self.providers = eDict['storageProviders'] self.public_key_bytes = base64.b64decode(eDict['publicKeyBytes']) self.ims_url = eDict['imsServiceUrl'] self.kms_user = eDict['kmsUser'] self.bucket = eDict['workspace'] self.ims_public_key_bytes = base64.b64decode(eDict['imsPublicKeyBytes']) def dict(self): return {'kmsServiceUrl': self.kms_url, 'privateKeyBytes': binascii.b2a_base64(self.private_key_bytes), 'sasServiceUrl': self.sas_url, 'imsUser': self.ims_user, 'storageProviders': self.providers, 'publicKeyBytes': binascii.b2a_base64(self.public_key_bytes), 'imsServiceUrl': self.ims_url, 'kmsUser': self.kms_user, 'workspace': self.bucket, 'imsPublicKeyBytes': binascii.b2a_base64(self.ims_public_key_bytes)} def __repr__(self): return json.dumps(self.dict(), sort_keys=True, indent=4) class UserPublicCertificate(object): username = None certificate = None def __init__(self, props=None, username="", certificate=None): if props: self.username = props['username'] self.certificate = props['key'] else: self.username = username self.certificate = certificate def dict(self): return { 'username': self.username, 'key': self.certificate } def __repr__(self): return json.dumps(self.dict(), sort_keys=True, indent=4) class UserPrivateKey(UserPublicCertificate): pass class Config(object): def __init__(self, ims, kms, key, secret): self.kmsUrl = kms self.imsUrl = ims self.client_key = key self.client_secret = secret class TrustStoreClientAuthenticationException(Exception): def __init__(self, message, isConfigured=False): self.message = message self.isConfigured = isConfigured def __str__(self): msg = repr(self.message) + " note: " if self.isConfigured: msg += " login attempt was made." else: msg += " not currently authenticated" return msg class OpenSSLVersionException(Exception): def __init__(self, version): self.version = version def __str__(self): return "The version of openssl (" + self.version + ") is too old!"