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"
Exemple #4
0
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
    }
Exemple #5
0
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())
Exemple #6
0
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()
Exemple #7
0
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()
Exemple #8
0
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