def test_encryptionpair_save(): '''Tests the save code of the EncryptionPair class''' test_folder = setup_test('encryption_encryptionpair_save') public_key = CryptoString( "CURVE25519:(B2XX5|<+lOSR>_0mQ=KX4o<aOvXe6M`Z5ldINd`") private_key = CryptoString( "CURVE25519:(Rj5)mmd1|YqlLCUP0vE;YZ#o;tJxtlAIzmPD7b&") kp = encryption.EncryptionPair(public_key, private_key) keypair_path = os.path.join(test_folder, 'testpair.jk') status = kp.save(keypair_path) assert not status.error( ), f"Failed to create saved encryption pair file: {status.info()}" fhandle = open(keypair_path) filedata = json.load(fhandle) fhandle.close() assert filedata['PublicKey'] == public_key.as_string( ), "Saved data does not match input data" assert filedata['PrivateKey'] == private_key.as_string( ), "Saved data does not match input data"
def test_signpair_save(): '''Tests the save code of the SigningPair class''' test_folder = setup_test('encryption_signpair_save') public_key = CryptoString( r"ED25519:PnY~pK2|;AYO#1Z;B%T$2}E$^kIpL=>>VzfMKsDx") private_key = CryptoString( r"ED25519:{^A@`5N*T%5ybCU%be892x6%*Rb2rnYd=SGeO4jF") sp = encryption.SigningPair(public_key, private_key) keypair_path = os.path.join(test_folder, 'testpair.jk') status = sp.save(keypair_path) assert not status.error( ), f"Failed to create saved signing pair file: {status.info()}" fhandle = open(keypair_path) filedata = json.load(fhandle) fhandle.close() assert filedata['VerificationKey'] == public_key.as_string(), \ "Saved data does not match input data" assert filedata['SigningKey'] == private_key.as_string( ), "Saved data does not match input data"
def test_secretkey_save(): '''Tests the save code of the SecretKey class''' test_folder = setup_test('encryption_secretkey_save') key = CryptoString(r"XSALSA20:J~T^ko3HCFb$1Z7NudpcJA-dzDpF52IF1Oysh+CY") sk = encryption.SecretKey(key) key_path = os.path.join(test_folder, 'testkey.jk') status = sk.save(key_path) assert not status.error(), "Failed to create saved encryption pair file" fhandle = open(key_path) filedata = json.load(fhandle) fhandle.close() assert filedata['SecretKey'] == key.as_string( ), "Saved data does not match input data"
def init_server(dbconn) -> dict: '''Adds basic data to the database as if setupconfig had been run. Returns data needed for tests, such as the keys''' # Start off by generating the org's root keycard entry and add to the database cur = dbconn.cursor() card = keycard.Keycard() root_entry = keycard.OrgEntry() root_entry.set_fields({ 'Name': 'Example, Inc.', 'Contact-Admin': 'c590b44c-798d-4055-8d72-725a7942f3f6/acme.com', 'Language': 'en', 'Domain': 'example.com', 'Primary-Verification-Key': 'ED25519:r#r*RiXIN-0n)BzP3bv`LA&t4LFEQNF0Q@$N~RF*', 'Encryption-Key': 'CURVE25519:SNhj2K`hgBd8>G>lW$!pXiM7S-B!Fbd9jT2&{{Az' }) initial_ovkey = CryptoString( r'ED25519:r#r*RiXIN-0n)BzP3bv`LA&t4LFEQNF0Q@$N~RF*') initial_oskey = CryptoString( r'ED25519:{UNQmjYhz<(-ikOBYoEQpXPt<irxUF*nq25PoW=_') initial_ovhash = CryptoString( r'BLAKE2B-256:ag29av@TUvh-V5KaB2l}H=m?|w`}dvkS1S1&{cMo') initial_epubkey = CryptoString( r'CURVE25519:SNhj2K`hgBd8>G>lW$!pXiM7S-B!Fbd9jT2&{{Az') initial_eprivkey = CryptoString( r'CURVE25519:WSHgOhi+bg=<bO^4UoJGF-z9`+TBN{ds?7RZ;w3o') initial_epubhash = CryptoString( r'BLAKE2B-256:-Zz4O7J;m#-rB)2llQ*xTHjtblwm&kruUVa_v(&W') # Organization hash, sign, and verify rv = root_entry.generate_hash('BLAKE2B-256') assert not rv.error(), 'entry failed to hash' rv = root_entry.sign(initial_oskey, 'Organization') assert not rv.error(), 'Unexpected RetVal error %s' % rv.error() assert root_entry.signatures['Organization'], 'entry failed to org sign' rv = root_entry.verify_signature(initial_ovkey, 'Organization') assert not rv.error(), 'org entry failed to verify' status = root_entry.is_compliant() assert not status.error(), f"OrgEntry wasn't compliant: {str(status)}" card.entries.append(root_entry) cur.execute("INSERT INTO keycards(owner,creationtime,index,entry,fingerprint) " \ "VALUES('organization',%s,%s,%s,%s);", (root_entry.fields['Timestamp'],root_entry.fields['Index'], root_entry.make_bytestring(-1).decode(), root_entry.hash)) cur.execute( "INSERT INTO orgkeys(creationtime, pubkey, privkey, purpose, fingerprint) " "VALUES(%s,%s,%s,'encrypt',%s);", (root_entry.fields['Timestamp'], initial_epubkey.as_string(), initial_eprivkey.as_string(), initial_epubhash.as_string())) cur.execute( "INSERT INTO orgkeys(creationtime, pubkey, privkey, purpose, fingerprint) " "VALUES(%s,%s,%s,'sign',%s);", (root_entry.fields['Timestamp'], initial_ovkey.as_string(), initial_oskey.as_string(), initial_ovhash.as_string())) cur.close() dbconn.commit() cur = dbconn.cursor() # Sleep for 1 second in order for the new entry's timestamp to be useful time.sleep(1) # Chain a new entry to the root status = card.chain(initial_oskey, True) assert not status.error(), f'keycard chain failed: {status}' # Save the keys to a separate RetVal so we can keep using status for return codes keys = status new_entry = status['entry'] new_entry.prev_hash = root_entry.hash new_entry.generate_hash('BLAKE2B-256') assert not status.error(), f'chained entry failed to hash: {status}' status = card.verify() assert not status.error(), f'keycard failed to verify: {status}' cur.execute("INSERT INTO keycards(owner,creationtime,index,entry,fingerprint) " \ "VALUES('organization',%s,%s,%s,%s);", (new_entry.fields['Timestamp'],new_entry.fields['Index'], new_entry.make_bytestring(-1).decode(), new_entry.hash)) cur.execute( "INSERT INTO orgkeys(creationtime, pubkey, privkey, purpose, fingerprint) " "VALUES(%s,%s,%s,'sign',%s);", (new_entry.fields['Timestamp'], keys['sign.public'], keys['sign.private'], keys['sign.pubhash'])) cur.execute( "INSERT INTO orgkeys(creationtime, pubkey, privkey, purpose, fingerprint) " "VALUES(%s,%s,%s,'encrypt',%s);", (new_entry.fields['Timestamp'], keys['encrypt.public'], keys['encrypt.private'], keys['encrypt.pubhash'])) if keys.has_value('altsign.public'): cur.execute( "INSERT INTO orgkeys(creationtime, pubkey, privkey, purpose, fingerprint) " "VALUES(%s,%s,%s,'altsign',%s);", (new_entry.fields['Timestamp'], keys['altsign.public'], keys['altsign.private'], keys['altsign.pubhash'])) # Prereg the admin account admin_wid = 'ae406c5e-2673-4d3e-af20-91325d9623ca' regcode = 'Undamaged Shining Amaretto Improve Scuttle Uptake' cur.execute( f"INSERT INTO prereg(wid, uid, domain, regcode) VALUES('{admin_wid}', 'admin', " f"'example.com', '{regcode}');") # Set up abuse/support forwarding to admin abuse_wid = 'f8cfdbdf-62fe-4275-b490-736f5fdc82e3' cur.execute( "INSERT INTO workspaces(wid, uid, domain, password, status, wtype) " f"VALUES('{abuse_wid}', 'abuse', 'example.com', '-', 'active', 'alias');" ) cur.execute(f"INSERT INTO aliases(wid, alias) VALUES('{abuse_wid}', " f"'{'/'.join([admin_wid, 'example.com'])}');") support_wid = 'f0309ef1-a155-4655-836f-55173cc1bc3b' cur.execute( f"INSERT INTO workspaces(wid, uid, domain, password, status, wtype) " f"VALUES('{support_wid}', 'support', 'example.com', '-', 'active', 'alias');" ) cur.execute(f"INSERT INTO aliases(wid, alias) VALUES('{support_wid}', " f"'{'/'.join([admin_wid, 'example.com'])}');") cur.close() dbconn.commit() return { 'ovkey': keys['sign.public'], 'oskey': keys['sign.private'], 'oekey': keys['encrypt.public'], 'odkey': keys['encrypt.private'], 'admin_wid': admin_wid, 'admin_regcode': regcode, 'root_org_entry': root_entry, 'second_org_entry': new_entry }
class EncryptionPair (CryptoKey): '''Represents an assymmetric encryption key pair''' def __init__(self, public=None, private=None): super().__init__() if public and private: if not isinstance(public, CryptoString) or not isinstance(private, CryptoString): raise TypeError if public.prefix != private.prefix: raise ValueError self.enctype = public.prefix self.public = public self.private = private else: key = nacl.public.PrivateKey.generate() self.enctype = 'CURVE25519' self.public = CryptoString('CURVE25519:' + \ base64.b85encode(key.public_key.encode()).decode()) self.private = CryptoString('CURVE25519:' + \ base64.b85encode(key.encode()).decode()) self.pubhash = blake2hash(self.public.data.encode()) self.privhash = blake2hash(self.private.data.encode()) def __str__(self): return '\n'.join([ self.public.as_string(), self.private.as_string() ]) def get_public_key(self) -> str: '''Returns the public key as a CryptoString string''' return self.public.as_string() def get_public_hash(self) -> str: '''Returns the hash of the public key as a CryptoString string''' return self.pubhash def get_private_key(self) -> str: '''Returns the private key as a CryptoString string''' return self.private.as_string() def get_private_hash(self) -> str: '''Returns the hash of the private key as a CryptoString string''' return self.privhash def save(self, path: str): '''Saves the keypair to a file''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if os.path.exists(path): return RetVal(ResourceExists, '%s exists' % path) outdata = { 'PublicKey' : self.get_public_key(), 'PublicHash' : self.pubhash, 'PrivateKey' : self.get_private_key(), 'PrivateHash' : self.privhash } try: fhandle = open(path, 'w') json.dump(outdata, fhandle, ensure_ascii=False, indent=1) fhandle.close() except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal() 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 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())
class SecretKey (CryptoKey): '''Represents a secret key used by symmetric encryption''' def __init__(self, key=None): super().__init__() if key: if type(key).__name__ != 'CryptoString': raise TypeError self.key = key else: self.enctype = 'XSALSA20' self.key = CryptoString('XSALSA20:' + \ base64.b85encode(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)).decode()) self.hash = blake2hash(self.key.data.encode()) def __str__(self): return self.get_key() def get_key(self) -> str: '''Returns the key encoded in base85''' return self.key.as_string() def save(self, path: str) -> RetVal: '''Saves the key to a file''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if os.path.exists(path): return RetVal(ResourceExists, '%s exists' % path) outdata = { 'SecretKey' : self.get_key() } try: fhandle = open(path, 'w') json.dump(outdata, fhandle, ensure_ascii=False, indent=1) fhandle.close() except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal() def decrypt(self, encdata : str) -> bytes: '''Decrypts the Base85-encoded encrypted data and returns it as bytes. Returns None on failure''' if encdata is None: return None if type(encdata).__name__ != 'str': raise TypeError secretbox = nacl.secret.SecretBox(self.key.raw_data()) return secretbox.decrypt(encdata, encoder=Base85Encoder) def encrypt(self, data : bytes) -> str: '''Encrypts the passed data and returns it as a Base85-encoded string. Returns None on failure''' if data is None: return None if type(data).__name__ != 'bytes': raise TypeError secretbox = nacl.secret.SecretBox(self.key.raw_data()) mynonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) return secretbox.encrypt(data,nonce=mynonce, encoder=Base85Encoder).decode()
class SigningPair: '''Represents an asymmetric signing key pair''' def __init__(self, public=None, private=None): super().__init__() if public and private: if type(public).__name__ != 'CryptoString' or \ type(private).__name__ != 'CryptoString': raise TypeError if public.prefix != private.prefix: raise ValueError self.enctype = public.prefix self.public = public self.private = private else: key = nacl.signing.SigningKey.generate() self.enctype = 'ED25519' self.public = CryptoString('ED25519:' + \ base64.b85encode(key.verify_key.encode()).decode()) self.private = CryptoString('ED25519:' + \ base64.b85encode(key.encode()).decode()) self.pubhash = blake2hash(self.public.data.encode()) self.privhash = blake2hash(self.private.data.encode()) def __str__(self): return '\n'.join([ self.public.as_string(), self.private.as_string() ]) def get_public_key(self) -> bytes: '''Returns the verification key as a CryptoString string''' return self.public.as_string() def get_public_hash(self) -> str: '''Returns the hash of the verification key as a CryptoString string''' return self.pubhash def get_private_key(self) -> str: '''Returns the signing key as a CryptoString string''' return self.private.as_string() def get_private_hash(self) -> str: '''Returns the hash of the signing key as a CryptoString string''' return self.privhash def save(self, path: str) -> RetVal: '''Saves the key to a file''' if not path: return RetVal(BadParameterValue, 'path may not be empty') if os.path.exists(path): return RetVal(ResourceExists, '%s exists' % path) outdata = { 'VerificationKey' : self.get_public_key(), 'VerificationHash' : self.pubhash, 'SigningKey' : self.get_private_key(), 'SigningHash' : self.privhash } try: fhandle = open(path, 'w') json.dump(outdata, fhandle, ensure_ascii=False, indent=1) fhandle.close() except Exception as e: return RetVal(ExceptionThrown, str(e)) return RetVal() 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 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 register(conn: ServerConnection, uid: str, pwhash: str, devicekey: CryptoString) -> RetVal: '''Creates an account on the server.''' if uid and len(re.findall(r'[\/" \s]',uid)) > 0: return RetVal(BadParameterValue, 'user id contains illegal characters') # This construct is a little strange, but it is to work around the minute possibility that # there is a WID collision, i.e. the WID generated by the client already exists on the server. # In such an event, it should try again. However, in the ridiculously small chance that the # client keeps generating collisions, it should wait 3 seconds after 10 collisions to reduce # server load. out = RetVal() devid = str(uuid.uuid4()) wid = '' response = dict() tries = 1 while not wid: if not tries % 10: time.sleep(3.0) # Technically, the active profile already has a WID, but it is not attached to a domain and # doesn't matter as a result. Rather than adding complexity, we just generate a new UUID # and always return the replacement value wid = str(uuid.uuid4()) request = { 'Action' : 'REGISTER', 'Data' : { 'Workspace-ID' : wid, 'Password-Hash' : pwhash, 'Device-ID' : devid, 'Device-Key' : devicekey.as_string() } } if uid: request['Data']['User-ID'] = uid status = conn.send_message(request) if status.error(): return status response = conn.read_response(server_response) if response.error(): return response if response['Code'] in [ 101, 201]: # Pending, Success out.set_values({ 'devid' : devid, 'wid' : wid, 'domain' : response['Data']['Domain'], 'uid' : uid }) break if response['Code'] == 408: # WID or UID exists if 'Field' not in response['Data']: return RetVal(ServerError, 'server sent 408 without telling what existed') if response['Data']['Field'] not in ['User-ID', 'Workspace-ID']: return RetVal(ServerError, 'server sent bad 408 response').set_value( \ 'Field', response['Data']['Field']) if response['Data']['Field'] == 'User-ID': return RetVal(ResourceExists, 'user id') tries = tries + 1 wid = '' else: # Something we didn't expect -- reg closed, payment req'd, etc. return wrap_server_error(response) return out