Verified Commit 0aae7ba1 authored by Didier WECKMANN's avatar Didier WECKMANN
Browse files

feat(io): implement a session writer service

- add a password mode for input dialog
- add ROII cursor and temporary file classes
- fixes regarding encryption
parent 8bec599b
......@@ -104,6 +104,7 @@
<type mode="reader" />
<selection mode="exclude" />
<addSelection service="sight::module::io::atoms::SReader" />
<addSelection service="sight::module::io::session::SReader" />
<addSelection service="sight::module::io::matrix::SAttachmentSeriesReader" />
</config>
</extension>
......@@ -204,4 +205,23 @@
</config>
</extension>
<!-- Service config used for session reader and writer -->
<extension implements="sight::service::extension::Config">
<id>DefaultSessionReaderConfig</id>
<service>sight::module::io::session::SReader</service>
<desc>Session reader</desc>
<config>
<extension name=".sight" description="Sight session file"/>
</config>
</extension>
<extension implements="sight::service::extension::Config">
<id>DefaultSessionWriterConfig</id>
<service>sight::module::io::session::SWriter</service>
<desc>Session writer</desc>
<config>
<extension name=".sight" description="Sight session file"/>
</config>
</extension>
</plugin>
sight_add_target( config_ui_activity TYPE MODULE )
add_dependencies(config_ui_activity
add_dependencies(config_ui_activity
data
module_activity
module_data
module_ui_base
module_ui_qt
module_io_session
module_io_atoms
module_io_activity
module_ui_icons
......
......@@ -67,9 +67,9 @@
<param name="ICON_PATH" />
<param name="seriesDB" />
<param name="SEQUENCER_CONFIG" />
<param name="WIZARD_CONFIG" default="defaultWizardConfig"/>
<param name="ACTIVITY_READER_CONFIG" default="ActivityReaderConfig" />
<param name="ACTIVITY_WRITER_CONFIG" default="ActivityWriterConfig" />
<param name="WIZARD_CONFIG" default="defaultWizardConfig" />
<param name="SESSION_READER_CONFIG" default="DefaultSessionReaderConfig" />
<param name="SESSION_WRITER_CONFIG" default="DefaultSessionWriterConfig" />
<param name="APPLICATION_ICON_PATH" default="module_ui_icons/sight_logo.svg" />
</parameters>
<desc>Configuration to launch activities sequentially</desc>
......@@ -77,7 +77,7 @@
<!-- ************************************ Begin Objects declaration **************************************** -->
<object uid="${seriesDB}" type="sight::data::SeriesDB" src="ref"/>
<object uid="${seriesDB}" type="sight::data::SeriesDB" src="ref" />
<!-- ************************************* Begin layouts declaration *************************************** -->
......@@ -121,41 +121,40 @@
<!-- ************************************* Begin editor declaration *************************************** -->
<service uid="openButton" type="sight::module::ui::qt::com::SSignalButton" >
<service uid="openButton" type="sight::module::ui::qt::com::SSignalButton">
<config>
<icon>sight::module::ui::icons/open.svg</icon>
<iconWidth>40</iconWidth>
<iconHeight>40</iconHeight>
<icon>sight::module::ui::icons/open.svg</icon>
<iconWidth>40</iconWidth>
<iconHeight>40</iconHeight>
</config>
</service>
<service uid="saveButton" type="sight::module::ui::qt::com::SSignalButton" >
<service uid="saveButton" type="sight::module::ui::qt::com::SSignalButton">
<config>
<icon>sight::module::ui::icons/save.svg</icon>
<iconWidth>40</iconWidth>
<iconHeight>40</iconHeight>
<icon>sight::module::ui::icons/save.svg</icon>
<iconWidth>40</iconWidth>
<iconHeight>40</iconHeight>
</config>
</service>
<service uid="appLogoView" type="sight::module::ui::qt::image::SImage" >
<service uid="appLogoView" type="sight::module::ui::qt::image::SImage">
<path>${APPLICATION_ICON_PATH}</path>
<height>70</height>
</service>
<service uid="ircadIhuLogoView" type="sight::module::ui::qt::image::SImage" >
<path>flaticons/IrcadIHU.svg</path>
<service uid="ircadIhuLogoView" type="sight::module::ui::qt::image::SImage">
<path>sight::module::ui::flaticons/IrcadIHU.svg</path>
<height>70</height>
</service>
<!-- ************************************* Begin reader/writer declaration *************************************** -->
<!-- ************************************* Begin reader/writer declaration ********************************* -->
<service uid="SDBReader" type="sight::module::ui::base::io::SSelector" config="${ACTIVITY_READER_CONFIG}">
<service uid="SDBReader" type="sight::module::io::session::SReader" config="${SESSION_READER_CONFIG}">
<inout key="data" uid="${seriesDB}" />
</service>
<!-- Service to save the launched activities -->
<service uid="SDBWriter" type="sight::module::ui::base::io::SSelector" config="${ACTIVITY_WRITER_CONFIG}">
<inout key="data" uid="${seriesDB}" />
<service uid="SDBWriter" type="sight::module::io::session::SWriter" config="${SESSION_WRITER_CONFIG}">
<in key="data" uid="${seriesDB}" />
</service>
<!-- *************************************** Begin view declaration **************************************** -->
......@@ -178,7 +177,7 @@
</service>
<!-- Launch the activity sequentially -->
<service uid="activitySequencer" type="sight::module::ui::qt::activity::SSequencer" config="${SEQUENCER_CONFIG}" >
<service uid="activitySequencer" type="sight::module::ui::qt::activity::SSequencer" config="${SEQUENCER_CONFIG}">
<inout key="seriesDB" uid="${seriesDB}" autoConnect="true" />
</service>
......@@ -264,4 +263,4 @@
<update uid="activitySequencer" />
</config>
</extension>
</extension>
\ No newline at end of file
<plugin id="sight::config::ui::activity">
<requirement id="sight::module::io::session" />
<requirement id="sight::module::io::atoms" />
<requirement id="sight::module::activity" />
<requirement id="sight::module::io::activity" />
......
......@@ -36,6 +36,8 @@
<parameter replace="WIZARD_CONFIG" by="${WIZARD_CONFIG}" />
<parameter replace="ICON_PATH" by="${ICON_PATH}" />
<parameter replace="WID_PARENT" by="activityView" />
<parameter replace="SESSION_READER_CONFIG" by="SessionReaderConfig" />
<parameter replace="SESSION_WRITER_CONFIG" by="SessionWriterConfig" />
</service>
<!-- ******************************* Start services ***************************************** -->
......
......@@ -100,6 +100,26 @@
</config>
</extension>
<extension implements="sight::service::extension::Config">
<id>SessionReaderConfig</id>
<service>sight::module::io::session::SReader</service>
<desc>Session reader</desc>
<config>
<extension name=".sample" description="Sample Sight session file"/>
<password policy="once" encryption="salted"/>
</config>
</extension>
<extension implements="sight::service::extension::Config">
<id>SessionWriterConfig</id>
<service>sight::module::io::session::SWriter</service>
<desc>Session writer</desc>
<config>
<extension name=".sample" description="Sample Sight session file"/>
<password policy="once" encryption="salted"/>
</config>
</extension>
<extension implements="::sight::service::extension::AppConfigParameters" >
<id>ExActivities_AppCfgParam</id>
<parameters>
......
......@@ -51,12 +51,10 @@ inline static T xxcrypt(
// Initialize key (265 bits) and iv(128 bits)
unsigned char key[HASH_SIZE];
unsigned char iv[EVP_MAX_IV_LENGTH];
unsigned char iv[EVP_MAX_IV_LENGTH] = {0};
// Compute password hash and randomize initialization vector
hash(password, key);
const auto pseudo_random = SIGHT_PSEUDO_RANDOM_HASH("");
std::memcpy(iv, pseudo_random.data(), sizeof(iv));
// Initialize the output with size of the original message expanded to block size
T output;
......
......@@ -23,14 +23,12 @@
#include "core/config.hpp"
#include "core/crypto/secure_string.hpp"
#include "core/tools/System.hpp"
// Convenience macro to generate a pseudo random hash in a pseudo predictable way.
#define SIGHT_PSEUDO_RANDOM_HASH(salt) \
::sight::core::crypto::hash( \
::sight::core::crypto::secure_string( \
std::to_string(::sight::core::tools::System::getPID()) \
+ __FILE__ \
sight::core::crypto::hash( \
sight::core::crypto::secure_string( \
__FILE__ \
+ std::to_string(__LINE__) \
+ salt \
) \
......
......@@ -102,6 +102,22 @@ void SystemTest::robustRenameTest()
std::filesystem::filesystem_error
);
// 3. Should NOT throw an exception, even if the original file already exists.
// (re)create the fake file.
{
std::fstream fs;
fs.open(originFile, std::ios::out);
fs.close();
}
CPPUNIT_ASSERT_NO_THROW(core::tools::System::robustRename(destinationFile, originFile, true));
CPPUNIT_ASSERT_NO_THROW(core::tools::System::robustRename(originFile, destinationFile, true));
CPPUNIT_ASSERT_MESSAGE("Destination file should exist.", std::filesystem::exists(destinationFile));
CPPUNIT_ASSERT_MESSAGE("Origin file shouldn't exist", !std::filesystem::exists(originFile));
// 4. Should do nothing if both path are indeed the same file
CPPUNIT_ASSERT_NO_THROW(core::tools::System::robustRename(destinationFile, destinationFile));
// Clean up.
std::filesystem::remove(destinationFile);
}
......
......@@ -330,35 +330,41 @@ void System::cleanAllTempFolders(const std::filesystem::path& dir) noexcept
//------------------------------------------------------------------------------
void System::robustRename(const std::filesystem::path& _p1, const std::filesystem::path& _p2)
void System::robustRename(
const std::filesystem::path& _p1,
const std::filesystem::path& _p2,
bool _force
)
{
std::error_code renameError;
// First try a basic rename.
std::filesystem::rename(_p1, _p2, renameError);
if(renameError) // Error
// If both paths are indeed the same, do nothing
if(_p1.lexically_normal().compare(_p2.lexically_normal()) == 0)
{
// Handle the Invalid cross-device link case: _p1 & _p2 are not on the same disk/volume.
if(renameError == std::make_error_code(std::errc::cross_device_link))
{
// Use a copy-remove scenario instead of the rename.
std::error_code copyError;
std::filesystem::copy(_p1, _p2, copyError);
if(!copyError) // Success
{
//Remove old file.
std::filesystem::remove(_p1); // throw an exception if it fails.
}
else // Error
{
throw std::filesystem::filesystem_error(copyError.message(), copyError);
}
return;
}
std::error_code error;
// Early return, copy-remove is done.
return;
// Try a basic rename.
std::filesystem::rename(_p1, _p2, error);
if(error)
{
// Try to remove target if force is enabled
if(_force)
{
std::filesystem::remove(_p2);
}
// Throw all others errors.
throw std::filesystem::filesystem_error(renameError.message(), renameError);
if(error == std::make_error_code(std::errc::cross_device_link))
{
std::filesystem::copy(_p1, _p2);
std::filesystem::remove(_p1);
}
else
{
// This will throw the expected exception
std::filesystem::rename(_p1, _p2);
}
}
}
......
......@@ -39,6 +39,38 @@ class CORE_CLASS_API System
{
public:
/// Convenience class that deletes the associated temporary file on destruction
class TemporaryFile
{
public:
TemporaryFile(const TemporaryFile&) = delete;
TemporaryFile(TemporaryFile&&) = delete;
TemporaryFile& operator=(const TemporaryFile&) = delete;
TemporaryFile& operator=(TemporaryFile&&) = delete;
inline TemporaryFile() :
m_filePath(getTemporaryFolder() / genTempFileName())
{
}
inline ~TemporaryFile()
{
std::filesystem::remove(m_filePath);
}
//------------------------------------------------------------------------------
inline std::filesystem::path getTemporaryFilePath() const
{
return m_filePath;
}
private:
const std::filesystem::path m_filePath;
};
/**
* @brief Returns the system's temporary folder.
* Returns the value returned by std::filesystem::temp_directory_path, or
......@@ -92,9 +124,14 @@ public:
* @brief renames file or folder, use std::filesystem::rename first, use a copy-remove scenario if rename fails.
* @param _from source path of the file to rename.
* @param _to destination path of the renamed file.
* @param _force remove the destination in all cases.
* @throws std::filesystem_error if it fails.
*/
CORE_API static void robustRename(const std::filesystem::path& _from, const std::filesystem::path& _to);
CORE_API static void robustRename(
const std::filesystem::path& _from,
const std::filesystem::path& _to,
bool _force = false
);
/**
* @brief Sets the temporary folder prefix.
......
......@@ -23,6 +23,7 @@
#include <core/crypto/AES256.hpp>
#include <core/crypto/SHA256.hpp>
#include <core/tools/System.hpp>
#include <iomanip>
#include <mutex>
......@@ -39,7 +40,7 @@ static core::crypto::secure_string s_password;
// This generate the hash used to encrypt the global password
inline static core::crypto::secure_string computeGlobalPasswordKey()
{
return SIGHT_PSEUDO_RANDOM_HASH("");
return SIGHT_PSEUDO_RANDOM_HASH(std::to_string(sight::core::tools::System::getPID()));
}
class PasswordKeeper::PasswordKeeperImpl final
......
......@@ -37,13 +37,24 @@ public:
SIGHT_DECLARE_CLASS(PasswordKeeper);
/// Enum to define a password policy
enum class PasswordPolicy : uint8_t
{
NEVER = 0, /// Never use a password
ONCE = 1, /// Ask for password once and reuse it later
ALWAYS = 2, /// Always ask for a password
DEFAULT = NEVER, /// Default behavior is nothing is set
UNKNOWN = 255 /// Used for error
INVALID = 255 /// Used for error management
};
/// Enum to define a encryption policy
enum class EncryptionPolicy : uint8_t
{
PASSWORD = 0, /// Use the given password for encryption
SALTED = 1, /// Use the given password with salt for encryption
FORCED = 2, /// Force encryption with a pseudo random hidden password
DEFAULT = PASSWORD, /// Default behavior is nothing is set
INVALID = 255 /// Used for error management
};
/// Delete default copy constructors and assignment operators
......@@ -127,7 +138,52 @@ public:
else
{
// Error case
return PasswordPolicy::UNKNOWN;
return PasswordPolicy::INVALID;
}
}
/// Convenience function to convert from EncryptionPolicy enum value to string
constexpr static std::string_view encryptionPolicyToString(EncryptionPolicy policy) noexcept
{
switch(policy)
{
case EncryptionPolicy::PASSWORD:
return "password";
case EncryptionPolicy::SALTED:
return "salted";
case EncryptionPolicy::FORCED:
return "forced";
default:
return "default";
}
}
/// Convenience function to convert from string to EncryptionPolicy enum value
constexpr static EncryptionPolicy stringToEncryptionPolicy(std::string_view policy) noexcept
{
if(constexpr auto PASSWORD = encryptionPolicyToString(EncryptionPolicy::PASSWORD); policy == PASSWORD)
{
return EncryptionPolicy::PASSWORD;
}
else if(constexpr auto SALTED = encryptionPolicyToString(EncryptionPolicy::SALTED); policy == SALTED)
{
return EncryptionPolicy::SALTED;
}
else if(constexpr auto FORCED = encryptionPolicyToString(EncryptionPolicy::FORCED); policy == FORCED)
{
return EncryptionPolicy::FORCED;
}
else if(policy.empty() || policy == "default")
{
return EncryptionPolicy::DEFAULT;
}
else
{
// Error case
return EncryptionPolicy::INVALID;
}
}
......
# sight::io::session
Library for writing and reading recursively a root data object, including all fields, from/to a session file.
The session file is indeed a standard "ZIP" archive, while the compression algorithm for files inside the session
archive is ZSTD. A standard archive reader could open a session file, if it is able to handle ZIP archive with ZSTD
compression
The archive can be password protected using AES256 algorithm and the compression level is set individually, depending
of the type of data to serialize.
The entry points are `SessionReader` and `SessionWriter`. They directly wrap `detail\Session[De]Serializer`, whose
contain the main \[de\]serialization algorithms. These algorithms call, for each data object, a specific serialization
and de-serialization function, stored in a separated header file (all of them are stored in `detail`).
## Classes
### Helper
- **PasswordKeeper**: keeps passwords in memory in a somewhat secure way (encrypted), so direct reading in memory is a bit more difficult. It can manage a global per application password and as many per `PasswordKeeper` instance.
### Reader / Writer
- **SessionReader**: reads a root data object from a session file on disk.
- **SessionWriter**: writes a root data object to a session file on disk.
## How to use it
### Writing
```c++
auto object = sight::data::String::New("Sample");
auto sessionWriter = io::session::SessionWriter::New();
// Configure the session writer
sessionWriter->setObject(object);
sessionWriter->setFile("Sample.zip");
// Setting a password means the session will be encrypted
sessionWriter->setPassword("Password");
// Write
sessionWriter->write();
```
### Reading
```c++
auto sessionReader = io::session::SessionReader::New();
// Configure the session reader
sessionReader->setFile(testPath);
// Setting a password means the session will be decrypted
sessionReader->setPassword(password);
// Read the session
sessionReader->read();
// Retrieve the object
auto object = sessionReader->getObject();
```
### CMake
```cmake
target_link_library(myTarget <PUBLIC|PRIVATE> io_session)
```
......@@ -46,7 +46,8 @@ public:
/// Constructor
inline SessionReaderImpl(SessionReader* const sessionReader) :
m_SessionReader(sessionReader),
m_password(std::make_unique<PasswordKeeper>())
m_password(std::make_unique<PasswordKeeper>()),
m_EncryptionPolicy(PasswordKeeper::EncryptionPolicy::DEFAULT)
{
}
......@@ -58,26 +59,20 @@ public:
{
// Create the session and deserialize the root object
detail::SessionDeserializer session;
m_object = session.deserialize(m_SessionReader->getFile(), m_password->getPassword());
}
/// Sets the password
/// @param password the new password
inline void setPassword(const core::crypto::secure_string& password)
{
m_password->setPassword(password);
m_object = session.deserialize(m_SessionReader->getFile(), m_password->getPassword(), m_EncryptionPolicy);
}
/// Use a shared_ptr to keep the object alive as it is the read() return value
core::tools::Object::sptr m_object;
private:
/// Pointer to the public interface
SessionReader* const m_SessionReader;
/// Keep the password in a vault
const std::unique_ptr<PasswordKeeper> m_password;
/// The encryption policy
PasswordKeeper::EncryptionPolicy m_EncryptionPolicy;
};
SessionReader::SessionReader(base::reader::IObjectReader::Key) :
......@@ -109,7 +104,14 @@ std::string SessionReader::extension()
void SessionReader::setPassword(const core::crypto::secure_string& password)
{
m_pimpl->setPassword(password);
m_pimpl->m_password->setPassword(password);
}
//------------------------------------------------------------------------------
void SessionReader::setEncryptionPolicy(const PasswordKeeper::EncryptionPolicy policy)
{
m_pimpl->m_EncryptionPolicy = policy;
}
} // namespace sight::io::session
......@@ -23,6 +23,7 @@
#pragma once
#include "io/session/config.hpp"
#include "io/session/PasswordKeeper.hpp"
#include <core/crypto/secure_string.hpp>
#include <core/location/SingleFile.hpp>
......@@ -32,6 +33,18 @@
namespace sight::io::session
{
/**
* @brief Session reader.
*
* @details Class to read a session file, and restore recursively a data object, including all fields.
* The session file is indeed a standard "ZIP" archive, while the compression algorithm for files inside
* the session archive is ZSTD. A standard archive reader could open a session file, if it is able to handle
* ZIP archive with ZSTD compression