def remove_device_session(db, devid: str) -> RetVal: ''' Removes an authorized device from the workspace. Returns a boolean success code. ''' cursor = db.cursor() cursor.execute("SELECT devid FROM sessions WHERE devid=?", (devid, )) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound) cursor.execute("DELETE FROM sessions WHERE devid=?", (devid, )) db.commit() return RetVal()
def write(self, text: str) -> RetVal: '''Sends a string over a socket''' if not self.socket: return RetVal(NetworkError, 'Invalid connection') try: self.socket.send(text.encode()) except Exception as exc: self.socket.close() return RetVal(ExceptionThrown, exc.__str__()) return RetVal()
def decrypt(self, data : str) -> RetVal: '''Decrypt the passed data using the private key and return the raw data in the field 'data'. Base85 decoding of the data is optional, but enabled by default.''' if not isinstance(data, str): return RetVal(BadParameterType, 'string expected') try: sealedbox = nacl.public.SealedBox(nacl.public.PrivateKey(self.private.raw_data())) decrypted_data = sealedbox.decrypt(data.encode(), Base85Encoder) except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal().set_value('data', decrypted_data.decode())
def sign(self, data : bytes) -> RetVal: '''Return a Base85-encoded signature for the supplied data in the field 'signature'.''' if not isinstance(data, bytes): return RetVal(BadParameterType, 'bytes expected for data') key = nacl.signing.SigningKey(self.private.raw_data()) try: signed = key.sign(data, Base85Encoder) except Exception as e: return RetVal(ExceptionThrown, e) return RetVal().set_value('signature', 'ED25519:' + signed.signature.decode())
def encrypt(self, data : bytes) -> RetVal: '''Encrypt the passed data using the public key and return the Base85-encoded data in the field 'data'.''' if not isinstance(data, bytes): return RetVal(BadParameterType, 'bytes expected') try: sealedbox = nacl.public.SealedBox(nacl.public.PublicKey(self.public.raw_data())) encrypted_data = sealedbox.encrypt(data, Base85Encoder).decode() except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal().set_value('data', encrypted_data)
def load_encryptionpair(path: str) -> RetVal: '''Instantiates a keypair from a file''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if not os.path.exists(path): return RetVal(ResourceNotFound, '%s exists' % path) indata = None try: with open(path, "r") as fhandle: indata = json.load(fhandle) except Exception as e: return RetVal(ExceptionThrown, e) if not isinstance(indata, dict): return RetVal(BadData, 'File does not contain an Anselus JSON keypair') try: jsonschema.validate(indata, __encryption_pair_schema) except jsonschema.ValidationError: return RetVal(BadData, "file data does not validate") except jsonschema.SchemaError: return RetVal(InternalError, "BUG: invalid EncryptionPair schema") public_key = CryptoString(indata['PublicKey']) private_key = CryptoString(indata['PrivateKey']) if not public_key.is_valid() or not private_key.is_valid(): return RetVal(BadData, 'Failure to base85 decode key data') return RetVal().set_value('keypair', EncryptionPair(public_key, private_key))
def send_message(self, command : dict) -> RetVal: '''Sends a message to the server with command sent as JSON data''' cmdstr = json.dumps(command) + '\r\n' if not self.socket: return RetVal(NetworkError, 'not connected') try: self.socket.send(cmdstr.encode()) except Exception as e: self.socket.close() return RetVal(ExceptionThrown, e) return RetVal()
def load_secretkey(path: str) -> RetVal: '''Instantiates a secret key from a file''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if not os.path.exists(path): return RetVal(ResourceNotFound, '%s exists' % path) indata = None try: with open(path, "r") as fhandle: indata = json.load(fhandle) except Exception as e: return RetVal(ExceptionThrown, e) if not isinstance(indata, dict): return RetVal(BadData, 'File does not contain an Anselus JSON secret key') try: jsonschema.validate(indata, __secret_key_schema) except jsonschema.ValidationError: return RetVal(BadData, "file data does not validate") except jsonschema.SchemaError: return RetVal(InternalError, "BUG: invalid SecretKey schema") key = CryptoString(indata['SecretKey']) if not key.is_valid(): return RetVal(BadData, 'Failure to base85 decode key data') return RetVal().set_value('key', SecretKey(key))
def add_device_session(db, address: str, devid: str, enctype: str, public_key: str, private_key: str, devname='') -> RetVal: '''Adds a device to a workspace''' if not address or not devid or not enctype or not public_key or not private_key: return RetVal(BadParameterValue, "Empty parameter") if enctype != 'curve25519': return RetVal(BadParameterValue, "enctype must be 'curve25519'") # Normally we don't validate the input, relying on the caller to ensure valid data because # in most cases, bad data just corrupts the database integrity, not crash the program. # We have to do some here to ensure there isn't a crash when the address is split. parts = utils.split_address(address) if parts.error(): return parts # address has to be valid and existing already cursor = db.cursor() cursor.execute("SELECT wid FROM workspaces WHERE wid=?", (parts['wid'], )) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound) # Can't have a session on the server already cursor.execute("SELECT address FROM sessions WHERE address=?", (address, )) results = cursor.fetchone() if results: return RetVal(ResourceExists) cursor = db.cursor() if devname: cursor.execute( '''INSERT INTO sessions( address, devid, enctype, public_key, private_key, devname) VALUES(?,?,?,?,?,?)''', (address, devid, enctype, public_key, private_key, devname)) else: cursor.execute( '''INSERT INTO sessions( address, devid, enctype, public_key, private_key) VALUES(?,?,?,?,?)''', (address, devid, enctype, public_key, private_key)) db.commit() return RetVal()
def verify(self) -> RetVal: '''Verifies the card's entire chain of entries''' if len(self.entries) == 0: return RetVal(ResourceNotFound, 'keycard contains no entries') if len(self.entries) == 1: return RetVal() for i in range(len(self.entries) - 1): status = self.entries[i + 1].verify_chain(self.entries[i]) if status.error(): return status return RetVal()
def add_to_db(self, pw: encryption.Password) -> RetVal: '''Adds a workspace to the storage database''' cursor = self.db.cursor() cursor.execute("SELECT wid FROM workspaces WHERE wid=?", (self.wid, )) results = cursor.fetchone() if results: return RetVal(ResourceExists, self.wid) cursor.execute( '''INSERT INTO workspaces(wid,domain,password,pwhashtype,type) VALUES(?,?,?,?,?)''', (self.wid, self.domain, pw.hashstring, pw.hashtype, self.type)) self.db.commit() return RetVal()
def regcode(conn: ServerConnection, regid: str, code: str, pwhash: str, devid: str, devpair: EncryptionPair, domain: str) -> RetVal: '''Finishes registration of a workspace''' request = { 'Action':'REGCODE', 'Data':{ 'Reg-Code': code, 'Password-Hash':pwhash, 'Device-ID':devid, 'Device-Key':devpair.public.as_string() } } if domain: request['Data']['Domain'] = domain if utils.validate_uuid(regid): request['Data']['Workspace-ID'] = regid else: request['Data']['User-ID'] = regid status = conn.send_message(request) if status.error(): return status response = conn.read_response(server_response) if response.error(): return response if response['Code'] != 201: return wrap_server_error(response) return RetVal()
def init_user(conn: serverconn.ServerConnection, config: dict) -> RetVal: '''Creates a test user for command testing''' userwid = '33333333-3333-3333-3333-333333333333' status = serverconn.preregister(conn, userwid, 'csimons', 'example.net') assert not status.error(), "init_user(): uid preregistration failed" assert status['domain'] == 'example.net' and 'wid' in status and 'regcode' in status and \ status['uid'] == 'csimons', "init_user(): failed to return expected data" regdata = status password = Password('MyS3cretPassw*rd') devpair = EncryptionPair() devid = '11111111-1111-1111-1111-111111111111' status = serverconn.regcode(conn, 'csimons', regdata['regcode'], password.hashstring, devid, devpair, 'example.net') assert not status.error(), "init_user(): uid regcode failed" config['user_wid'] = userwid config['user_uid'] = regdata['uid'] config['user_domain'] = regdata['domain'] config['user_devid'] = devid config['user_devpair'] = devpair config['user_password'] = password return RetVal()
def verify(self, data : bytes, data_signature : CryptoString) -> RetVal: '''Return a Base85-encoded signature for the supplied data in the field 'signature'.''' if not isinstance(data, bytes): return RetVal(BadParameterType, 'bytes expected for data') if not isinstance(data_signature, CryptoString): return RetVal(BadParameterType, 'signature parameter must be a CryptoString') key = nacl.signing.VerifyKey(self.public.raw_data()) try: key.verify(data, data_signature.raw_data()) except Exception as e: return RetVal(VerificationError, e) return RetVal()
def set_expiration(self, numdays=-1) -> RetVal: '''Sets the expiration field to the number of days specified after the current date''' if numdays < 0: if self.type == 'Organization': numdays = 365 elif self.type == 'User': numdays = 90 else: return RetVal(UnsupportedKeycardType) # An expiration date can be no longer than 3 years if numdays > 1095: numdays = 1095 expiration = datetime.datetime.utcnow() + datetime.timedelta(numdays) self.fields['Expires'] = expiration.strftime("%Y%m%d") return RetVal()
def Assign(self, pwhash) -> RetVal: ''' Takes a PHC hash format string and assigns the password object to it. Returns: [dict] error : string ''' self.hashstring = pwhash return RetVal()
def device(conn: ServerConnection, devid: str, devpair: EncryptionPair) -> RetVal: '''Completes the login process by submitting device ID and its session string.''' if not utils.validate_uuid(devid): return RetVal(AnsBadRequest, 'Invalid device ID').set_value('status', 400) conn.send_message({ 'Action' : "DEVICE", 'Data' : { 'Device-ID' : devid, 'Device-Key' : devpair.public.as_string() } }) # Receive, decrypt, and return the server challenge response = conn.read_response(server_response) if response.error(): return response if response['Code'] != 100: return wrap_server_error(response) if 'Challenge' not in response['Data']: return RetVal(ServerError, 'server did not return a device challenge') status = devpair.decrypt(response['Data']['Challenge']) if status.error(): cancel(conn) return RetVal(DecryptionFailure, 'failed to decrypt device challenge') conn.send_message({ 'Action' : "DEVICE", 'Data' : { 'Device-ID' : devid, 'Device-Key' : devpair.public.as_string(), 'Response' : status['data'] } }) response = conn.read_response(None) if response.error(): return response if response['Code'] == 200: return RetVal() return wrap_server_error(response)
def verify_chain(self, previous: EntryBase) -> RetVal: '''Verifies the chain of custody between the provided previous entry and the current one.''' if previous.type != 'User': return RetVal(BadParameterValue, 'entry type mismatch') if 'Custody' not in self.signatures or not self.signatures['Custody']: return RetVal(ResourceNotFound, 'custody signature missing') if 'Contact-Request-Verification-Key' not in previous.fields or \ not previous.fields['Contact-Request-Verification-Key']: return RetVal(ResourceNotFound, 'signing key missing') status = self.verify_signature( CryptoString(previous.fields['Contact-Request-Verification-Key']), 'Custody') return status
def remove_key(db: sqlite3.Connection, keyid: str) -> RetVal: '''Deletes an encryption key from a workspace. Parameters: keyid : uuid Returns: error : string ''' cursor = db.cursor() cursor.execute("SELECT keyid FROM keys WHERE keyid=?", (keyid, )) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound) cursor.execute("DELETE FROM keys WHERE keyid=?", (keyid, )) db.commit() return RetVal()
def set_credentials(db, wid: str, domain: str, pw: encryption.Password) -> RetVal: '''Sets the password and hash type for the specified workspace. A boolean success value is returned.''' cursor = db.cursor() cursor.execute("SELECT wid FROM workspaces WHERE wid=? AND domain=?", (wid, domain)) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound) cursor = db.cursor() cursor.execute( "UPDATE workspaces SET password=?,pwhashtype=? WHERE wid=? AND domain=?", (pw.hashstring, pw.hashtype, wid, domain)) db.commit() return RetVal()
def remove_workspace_entry(self, wid: str, domain: str) -> RetVal: ''' Removes a workspace from the storage database. NOTE: this only removes the workspace entry itself. It does not remove keys, sessions, or other associated data. ''' cursor = self.db.cursor() cursor.execute("SELECT wid FROM workspaces WHERE wid=? AND domain=?", (wid, domain)) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound, "%s/%s not found" % (wid, domain)) cursor.execute("DELETE FROM workspaces WHERE wid=? AND domain=?", (wid, domain)) self.db.commit() return RetVal()
def remove_folder(self, fid: encryption.FolderMapping) -> RetVal: '''Deletes a folder mapping. Parameters: fid : uuid Returns: error : string ''' cursor = self.db.cursor() cursor.execute("SELECT fid FROM folders WHERE fid=?", (fid, )) results = cursor.fetchone() if not results or not results[0]: return RetVal(ResourceNotFound, fid) cursor.execute("DELETE FROM folders WHERE fid=?", (fid, )) self.db.commit() return RetVal()
def set_userid(self, userid: str) -> RetVal: '''set_userid() sets the human-friendly name for the workspace''' if ' ' or '"' in userid: return RetVal(BadParameterValue, '" and space not permitted') cursor = self.db.cursor() sqlcmd = ''' UPDATE workspaces SET userid=? WHERE wid=? and domain=? ''' cursor.execute(sqlcmd, (userid, self.wid, self.domain)) self.db.commit() self.uid = userid return RetVal()
def connect(self, host: str, port) -> RetVal: '''Creates a connection to the server.''' try: self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Set a short timeout in case the server doesn't respond immediately, # which is the expectation as soon as a client connects. self.__sock.settimeout(10.0) except: return RetVal(NetworkError, "Couldn't create a socket") out_data = RetVal() out_data.set_value('socket', self.__sock) try: self.ip = socket.gethostbyname(host) except socket.gaierror: self.disconnect() return RetVal(ResourceNotFound, "Couldn't locate host %s" % host) try: self.__sock.connect((self.ip, port)) self.port = port status = self.read_msg(pyanselus.rpc_schemas.greeting) if not status.error(): self.version = status['msg']['version'].strip() except Exception as exc: self.disconnect() return RetVal(NetworkError, f"Couldn't connect to host {host}: {exc}") # Set a timeout of 30 minutes self.__sock.settimeout(1800.0) return out_data
def verify_signature(self, verify_key: CryptoString, sigtype: str) -> RetVal: '''Verifies a signature, given a verification key''' if not verify_key.is_valid(): return RetVal(BadParameterValue, 'bad verify key') sig_names = [x['name'] for x in self.signature_info] if sigtype not in sig_names: return RetVal(BadParameterValue, 'bad signature type') if verify_key.prefix != 'ED25519': return RetVal(UnsupportedEncryptionType, verify_key.prefix) if sigtype in self.signatures and not self.signatures[sigtype]: return RetVal(NotCompliant, 'empty signature ' + sigtype) sig = CryptoString() status = sig.set(self.signatures[sigtype]) if status.error(): return status try: vkey = nacl.signing.VerifyKey(verify_key.raw_data()) except Exception as e: return RetVal(ExceptionThrown, e) try: data = self.make_bytestring(sig_names.index(sigtype)) vkey.verify(data, sig.raw_data()) except nacl.exceptions.BadSignatureError: return RetVal(InvalidKeycard) return RetVal()
def password(conn: ServerConnection, wid: str, pwhash: str) -> RetVal: '''Continues the login process sending a password hash to the server.''' if not password or not utils.validate_uuid(wid): return RetVal(BadParameterValue) conn.send_message({ 'Action' : "PASSWORD", 'Data' : { 'Password-Hash' : pwhash } }) response = conn.read_response(server_response) if response.error(): return response if response['Code'] != 100: return wrap_server_error(response) return RetVal()
def add_folder(self, folder: encryption.FolderMapping) -> RetVal: ''' Adds a mapping of a folder ID to a specific path in the workspace. Parameters: folder : FolderMapping object ''' cursor = self.db.cursor() cursor.execute("SELECT fid FROM folders WHERE fid=?", (folder.fid, )) results = cursor.fetchone() if results: return RetVal(ResourceExists, folder.fid) cursor.execute( '''INSERT INTO folders(fid,address,keyid,path,permissions) VALUES(?,?,?,?,?)''', (folder.fid, folder.address, folder.keyid, folder.path, folder.permissions)) self.db.commit() return RetVal()
def save(self, path: str, clobber=False) -> RetVal: '''Saves to the specified path, forcing CRLF line endings to prevent any weird behavior caused by line endings invalidating signatures.''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if os.path.exists(path) and not clobber: return RetVal(ResourceExists) try: with open(path, 'wb') as f: f.write(self.make_bytestring(-1)) except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal()
def rename_profile(self, oldname: str, newname: str) -> RetVal: '''Renames the specified profile''' status = self.fs.pman.rename_profile(oldname, newname) if status.error() != '': return status if self.active_profile == oldname: self.active_profile = newname return RetVal()
def test_count(): '''Tests count()''' r = RetVal() assert r.count() == 0, '''Unused RetVal is not empty''' r['foo'] = 'bar' assert r.count() == 1, '''Incorrect item count in RetVal''' r.empty() assert r.count() == 0, '''Emptied RetVal is not empty'''