diff --git a/.gitattributes b/.gitattributes index dfe0770..0a697e1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,9 @@ # Auto detect text files and perform LF normalization -* text=auto +* text eol=lf +*.c eol=crlf +*.json eol=crlf +*.png binary +*.jpg binary +*.ico binary +*.pdf binary +*.exe binary diff --git a/CMakeLists.txt b/CMakeLists.txt index def22d5..b1ba42c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(SQLITE_HAS_CODEC ON) if (POLICY CMP0141) cmake_policy(SET CMP0141 NEW) @@ -21,7 +22,10 @@ add_library (${PROJECT_NAME} SHARED src/helpers/stringhelpers.cpp src/helpers/webhelpers.cpp src/keyring/credential.cpp + src/keyring/keyring.cpp + src/keyring/keyringdialogcontroller.cpp src/keyring/passwordgenerator.cpp + src/keyring/store.cpp src/keyring/systemcredentials.cpp src/update/updater.cpp src/appinfo.cpp @@ -44,8 +48,6 @@ find_package(CURL REQUIRED) target_link_libraries(${PROJECT_NAME} PUBLIC CURL::libcurl) find_package(maddy REQUIRED) target_link_libraries(${PROJECT_NAME} PUBLIC maddy::maddy) -find_package(sqlcipher REQUIRED) -target_link_libraries(${PROJECT_NAME} PUBLIC sqlcipher::sqlcipher) find_package(SQLiteCpp REQUIRED) target_link_libraries(${PROJECT_NAME} PUBLIC SQLiteCpp) if(LINUX) diff --git a/conanfile-linux.txt b/conanfile-linux.txt index 23a0388..e247686 100644 --- a/conanfile-linux.txt +++ b/conanfile-linux.txt @@ -6,6 +6,7 @@ maddy/1.3.0 sqlcipher/4.5.1 sqlitecpp/3.3.1 libsecret/0.20.5 +gtest/1.14.0 [generators] CMakeDeps diff --git a/conanfile-windows.txt b/conanfile-windows.txt index 139732d..1008e90 100644 --- a/conanfile-windows.txt +++ b/conanfile-windows.txt @@ -5,6 +5,7 @@ libcurl/8.4.0 maddy/1.3.0 sqlcipher/4.5.1 sqlitecpp/3.3.1 +gtest/1.14.0 [generators] CMakeDeps diff --git a/include/aura.h b/include/aura.h index 997a098..76aac1a 100644 --- a/include/aura.h +++ b/include/aura.h @@ -2,7 +2,7 @@ #define AURA_H #include -#include +#include #include #include "appinfo.h" #include "configurationbase.h" @@ -61,7 +61,7 @@ namespace Nickvision::Aura static Aura& getActive(); private: - static std::optional m_instance; + static std::unique_ptr m_instance; }; } diff --git a/include/dependencylocator.h b/include/dependencylocator.h index 1eff089..f8380b6 100644 --- a/include/dependencylocator.h +++ b/include/dependencylocator.h @@ -2,7 +2,6 @@ #define DEPENDENCYLOCATOR_H #include -#include #include namespace Nickvision::Aura::DependencyLocator @@ -10,9 +9,9 @@ namespace Nickvision::Aura::DependencyLocator /** * @brief Finds the path of a given dependency. * @param dependency The name of the dependency to find - * @return The path of the dependency if found, else std::nullopt + * @return The path of the dependency if found, else empty path */ - const std::optional& find(std::string dependency); + const std::filesystem::path& find(std::string dependency); } #endif //DEPENDENCYLOCATOR_H \ No newline at end of file diff --git a/include/enumflags.h b/include/enumflags.h new file mode 100644 index 0000000..ea9cfe7 --- /dev/null +++ b/include/enumflags.h @@ -0,0 +1,34 @@ +#ifndef ENUMFLAGS_H +#define ENUMFLAGS_H + +#define DEFINE_ENUM_FLAG_OPERATORS(T) \ +inline T operator~(T a) \ +{ \ + return (T)~(int)a; \ +} \ +inline T operator|(T a, T b) \ +{ \ + return (T)((int)a | (int)b); \ +} \ +inline T operator&(T a, T b) \ +{ \ + return (T)((int)a & (int)b); \ +} \ +inline T operator^(T a, T b) \ +{ \ + return (T)((int)a ^ (int)b); \ +} \ +inline T& operator|=(T& a, T b) \ +{ \ + return (T&)((int&)a |= (int)b); \ +} \ +inline T& operator&=(T& a, T b) \ +{ \ + return (T&)((int&)a &= (int)b); \ +} \ +inline T& operator^=(T& a, T b) \ +{ \ + return (T&)((int&)a ^= (int)b); \ +} \ + +#endif //ENUMFLAGS_H \ No newline at end of file diff --git a/include/events/event.h b/include/events/event.h index 4a047b4..43041db 100644 --- a/include/events/event.h +++ b/include/events/event.h @@ -35,7 +35,7 @@ namespace Nickvision::Aura::Events /** * @brief Constructs an Event via move. */ - Event(Event&& e) + Event(Event&& e) noexcept { std::lock_guard lock{ e.m_mutex }; m_handlers = std::move(e.m_handlers); @@ -96,6 +96,8 @@ namespace Nickvision::Aura::Events } /** * @brief Copies an Event + * @param e The Event to copy + * @return this */ Event& operator=(const Event& e) { @@ -109,6 +111,8 @@ namespace Nickvision::Aura::Events } /** * @brief Moves an Event + * @param e The Event to move + * @return this */ Event& operator=(Event&& e) { diff --git a/include/helpers/stringhelpers.h b/include/helpers/stringhelpers.h index 90a6e2b..ed2c732 100644 --- a/include/helpers/stringhelpers.h +++ b/include/helpers/stringhelpers.h @@ -48,6 +48,12 @@ namespace Nickvision::Aura::StringHelpers * @return The guid value */ std::string newGuid(); + /** + * @brief Gets whether or not the provided string is a valid url + * @param s The string to check + * @return True if the string is a valid url, else false + */ + bool isValidUrl(const std::string& s); } diff --git a/include/keyring/credentialcheckstatus.h b/include/keyring/credentialcheckstatus.h new file mode 100644 index 0000000..0fa9b57 --- /dev/null +++ b/include/keyring/credentialcheckstatus.h @@ -0,0 +1,22 @@ +#ifndef CREDENTIALCHECKSTATUS_H +#define CREDENTIALCHECKSTATUS_H + +#include "enumflags.h" + +namespace Nickvision::Aura::Keyring +{ + /** + * @brief Flags to describe the status of a validated credential + */ + enum class CredentialCheckStatus + { + Valid = 1, + EmptyName = 2, + EmptyUsernamePassword = 4, + InvalidUri = 8 + }; + + DEFINE_ENUM_FLAG_OPERATORS(CredentialCheckStatus); +} + +#endif //CREDENTIALCHECKSTATUS_H \ No newline at end of file diff --git a/include/keyring/keyring.h b/include/keyring/keyring.h new file mode 100644 index 0000000..c519ea8 --- /dev/null +++ b/include/keyring/keyring.h @@ -0,0 +1,94 @@ +#ifndef KEYRING_H +#define KEYRING_H + +#include +#include +#include +#include "credential.h" +#include "store.h" + +namespace Nickvision::Aura::Keyring +{ + /** + * @brief A model of a keyring object for managing credentials. + */ + class Keyring + { + public: + /** + * @brief Gets the name of the keyring. + * @return The name of the keyring + */ + const std::string& getName() const; + /** + * @brief Gets all credentials in the keyring. + * @return The list of all credentials + */ + std::vector getAllCredentials() const; + /** + * @brief Gets the credential matching the provided id. + * @param id The id of the credential + * @return The credential matching the id, std::nullopt if no matching credential + */ + std::optional getCredential(int id) const; + /** + * @brief Gets the credentials containing the provided name. + * @param name The name + * @return The list of credentials matching the name + */ + std::vector getCredentials(const std::string& name) const; + /** + * @brief Adds a credential to the keyring. + * @param credential The Credential to add + * @return True if successful, else false + */ + bool addCredential(const Credential& credential); + /** + * @brief Updates a credential in the keyring. + * @param credential The Credential to update + * @return True if successful, else false + */ + bool updateCredential(const Credential& credential); + /** + * @brief Deletes a credential from the keyring. + * @param id The id of the credential to delete + * @return True if successful, else false + */ + bool deleteCredential(int id); + /** + * @brief Destroys the keyring and all of its data from disk. Once this method is called, the object should no longer be referenced, with or without success. + * @return True if successful, else false + */ + bool destroy(); + + private: + Keyring(const Store& store); + Store m_store; + + public: + /** + * @brief Accesses a Keyring. The Keyring will first attempt to load the Store. If the Store doesn't exist, it will create a new Store. + * @param name The name of the store + * @param password The password to use for the store. If empty, the password will be fetched/created from the system's credential store + * @return The newly accessed keyring, std::nullopt if accessing failed + */ + static std::optional access(const std::string& name, std::string password = ""); + /** + * @brief Gets whether or not a keyring exists with the provided name. + * @param name The name of the keyring to check + * @return True if a keyring with the provied name exists, else false + */ + static bool exists(const std::string& name); + /** + * @brief Destroys a keyring and all of its data from disk. + * @param The name of the keyring to destroy + * @return True if successful, else false + */ + static bool destroy(const std::string& name); + + private: + + }; +} + +#endif //KEYRING_H \ No newline at end of file diff --git a/include/keyring/keyringdialogcontroller.h b/include/keyring/keyringdialogcontroller.h new file mode 100644 index 0000000..6e60992 --- /dev/null +++ b/include/keyring/keyringdialogcontroller.h @@ -0,0 +1,89 @@ +#ifndef KEYRINGDIALOGCONTROLLER_H +#define KEYRINGDIALOGCONTROLLER_H + +#include +#include +#include +#include "credential.h" +#include "credentialcheckstatus.h" +#include "keyring.h" + +namespace Nickvision::Aura::Keyring +{ + class KeyringDialogController + { + public: + /** + * @brief Constructs a KeyringDialogController. + * @param name The name of the controller + * @param keyring The keyring managed by the controller, if available + */ + KeyringDialogController(const std::string& name, const std::optional& keyring); + /** + * @brief Gets the keyring managed by the controller, if available. + * @return The keyring if available, else std::nullopt + */ + const std::optional& getKeyring(); + /** + * @brief Gets whether or not the keyring is enabled (unlocked). + * @retrun True if enabled, else false + */ + bool isEnabled() const; + /** + * @brief Gets whether or not the keyring state is valid. + * @return True if valid, else false + */ + bool isValid() const; + /** + * @brief Enables the keyring. + * @param password The password of the keyring + * @return True if successful, else false + */ + bool enableKeyring(const std::string& password = ""); + /** + * @brief Disables the keyring and destroys its data. + * @return True if successful, else false + */ + bool disableKeyring(); + /** + * @brief Resets the keyring and destroys its data. To be used only if the keyring is locked. If unlocked, use disableKeyring() . + * @return True if successful, else false + */ + bool resetKeyring(); + /** + * @brief Validates a Credential object. + * @param credential The credential to validate + * @return CredentialCheckStatus + */ + CredentialCheckStatus validateCredential(const Credential& credential) const; + /** + * @brief Gets all credentials in the keyring. + * @return The list of all credentials + */ + std::vector getAllCredentials() const; + /** + * @brief Adds a credential to the keyring. + * @param credential The Credential to add + * @return True if successful, else false + */ + bool addCredential(const Credential& credential); + /** + * @brief Updates a credential in the keyring. + * @param credential The Credential to update + * @return True if successful, else false + */ + bool updateCredential(const Credential& credential); + /** + * @brief Deletes a credential from the keyring. + * @param id The id of the credential to delete + * @return True if successful, else false + */ + bool deleteCredential(int id); + + private: + std::string m_name; + std::optional m_keyring; + }; +} + +#endif //KEYRINGDIALOGCONTROLLER_H \ No newline at end of file diff --git a/include/keyring/passwordcontent.h b/include/keyring/passwordcontent.h index c44889d..7b9ad12 100644 --- a/include/keyring/passwordcontent.h +++ b/include/keyring/passwordcontent.h @@ -1,6 +1,8 @@ #ifndef PASSWORDCONTENT_H #define PASSWORDCONTENT_H +#include "enumflags.h" + namespace Nickvision::Aura::Keyring { /** @@ -14,40 +16,7 @@ namespace Nickvision::Aura::Keyring Special = 8 }; - inline PasswordContent operator~(PasswordContent a) - { - return (PasswordContent)~(int)a; - } - - inline PasswordContent operator|(PasswordContent a, PasswordContent b) - { - return (PasswordContent)((int)a | (int)b); - } - - inline PasswordContent operator&(PasswordContent a, PasswordContent b) - { - return (PasswordContent)((int)a & (int)b); - } - - inline PasswordContent operator^(PasswordContent a, PasswordContent b) - { - return (PasswordContent)((int)a ^ (int)b); - } - - inline PasswordContent& operator|=(PasswordContent& a, PasswordContent b) - { - return (PasswordContent&)((int&)a |= (int)b); - } - - inline PasswordContent& operator&=(PasswordContent& a, PasswordContent b) - { - return (PasswordContent&)((int&)a &= (int)b); - } - - inline PasswordContent& operator^=(PasswordContent& a, PasswordContent b) - { - return (PasswordContent&)((int&)a ^= (int)b); - } + DEFINE_ENUM_FLAG_OPERATORS(PasswordContent); } #endif //PASSWORDCONTENT_H \ No newline at end of file diff --git a/include/keyring/store.h b/include/keyring/store.h new file mode 100644 index 0000000..c9cf061 --- /dev/null +++ b/include/keyring/store.h @@ -0,0 +1,137 @@ +#ifndef STORE_H +#define STORE_H + +#include +#include +#include +#include +#include +#include +#include +#include "credential.h" + +namespace Nickvision::Aura::Keyring +{ + /** + * @brief A store object for credentials. Backed by sqlciphe. + */ + class Store + { + public: + /** + * @brief Copies a Store object. + * @parma store The object to move + */ + Store(const Store& store); + /** + * @brief Moves a Store object. + * @parma store The object to move + */ + Store(Store&& store) noexcept; + /** + * @brief Gets the name of the store. + * @return The name of the store + */ + const std::string& getName() const; + /** + * @brief Gets the file path of the store on disk. + * @return The file path of the store + */ + const std::filesystem::path& getPath() const; + /** + * @brief Gets all credentials in the store. + * @return The list of all credentials + */ + std::vector getAllCredentials() const; + /** + * @brief Gets the credential matching the provided id. + * @param id The id of the credential + * @return The credential matching the id, std::nullopt if no matching credential + */ + std::optional getCredential(int id) const; + /** + * @brief Gets the credentials containing the provided name. + * @param name The name + * @return The list of credentials matching the name + */ + std::vector getCredentials(const std::string& name) const; + /** + * @brief Adds a credential to the store. + * @param credential The Credential to add + * @return True if successful, else false + */ + bool addCredential(const Credential& credential); + /** + * @brief Updates a credential in the store. + * @param credential The Credential to update + * @return True if successful, else false + */ + bool updateCredential(const Credential& credential); + /** + * @brief Deletes a credential from the store. + * @param id The id of the credential to delete + * @return True if successful, else false + */ + bool deleteCredential(int id); + /** + * @brief Destroys the store and all of its data from disk. Once this method is called, the object should no longer be referenced, with or without success. + * @return True if successful, else false + */ + bool destroy(); + /** + * @brief Copies an Store + * @param store The Store to copy + * @return this + */ + Store& operator=(const Store& store); + /** + * @brief Moves an Store + * @param store The Store to move + * @return this + */ + Store& operator=(Store&& store); + + private: + Store(const std::string& name, const std::shared_ptr& database); + mutable std::mutex m_mutex; + std::shared_ptr m_database; + std::string m_name; + std::filesystem::path m_path; + + public: + /** + * @brief Gets the directory where stores are saved on disk. + * @return The directory for stores + */ + static std::filesystem::path getStoreDir(); + /** + * @brief Creates a new store. + * @param name The name of the store + * @param password The password of the store + * @param overwrite Whether or not to overwrite a store that already exists with the provided name + * @return The created Store object, std::nullopt if creation failed + */ + static std::optional create(const std::string& name, const std::string& password, bool overwrite = true); + /** + * @brief Loads a store. + * @param name The name of the store + * @param password The password of the store + * @return The loaded Store object, std::nullopt if loading failed + */ + static std::optional load(const std::string& name, const std::string& password); + /** + * @brief Gets whether or not a store exists with the provided name. + * @param name The name of the store to check + * @return True if a store with the provied name exists, else false + */ + static bool exists(const std::string& name); + /** + * @brief Destroys a store and all of its data from disk. + * @param The name of the store to destroy + * @return True if successful, else false + */ + static bool destroy(const std::string& name); + }; +} + +#endif //STORE_H \ No newline at end of file diff --git a/include/keyring/systemcredentials.h b/include/keyring/systemcredentials.h index 560c9bb..4824890 100644 --- a/include/keyring/systemcredentials.h +++ b/include/keyring/systemcredentials.h @@ -18,13 +18,25 @@ namespace Nickvision::Aura::Keyring::SystemCredentials * @param name The name of the credential * @return The new Credential object, if successful */ - std::optional addNewCredential(const std::string& name); + std::optional addCredential(const std::string& name); /** - * @brief Deletes a Credential from the system's credential manager - * @param credential The Credential to delete + * @brief Adds a new credential to the system's credential manager. + * @param name The new credential object * @return True if successful, else false */ - bool deleteCredential(const Credential& credential); + bool addCredential(const Credential& credential); + /** + * @brief Updates a credential in the system's credential manager. + * @param name The updated credential object + * @return True if successful, else false + */ + bool updateCredential(const Credential& credential); + /** + * @brief Deletes a credential from the system's credential manager. + * @param name The name of the credential to delete + * @return True if successful, else false + */ + bool deleteCredential(const std::string& name); } #endif //SYSTEMCREDENTIALS_H \ No newline at end of file diff --git a/include/update/updater.h b/include/update/updater.h index d30fbeb..099b065 100644 --- a/include/update/updater.h +++ b/include/update/updater.h @@ -1,6 +1,7 @@ #ifndef UPDATER_H #define UPDATER_H +#include #include #include #include "../version.h" @@ -19,14 +20,14 @@ namespace Nickvision::Aura::Update Updater(); /** * @brief Gets the latest stable version from the GitHub repo. - * @return The current stable version, if available + * @return The current stable version if available, else empty Version */ - std::optional getCurrentStableVersion(); + Version fetchCurrentStableVersion(); /** * @brief Gets the latest preview version from the GitHub repo. - * @return The current preview version, if available + * @return The current preview version if available, else empty Version */ - std::optional getCurrentPreviewVersion(); + Version fetchCurrentPreviewVersion(); /** * @brief Downloads and installs an application update for Windows. getCurrentStableVersion or getCurrentPreviewVersion should be called first before running this method. This method will force quit the current running application to install the update. * @param versionType The type of version update to install @@ -35,11 +36,12 @@ namespace Nickvision::Aura::Update bool windowsUpdate(VersionType versionType); private: + mutable std::mutex m_mutex; std::string m_repoOwner; std::string m_repoName; int m_latestStableReleaseId; int m_latestPreviewReleaseId; - std::optional getCurrentVersion(VersionType versionType); + Version fetchCurrentVersion(VersionType versionType); }; } diff --git a/include/version.h b/include/version.h index a3a6bda..f035360 100644 --- a/include/version.h +++ b/include/version.h @@ -55,6 +55,11 @@ namespace Nickvision::Aura * @return The string representation of the Version */ const std::string& toString() const; + /** + * @brief Gets whether or not the Version object is empty + * @return True if empty, else false + */ + bool empty() const; /** * @brief Compares Version objects via < operator * @param compare The Version object to compare too diff --git a/src/appinfo.cpp b/src/appinfo.cpp index 83fd80e..dab6cad 100644 --- a/src/appinfo.cpp +++ b/src/appinfo.cpp @@ -94,7 +94,7 @@ namespace Nickvision::Aura markdown << StringHelpers::trim(line); markdown << std::endl; } - std::shared_ptr parser{ std::make_shared() }; + std::unique_ptr parser{ std::make_unique() }; m_htmlChangelog = parser->Parse(markdown); } diff --git a/src/aura.cpp b/src/aura.cpp index 9481958..3e5536d 100644 --- a/src/aura.cpp +++ b/src/aura.cpp @@ -3,7 +3,7 @@ namespace Nickvision::Aura { - std::optional Aura::m_instance = std::nullopt; + std::unique_ptr Aura::m_instance = nullptr; Aura::Aura(const std::string& id, const std::string& name) { @@ -26,16 +26,16 @@ namespace Nickvision::Aura Aura& Aura::init(const std::string& id, const std::string& name) { - if (!m_instance.has_value()) + if (!m_instance) { - m_instance = { id, name }; + m_instance = std::unique_ptr(new Aura(id, name)); } return *m_instance; } Aura& Aura::getActive() { - if (!m_instance.has_value()) + if (!m_instance) { throw std::logic_error("Aura::init() must be called first."); } diff --git a/src/dependencylocator.cpp b/src/dependencylocator.cpp index b09d6c0..eb64404 100644 --- a/src/dependencylocator.cpp +++ b/src/dependencylocator.cpp @@ -1,12 +1,15 @@ #include "dependencylocator.h" #include +#include #include "systemdirectories.h" namespace Nickvision::Aura { - const std::optional& DependencyLocator::find(std::string dependency) + const std::filesystem::path& DependencyLocator::find(std::string dependency) { - static std::map> locations; + static std::map locations; + static std::mutex mutex; + std::lock_guard lock{ mutex }; #ifdef _WIN32 if (!std::filesystem::path(dependency).has_extension()) { @@ -15,13 +18,13 @@ namespace Nickvision::Aura #endif if (locations.contains(dependency)) { - const std::optional& location = locations[dependency]; - if (location.has_value() && std::filesystem::exists(location.value())) + const std::filesystem::path& location = locations[dependency]; + if (std::filesystem::exists(location)) { return location; } } - locations[dependency] = std::nullopt; + locations[dependency] = std::filesystem::path(); std::filesystem::path current{ std::filesystem::current_path() / dependency }; if (std::filesystem::exists(current)) { diff --git a/src/helpers/stringhelpers.cpp b/src/helpers/stringhelpers.cpp index f4e128e..cf739ab 100644 --- a/src/helpers/stringhelpers.cpp +++ b/src/helpers/stringhelpers.cpp @@ -1,6 +1,7 @@ #include "helpers/stringhelpers.h" #include #include +#include #include #ifdef _WIN32 #include @@ -91,4 +92,14 @@ namespace Nickvision::Aura out << std::setfill('0') << std::setw(2) << std::hex << static_cast(guid[15]); return out.str(); } + + bool StringHelpers::isValidUrl(const std::string& s) + { + if (s.empty()) + { + return false; + } + std::regex urlRegex{ "https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)" }; + return std::regex_match(s, urlRegex); + } } \ No newline at end of file diff --git a/src/keyring/keyring.cpp b/src/keyring/keyring.cpp new file mode 100644 index 0000000..afc4fa3 --- /dev/null +++ b/src/keyring/keyring.cpp @@ -0,0 +1,120 @@ +#include "keyring/keyring.h" +#include "keyring/systemcredentials.h" + +namespace Nickvision::Aura::Keyring +{ + Keyring::Keyring(const Store& store) + : m_store{ store } + { + + } + + const std::string& Keyring::getName() const + { + return m_store.getName(); + } + + std::vector Keyring::getAllCredentials() const + { + return m_store.getAllCredentials(); + } + + std::optional Keyring::getCredential(int id) const + { + return m_store.getCredential(id); + } + + std::vector Keyring::getCredentials(const std::string& name) const + { + return m_store.getCredentials(name); + } + + bool Keyring::addCredential(const Credential& credential) + { + return m_store.addCredential(credential); + } + + bool Keyring::updateCredential(const Credential& credential) + { + return m_store.updateCredential(credential); + } + + bool Keyring::deleteCredential(int id) + { + return m_store.deleteCredential(id); + } + + bool Keyring::destroy() + { + if (m_store.destroy()) + { + return SystemCredentials::deleteCredential(m_store.getName()); + } + return false; + } + + std::optional Keyring::access(const std::string& name, std::string password) + { + //If password empty, get password from system credential store + if (password.empty()) + { + std::optional cred{ SystemCredentials::getCredential(name) }; + password = cred ? cred->getPassword() : ""; + } + //If password not empty (a.k.a user-provided or system-provided), get store + if (!password.empty()) + { + //Load store + std::optional storeLoad{ Store::load(name, password) }; + if (storeLoad) + { + return { { *storeLoad } }; + } + //Store unable to be loaded, create a new one + else + { + std::optional storeCreate{ Store::create(name, password) }; + if (storeCreate) + { + return { { *storeCreate } }; + } + else + { + return std::nullopt; + } + } + } + //Create a new store with a new password from the system credential store + else + { + std::optional cred{ SystemCredentials::addCredential(name) }; + if (cred) + { + std::optional storeCreate{ Store::create(name, cred->getPassword()) }; + if (storeCreate) + { + return { { *storeCreate } }; + } + else + { + return std::nullopt; + } + } + } + return std::nullopt; + } + + bool Keyring::exists(const std::string& name) + { + return Store::exists(name); + } + + bool Keyring::destroy(const std::string& name) + { + if (Store::destroy(name)) + { + return SystemCredentials::deleteCredential(name); + } + return false; + } +} \ No newline at end of file diff --git a/src/keyring/keyringdialogcontroller.cpp b/src/keyring/keyringdialogcontroller.cpp new file mode 100644 index 0000000..fdcf375 --- /dev/null +++ b/src/keyring/keyringdialogcontroller.cpp @@ -0,0 +1,118 @@ +#include "keyring/keyringdialogcontroller.h" +#include "helpers/stringhelpers.h" + +namespace Nickvision::Aura::Keyring +{ + KeyringDialogController::KeyringDialogController(const std::string& name, const std::optional& keyring) + : m_name{ name }, + m_keyring{ keyring } + { + + } + + const std::optional& KeyringDialogController::getKeyring() + { + return m_keyring; + } + + bool KeyringDialogController::isEnabled() const + { + return m_keyring.has_value(); + } + + bool KeyringDialogController::isValid() const + { + return !(!m_keyring && Keyring::exists(m_name)); + } + + bool KeyringDialogController::enableKeyring(const std::string& password) + { + if (!m_keyring) + { + m_keyring = Keyring::access(m_name, password); + return m_keyring.has_value(); + } + return false; + } + + bool KeyringDialogController::disableKeyring() + { + if (m_keyring) + { + m_keyring->destroy(); + m_keyring = std::nullopt; + return true; + } + return false; + } + + bool KeyringDialogController::resetKeyring() + { + if (!m_keyring) + { + return Keyring::destroy(m_name); + } + return false; + } + + CredentialCheckStatus KeyringDialogController::validateCredential(const Credential& credential) const + { + CredentialCheckStatus result = (CredentialCheckStatus)0; + if (credential.getName().empty()) + { + result |= CredentialCheckStatus::EmptyName; + } + if (credential.getUsername().empty() && credential.getPassword().empty()) + { + result |= CredentialCheckStatus::EmptyUsernamePassword; + } + if (!credential.getUri().empty() && !StringHelpers::isValidUrl(credential.getUri())) + { + result |= CredentialCheckStatus::InvalidUri; + } + if ((int)result != 0) + { + return result; + } + return CredentialCheckStatus::Valid; + } + + std::vector KeyringDialogController::getAllCredentials() const + { + if (m_keyring) + { + return m_keyring->getAllCredentials(); + } + return {}; + } + + bool KeyringDialogController::addCredential(const Credential& credential) + { + if (m_keyring && (validateCredential(credential) & CredentialCheckStatus::Valid) == CredentialCheckStatus::Valid) + { + return m_keyring->addCredential(credential); + } + return false; + } + + bool KeyringDialogController::updateCredential(const Credential& credential) + { + if (m_keyring && (validateCredential(credential) & CredentialCheckStatus::Valid) == CredentialCheckStatus::Valid) + { + if (m_keyring->getCredential(credential.getId())) + { + return m_keyring->updateCredential(credential); + } + } + return false; + } + + bool KeyringDialogController::deleteCredential(int id) + { + if (m_keyring) + { + return m_keyring->deleteCredential(id); + } + return false; + } +} \ No newline at end of file diff --git a/src/keyring/store.cpp b/src/keyring/store.cpp new file mode 100644 index 0000000..93a43c7 --- /dev/null +++ b/src/keyring/store.cpp @@ -0,0 +1,226 @@ +#include "keyring/store.h" +#include "userdirectories.h" + +using namespace SQLite; + +namespace Nickvision::Aura::Keyring +{ + std::filesystem::path getPathFromName(const std::string& name) + { + return Store::getStoreDir() / (name + ".ring"); + } + + Store::Store(const std::string& name, const std::shared_ptr& database) + : m_name{ name }, + m_database{ database }, + m_path{ getPathFromName(name) } + { + m_database->exec("CREATE TABLE IF NOT EXISTS credentials (id TEXT PRIMARY KEY, name TEXT, uri TEXT, username TEXT, password TEXT)"); + } + + Store::Store(const Store& store) + { + std::lock_guard lock{ store.m_mutex }; + m_name = store.m_name; + m_database = store.m_database; + m_path = store.m_path; + } + + Store::Store(Store&& store) noexcept + { + std::lock_guard lock{ store.m_mutex }; + m_name = std::move(store.m_name); + m_database = std::move(store.m_database); + m_path = std::move(store.m_path); + } + + const std::string& Store::getName() const + { + std::lock_guard lock{ m_mutex }; + return m_name; + } + + const std::filesystem::path& Store::getPath() const + { + std::lock_guard lock{ m_mutex }; + return m_path; + } + + std::vector Store::getAllCredentials() const + { + std::lock_guard lock{ m_mutex }; + std::vector creds; + Statement query{ *m_database, "SELECT * FROM credentials" }; + while (query.executeStep()) + { + creds.push_back({ query.getColumn(0).getInt(), query.getColumn(1).getString(), query.getColumn(2).getString(), query.getColumn(3).getString(), query.getColumn(4).getString() }); + } + return creds; + } + + std::optional Store::getCredential(int id) const + { + std::lock_guard lock{ m_mutex }; + Statement query{ *m_database, "SELECT * FROM credentials where id = ?" }; + query.bind(1, id); + query.executeStep(); + if (query.hasRow()) + { + return { { query.getColumn(0).getInt(), query.getColumn(1).getString(), query.getColumn(2).getString(), query.getColumn(3).getString(), query.getColumn(4).getString() } }; + } + return std::nullopt; + } + + std::vector Store::getCredentials(const std::string& name) const + { + std::lock_guard lock{ m_mutex }; + std::vector creds; + Statement query{ *m_database, "SELECT * FROM credentials where name = ?" }; + query.bind(1, name); + while (query.executeStep()) + { + creds.push_back({ query.getColumn(0).getInt(), query.getColumn(1).getString(), query.getColumn(2).getString(), query.getColumn(3).getString(), query.getColumn(4).getString() }); + } + return creds; + } + + bool Store::addCredential(const Credential& credential) + { + std::lock_guard lock{ m_mutex }; + Statement query{ *m_database, "INSERT INTO credentials (id, name, uri, username, password) VALUES (?, ?, ?, ?, ?)" }; + query.bind(1, credential.getId()); + query.bind(2, credential.getName()); + query.bind(3, credential.getUri()); + query.bind(4, credential.getUsername()); + query.bind(5, credential.getPassword()); + return query.exec() > 0; + } + + bool Store::updateCredential(const Credential& credential) + { + std::lock_guard lock{ m_mutex }; + Statement query{ *m_database, "UPDATE credentials SET name = ?, uri = ?, username = ?, password = ? where id = ?" }; + query.bind(5, credential.getId()); + query.bind(1, credential.getName()); + query.bind(2, credential.getUri()); + query.bind(3, credential.getUsername()); + query.bind(4, credential.getPassword()); + return query.exec() > 0; + } + + bool Store::deleteCredential(int id) + { + std::lock_guard lock{ m_mutex }; + Statement query{ *m_database, "DELETE FROM credentials WHERE id = ?" }; + query.bind(1, id); + return query.exec() > 0; + } + + bool Store::destroy() + { + std::lock_guard lock{ m_mutex }; + m_database->~Database(); + m_database.reset(); + return std::filesystem::remove(m_path); + } + + Store& Store::operator=(const Store& store) + { + if (this != &store) + { + std::lock_guard lock{ m_mutex }; + std::lock_guard lock2{ store.m_mutex }; + m_name = store.m_name; + m_database = store.m_database; + m_path = store.m_path; + } + return *this; + } + + Store& Store::operator=(Store&& store) + { + if (this != &store) + { + std::lock_guard lock{ m_mutex }; + std::lock_guard lock2{ store.m_mutex }; + std::swap(m_name, store.m_name); + std::swap(m_database, store.m_database); + std::swap(m_path, store.m_path); + } + return *this; + } + + std::filesystem::path Store::getStoreDir() + { + return UserDirectories::getConfig() / "Nickvision" / "Keyring"; + } + + std::optional Store::create(const std::string& name, const std::string& password, bool overwrite) + { + std::filesystem::create_directories(getStoreDir()); + if (name.empty() || password.empty()) + { + return std::nullopt; + } + std::filesystem::path path{ getPathFromName(name) }; + if (std::filesystem::exists(path)) + { + if (overwrite) + { + std::filesystem::remove(path); + } + else + { + return std::nullopt; + } + } + try + { + std::shared_ptr database{ std::make_shared(path.string(), OPEN_READWRITE | OPEN_CREATE) }; + database->key(password); + return { {name, database} }; + } + catch (...) + { + return std::nullopt; + } + } + + std::optional Store::load(const std::string& name, const std::string& password) + { + if (name.empty() || password.empty()) + { + return std::nullopt; + } + std::filesystem::path path{ getPathFromName(name) }; + if (!std::filesystem::exists(path)) + { + return std::nullopt; + } + try + { + std::shared_ptr database{ std::make_shared(path.string(), OPEN_READWRITE | OPEN_CREATE) }; + database->key(password); + return { {name, database} }; + } + catch (...) + { + return std::nullopt; + } + } + + bool Store::exists(const std::string& name) + { + return std::filesystem::exists(getPathFromName(name)); + } + + bool Store::destroy(const std::string& name) + { + std::filesystem::path path{ getPathFromName(name) }; + if (std::filesystem::exists(path)) + { + return std::filesystem::remove(path); + } + return false; + } +} \ No newline at end of file diff --git a/src/keyring/systemcredentials.cpp b/src/keyring/systemcredentials.cpp index 1af6a5a..1864982 100644 --- a/src/keyring/systemcredentials.cpp +++ b/src/keyring/systemcredentials.cpp @@ -41,10 +41,19 @@ namespace Nickvision::Aura::Keyring return std::nullopt; } - std::optional SystemCredentials::addNewCredential(const std::string& name) + std::optional SystemCredentials::addCredential(const std::string& name) { PasswordGenerator passGen; Credential c{ name, "", "NickvisionKeyring", passGen.next() }; + if (addCredential(c)) + { + return c; + } + return std::nullopt; + } + + bool SystemCredentials::addCredential(const Credential& credential) + { #ifdef _WIN32 CREDENTIALA* cred{ new CREDENTIALA() }; cred->AttributeCount = 0; @@ -52,36 +61,70 @@ namespace Nickvision::Aura::Keyring cred->Comment = nullptr; cred->Type = CRED_TYPE_GENERIC; cred->Persist = CRED_PERSIST_LOCAL_MACHINE; - cred->TargetName = LPSTR(c.getName().c_str()); - cred->UserName = LPSTR(c.getUsername().c_str()); - cred->CredentialBlobSize = (unsigned long)c.getPassword().size(); - cred->CredentialBlob = LPBYTE(c.getPassword().c_str()); + cred->TargetName = LPSTR(credential.getName().c_str()); + cred->UserName = LPSTR(credential.getUsername().c_str()); + cred->CredentialBlobSize = (unsigned long)credential.getPassword().size(); + cred->CredentialBlob = LPBYTE(credential.getPassword().c_str()); bool res = CredWriteA(cred, 0); CredFree(cred); - if (!res) - { - return std::nullopt; - } - return c; + return res; #else GError* error{ nullptr }; - secret_password_store_sync(&KEYRING_SCHEMA, SECRET_COLLECTION_DEFAULT, c.getName().c_str(), c.getPassword().c_str(), nullptr, &error, "application", c.getName().c_str(), NULL); + secret_password_store_sync(&KEYRING_SCHEMA, SECRET_COLLECTION_DEFAULT, credential.getName().c_str(), credential.getPassword().c_str(), nullptr, &error, "application", credential.getName().c_str(), NULL); if (error) { g_error_free(error); - return std::nullopt; + return false; + } + return true; +#endif + } + + bool SystemCredentials::updateCredential(const Credential& credential) + { +#ifdef _WIN32 + CREDENTIALA* cred{ nullptr }; + if (CredReadA(credential.getName().c_str(), CRED_TYPE_GENERIC, 0, &cred)) + { + cred->AttributeCount = 0; + cred->Attributes = nullptr; + cred->Comment = nullptr; + cred->Type = CRED_TYPE_GENERIC; + cred->Persist = CRED_PERSIST_LOCAL_MACHINE; + cred->TargetName = LPSTR(credential.getName().c_str()); + cred->UserName = LPSTR(credential.getUsername().c_str()); + cred->CredentialBlobSize = (unsigned long)credential.getPassword().size(); + cred->CredentialBlob = LPBYTE(credential.getPassword().c_str()); + bool res = CredWriteA(cred, 0); + CredFree(cred); + return res; + } +#else + GError* error{ nullptr }; + char* password = secret_password_lookup_sync(&KEYRING_SCHEMA, nullptr, &error, "application", credential.getName().c_str(), NULL); + if (!error && password) + { + secret_password_free(password); + secret_password_store_sync(&KEYRING_SCHEMA, SECRET_COLLECTION_DEFAULT, credential.getName().c_str(), credential.getPassword().c_str(), nullptr, &error, "application", credential.getName().c_str(), NULL); + if (error) + { + g_error_free(error); + return false; + } + return true; } - return c; + g_error_free(error); #endif + return false; } - bool SystemCredentials::deleteCredential(const Credential& credential) + bool SystemCredentials::deleteCredential(const std::string& name) { #ifdef _WIN32 - return CredDeleteA(credential.getName().c_str(), CRED_TYPE_GENERIC, 0); + return CredDeleteA(name.c_str(), CRED_TYPE_GENERIC, 0); #else GError* error{ nullptr }; - bool res = secret_password_clear_sync(&KEYRING_SCHEMA, nullptr, &error, "application", credential.getName().c_str(), NULL); + bool res = secret_password_clear_sync(&KEYRING_SCHEMA, nullptr, &error, "application", name.c_str(), NULL); if (!error) { return res; diff --git a/src/update/updater.cpp b/src/update/updater.cpp index 4e63fe9..7d9fa63 100644 --- a/src/update/updater.cpp +++ b/src/update/updater.cpp @@ -27,18 +27,19 @@ namespace Nickvision::Aura::Update m_repoName = fields[4]; } - std::optional Updater::getCurrentStableVersion() + Version Updater::fetchCurrentStableVersion() { - return getCurrentVersion(VersionType::Stable); + return fetchCurrentVersion(VersionType::Stable); } - std::optional Updater::getCurrentPreviewVersion() + Version Updater::fetchCurrentPreviewVersion() { - return getCurrentVersion(VersionType::Preview); + return fetchCurrentVersion(VersionType::Preview); } bool Updater::windowsUpdate(VersionType versionType) { + std::lock_guard lock{ m_mutex }; #ifdef _WIN32 if (versionType == VersionType::Stable ? m_latestStableReleaseId == -1 : m_latestPreviewReleaseId == -1) { @@ -74,8 +75,9 @@ namespace Nickvision::Aura::Update return false; } - std::optional Updater::getCurrentVersion(VersionType versionType) + Version Updater::fetchCurrentVersion(VersionType versionType) { + std::lock_guard lock{ m_mutex }; std::string releases = WebHelpers::fetchJsonString("https://api.github.com/repos/" + m_repoOwner + "/" + m_repoName + "/releases"); if (!releases.empty()) { @@ -89,16 +91,16 @@ namespace Nickvision::Aura::Update if (versionType == VersionType::Stable && version.find('-') == std::string::npos) { m_latestStableReleaseId = release.get("id", -1).asInt(); - return { version }; + return version; } if (versionType == VersionType::Preview && version.find('-') != std::string::npos) { m_latestPreviewReleaseId = release.get("id", -1).asInt(); - return { version }; + return version; } } } } - return std::nullopt; + return {}; } } \ No newline at end of file diff --git a/src/userdirectories.cpp b/src/userdirectories.cpp index d6c4030..629cdaf 100644 --- a/src/userdirectories.cpp +++ b/src/userdirectories.cpp @@ -78,14 +78,14 @@ namespace Nickvision::Aura result = getXDGDir("XDG_CONFIG_HOME"); result = result.empty() ? (getHome() / ".config") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } std::filesystem::path UserDirectories::getApplicationConfig() { std::filesystem::path result = getConfig() / Aura::getActive().getAppInfo().getName(); - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -103,14 +103,14 @@ namespace Nickvision::Aura result = getXDGDir("XDG_CACHE_HOME"); result = result.empty() ? (getHome() / ".cache") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } std::filesystem::path UserDirectories::getApplicationCache() { std::filesystem::path result = getCache() / Aura::getActive().getAppInfo().getName(); - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -123,14 +123,14 @@ namespace Nickvision::Aura result = getXDGDir("XDG_DATA_HOME"); result = result.empty() ? (getHome() / ".local/share") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } std::filesystem::path UserDirectories::getApplicationLocalData() { std::filesystem::path result = getLocalData() / Aura::getActive().getAppInfo().getName(); - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -158,7 +158,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_DESKTOP_DIR"); result = result.empty() ? (getHome() / "Desktop") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -176,7 +176,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_DOCUMENTS_DIR"); result = result.empty() ? (getHome() / "Documents") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -194,7 +194,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_DOWNLOAD_DIR"); result = result.empty() ? (getHome() / "Downloads") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -212,7 +212,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_MUSIC_DIR"); result = result.empty() ? (getHome() / "Music") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -230,7 +230,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_PICTURES_DIR"); result = result.empty() ? (getHome() / "Pictures") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -257,7 +257,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_TEMPLATES_DIR"); result = result.empty() ? (getHome() / "Templates") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } @@ -275,7 +275,7 @@ namespace Nickvision::Aura result = getXDGDir("XDG_VIDEOS_DIR"); result = result.empty() ? (getHome() / "Videos") : result; #endif - std::filesystem::create_directory(result); + std::filesystem::create_directories(result); return result; } } \ No newline at end of file diff --git a/src/version.cpp b/src/version.cpp index 661cbc6..3593fc6 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -85,6 +85,11 @@ namespace Nickvision::Aura return m_str; } + bool Version::empty() const + { + return m_major == 0 && m_minor == 0 && m_build == 0 && m_dev.empty(); + } + bool Version::operator<(const Version& compare) const { if (m_major < compare.m_major)