From 273d4b66129434cc2ca6f22e69a6e3ba2dbe2213 Mon Sep 17 00:00:00 2001 From: Netkas Date: Tue, 6 Dec 2022 04:26:43 -0500 Subject: [PATCH] Refactored CredentialManager & Vault https://git.n64.cc/nosial/ncc/-/issues/27 --- ...icationType.php => AuthenticationType.php} | 6 +- src/ncc/Interfaces/PasswordInterface.php | 30 ++ src/ncc/Managers/CredentialManager.php | 119 +++--- src/ncc/Objects/Vault.php | 180 +++++++-- src/ncc/Objects/Vault/DefaultEntry.php | 60 --- src/ncc/Objects/Vault/Entry.php | 359 ++++++++++++++---- .../Objects/Vault/Password/AccessToken.php | 74 ++++ .../Vault/Password/UsernamePassword.php | 93 +++++ 8 files changed, 677 insertions(+), 244 deletions(-) rename src/ncc/Abstracts/{RemoteAuthenticationType.php => AuthenticationType.php} (60%) create mode 100644 src/ncc/Interfaces/PasswordInterface.php delete mode 100644 src/ncc/Objects/Vault/DefaultEntry.php create mode 100644 src/ncc/Objects/Vault/Password/AccessToken.php create mode 100644 src/ncc/Objects/Vault/Password/UsernamePassword.php diff --git a/src/ncc/Abstracts/RemoteAuthenticationType.php b/src/ncc/Abstracts/AuthenticationType.php similarity index 60% rename from src/ncc/Abstracts/RemoteAuthenticationType.php rename to src/ncc/Abstracts/AuthenticationType.php index 64233b3..044a977 100644 --- a/src/ncc/Abstracts/RemoteAuthenticationType.php +++ b/src/ncc/Abstracts/AuthenticationType.php @@ -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; } \ No newline at end of file diff --git a/src/ncc/Interfaces/PasswordInterface.php b/src/ncc/Interfaces/PasswordInterface.php new file mode 100644 index 0000000..488c7dd --- /dev/null +++ b/src/ncc/Interfaces/PasswordInterface.php @@ -0,0 +1,30 @@ +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 - { - $ResolvedScope = Resolver::resolveScope(); - - if($ResolvedScope !== Scopes::System) + try { - 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'); - // 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); + IO::fwrite($this->CredentialsPath, ZiProto::encode($this->Vault->toArray()), 0744); } + /** - * @return null + * @return string + * @noinspection PhpUnused */ - public function getCredentialsPath(): ?string + public function getCredentialsPath(): string { return $this->CredentialsPath; } diff --git a/src/ncc/Objects/Vault.php b/src/ncc/Objects/Vault.php index 8a60105..c8440e3 100644 --- a/src/ncc/Objects/Vault.php +++ b/src/ncc/Objects/Vault.php @@ -1,9 +1,16 @@ 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; } + } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/DefaultEntry.php b/src/ncc/Objects/Vault/DefaultEntry.php deleted file mode 100644 index c834f0d..0000000 --- a/src/ncc/Objects/Vault/DefaultEntry.php +++ /dev/null @@ -1,60 +0,0 @@ - $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; - } - - } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Entry.php b/src/ncc/Objects/Vault/Entry.php index 27c778f..e85df14 100644 --- a/src/ncc/Objects/Vault/Entry.php +++ b/src/ncc/Objects/Vault/Entry.php @@ -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'])) - { - $EntryObject->SourceHost = $data['source_host']; - } + /** + * @return bool + */ + public function isEncrypted(): bool + { + return $this->Encrypted; + } - if(isset($data['authentication_type'])) - { - $EntryObject->AuthenticationType = $data['authentication_type']; - } + /** + * Returns false if the entry needs to be decrypted first + * + * @param bool $Encrypted + * @return bool + */ + public function setEncrypted(bool $Encrypted): bool + { + if(!$this->IsCurrentlyDecrypted) + return false; - if(isset($data['encrypted'])) - { - $EntryObject->Encrypted = $data['encrypted']; - } + $this->Encrypted = $Encrypted; + return true; + } - if(isset($data['authentication'])) - { - $EntryObject->Authentication = $data['authentication']; - } + /** + * @return string + */ + public function getName(): string + { + return $this->Name; + } - return $EntryObject; + /** + * @param string $Name + */ + public function setName(string $Name): void + { + $this->Name = $Name; + } + + /** + * @return PasswordInterface|null + */ + public function getPassword(): ?PasswordInterface + { + if(!$this->IsCurrentlyDecrypted) + return null; + + return $this->Password; } } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Password/AccessToken.php b/src/ncc/Objects/Vault/Password/AccessToken.php new file mode 100644 index 0000000..ec00407 --- /dev/null +++ b/src/ncc/Objects/Vault/Password/AccessToken.php @@ -0,0 +1,74 @@ + 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; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Password/UsernamePassword.php b/src/ncc/Objects/Vault/Password/UsernamePassword.php new file mode 100644 index 0000000..25097a6 --- /dev/null +++ b/src/ncc/Objects/Vault/Password/UsernamePassword.php @@ -0,0 +1,93 @@ + 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; + } + } \ No newline at end of file