1.0.0 Alpha Release #59

Merged
netkas merged 213 commits from v1.0.0_alpha into master 2023-01-29 23:27:58 +00:00
8 changed files with 677 additions and 244 deletions
Showing only changes of commit 273d4b6612 - Show all commits

View file

@ -2,15 +2,15 @@
namespace ncc\Abstracts;
abstract class RemoteAuthenticationType
abstract class AuthenticationType
{
/**
* A combination of a username and password is used for authentication
*/
const UsernamePassword = 'USERNAME_PASSWORD';
const UsernamePassword = 1;
/**
* A single private access token is used for authentication
*/
const PrivateAccessToken = 'PRIVATE_ACCESS_TOKEN';
const AccessToken = 2;
}

View file

@ -0,0 +1,30 @@
<?php
namespace ncc\Interfaces;
use ncc\Abstracts\AuthenticationType;
interface PasswordInterface
{
/**
* @param bool $bytecode
* @return array
*/
public function toArray(bool $bytecode=false): array;
/**
* @param array $data
* @return static
*/
public static function fromArray(array $data): self;
/**
* @return string
*/
public function getAuthenticationType(): string;
/**
* @return string
*/
public function __toString(): string;
}

View file

@ -8,7 +8,7 @@
use ncc\Abstracts\Scopes;
use ncc\Abstracts\Versions;
use ncc\Exceptions\AccessDeniedException;
use ncc\Exceptions\InvalidCredentialsEntryException;
use ncc\Exceptions\FileNotFoundException;
use ncc\Exceptions\IOException;
use ncc\Exceptions\RuntimeException;
use ncc\Objects\Vault;
@ -20,10 +20,16 @@
class CredentialManager
{
/**
* @var null
* @var string
*/
private $CredentialsPath;
/**
* @var Vault
*/
private $Vault;
/**
* Public Constructor
*/
@ -31,23 +37,16 @@
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->CredentialsPath = PathFinder::getDataPath(Scopes::System) . DIRECTORY_SEPARATOR . 'credentials.store';
}
$this->Vault = null;
/**
* Determines if CredentialManager has correct access to manage credentials on the system
*
* @return bool
*/
public function checkAccess(): bool
try
{
$ResolvedScope = Resolver::resolveScope();
if($ResolvedScope !== Scopes::System)
{
return False;
$this->loadVault();
}
catch(Exception $e)
{
unset($e);
}
return True;
}
/**
@ -64,95 +63,67 @@
if(file_exists($this->CredentialsPath))
return;
if(!$this->checkAccess())
{
if(Resolver::resolveScope() !== Scopes::System)
throw new AccessDeniedException('Cannot construct credentials store without system permissions');
}
$VaultObject = new Vault();
$VaultObject->Version = Versions::CredentialsStoreVersion;
IO::fwrite($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()), 0600);
IO::fwrite($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()), 0744);
}
/**
* Returns the vault object from the credentials store file.
* Loads the vault from the disk
*
* @return Vault
* @throws AccessDeniedException
* @throws IOException
* @throws RuntimeException
*/
public function getVault(): Vault
{
$this->constructStore();
if(!$this->checkAccess())
{
throw new AccessDeniedException('Cannot read credentials store without system permissions');
}
try
{
$Vault = ZiProto::decode(IO::fread($this->CredentialsPath));
}
catch(Exception $e)
{
// TODO: Implement error-correction for corrupted credentials store.
throw new RuntimeException($e->getMessage(), $e);
}
return Vault::fromArray($Vault);
}
/**
* Saves the vault object to the credentials store
*
* @param Vault $vault
* @return void
* @throws AccessDeniedException
* @throws IOException
* @throws RuntimeException
* @throws FileNotFoundException
*/
public function saveVault(Vault $vault): void
public function loadVault(): void
{
if(!$this->checkAccess())
if($this->Vault !== null)
return;
if(!file_exists($this->CredentialsPath))
{
throw new AccessDeniedException('Cannot write to credentials store without system permissions');
$this->Vault = new Vault();
return;
}
IO::fwrite($this->CredentialsPath, ZiProto::encode($vault->toArray()), 0600);
$VaultArray = ZiProto::decode(IO::fread($this->CredentialsPath));
$VaultObject = new Vault();
$VaultObject->fromArray($VaultArray);
if($VaultObject->Version !== Versions::CredentialsStoreVersion)
throw new RuntimeException('Credentials store version mismatch');
$this->Vault = $VaultObject;
}
/**
* Registers an entry to the credentials store file
* Saves the vault to the disk
*
* @param Vault\Entry $entry
* @return void
* @throws AccessDeniedException
* @throws InvalidCredentialsEntryException
* @throws RuntimeException
* @throws IOException
* @noinspection PhpUnused
*/
public function registerEntry(Vault\Entry $entry): void
public function saveVault(): void
{
if(!preg_match('/^[\w-]+$/', $entry->Alias))
{
throw new InvalidCredentialsEntryException('The property \'Alias\' must be alphanumeric (Regex error)');
if(Resolver::resolveScope() !== Scopes::System)
throw new AccessDeniedException('Cannot save credentials store without system permissions');
IO::fwrite($this->CredentialsPath, ZiProto::encode($this->Vault->toArray()), 0744);
}
// TODO: Implement more validation checks for the rest of the entry properties.
// TODO: Implement encryption for entries that require encryption (For securing passwords and data)
$Vault = $this->getVault();
$Vault->Entries[] = $entry;
$this->saveVault($Vault);
}
/**
* @return null
* @return string
* @noinspection PhpUnused
*/
public function getCredentialsPath(): ?string
public function getCredentialsPath(): string
{
return $this->CredentialsPath;
}

View file

@ -1,9 +1,16 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace ncc\Objects;
use ncc\Objects\Vault\DefaultEntry;
use ncc\Abstracts\AuthenticationType;
use ncc\Defuse\Crypto\Crypto;
use ncc\Exceptions\RuntimeException;
use ncc\Interfaces\PasswordInterface;
use ncc\Objects\Vault\Entry;
use ncc\Utilities\Functions;
use ncc\ZiProto\ZiProto;
class Vault
{
@ -21,13 +28,6 @@
*/
public $Entries;
/**
*
*
* @var DefaultEntry[]
*/
public $DefaultEntries;
/**
* Public Constructor
*/
@ -37,46 +37,168 @@
}
/**
* Returns an array representation of the object
* Adds a new entry to the vault
*
* @return array
* @param string $name
* @param PasswordInterface $password
* @param bool $encrypt
* @return bool
* @noinspection PhpUnused
*/
public function toArray(): array
public function addEntry(string $name, PasswordInterface $password, bool $encrypt=true): bool
{
$Entries = [];
// Check if the entry already exists
foreach($this->Entries as $entry)
{
$Entries[] = $entry->toArray();
if($entry->getName() === $name)
return false;
}
// Create the new entry
$entry = new Entry();
$entry->setName($name);
$entry->setEncrypted($encrypt);
$entry->setAuthentication($password);
// Add the entry to the vault
$this->Entries[] = $entry;
return true;
}
/**
* Deletes an entry from the vault
*
* @param string $name
* @return bool
* @noinspection PhpUnused
*/
public function deleteEntry(string $name): bool
{
foreach($this->Entries as $entry)
{
if($entry->getName() === $name)
{
$this->Entries = array_diff($this->Entries, [$entry]);
return true;
}
}
return false;
}
/**
* Returns all the entries in the vault
*
* @return array|Entry[]
* @noinspection PhpUnused
*/
public function getEntries(): array
{
return $this->Entries;
}
/**
* Returns an existing entry from the vault
*
* @param string $name
* @return Entry|null
*/
public function getEntry(string $name): ?Entry
{
foreach($this->Entries as $entry)
{
if($entry->getName() === $name)
return $entry;
}
return null;
}
/**
* Authenticates an entry in the vault
*
* @param string $name
* @param string $password
* @return bool
* @throws RuntimeException
* @noinspection PhpUnused
*/
public function authenticate(string $name, string $password): bool
{
$entry = $this->getEntry($name);
if($entry === null)
return false;
if($entry->getPassword() === null)
{
if($entry->isEncrypted() && !$entry->isIsCurrentlyDecrypted())
{
return $entry->unlock($password);
}
}
$input = [];
switch($entry->getPassword()->getAuthenticationType())
{
case AuthenticationType::UsernamePassword:
$input = ['password' => $password];
break;
case AuthenticationType::AccessToken:
$input = ['token' => $password];
break;
}
return $entry->authenticate($input);
}
/**
* Returns an array representation of the object
*
* @param bool $bytecode
* @return array
*/
public function toArray(bool $bytecode=false): array
{
$entries = [];
foreach($this->Entries as $entry)
{
$entry_array = $entry->toArray($bytecode);
if($entry->getPassword() !== null && $entry->isEncrypted())
{
$entry_array['password'] = Crypto::encryptWithPassword(
ZiProto::encode($entry_array['password']), $entry->getPassword()->__toString(), $bytecode
);
}
$entries[] = $entry_array;
}
return [
'version' => $this->Version,
'entries' => $Entries
($bytecode ? Functions::cbc('version') : 'version') => $this->Version,
($bytecode ? Functions::cbc('entries') : 'entries') => $entries,
];
}
/**
* Constructs an object from an array representation
* Constructs a new object from an array
*
* @param array $data
* @param array $array
* @return Vault
*/
public static function fromArray(array $data): Vault
public static function fromArray(array $array): Vault
{
$VaultObject = new Vault();
$vault = new Vault();
$vault->Version = Functions::array_bc($array, 'version');
$entries = Functions::array_bc($array, 'entries');
if(isset($data['version']))
$VaultObject->Version = $data['version'];
if(isset($data['entries']))
foreach($entries as $entry)
{
foreach($data['entries'] as $entry)
{
$VaultObject->Entries[] = Entry::fromArray($entry);
}
$entry = Entry::fromArray($entry);
$vault->Entries[] = $entry;
}
return $VaultObject;
return $vault;
}
}

View file

@ -1,60 +0,0 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace ncc\Objects\Vault;
class DefaultEntry
{
/**
* The alias entry to use for default authentication
*
* @var string
*/
public $Alias;
/**
* The source that the alias is for
*
* @var string
*/
public $Source;
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
return [
'alias' => $this->Alias,
'source' => $this->Source
];
}
/**
* Constructs the object from an array representation
*
* @param array $data
* @return DefaultEntry
*/
public static function fromArray(array $data): DefaultEntry
{
$DefaultEntryObject = new DefaultEntry();
if(isset($data['alias']))
{
$DefaultEntryObject->Alias = $data['alias'];
}
if(isset($data['source']))
{
$DefaultEntryObject->Source = $data['source'];
}
return $DefaultEntryObject;
}
}

View file

@ -4,124 +4,327 @@
namespace ncc\Objects\Vault;
use ncc\Abstracts\RemoteAuthenticationType;
use ncc\Abstracts\RemoteSource;
use ncc\Abstracts\AuthenticationType;
use ncc\Defuse\Crypto\Crypto;
use ncc\Defuse\Crypto\Exception\EnvironmentIsBrokenException;
use ncc\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
use ncc\Exceptions\RuntimeException;
use ncc\Interfaces\PasswordInterface;
use ncc\Objects\Vault\Password\AccessToken;
use ncc\Objects\Vault\Password\UsernamePassword;
use ncc\Utilities\Functions;
use ncc\ZiProto\ZiProto;
class Entry
{
/**
* The unique alias of the source entry, can also be used for remote resource fetching for dependencies with the
* following example schemes;
*
* - alias@github.com/org/package
* - alias@git.example.org/org/package
* - alias@gitlab.com/org/package
* The entry's unique identifier
*
* @var string
*/
public $Alias;
private $Name;
/**
* The remote source of the entry, currently only supported sources are allowed.
*
* @var string|RemoteSource
*/
public $Source;
/**
* The host of the remote source, eg; github.com or git.example.org, will be used for remote resource fetching
* for dependencies with the following example schemes;
*
* - github.com/org/package
* - git.example.org/org/package
* - gitlab.com/org/package
*
* @var string
*/
public $SourceHost;
/**
* @var string|RemoteAuthenticationType
*/
public $AuthenticationType;
/**
* Indicates if the authentication details are encrypted or not, if encrypted a passphrase is required
* by the user
* Whether the entry's password is encrypted
*
* @var bool
*/
public $Encrypted;
private $Encrypted;
/**
* The authentication details.
* The entry's password
*
* If the remote authentication type is private access token, the first index (0) would be the key itself
* If the remote authentication type is a username and password, first index would be Username and second
* would be the password.
*
* @var array
* @var PasswordInterface|string|null
*/
public $Authentication;
private $Password;
/**
* Whether the entry's password is currently decrypted in memory
* (Not serialized)
*
* @var bool
*/
private $IsCurrentlyDecrypted;
/**
* Returns an array representation of the object
*
* @return array
* @noinspection PhpArrayShapeAttributeCanBeAddedInspection
*/
public function toArray(): array
public function __construct()
{
$this->Encrypted = true;
$this->IsCurrentlyDecrypted = true;
}
/**
* Test Authenticates the entry
*
* For UsernamePassword the $input parameter expects an array with the keys 'username' and 'password'
* For AccessToken the $input parameter expects an array with the key 'token'
*
* @param array $input
* @return bool
* @noinspection PhpUnused
*/
public function authenticate(array $input): bool
{
if(!$this->IsCurrentlyDecrypted)
return false;
if($this->Password == null)
return false;
switch($this->Password->getAuthenticationType())
{
case AuthenticationType::UsernamePassword:
if(!($this->Password instanceof UsernamePassword))
return false;
$username = $input['username'] ?? null;
$password = $input['password'] ?? null;
if($username === null && $password === null)
return false;
if($username == null)
return $password == $this->Password->Password;
if($password == null)
return $username == $this->Password->Username;
return $username == $this->Password->Username && $password == $this->Password->Password;
case AuthenticationType::AccessToken:
if(!($this->Password instanceof AccessToken))
return false;
$token = $input['token'] ?? null;
if($token === null)
return false;
return $token == $this->Password->AccessToken;
default:
return false;
}
}
/**
* @param PasswordInterface $password
* @return void
*/
public function setAuthentication(PasswordInterface $password): void
{
$this->Password = $password;
}
/**
* @return bool
* @noinspection PhpUnused
*/
public function isIsCurrentlyDecrypted(): bool
{
return $this->IsCurrentlyDecrypted;
}
/**
* Locks the entry by encrypting the password
*
* @return bool
*/
public function lock(): bool
{
if($this->Password == null)
return false;
if($this->Encrypted)
return false;
if(!$this->IsCurrentlyDecrypted)
return false;
if(!($this->Password instanceof PasswordInterface))
return false;
$this->Password = $this->encrypt();
return true;
}
/**
* Unlocks the entry by decrypting the password
*
* @param string $password
* @return bool
* @throws RuntimeException
* @noinspection PhpUnused
*/
public function unlock(string $password): bool
{
if($this->Password == null)
return false;
if(!$this->Encrypted)
return false;
if($this->IsCurrentlyDecrypted)
return false;
if(!is_string($this->Password))
return false;
try
{
$password = Crypto::decryptWithPassword($this->Password, $password, true);
}
catch (EnvironmentIsBrokenException $e)
{
throw new RuntimeException('Cannot decrypt password', $e);
}
catch (WrongKeyOrModifiedCiphertextException $e)
{
unset($e);
return false;
}
$this->Password = ZiProto::decode($password);
$this->IsCurrentlyDecrypted = true;
return true;
}
/**
* Returns the password object as an encrypted binary string
*
* @return string|null
*/
private function encrypt(): ?string
{
if(!$this->IsCurrentlyDecrypted)
return false;
if($this->Password == null)
return false;
if(!($this->Password instanceof PasswordInterface))
return null;
$password = ZiProto::encode($this->Password->toArray(true));
return Crypto::encryptWithPassword($password, $password, true);
}
/**
* Returns an array representation of the object
*
* @param bool $bytecode
* @return array
*/
public function toArray(bool $bytecode=false): array
{
if(!$this->Password)
{
if($this->Encrypted && $this->IsCurrentlyDecrypted)
{
$password = $this->encrypt();
}
else
{
$password = $this->Password->toArray(true);
}
}
else
{
$password = $this->Password;
}
return [
'alias' => $this->Alias,
'source' => $this->Source,
'source_host' => $this->SourceHost,
'authentication_type' => $this->AuthenticationType,
'encrypted' => $this->Encrypted,
'authentication' => $this->Authentication
($bytecode ? Functions::cbc('name') : 'name') => $this->Name,
($bytecode ? Functions::cbc('encrypted') : 'encrypted') => $this->Encrypted,
($bytecode ? Functions::cbc('password') : 'password') => $password,
];
}
/**
* Returns an array representation of the object
* Constructs an object from an array representation
*
* @param array $data
* @return Entry
*/
public static function fromArray(array $data): Entry
public static function fromArray(array $data): self
{
$EntryObject = new Entry();
$self = new self();
if(isset($data['alias']))
$self->Name = Functions::array_bc($data, 'name');
$self->Encrypted = Functions::array_bc($data, 'encrypted');
$password = Functions::array_bc($data, 'password');
if($password !== null)
{
$EntryObject->Alias = $data['alias'];
if($self->Encrypted)
{
$self->Password = $password;
$self->IsCurrentlyDecrypted = false;
}
elseif(gettype($password) == 'array')
{
$self->Password = match (Functions::array_bc($data, 'authentication_type')) {
AuthenticationType::UsernamePassword => UsernamePassword::fromArray($password),
AuthenticationType::AccessToken => AccessToken::fromArray($password)
};
}
}
if(isset($data['source']))
{
$EntryObject->Source = $data['source'];
return $self;
}
if(isset($data['source_host']))
/**
* @return bool
*/
public function isEncrypted(): bool
{
$EntryObject->SourceHost = $data['source_host'];
return $this->Encrypted;
}
if(isset($data['authentication_type']))
/**
* Returns false if the entry needs to be decrypted first
*
* @param bool $Encrypted
* @return bool
*/
public function setEncrypted(bool $Encrypted): bool
{
$EntryObject->AuthenticationType = $data['authentication_type'];
if(!$this->IsCurrentlyDecrypted)
return false;
$this->Encrypted = $Encrypted;
return true;
}
if(isset($data['encrypted']))
/**
* @return string
*/
public function getName(): string
{
$EntryObject->Encrypted = $data['encrypted'];
return $this->Name;
}
if(isset($data['authentication']))
/**
* @param string $Name
*/
public function setName(string $Name): void
{
$EntryObject->Authentication = $data['authentication'];
$this->Name = $Name;
}
return $EntryObject;
/**
* @return PasswordInterface|null
*/
public function getPassword(): ?PasswordInterface
{
if(!$this->IsCurrentlyDecrypted)
return null;
return $this->Password;
}
}

View file

@ -0,0 +1,74 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace ncc\Objects\Vault\Password;
use ncc\Abstracts\AuthenticationType;
use ncc\Interfaces\PasswordInterface;
use ncc\Utilities\Functions;
class AccessToken implements PasswordInterface
{
/**
* The entry's access token
*
* @var string
*/
public $AccessToken;
/**
* Returns an array representation of the object
*
* @param bool $bytecode
* @return array
*/
public function toArray(bool $bytecode=false): array
{
return [
($bytecode ? Functions::cbc('authentication_type') : 'authentication_type') => AuthenticationType::AccessToken,
($bytecode ? Functions::cbc('access_token') : 'access_token') => $this->AccessToken,
];
}
/**
* Constructs an object from an array representation
*
* @param array $data
* @return static
*/
public static function fromArray(array $data): self
{
$object = new self();
$object->AccessToken = Functions::array_bc($data, 'access_token');
return $object;
}
/**
* @return string
*/
public function getAccessToken(): string
{
return $this->AccessToken;
}
/**
* @inheritDoc
*/
public function getAuthenticationType(): string
{
return AuthenticationType::AccessToken;
}
/**
* Returns a string representation of the object
*
* @return string
*/
public function __toString(): string
{
return $this->AccessToken;
}
}

View file

@ -0,0 +1,93 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace ncc\Objects\Vault\Password;
use ncc\Abstracts\AuthenticationType;
use ncc\Interfaces\PasswordInterface;
use ncc\Utilities\Functions;
class UsernamePassword implements PasswordInterface
{
/**
* The entry's username
*
* @var string
*/
public $Username;
/**
* The entry's password
*
* @var string
*/
public $Password;
/**
* Returns an array representation of the object
*
* @param bool $bytecode
* @return array
*/
public function toArray(bool $bytecode=false): array
{
return [
($bytecode ? Functions::cbc('authentication_type') : 'authentication_type') => AuthenticationType::UsernamePassword,
($bytecode ? Functions::cbc('username') : 'username') => $this->Username,
($bytecode ? Functions::cbc('password') : 'password') => $this->Password,
];
}
/**
* Constructs an object from an array representation
*
* @param array $data
* @return static
*/
public static function fromArray(array $data): self
{
$instance = new self();
$instance->Username = Functions::array_bc($data, 'username');
$instance->Password = Functions::array_bc($data, 'password');
return $instance;
}
/**
* @return string
* @noinspection PhpUnused
*/
public function getUsername(): string
{
return $this->Username;
}
/**
* @return string
* @noinspection PhpUnused
*/
public function getPassword(): string
{
return $this->Password;
}
/**
* @inheritDoc
*/
public function getAuthenticationType(): string
{
return AuthenticationType::UsernamePassword;
}
/**
* Returns a string representation of the object
*
* @return string
*/
public function __toString(): string
{
return $this->Password;
}
}