diff --git a/Modules/CryptoLib/Package.swift b/Modules/CryptoLib/Package.swift index 9eb2adc8..bdb7aa3a 100644 --- a/Modules/CryptoLib/Package.swift +++ b/Modules/CryptoLib/Package.swift @@ -13,7 +13,8 @@ let package = Package( .library( name: "CryptoLib", targets: ["CryptoSwift"] - ) + ), + .library(name: "CryptoLibMocks", targets: ["CryptoLibMocks"]) ], dependencies: [ .package(url: "https://github.com/filom/ASN1Decoder", exact: .init(1, 10, 0)), @@ -85,6 +86,23 @@ let package = Package( .enableUpcomingFeature("NonisolatedNonsendingByDefault"), .enableUpcomingFeature("InferIsolatedConformances") ] + ), + .target( + name: "CryptoLibMocks", + dependencies: ["CryptoSwift"], + path: "Tests/Mocks" + ), + .testTarget( + name: "CryptoSwiftTests", + dependencies: [ + "ConfigLib", + "CryptoLibMocks", + "CryptoObjCWrapper", + "CommonsLib", + "UtilsLib", + .product(name: "FactoryTesting", package: "Factory"), + .product(name: "CommonsLibMocks", package: "commonslib") + ] ) ] ) diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDoc.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDoc.h index 7bad17c7..d64c6368 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDoc.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDoc.h @@ -21,20 +21,29 @@ #include "Exports.h" +#include #include -#include - -#ifndef LIBCDOC_TESTING -// Remove this in production code -#define LIBCDOC_TESTING 1 -#endif namespace libcdoc { /** * @brief A typedef that indicates that integer value may contain libcdoc result code */ -typedef int64_t result_t; +using result_t = int64_t; + +/** + * @brief The public key type + */ +enum class PKType : uint8_t { + /** + * Elliptic curve + */ + ECC, + /** + * RSA + */ + RSA +}; enum { /** @@ -130,10 +139,83 @@ enum { UNSPECIFIED_ERROR = -199, }; +/** + * @brief Get the standard text description of error code + * + * @param code the error code + * @return the text description + */ CDOC_EXPORT std::string getErrorStr(int64_t code); +/** + * @brief Get the library version + * + * @return The version string + */ CDOC_EXPORT std::string getVersion(); +// Logging interface + +/** + * @brief Log-level enumeration to indicate severity of the log message. + */ +enum LogLevel : uint8_t +{ + /** + * @brief Most critical level. Application is about to abort. + */ + LEVEL_FATAL, + + /** + * @brief Errors where functionality has failed or an exception have been caught. + */ + LEVEL_ERROR, + + /** + * @brief Warnings about validation issues or temporary failures that can be recovered. + */ + LEVEL_WARNING, + + /** + * @brief Information that highlights progress or application lifetime events. + */ + LEVEL_INFO, + + /** + * @brief Debugging the application behavior from internal events of interest. + */ + LEVEL_DEBUG, + + /** + * @brief The most verbose level. Present only in development builds, ignored in production code. + */ + LEVEL_TRACE +}; + +class Logger; + +/** + * @brief Set the Logger object for library + * + * @param logger the Logger implementation + */ +CDOC_EXPORT void setLogger(Logger *logger); +/** + * @brief Set logging level + * + * @param level the requested logging level + */ +CDOC_EXPORT void setLogLevel(LogLevel level); +/** + * @brief Log a message to the library logging system + * + * @param level logging level + * @param file the source file name + * @param line the line in source file + * @param msg the message + */ +CDOC_EXPORT void log(LogLevel level, std::string_view file, int line, std::string_view msg); + /** * @brief A simple container of file name and size * @@ -144,6 +226,38 @@ struct FileInfo { int64_t size; }; +namespace CDoc2 { +namespace Label { + /** + * @brief Recipient types for machine-readable labels + * + */ + static constexpr std::string_view TYPE_PASSWORD = "pw"; + static constexpr std::string_view TYPE_SYMMETRIC = "secret"; + static constexpr std::string_view TYPE_PUBLIC_KEY = "pub_key"; + static constexpr std::string_view TYPE_CERTIFICATE = "cert"; + static constexpr std::string_view TYPE_UNKNOWN = "Unknown"; + static constexpr std::string_view TYPE_ID_CARD = "ID-card"; + static constexpr std::string_view TYPE_DIGI_ID = "Digi-ID"; + static constexpr std::string_view TYPE_DIGI_ID_E_RESIDENT = "Digi-ID E-RESIDENT"; + + /** + * @brief Recipient data for machine-readable labels + * + */ + static constexpr std::string_view VERSION = "v"; + static constexpr std::string_view TYPE = "type"; + static constexpr std::string_view FILE = "file"; + static constexpr std::string_view LABEL = "label"; + static constexpr std::string_view CN = "cn"; + static constexpr std::string_view SERIAL_NUMBER = "serial_number"; + static constexpr std::string_view LAST_NAME = "last_name"; + static constexpr std::string_view FIRST_NAME = "first_name"; + static constexpr std::string_view CERT_SHA1 = "cert_sha1"; + static constexpr const char* EXPIRY = "server_exp"; +} +} + }; // namespace libcdoc #endif // CDOC_H diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocReader.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocReader.h index 5c033aa1..1aa578a2 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocReader.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocReader.h @@ -21,7 +21,7 @@ #include "CDoc.h" -#include +#include namespace libcdoc { @@ -39,7 +39,7 @@ struct NetworkBackend; */ class CDOC_EXPORT CDocReader { public: - virtual ~CDocReader() = default; + virtual ~CDocReader() noexcept = default; /** * @brief The container version (1 or 2) @@ -200,10 +200,6 @@ class CDOC_EXPORT CDocReader { */ static CDocReader *createReader(std::istream& ifs, Configuration *conf, CryptoBackend *crypto, NetworkBackend *network); -#if LIBCDOC_TESTING - virtual int64_t testConfig(std::vector& dst); - virtual int64_t testNetwork(std::vector>& dst); -#endif protected: explicit CDocReader(int _version) : version(_version) {}; @@ -214,6 +210,9 @@ class CDOC_EXPORT CDocReader { Configuration *conf = nullptr; CryptoBackend *crypto = nullptr; NetworkBackend *network = nullptr; + +private: + CDOC_DISABLE_MOVE_COPY(CDocReader); }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocWriter.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocWriter.h index a2595484..2622b537 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocWriter.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CDocWriter.h @@ -21,7 +21,7 @@ #include "CDoc.h" -#include +#include namespace libcdoc { struct Configuration; @@ -38,7 +38,7 @@ namespace libcdoc { */ class CDOC_EXPORT CDocWriter { public: - virtual ~CDocWriter(); + virtual ~CDocWriter() noexcept; /** * @brief The container version (1 or 2) @@ -154,6 +154,7 @@ class CDOC_EXPORT CDocWriter { static CDocWriter *createWriter(int version, const std::string& path, Configuration *conf, CryptoBackend *crypto, NetworkBackend *network); protected: explicit CDocWriter(int _version, DataConsumer *dst, bool take_ownership); + CDOC_DISABLE_MOVE_COPY(CDocWriter); void setLastError(const std::string& message) { last_error = message; } diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Configuration.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Configuration.h index 309c2e31..4ca72e60 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Configuration.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Configuration.h @@ -42,6 +42,7 @@ struct CDOC_EXPORT Configuration { * @brief Fetch URL of keyserver (Domain is server id) */ static constexpr char const *KEYSERVER_FETCH_URL = "KEYSERVER_FETCH_URL"; +#ifdef HAS_KEYSHARES /** * @brief JSON array of share server base urls (Domain is server id) */ @@ -74,6 +75,7 @@ struct CDOC_EXPORT Configuration { * @brief Mobile ID phone number (domain is MOBILE_ID) */ static constexpr char const *PHONE_NUMBER = "PHONE_NUMBER"; +#endif Configuration() = default; virtual ~Configuration() noexcept = default; @@ -92,36 +94,32 @@ struct CDOC_EXPORT Configuration { virtual std::string getValue(std::string_view domain, std::string_view param) const {return {};} /** - * @brief get a value of configuration parameter from default domain + * @brief get a value of configuration parameter from the default domain * @param param the parameter name. * @return a string value or empty string if parameter is not defined. */ std::string getValue(std::string_view param) const {return getValue({}, param);} /** - * @brief get boolean value of configuration parameter from default domain + * @brief get boolean value of configuration parameter from the default domain * @param param the parameter name * @param def_val the default value to return if parameter is not set * @return the parameter value */ bool getBoolean(std::string_view param, bool def_val = false) const; /** - * @brief get integer value of configuration parameter from default domain + * @brief get integer value of configuration parameter from the default domain * @param param the parameter name * @param def_val the default value to return if parameter is not set * @return the key value */ int getInt(std::string_view param, int def_val = 0) const; - -#if LIBCDOC_TESTING - virtual int64_t test(std::vector& dst) { return OK; } -#endif }; /** * @brief A Configuration object implementation that reads values from JSON file * * The file should represent a single object with key/value pairs - * Domain should contain sub-objects with corresponding key/value pairs + * Domains are sub-objects with corresponding key/value pairs * Strings are returned unquoted, everything else is returned as JSON * */ diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ConsoleLogger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ConsoleLogger.h deleted file mode 100644 index 06061d7f..00000000 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ConsoleLogger.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * libcdoc - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#pragma once - -#include "ILogger.h" - -#include - -namespace libcdoc -{ - -/** - * @brief Console logger - * - * An ILogger subclass that logs text to console. - * - * Info messages are logged to cout, all others to cerr. - */ -class ConsoleLogger : public ILogger -{ -public: - virtual void LogMessage(LogLevel level, std::string_view file, int line, std::string_view message) override - { - // We ignore by default the file name and line number, and call LogMessage with the level and message. - if (level <= minLogLevel) - { - std::ostream& ofs = (level == LEVEL_INFO) ? std::cout : std::cerr; - ofs << message << '\n'; - } - } -}; - - -} diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CryptoBackend.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CryptoBackend.h index 56e64475..bf63d5b4 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CryptoBackend.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/CryptoBackend.h @@ -21,7 +21,7 @@ #include -#include +#include namespace libcdoc { diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ILogger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ILogger.h deleted file mode 100644 index 3bf1c72c..00000000 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/ILogger.h +++ /dev/null @@ -1,176 +0,0 @@ -/* - * libcdoc - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef __ILOGGER_H__INCLUDED__ -#define __ILOGGER_H__INCLUDED__ - -#include - -#include - -#ifdef __cpp_lib_format -#include -namespace fmt = std; -#else -#define FMT_HEADER_ONLY -#include "fmt/format.h" -#endif - -#define FORMAT fmt::format - -namespace libcdoc -{ - -/** - * @brief Generic interface to implement a logger. - */ -class CDOC_EXPORT ILogger -{ -public: - /** - * @brief Log-level enumeration to indicate severity of the log message. - */ - enum LogLevel - { - /** - * @brief Most critical level. Application is about to abort. - */ - LEVEL_FATAL, - - /** - * @brief Errors where functionality has failed or an exception have been caught. - */ - LEVEL_ERROR, - - /** - * @brief Warnings about validation issues or temporary failures that can be recovered. - */ - LEVEL_WARNING, - - /** - * @brief Information that highlights progress or application lifetime events. - */ - LEVEL_INFO, - - /** - * @brief Debugging the application behavior from internal events of interest. - */ - LEVEL_DEBUG, - - /** - * @brief Most verbose level. Used for development, NOP in production code. - */ - LEVEL_TRACE - }; - - ILogger() : minLogLevel(LEVEL_WARNING) {} - virtual ~ILogger() {} - - /** - * @brief Logs given message with given severity, file name and line number. - * @param level Severity of the log message. - * @param file File name where the log message was recorded. - * @param line Line number in the file where the log message was recorded. - * @param message The log message. - * - * Every class implementing the ILogger interface must implement the member function. - * Default implementation does nothing. - */ - virtual void LogMessage(LogLevel level, std::string_view file, int line, std::string_view message) {} - - /** - * @brief Returns current minimum log level of the logger. - * @return Minimum log level. - */ - LogLevel GetMinLogLevel() const noexcept { return minLogLevel; } - - /** - * @brief Sets minimum log level for the logger. - * @param level minimum level to log. - * - * Sets minimum level of log messages to log. For example, if the minimum log level is set - * to LogLevelInfo (default), then LogLevelFatal, LogLevelError, LogLevelWarning and LogLevelInfo - * messages are logged, but not LogLevelDebug or LogLevelTrace messages. - */ - void SetMinLogLevel(LogLevel level) noexcept { minLogLevel = level; } - - /** - * @brief Adds ILogger implementation to logging queue. - * - * This function does not take ownership of the logger's instance. - * It is up to the caller to free the resources of the logger's instance and - * keep it alive until removed from the queue. - * - * @param logger Logger's instance to be added. - * @return Unique cookie identifying the logger's instance in the logging queue. - */ - static int addLogger(ILogger* logger); - - /** - * @brief Removes logger's instance from the logging queue. - * @param cookie Unique cookie returned by the add_logger function when the logger was added. - * @return Pointer to ILogger object that is removed. It's up to user to free the resources. - */ - static ILogger* removeLogger(int cookie); - - /** - * @brief Returns global logger's instance. - * @return Global logger's instance. - */ - static ILogger* getLogger(); - - static void setLogger(ILogger *logger); - -protected: - /** - * @brief Minimum level of log messages to log. - */ - LogLevel minLogLevel; -}; - -#ifndef SWIG -template -static inline void LogFormat(ILogger::LogLevel level, std::string_view file, int line, fmt::format_string fmt, Args&&... args) -{ - auto msg = fmt::format(fmt, std::forward(args)...); - ILogger::getLogger()->LogMessage(level, file, line, msg); -} - -static inline void LogFormat(ILogger::LogLevel level, std::string_view file, int line, std::string_view msg) -{ - ILogger::getLogger()->LogMessage(level, file, line, msg); -} -#endif - -#define LOG(l,...) LogFormat((l), __FILE__, __LINE__, __VA_ARGS__) -#define LOG_ERROR(...) LogFormat(libcdoc::ILogger::LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_WARN(...) LogFormat(libcdoc::ILogger::LEVEL_WARNING, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_INFO(...) LogFormat(libcdoc::ILogger::LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_DBG(...) LogFormat(libcdoc::ILogger::LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) - -#ifdef NDEBUG -#define LOG_TRACE(...) -#define LOG_TRACE_KEY(MSG, KEY) -#else -#define LOG_TRACE(...) LogFormat(libcdoc::ILogger::LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_TRACE_KEY(MSG, KEY) LogFormat(libcdoc::ILogger::LEVEL_TRACE, __FILE__, __LINE__, MSG, toHex(KEY)) -#endif - -} - -#endif diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Io.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Io.h index dfc04199..5bc5eecf 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Io.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Io.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace libcdoc { @@ -209,45 +210,6 @@ struct CDOC_EXPORT MultiDataSource : public DataSource { result_t next(FileInfo& info) { return next(info.name, info.size); } }; -struct CDOC_EXPORT ChainedConsumer : public DataConsumer { - ChainedConsumer(DataConsumer *dst, bool take_ownership) : _dst(dst), _owned(take_ownership) {} - ~ChainedConsumer() { - if (_owned) delete _dst; - } - result_t write(const uint8_t *src, size_t size) noexcept override { - return _dst->write(src, size); - } - result_t close() noexcept override { - if (_owned) return _dst->close(); - return OK; - } - bool isError() noexcept override { - return _dst->isError(); - } -protected: - DataConsumer *_dst; - bool _owned; -}; - -struct CDOC_EXPORT ChainedSource : public DataSource { - ChainedSource(DataSource *src, bool take_ownership) : _src(src), _owned(take_ownership) {} - ~ChainedSource() { - if (_owned) delete _src; - } - result_t read(uint8_t *dst, size_t size) noexcept override { - return _src->read(dst, size); - } - bool isError() noexcept override { - return _src->isError(); - } - bool isEof() noexcept override { - return _src->isEof(); - } -protected: - DataSource *_src; - bool _owned; -}; - struct CDOC_EXPORT IStreamSource : public DataSource { IStreamSource(std::istream *ifs, bool take_ownership = false) : _ifs(ifs), _owned(take_ownership) {} IStreamSource(const std::string& path); @@ -259,7 +221,7 @@ struct CDOC_EXPORT IStreamSource : public DataSource { if(_ifs->bad()) return INPUT_STREAM_ERROR; _ifs->clear(); _ifs->seekg(pos); - return bool(_ifs->bad()) ? INPUT_STREAM_ERROR : OK; + return _ifs->bad() ? INPUT_STREAM_ERROR : OK; } result_t read(uint8_t *dst, size_t size) noexcept override try { @@ -302,7 +264,7 @@ struct CDOC_EXPORT OStreamConsumer : public DataConsumer { }; struct CDOC_EXPORT VectorSource : public DataSource { - VectorSource(const std::vector& data) : _data(data), _ptr(0) {} + VectorSource(const std::vector& data) : _data(data) {} result_t seek(size_t pos) override { if (pos > _data.size()) return INPUT_STREAM_ERROR; @@ -321,7 +283,7 @@ struct CDOC_EXPORT VectorSource : public DataSource { bool isEof() noexcept override { return _ptr >= _data.size(); } protected: const std::vector& _data; - size_t _ptr; + size_t _ptr{0}; }; struct CDOC_EXPORT VectorConsumer : public DataConsumer { @@ -333,7 +295,7 @@ struct CDOC_EXPORT VectorConsumer : public DataConsumer { return OUTPUT_STREAM_ERROR; } result_t close() noexcept final { return OK; } - virtual bool isError() noexcept final { return false; } + bool isError() noexcept final { return false; } protected: std::vector& _data; }; @@ -355,25 +317,7 @@ struct CDOC_EXPORT FileListConsumer : public MultiDataConsumer { bool isError() noexcept final { return ofs.bad(); } - result_t open(const std::string& name, int64_t size) override final { - std::string fileName; - if (ofs.is_open()) { - ofs.close(); - } - size_t lastSlashPos = name.find_last_of("\\/"); - if (lastSlashPos != std::string::npos) - { - fileName = name.substr(lastSlashPos + 1); - } - else - { - fileName = name; - } - std::filesystem::path path(base); - path /= fileName; - ofs.open(path.string(), std::ios_base::binary); - return ofs.bad() ? OUTPUT_STREAM_ERROR : OK; - } + result_t open(const std::string &name, int64_t size) final; protected: std::filesystem::path base; @@ -390,7 +334,7 @@ struct CDOC_EXPORT FileListSource : public MultiDataSource { protected: std::filesystem::path _base; const std::vector& _files; - int64_t _current; + int64_t _current = -1; std::ifstream _ifs; }; diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Lock.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Lock.h index 3011f871..c46cea21 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Lock.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Lock.h @@ -19,7 +19,7 @@ #ifndef __LOCK_H__ #define __LOCK_H__ -#include +#include "CDoc.h" #include #include @@ -42,7 +42,7 @@ struct CDOC_EXPORT Lock /** * @brief The lock type */ - enum Type : unsigned char { + enum Type : unsigned char { /** * @brief Valid capsule but not supported by this library version * @@ -51,7 +51,7 @@ struct CDOC_EXPORT Lock /** * @brief Symmetric AES key */ - SYMMETRIC_KEY, + SYMMETRIC_KEY, /** * @brief PBKDF key (derived from password) */ @@ -68,30 +68,18 @@ struct CDOC_EXPORT Lock * @brief Public key stored on keyserver */ SERVER, +#ifdef HAS_KEYSHARES /** * @brief Symmetric key distributed on several servers */ SHARE_SERVER - }; - - /** - * @brief The public key type - */ - enum PKType : unsigned char { - /** - * Elliptic curve - */ - ECC, - /** - * RSA - */ - RSA - }; +#endif + }; /** * @brief Extra parameters depending on key type */ - enum Params : unsigned int { + enum Params : unsigned int { /** * @brief HKDF salt (SYMMETRIC_KEY, PASSWORD and SHARE_SERVER) */ @@ -128,10 +116,12 @@ struct CDOC_EXPORT Lock * @brief Keyshare recipient ID */ RECIPIENT_ID, +#ifdef HAS_KEYSHARES /** * @brief Keyshare server urls (separated by ';') */ SHARE_URLS, +#endif /** * @brief CDoc1 specific */ @@ -152,7 +142,7 @@ struct CDOC_EXPORT Lock * @brief CDoc1 specific */ PARTY_VINFO - }; + }; /** * @brief get lock parameter value @@ -176,20 +166,20 @@ struct CDOC_EXPORT Lock /** * @brief The lock type */ - Type type = Type::UNKNOWN; + Type type = Type::UNKNOWN; /** * @brief algorithm type for public key based locks */ - PKType pk_type = PKType::ECC; + PKType pk_type = PKType::ECC; /** * @brief the lock label */ - std::string label; + std::string label; /** * @brief encrypted FMK (File Master Key) */ - std::vector encrypted_fmk; + std::vector encrypted_fmk; /** * @brief check whether lock is valid @@ -206,11 +196,6 @@ struct CDOC_EXPORT Lock * @return true if type is CDOC1, PUBLIC_KEY or SERVER */ constexpr bool isPKI() const noexcept { return (type == Type::CDOC1) || (type == Type::PUBLIC_KEY) || (type == Type::SERVER); } - /** - * @brief check whether lock is based on certificate - * @return true if type is CDOC1 - */ - constexpr bool isCertificate() const noexcept { return (type == Type::CDOC1); } /** * @brief check whether lock is CDoc1 version * @return true if type is CDOC1 @@ -222,40 +207,21 @@ struct CDOC_EXPORT Lock */ constexpr bool isRSA() const noexcept { return pk_type == PKType::RSA; } - /** - * @brief check whether two locks have the same public key - * - * This convenience method checks whether both locks are public key based, and if they are, - * whether the RCPT_KEY parameters are identical (i.e. both can be decrypted by the same private key) - * @param other the other lock - * @return true if both have the same public key - */ - bool hasTheSameKey(const Lock &other) const; - /** - * @brief check whether lock has the given public key - * - * This convenience method checks whether lock is public key based, and if it is, - * whether the RCPT_KEY parameters is identical to ptovided key(i.e. it can be decrypted by the corresponding private key) - * @param public_key the public key (short format) - * @return true if lock has the same public key - */ - bool hasTheSameKey(const std::vector& public_key) const; - - Lock() noexcept = default; - Lock(Type _type) noexcept : type(_type) {}; + Lock() noexcept = default; + Lock(Type _type) noexcept : type(_type) {}; /** * @brief Set lock parameter value * @param param a parameter type * @param val the value */ - void setBytes(Params param, const std::vector& val) { params[param] = val; } + void setBytes(Params param, std::vector val) { params[param] = std::move(val); } /** * @brief Set lock parameter value from string * @param param a parameter type * @param val the value */ - void setString(Params param, const std::string& val) { params[param] = std::vector(val.cbegin(), val.cend()); } + void setString(Params param, const std::string& val) { setBytes(param, {val.cbegin(), val.cend()}); } /** * @brief Set lock parameter value from integer * @param param a parameter type @@ -264,15 +230,16 @@ struct CDOC_EXPORT Lock void setInt(Params param, int32_t val); /** - * @brief A convenience method to initialize CERTIFICATE, RCPT_KEY and PK_TYPE values from given certificate - * @param cert the certificate (der-encoded) + * @brief parse machine-readable CDoc2 label + * @param label the label + * @return a map of key-value pairs */ - void setCertificate(const std::vector& cert); + static std::map parseLabel(const std::string& label); - bool operator== (const Lock& other) const = default; + bool operator== (const Lock& other) const noexcept = default; private: - std::map> params; + std::map> params; }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Logger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Logger.h new file mode 100644 index 00000000..a3d27234 --- /dev/null +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Logger.h @@ -0,0 +1,79 @@ +/* + * libcdoc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace libcdoc +{ + +/** + * @brief Generic interface to implement a logger. + */ +class CDOC_EXPORT Logger +{ +public: + virtual ~Logger() noexcept = default; + + /** + * @brief Logs given message with given severity, file name and line number. + * + * It tests the log level and if <= min_level invokes logMessage + * + * @param level Severity of the log message. + * @param file File name where the log message was recorded. + * @param line Line number in the file where the log message was recorded. + * @param msg The log message. + */ + void log(LogLevel level, std::string_view file, int line, std::string_view msg) { + if (level <= min_level) logMessage(level, file, line, msg); + } + + /** + * @brief Sets minimum log level for the logger. + * @param level minimum level to log. + * + * Sets minimum level of log messages to log. For example, if the minimum log level is set + * to LEVEL_INFO (default), then LEVEL_FATAL, LEVEL_ERROR, LEVEL_WARNING and LEVEL_INFO + * messages are logged, but not LEVEL_DEBUG or LEVEL_TRACE messages. + */ + constexpr void setMinLogLevel(LogLevel level) noexcept { min_level = level; } + +protected: + /** + * @brief Logs given message with given severity, file name and line number. + * + * Every class implementing the ILogger interface must implement this member function. + * The efault implementation does nothing. + * The level should be checked by caller, thus the implementation should expect that level <= min_level + * + * @param level Severity of the log message. + * @param file File name where the log message was recorded. + * @param line Line number in the file where the log message was recorded. + * @param msg The log message. + */ + virtual void logMessage(LogLevel level, std::string_view file, int line, std::string_view msg) {} + + /** + * @brief Minimum level of log messages to log. + */ + LogLevel min_level = LEVEL_WARNING; +}; + +} diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/NetworkBackend.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/NetworkBackend.h index 6c082240..4d1dc3d9 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/NetworkBackend.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/NetworkBackend.h @@ -29,6 +29,7 @@ struct CDOC_EXPORT NetworkBackend { * */ static constexpr int NETWORK_ERROR = -300; +#ifdef HAS_KEYSHARES // MID/SID error codes // User refused the session static constexpr int MIDSID_USER_REFUSED = -350; @@ -62,9 +63,10 @@ struct CDOC_EXPORT NetworkBackend { static constexpr int MIDSID_DELIVERY_ERROR = -364; // Invalid response from card static constexpr int MIDSID_SIM_ERROR = -365; +#endif /** - * @brief Share information returned by server + * @brief Capsule information returned by capsule server * */ struct CapsuleInfo { @@ -79,8 +81,9 @@ struct CDOC_EXPORT NetworkBackend { */ uint64_t expiry_time; }; +#ifdef HAS_KEYSHARES /** - * @brief Share information returned by server + * @brief Share information returned by share server * */ struct ShareInfo { @@ -95,6 +98,7 @@ struct CDOC_EXPORT NetworkBackend { */ std::string recipient; }; +#endif /** * @brief Proxy credentials used for network access @@ -146,6 +150,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t sendKey (CapsuleInfo& dst, const std::string& url, const std::vector& rcpt_key, const std::vector &key_material, const std::string& type, uint64_t expiry_ts); +#ifdef HAS_KEYSHARES /** * @brief send key share to server * @@ -157,6 +162,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t sendShare(std::vector& dst, const std::string& url, const std::string& recipient, const std::vector& share); +#endif /** * @brief fetch key material from keyserver * @@ -167,6 +173,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); +#ifdef HAS_KEYSHARES /** * @brief fetch authentication nonce from share server * @param dst a destination container for nonce @@ -185,7 +192,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& ticket, const std::vector& cert); - +#endif /** * @brief get client TLS certificate in der format @@ -234,6 +241,7 @@ struct CDOC_EXPORT NetworkBackend { return NOT_IMPLEMENTED; } +#ifdef HAS_KEYSHARES /** * @brief show MID/SID verification code * @@ -277,9 +285,6 @@ struct CDOC_EXPORT NetworkBackend { result_t signMID(std::vector& dst, std::vector& cert, const std::string& url, const std::string& rp_uuid, const std::string& rp_name, const std::string& phone, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); - -#if LIBCDOC_TESTING - virtual int64_t test(std::vector> &dst); #endif }; diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Recipient.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Recipient.h index 0b04755b..8374bc4b 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Recipient.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Headers/Recipient.h @@ -19,15 +19,17 @@ #ifndef __RECIPIENT_H__ #define __RECIPIENT_H__ -#include +#include "CDoc.h" -#include #include #include +#include #include namespace libcdoc { +struct Lock; + /** * @brief A descriptor of encryption method and key to be used in container * @@ -50,24 +52,12 @@ struct CDOC_EXPORT Recipient { * @brief Public key */ PUBLIC_KEY, +#ifdef HAS_KEYSHARES /** * @brief n of n shared symmetric key */ KEYSHARE - }; - - /** - * @brief The public key type - */ - enum PKType : uint8_t { - /** - * Elliptic curve - */ - ECC, - /** - * RSA - */ - RSA +#endif }; Recipient() = default; @@ -96,10 +86,12 @@ struct CDOC_EXPORT Recipient { * @brief The recipient's certificate (if present) */ std::vector cert; +#ifdef HAS_KEYSHARES /** * @brief The recipient id for share server (PNOEE-XXXXXXXXXXX) */ std::string id; +#endif /** * @brief The keyserver or share server list id (if present) */ @@ -109,16 +101,6 @@ struct CDOC_EXPORT Recipient { * */ uint64_t expiry_ts = 0; - /** - * @brief key/certificate filename for machine-readable label - * - */ - std::string file_name; - /** - * @brief public key/password name for machine-readable label - * - */ - std::string key_name; /** * @brief test whether the Recipient structure is initialized @@ -145,11 +127,13 @@ struct CDOC_EXPORT Recipient { * @return true if type is SERVER */ bool isKeyServer() const { return (type == Type::PUBLIC_KEY) && !server_id.empty(); } +#ifdef HAS_KEYSHARES /** * @brief check whether Recipient is keyshare * @return true if type is KEYSHARE */ bool isKeyShare() const { return type == Type::KEYSHARE; } +#endif /** * @brief Clear all values and set type to NONE @@ -184,6 +168,12 @@ struct CDOC_EXPORT Recipient { * @return a new Recipient structure */ static Recipient makePublicKey(std::string label, std::vector public_key, PKType pk_type); + /** + * @brief Create a new public key based Recipient + * @param lock Lock to derive parameters from + * @return a new Recipient structure + */ + static Recipient makePublicKey(const Lock &lock); /** * @brief Create a new certificate based Recipient * @param label the label text @@ -215,6 +205,16 @@ struct CDOC_EXPORT Recipient { */ static Recipient makeServer(std::string label, std::vector cert, std::string server_id); + /** + * @brief Create a new capsule server based Recipient + * + * @param lock Lock to derive parameters from + * @param server_id the keyserver id + * @return a new Recipient structure + */ + static Recipient makeServer(const Lock &lock, std::string server_id); + +#ifdef HAS_KEYSHARES /** * @brief Create new keyshare recipient * @@ -224,6 +224,7 @@ struct CDOC_EXPORT Recipient { * @return Recipient a new Recipient structure */ static Recipient makeShare(std::string label, std::string server_id, std::string recipient_id); +#endif /** * @brief Get the label for this recipient @@ -233,18 +234,30 @@ struct CDOC_EXPORT Recipient { * @param extra additional parameter values to use * @return a label value */ - std::string getLabel(const std::vector> &extra) const; + std::string getLabel(std::map extra) const; + + /** + * @brief Set a property for automatic label generation + * + * @param key the property name + * @param value the property value + */ + void setLabelValue(std::string_view key, std::string_view value) { + lbl_parts[std::string(key)] = value; + } /** - * @brief parse machine-readable CDoc2 label - * @param label the label - * @return a map of key-value pairs + * @brief Validate recipient record + * + * @return true if Recipient is valid */ - static std::map parseLabel(const std::string& label); + bool validate() const; bool operator== (const Recipient& other) const = default; protected: Recipient(Type _type) : type(_type) {}; +private: + std::map lbl_parts; }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Info.plist b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Info.plist index f1db506c..3f02ba10 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Info.plist +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Info.plist @@ -12,17 +12,19 @@ ee.ria.cdoc CFBundleInfoDictionaryVersion 6.0 + CFBundleName + CFBundlePackageType FMWK CFBundleShortVersionString - 0.1.8 + 0.5.0 CFBundleSignature ???? CFBundleVersion - 0 + 32 CSResourcesFileMapped MinimumOSVersion - 15.0 + 16.3 diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Modules/module.modulemap b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Modules/module.modulemap index db570a56..68872550 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Modules/module.modulemap +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/Modules/module.modulemap @@ -10,8 +10,7 @@ framework module cdoc { header "CryptoBackend.h" header "NetworkBackend.h" header "PKCS11Backend.h" - header "ILogger.h" - header "ConsoleLogger.h" + header "Logger.h" export * requires cplusplus } \ No newline at end of file diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/cdoc b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/cdoc index 3a4b0989..cd5a62fd 100755 Binary files a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/cdoc and b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64/cdoc.framework/cdoc differ diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDoc.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDoc.h index 7bad17c7..d64c6368 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDoc.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDoc.h @@ -21,20 +21,29 @@ #include "Exports.h" +#include #include -#include - -#ifndef LIBCDOC_TESTING -// Remove this in production code -#define LIBCDOC_TESTING 1 -#endif namespace libcdoc { /** * @brief A typedef that indicates that integer value may contain libcdoc result code */ -typedef int64_t result_t; +using result_t = int64_t; + +/** + * @brief The public key type + */ +enum class PKType : uint8_t { + /** + * Elliptic curve + */ + ECC, + /** + * RSA + */ + RSA +}; enum { /** @@ -130,10 +139,83 @@ enum { UNSPECIFIED_ERROR = -199, }; +/** + * @brief Get the standard text description of error code + * + * @param code the error code + * @return the text description + */ CDOC_EXPORT std::string getErrorStr(int64_t code); +/** + * @brief Get the library version + * + * @return The version string + */ CDOC_EXPORT std::string getVersion(); +// Logging interface + +/** + * @brief Log-level enumeration to indicate severity of the log message. + */ +enum LogLevel : uint8_t +{ + /** + * @brief Most critical level. Application is about to abort. + */ + LEVEL_FATAL, + + /** + * @brief Errors where functionality has failed or an exception have been caught. + */ + LEVEL_ERROR, + + /** + * @brief Warnings about validation issues or temporary failures that can be recovered. + */ + LEVEL_WARNING, + + /** + * @brief Information that highlights progress or application lifetime events. + */ + LEVEL_INFO, + + /** + * @brief Debugging the application behavior from internal events of interest. + */ + LEVEL_DEBUG, + + /** + * @brief The most verbose level. Present only in development builds, ignored in production code. + */ + LEVEL_TRACE +}; + +class Logger; + +/** + * @brief Set the Logger object for library + * + * @param logger the Logger implementation + */ +CDOC_EXPORT void setLogger(Logger *logger); +/** + * @brief Set logging level + * + * @param level the requested logging level + */ +CDOC_EXPORT void setLogLevel(LogLevel level); +/** + * @brief Log a message to the library logging system + * + * @param level logging level + * @param file the source file name + * @param line the line in source file + * @param msg the message + */ +CDOC_EXPORT void log(LogLevel level, std::string_view file, int line, std::string_view msg); + /** * @brief A simple container of file name and size * @@ -144,6 +226,38 @@ struct FileInfo { int64_t size; }; +namespace CDoc2 { +namespace Label { + /** + * @brief Recipient types for machine-readable labels + * + */ + static constexpr std::string_view TYPE_PASSWORD = "pw"; + static constexpr std::string_view TYPE_SYMMETRIC = "secret"; + static constexpr std::string_view TYPE_PUBLIC_KEY = "pub_key"; + static constexpr std::string_view TYPE_CERTIFICATE = "cert"; + static constexpr std::string_view TYPE_UNKNOWN = "Unknown"; + static constexpr std::string_view TYPE_ID_CARD = "ID-card"; + static constexpr std::string_view TYPE_DIGI_ID = "Digi-ID"; + static constexpr std::string_view TYPE_DIGI_ID_E_RESIDENT = "Digi-ID E-RESIDENT"; + + /** + * @brief Recipient data for machine-readable labels + * + */ + static constexpr std::string_view VERSION = "v"; + static constexpr std::string_view TYPE = "type"; + static constexpr std::string_view FILE = "file"; + static constexpr std::string_view LABEL = "label"; + static constexpr std::string_view CN = "cn"; + static constexpr std::string_view SERIAL_NUMBER = "serial_number"; + static constexpr std::string_view LAST_NAME = "last_name"; + static constexpr std::string_view FIRST_NAME = "first_name"; + static constexpr std::string_view CERT_SHA1 = "cert_sha1"; + static constexpr const char* EXPIRY = "server_exp"; +} +} + }; // namespace libcdoc #endif // CDOC_H diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocReader.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocReader.h index 5c033aa1..1aa578a2 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocReader.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocReader.h @@ -21,7 +21,7 @@ #include "CDoc.h" -#include +#include namespace libcdoc { @@ -39,7 +39,7 @@ struct NetworkBackend; */ class CDOC_EXPORT CDocReader { public: - virtual ~CDocReader() = default; + virtual ~CDocReader() noexcept = default; /** * @brief The container version (1 or 2) @@ -200,10 +200,6 @@ class CDOC_EXPORT CDocReader { */ static CDocReader *createReader(std::istream& ifs, Configuration *conf, CryptoBackend *crypto, NetworkBackend *network); -#if LIBCDOC_TESTING - virtual int64_t testConfig(std::vector& dst); - virtual int64_t testNetwork(std::vector>& dst); -#endif protected: explicit CDocReader(int _version) : version(_version) {}; @@ -214,6 +210,9 @@ class CDOC_EXPORT CDocReader { Configuration *conf = nullptr; CryptoBackend *crypto = nullptr; NetworkBackend *network = nullptr; + +private: + CDOC_DISABLE_MOVE_COPY(CDocReader); }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocWriter.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocWriter.h index a2595484..2622b537 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocWriter.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CDocWriter.h @@ -21,7 +21,7 @@ #include "CDoc.h" -#include +#include namespace libcdoc { struct Configuration; @@ -38,7 +38,7 @@ namespace libcdoc { */ class CDOC_EXPORT CDocWriter { public: - virtual ~CDocWriter(); + virtual ~CDocWriter() noexcept; /** * @brief The container version (1 or 2) @@ -154,6 +154,7 @@ class CDOC_EXPORT CDocWriter { static CDocWriter *createWriter(int version, const std::string& path, Configuration *conf, CryptoBackend *crypto, NetworkBackend *network); protected: explicit CDocWriter(int _version, DataConsumer *dst, bool take_ownership); + CDOC_DISABLE_MOVE_COPY(CDocWriter); void setLastError(const std::string& message) { last_error = message; } diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Configuration.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Configuration.h index 309c2e31..4ca72e60 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Configuration.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Configuration.h @@ -42,6 +42,7 @@ struct CDOC_EXPORT Configuration { * @brief Fetch URL of keyserver (Domain is server id) */ static constexpr char const *KEYSERVER_FETCH_URL = "KEYSERVER_FETCH_URL"; +#ifdef HAS_KEYSHARES /** * @brief JSON array of share server base urls (Domain is server id) */ @@ -74,6 +75,7 @@ struct CDOC_EXPORT Configuration { * @brief Mobile ID phone number (domain is MOBILE_ID) */ static constexpr char const *PHONE_NUMBER = "PHONE_NUMBER"; +#endif Configuration() = default; virtual ~Configuration() noexcept = default; @@ -92,36 +94,32 @@ struct CDOC_EXPORT Configuration { virtual std::string getValue(std::string_view domain, std::string_view param) const {return {};} /** - * @brief get a value of configuration parameter from default domain + * @brief get a value of configuration parameter from the default domain * @param param the parameter name. * @return a string value or empty string if parameter is not defined. */ std::string getValue(std::string_view param) const {return getValue({}, param);} /** - * @brief get boolean value of configuration parameter from default domain + * @brief get boolean value of configuration parameter from the default domain * @param param the parameter name * @param def_val the default value to return if parameter is not set * @return the parameter value */ bool getBoolean(std::string_view param, bool def_val = false) const; /** - * @brief get integer value of configuration parameter from default domain + * @brief get integer value of configuration parameter from the default domain * @param param the parameter name * @param def_val the default value to return if parameter is not set * @return the key value */ int getInt(std::string_view param, int def_val = 0) const; - -#if LIBCDOC_TESTING - virtual int64_t test(std::vector& dst) { return OK; } -#endif }; /** * @brief A Configuration object implementation that reads values from JSON file * * The file should represent a single object with key/value pairs - * Domain should contain sub-objects with corresponding key/value pairs + * Domains are sub-objects with corresponding key/value pairs * Strings are returned unquoted, everything else is returned as JSON * */ diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ConsoleLogger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ConsoleLogger.h deleted file mode 100644 index 06061d7f..00000000 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ConsoleLogger.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * libcdoc - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#pragma once - -#include "ILogger.h" - -#include - -namespace libcdoc -{ - -/** - * @brief Console logger - * - * An ILogger subclass that logs text to console. - * - * Info messages are logged to cout, all others to cerr. - */ -class ConsoleLogger : public ILogger -{ -public: - virtual void LogMessage(LogLevel level, std::string_view file, int line, std::string_view message) override - { - // We ignore by default the file name and line number, and call LogMessage with the level and message. - if (level <= minLogLevel) - { - std::ostream& ofs = (level == LEVEL_INFO) ? std::cout : std::cerr; - ofs << message << '\n'; - } - } -}; - - -} diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CryptoBackend.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CryptoBackend.h index 56e64475..bf63d5b4 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CryptoBackend.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/CryptoBackend.h @@ -21,7 +21,7 @@ #include -#include +#include namespace libcdoc { diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ILogger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ILogger.h deleted file mode 100644 index 3bf1c72c..00000000 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/ILogger.h +++ /dev/null @@ -1,176 +0,0 @@ -/* - * libcdoc - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef __ILOGGER_H__INCLUDED__ -#define __ILOGGER_H__INCLUDED__ - -#include - -#include - -#ifdef __cpp_lib_format -#include -namespace fmt = std; -#else -#define FMT_HEADER_ONLY -#include "fmt/format.h" -#endif - -#define FORMAT fmt::format - -namespace libcdoc -{ - -/** - * @brief Generic interface to implement a logger. - */ -class CDOC_EXPORT ILogger -{ -public: - /** - * @brief Log-level enumeration to indicate severity of the log message. - */ - enum LogLevel - { - /** - * @brief Most critical level. Application is about to abort. - */ - LEVEL_FATAL, - - /** - * @brief Errors where functionality has failed or an exception have been caught. - */ - LEVEL_ERROR, - - /** - * @brief Warnings about validation issues or temporary failures that can be recovered. - */ - LEVEL_WARNING, - - /** - * @brief Information that highlights progress or application lifetime events. - */ - LEVEL_INFO, - - /** - * @brief Debugging the application behavior from internal events of interest. - */ - LEVEL_DEBUG, - - /** - * @brief Most verbose level. Used for development, NOP in production code. - */ - LEVEL_TRACE - }; - - ILogger() : minLogLevel(LEVEL_WARNING) {} - virtual ~ILogger() {} - - /** - * @brief Logs given message with given severity, file name and line number. - * @param level Severity of the log message. - * @param file File name where the log message was recorded. - * @param line Line number in the file where the log message was recorded. - * @param message The log message. - * - * Every class implementing the ILogger interface must implement the member function. - * Default implementation does nothing. - */ - virtual void LogMessage(LogLevel level, std::string_view file, int line, std::string_view message) {} - - /** - * @brief Returns current minimum log level of the logger. - * @return Minimum log level. - */ - LogLevel GetMinLogLevel() const noexcept { return minLogLevel; } - - /** - * @brief Sets minimum log level for the logger. - * @param level minimum level to log. - * - * Sets minimum level of log messages to log. For example, if the minimum log level is set - * to LogLevelInfo (default), then LogLevelFatal, LogLevelError, LogLevelWarning and LogLevelInfo - * messages are logged, but not LogLevelDebug or LogLevelTrace messages. - */ - void SetMinLogLevel(LogLevel level) noexcept { minLogLevel = level; } - - /** - * @brief Adds ILogger implementation to logging queue. - * - * This function does not take ownership of the logger's instance. - * It is up to the caller to free the resources of the logger's instance and - * keep it alive until removed from the queue. - * - * @param logger Logger's instance to be added. - * @return Unique cookie identifying the logger's instance in the logging queue. - */ - static int addLogger(ILogger* logger); - - /** - * @brief Removes logger's instance from the logging queue. - * @param cookie Unique cookie returned by the add_logger function when the logger was added. - * @return Pointer to ILogger object that is removed. It's up to user to free the resources. - */ - static ILogger* removeLogger(int cookie); - - /** - * @brief Returns global logger's instance. - * @return Global logger's instance. - */ - static ILogger* getLogger(); - - static void setLogger(ILogger *logger); - -protected: - /** - * @brief Minimum level of log messages to log. - */ - LogLevel minLogLevel; -}; - -#ifndef SWIG -template -static inline void LogFormat(ILogger::LogLevel level, std::string_view file, int line, fmt::format_string fmt, Args&&... args) -{ - auto msg = fmt::format(fmt, std::forward(args)...); - ILogger::getLogger()->LogMessage(level, file, line, msg); -} - -static inline void LogFormat(ILogger::LogLevel level, std::string_view file, int line, std::string_view msg) -{ - ILogger::getLogger()->LogMessage(level, file, line, msg); -} -#endif - -#define LOG(l,...) LogFormat((l), __FILE__, __LINE__, __VA_ARGS__) -#define LOG_ERROR(...) LogFormat(libcdoc::ILogger::LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_WARN(...) LogFormat(libcdoc::ILogger::LEVEL_WARNING, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_INFO(...) LogFormat(libcdoc::ILogger::LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_DBG(...) LogFormat(libcdoc::ILogger::LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) - -#ifdef NDEBUG -#define LOG_TRACE(...) -#define LOG_TRACE_KEY(MSG, KEY) -#else -#define LOG_TRACE(...) LogFormat(libcdoc::ILogger::LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__) -#define LOG_TRACE_KEY(MSG, KEY) LogFormat(libcdoc::ILogger::LEVEL_TRACE, __FILE__, __LINE__, MSG, toHex(KEY)) -#endif - -} - -#endif diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Io.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Io.h index dfc04199..5bc5eecf 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Io.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Io.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace libcdoc { @@ -209,45 +210,6 @@ struct CDOC_EXPORT MultiDataSource : public DataSource { result_t next(FileInfo& info) { return next(info.name, info.size); } }; -struct CDOC_EXPORT ChainedConsumer : public DataConsumer { - ChainedConsumer(DataConsumer *dst, bool take_ownership) : _dst(dst), _owned(take_ownership) {} - ~ChainedConsumer() { - if (_owned) delete _dst; - } - result_t write(const uint8_t *src, size_t size) noexcept override { - return _dst->write(src, size); - } - result_t close() noexcept override { - if (_owned) return _dst->close(); - return OK; - } - bool isError() noexcept override { - return _dst->isError(); - } -protected: - DataConsumer *_dst; - bool _owned; -}; - -struct CDOC_EXPORT ChainedSource : public DataSource { - ChainedSource(DataSource *src, bool take_ownership) : _src(src), _owned(take_ownership) {} - ~ChainedSource() { - if (_owned) delete _src; - } - result_t read(uint8_t *dst, size_t size) noexcept override { - return _src->read(dst, size); - } - bool isError() noexcept override { - return _src->isError(); - } - bool isEof() noexcept override { - return _src->isEof(); - } -protected: - DataSource *_src; - bool _owned; -}; - struct CDOC_EXPORT IStreamSource : public DataSource { IStreamSource(std::istream *ifs, bool take_ownership = false) : _ifs(ifs), _owned(take_ownership) {} IStreamSource(const std::string& path); @@ -259,7 +221,7 @@ struct CDOC_EXPORT IStreamSource : public DataSource { if(_ifs->bad()) return INPUT_STREAM_ERROR; _ifs->clear(); _ifs->seekg(pos); - return bool(_ifs->bad()) ? INPUT_STREAM_ERROR : OK; + return _ifs->bad() ? INPUT_STREAM_ERROR : OK; } result_t read(uint8_t *dst, size_t size) noexcept override try { @@ -302,7 +264,7 @@ struct CDOC_EXPORT OStreamConsumer : public DataConsumer { }; struct CDOC_EXPORT VectorSource : public DataSource { - VectorSource(const std::vector& data) : _data(data), _ptr(0) {} + VectorSource(const std::vector& data) : _data(data) {} result_t seek(size_t pos) override { if (pos > _data.size()) return INPUT_STREAM_ERROR; @@ -321,7 +283,7 @@ struct CDOC_EXPORT VectorSource : public DataSource { bool isEof() noexcept override { return _ptr >= _data.size(); } protected: const std::vector& _data; - size_t _ptr; + size_t _ptr{0}; }; struct CDOC_EXPORT VectorConsumer : public DataConsumer { @@ -333,7 +295,7 @@ struct CDOC_EXPORT VectorConsumer : public DataConsumer { return OUTPUT_STREAM_ERROR; } result_t close() noexcept final { return OK; } - virtual bool isError() noexcept final { return false; } + bool isError() noexcept final { return false; } protected: std::vector& _data; }; @@ -355,25 +317,7 @@ struct CDOC_EXPORT FileListConsumer : public MultiDataConsumer { bool isError() noexcept final { return ofs.bad(); } - result_t open(const std::string& name, int64_t size) override final { - std::string fileName; - if (ofs.is_open()) { - ofs.close(); - } - size_t lastSlashPos = name.find_last_of("\\/"); - if (lastSlashPos != std::string::npos) - { - fileName = name.substr(lastSlashPos + 1); - } - else - { - fileName = name; - } - std::filesystem::path path(base); - path /= fileName; - ofs.open(path.string(), std::ios_base::binary); - return ofs.bad() ? OUTPUT_STREAM_ERROR : OK; - } + result_t open(const std::string &name, int64_t size) final; protected: std::filesystem::path base; @@ -390,7 +334,7 @@ struct CDOC_EXPORT FileListSource : public MultiDataSource { protected: std::filesystem::path _base; const std::vector& _files; - int64_t _current; + int64_t _current = -1; std::ifstream _ifs; }; diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Lock.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Lock.h index 3011f871..c46cea21 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Lock.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Lock.h @@ -19,7 +19,7 @@ #ifndef __LOCK_H__ #define __LOCK_H__ -#include +#include "CDoc.h" #include #include @@ -42,7 +42,7 @@ struct CDOC_EXPORT Lock /** * @brief The lock type */ - enum Type : unsigned char { + enum Type : unsigned char { /** * @brief Valid capsule but not supported by this library version * @@ -51,7 +51,7 @@ struct CDOC_EXPORT Lock /** * @brief Symmetric AES key */ - SYMMETRIC_KEY, + SYMMETRIC_KEY, /** * @brief PBKDF key (derived from password) */ @@ -68,30 +68,18 @@ struct CDOC_EXPORT Lock * @brief Public key stored on keyserver */ SERVER, +#ifdef HAS_KEYSHARES /** * @brief Symmetric key distributed on several servers */ SHARE_SERVER - }; - - /** - * @brief The public key type - */ - enum PKType : unsigned char { - /** - * Elliptic curve - */ - ECC, - /** - * RSA - */ - RSA - }; +#endif + }; /** * @brief Extra parameters depending on key type */ - enum Params : unsigned int { + enum Params : unsigned int { /** * @brief HKDF salt (SYMMETRIC_KEY, PASSWORD and SHARE_SERVER) */ @@ -128,10 +116,12 @@ struct CDOC_EXPORT Lock * @brief Keyshare recipient ID */ RECIPIENT_ID, +#ifdef HAS_KEYSHARES /** * @brief Keyshare server urls (separated by ';') */ SHARE_URLS, +#endif /** * @brief CDoc1 specific */ @@ -152,7 +142,7 @@ struct CDOC_EXPORT Lock * @brief CDoc1 specific */ PARTY_VINFO - }; + }; /** * @brief get lock parameter value @@ -176,20 +166,20 @@ struct CDOC_EXPORT Lock /** * @brief The lock type */ - Type type = Type::UNKNOWN; + Type type = Type::UNKNOWN; /** * @brief algorithm type for public key based locks */ - PKType pk_type = PKType::ECC; + PKType pk_type = PKType::ECC; /** * @brief the lock label */ - std::string label; + std::string label; /** * @brief encrypted FMK (File Master Key) */ - std::vector encrypted_fmk; + std::vector encrypted_fmk; /** * @brief check whether lock is valid @@ -206,11 +196,6 @@ struct CDOC_EXPORT Lock * @return true if type is CDOC1, PUBLIC_KEY or SERVER */ constexpr bool isPKI() const noexcept { return (type == Type::CDOC1) || (type == Type::PUBLIC_KEY) || (type == Type::SERVER); } - /** - * @brief check whether lock is based on certificate - * @return true if type is CDOC1 - */ - constexpr bool isCertificate() const noexcept { return (type == Type::CDOC1); } /** * @brief check whether lock is CDoc1 version * @return true if type is CDOC1 @@ -222,40 +207,21 @@ struct CDOC_EXPORT Lock */ constexpr bool isRSA() const noexcept { return pk_type == PKType::RSA; } - /** - * @brief check whether two locks have the same public key - * - * This convenience method checks whether both locks are public key based, and if they are, - * whether the RCPT_KEY parameters are identical (i.e. both can be decrypted by the same private key) - * @param other the other lock - * @return true if both have the same public key - */ - bool hasTheSameKey(const Lock &other) const; - /** - * @brief check whether lock has the given public key - * - * This convenience method checks whether lock is public key based, and if it is, - * whether the RCPT_KEY parameters is identical to ptovided key(i.e. it can be decrypted by the corresponding private key) - * @param public_key the public key (short format) - * @return true if lock has the same public key - */ - bool hasTheSameKey(const std::vector& public_key) const; - - Lock() noexcept = default; - Lock(Type _type) noexcept : type(_type) {}; + Lock() noexcept = default; + Lock(Type _type) noexcept : type(_type) {}; /** * @brief Set lock parameter value * @param param a parameter type * @param val the value */ - void setBytes(Params param, const std::vector& val) { params[param] = val; } + void setBytes(Params param, std::vector val) { params[param] = std::move(val); } /** * @brief Set lock parameter value from string * @param param a parameter type * @param val the value */ - void setString(Params param, const std::string& val) { params[param] = std::vector(val.cbegin(), val.cend()); } + void setString(Params param, const std::string& val) { setBytes(param, {val.cbegin(), val.cend()}); } /** * @brief Set lock parameter value from integer * @param param a parameter type @@ -264,15 +230,16 @@ struct CDOC_EXPORT Lock void setInt(Params param, int32_t val); /** - * @brief A convenience method to initialize CERTIFICATE, RCPT_KEY and PK_TYPE values from given certificate - * @param cert the certificate (der-encoded) + * @brief parse machine-readable CDoc2 label + * @param label the label + * @return a map of key-value pairs */ - void setCertificate(const std::vector& cert); + static std::map parseLabel(const std::string& label); - bool operator== (const Lock& other) const = default; + bool operator== (const Lock& other) const noexcept = default; private: - std::map> params; + std::map> params; }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Logger.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Logger.h new file mode 100644 index 00000000..a3d27234 --- /dev/null +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Logger.h @@ -0,0 +1,79 @@ +/* + * libcdoc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +namespace libcdoc +{ + +/** + * @brief Generic interface to implement a logger. + */ +class CDOC_EXPORT Logger +{ +public: + virtual ~Logger() noexcept = default; + + /** + * @brief Logs given message with given severity, file name and line number. + * + * It tests the log level and if <= min_level invokes logMessage + * + * @param level Severity of the log message. + * @param file File name where the log message was recorded. + * @param line Line number in the file where the log message was recorded. + * @param msg The log message. + */ + void log(LogLevel level, std::string_view file, int line, std::string_view msg) { + if (level <= min_level) logMessage(level, file, line, msg); + } + + /** + * @brief Sets minimum log level for the logger. + * @param level minimum level to log. + * + * Sets minimum level of log messages to log. For example, if the minimum log level is set + * to LEVEL_INFO (default), then LEVEL_FATAL, LEVEL_ERROR, LEVEL_WARNING and LEVEL_INFO + * messages are logged, but not LEVEL_DEBUG or LEVEL_TRACE messages. + */ + constexpr void setMinLogLevel(LogLevel level) noexcept { min_level = level; } + +protected: + /** + * @brief Logs given message with given severity, file name and line number. + * + * Every class implementing the ILogger interface must implement this member function. + * The efault implementation does nothing. + * The level should be checked by caller, thus the implementation should expect that level <= min_level + * + * @param level Severity of the log message. + * @param file File name where the log message was recorded. + * @param line Line number in the file where the log message was recorded. + * @param msg The log message. + */ + virtual void logMessage(LogLevel level, std::string_view file, int line, std::string_view msg) {} + + /** + * @brief Minimum level of log messages to log. + */ + LogLevel min_level = LEVEL_WARNING; +}; + +} diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/NetworkBackend.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/NetworkBackend.h index 6c082240..4d1dc3d9 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/NetworkBackend.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/NetworkBackend.h @@ -29,6 +29,7 @@ struct CDOC_EXPORT NetworkBackend { * */ static constexpr int NETWORK_ERROR = -300; +#ifdef HAS_KEYSHARES // MID/SID error codes // User refused the session static constexpr int MIDSID_USER_REFUSED = -350; @@ -62,9 +63,10 @@ struct CDOC_EXPORT NetworkBackend { static constexpr int MIDSID_DELIVERY_ERROR = -364; // Invalid response from card static constexpr int MIDSID_SIM_ERROR = -365; +#endif /** - * @brief Share information returned by server + * @brief Capsule information returned by capsule server * */ struct CapsuleInfo { @@ -79,8 +81,9 @@ struct CDOC_EXPORT NetworkBackend { */ uint64_t expiry_time; }; +#ifdef HAS_KEYSHARES /** - * @brief Share information returned by server + * @brief Share information returned by share server * */ struct ShareInfo { @@ -95,6 +98,7 @@ struct CDOC_EXPORT NetworkBackend { */ std::string recipient; }; +#endif /** * @brief Proxy credentials used for network access @@ -146,6 +150,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t sendKey (CapsuleInfo& dst, const std::string& url, const std::vector& rcpt_key, const std::vector &key_material, const std::string& type, uint64_t expiry_ts); +#ifdef HAS_KEYSHARES /** * @brief send key share to server * @@ -157,6 +162,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t sendShare(std::vector& dst, const std::string& url, const std::string& recipient, const std::vector& share); +#endif /** * @brief fetch key material from keyserver * @@ -167,6 +173,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t fetchKey (std::vector& dst, const std::string& url, const std::string& transaction_id); +#ifdef HAS_KEYSHARES /** * @brief fetch authentication nonce from share server * @param dst a destination container for nonce @@ -185,7 +192,7 @@ struct CDOC_EXPORT NetworkBackend { * @return error code or OK */ virtual result_t fetchShare(ShareInfo& share, const std::string& url, const std::string& share_id, const std::string& ticket, const std::vector& cert); - +#endif /** * @brief get client TLS certificate in der format @@ -234,6 +241,7 @@ struct CDOC_EXPORT NetworkBackend { return NOT_IMPLEMENTED; } +#ifdef HAS_KEYSHARES /** * @brief show MID/SID verification code * @@ -277,9 +285,6 @@ struct CDOC_EXPORT NetworkBackend { result_t signMID(std::vector& dst, std::vector& cert, const std::string& url, const std::string& rp_uuid, const std::string& rp_name, const std::string& phone, const std::string& rcpt_id, const std::vector& digest, CryptoBackend::HashAlgorithm algo); - -#if LIBCDOC_TESTING - virtual int64_t test(std::vector> &dst); #endif }; diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Recipient.h b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Recipient.h index 0b04755b..8374bc4b 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Recipient.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Headers/Recipient.h @@ -19,15 +19,17 @@ #ifndef __RECIPIENT_H__ #define __RECIPIENT_H__ -#include +#include "CDoc.h" -#include #include #include +#include #include namespace libcdoc { +struct Lock; + /** * @brief A descriptor of encryption method and key to be used in container * @@ -50,24 +52,12 @@ struct CDOC_EXPORT Recipient { * @brief Public key */ PUBLIC_KEY, +#ifdef HAS_KEYSHARES /** * @brief n of n shared symmetric key */ KEYSHARE - }; - - /** - * @brief The public key type - */ - enum PKType : uint8_t { - /** - * Elliptic curve - */ - ECC, - /** - * RSA - */ - RSA +#endif }; Recipient() = default; @@ -96,10 +86,12 @@ struct CDOC_EXPORT Recipient { * @brief The recipient's certificate (if present) */ std::vector cert; +#ifdef HAS_KEYSHARES /** * @brief The recipient id for share server (PNOEE-XXXXXXXXXXX) */ std::string id; +#endif /** * @brief The keyserver or share server list id (if present) */ @@ -109,16 +101,6 @@ struct CDOC_EXPORT Recipient { * */ uint64_t expiry_ts = 0; - /** - * @brief key/certificate filename for machine-readable label - * - */ - std::string file_name; - /** - * @brief public key/password name for machine-readable label - * - */ - std::string key_name; /** * @brief test whether the Recipient structure is initialized @@ -145,11 +127,13 @@ struct CDOC_EXPORT Recipient { * @return true if type is SERVER */ bool isKeyServer() const { return (type == Type::PUBLIC_KEY) && !server_id.empty(); } +#ifdef HAS_KEYSHARES /** * @brief check whether Recipient is keyshare * @return true if type is KEYSHARE */ bool isKeyShare() const { return type == Type::KEYSHARE; } +#endif /** * @brief Clear all values and set type to NONE @@ -184,6 +168,12 @@ struct CDOC_EXPORT Recipient { * @return a new Recipient structure */ static Recipient makePublicKey(std::string label, std::vector public_key, PKType pk_type); + /** + * @brief Create a new public key based Recipient + * @param lock Lock to derive parameters from + * @return a new Recipient structure + */ + static Recipient makePublicKey(const Lock &lock); /** * @brief Create a new certificate based Recipient * @param label the label text @@ -215,6 +205,16 @@ struct CDOC_EXPORT Recipient { */ static Recipient makeServer(std::string label, std::vector cert, std::string server_id); + /** + * @brief Create a new capsule server based Recipient + * + * @param lock Lock to derive parameters from + * @param server_id the keyserver id + * @return a new Recipient structure + */ + static Recipient makeServer(const Lock &lock, std::string server_id); + +#ifdef HAS_KEYSHARES /** * @brief Create new keyshare recipient * @@ -224,6 +224,7 @@ struct CDOC_EXPORT Recipient { * @return Recipient a new Recipient structure */ static Recipient makeShare(std::string label, std::string server_id, std::string recipient_id); +#endif /** * @brief Get the label for this recipient @@ -233,18 +234,30 @@ struct CDOC_EXPORT Recipient { * @param extra additional parameter values to use * @return a label value */ - std::string getLabel(const std::vector> &extra) const; + std::string getLabel(std::map extra) const; + + /** + * @brief Set a property for automatic label generation + * + * @param key the property name + * @param value the property value + */ + void setLabelValue(std::string_view key, std::string_view value) { + lbl_parts[std::string(key)] = value; + } /** - * @brief parse machine-readable CDoc2 label - * @param label the label - * @return a map of key-value pairs + * @brief Validate recipient record + * + * @return true if Recipient is valid */ - static std::map parseLabel(const std::string& label); + bool validate() const; bool operator== (const Recipient& other) const = default; protected: Recipient(Type _type) : type(_type) {}; +private: + std::map lbl_parts; }; } // namespace libcdoc diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Info.plist b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Info.plist index f1db506c..3f02ba10 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Info.plist +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Info.plist @@ -12,17 +12,19 @@ ee.ria.cdoc CFBundleInfoDictionaryVersion 6.0 + CFBundleName + CFBundlePackageType FMWK CFBundleShortVersionString - 0.1.8 + 0.5.0 CFBundleSignature ???? CFBundleVersion - 0 + 32 CSResourcesFileMapped MinimumOSVersion - 15.0 + 16.3 diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Modules/module.modulemap b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Modules/module.modulemap index db570a56..68872550 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Modules/module.modulemap +++ b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/Modules/module.modulemap @@ -10,8 +10,7 @@ framework module cdoc { header "CryptoBackend.h" header "NetworkBackend.h" header "PKCS11Backend.h" - header "ILogger.h" - header "ConsoleLogger.h" + header "Logger.h" export * requires cplusplus } \ No newline at end of file diff --git a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/cdoc b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/cdoc index e0c30e51..8edfd1ba 100755 Binary files a/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/cdoc and b/Modules/CryptoLib/Sources/CryptoObjC/Libs/cdoc.xcframework/ios-arm64_x86_64-simulator/cdoc.framework/cdoc differ diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm index 4d30bd1b..c147ffce 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Decrypt.mm @@ -28,40 +28,56 @@ #include #include +static CertType certTypeFromLabel(NSString * _Nullable type) { + if (type == nil) return CertTypeESealType; + if ([type isEqualToString:@"ID-card"] || + [type isEqualToString:@"cert"]) return CertTypeIDCardType; + if ([type isEqualToString:@"Digi-ID"]) return CertTypeDigiIDType; + if ([type isEqualToString:@"Digi-ID E-RESIDENT"]) return CertTypeEResidentType; + return CertTypeUnknownType; +} + @implementation Addressee (label) - (instancetype)initWithLabel:(const std::string &)label pub:(NSData*)pub concatKDFAlgorithmURI:(NSString *)concatKDFAlgorithmURI { - std::map info = libcdoc::Recipient::parseLabel(label); - id cn = info.contains("cn") ? [NSString stringWithStdString:info["cn"]] : [NSString stringWithStdString:label]; - id type = info.contains("type") ? [NSString stringWithStdString:info["type"]] : nil; - id serial = info.contains("serial_number") ? [NSString stringWithStdString:info["serial_number"]] : nil; - CertType certType = CertTypeUnknownType; + std::map info = libcdoc::Lock::parseLabel(label); + NSString *cn = info.contains("cn") ? [NSString stringWithStdString:info["cn"]] : [NSString stringWithStdString:label]; + NSString *type = info.contains("type") ? [NSString stringWithStdString:info["type"]] : nil; + NSString *serial = info.contains("serial_number") ? [NSString stringWithStdString:info["serial_number"]] : nil; + + // A single-segment CN with no explicit last_name key is an e-seal, not a person. NSArray *split = [cn componentsSeparatedByString:@","]; if (!info.contains("last_name") && split.count == 1) { type = nil; } - - if ([type isEqualToString:@"ID-card"] || [type isEqualToString:@"cert"]) { - certType = CertTypeIDCardType; - } else if ([type isEqualToString:@"Digi-ID"]) { - certType = CertTypeDigiIDType; - } else if ([type isEqualToString:@"Digi-ID E-RESIDENT"]) { - certType = CertTypeEResidentType; - } else if (type == nil) { - certType = CertTypeESealType; - } - id validTo = nil; + + NSDate *validTo = nil; if (info.contains("server_exp")) { long long epochTime = [[NSString stringWithStdString:info["server_exp"]] longLongValue]; validTo = [NSDate dateWithTimeIntervalSince1970:epochTime]; } - if (self = [self initWithCnVal:cn serialNumber:serial certType:certType validTo:validTo data:pub concatKDFAlgorithmURI:concatKDFAlgorithmURI]) { + + if (self = [self initWithCnVal:cn serialNumber:serial certType:certTypeFromLabel(type) validTo:validTo data:pub concatKDFAlgorithmURI:concatKDFAlgorithmURI lockLabel:@"" lockType:@""]) { } return self; } @end +static NSString *lockTypeName(libcdoc::Lock::Type type) { + switch (type) { + case libcdoc::Lock::Type::PASSWORD: return @"PASSWORD"; + case libcdoc::Lock::Type::SYMMETRIC_KEY: return @"SYMMETRIC_KEY"; + case libcdoc::Lock::Type::PUBLIC_KEY: return @"PUBLIC_KEY"; + case libcdoc::Lock::Type::CDOC1: return @"CDOC1"; + case libcdoc::Lock::Type::SERVER: return @"SERVER"; +#ifdef HAS_KEYSHARES + case libcdoc::Lock::Type::SHARE_SERVER: return @"SHARE_SERVER"; +#endif + default: return @"UNKNOWN"; + } +} + @implementation Decrypt + (void)setCerts:(nullable NSArray *)certs { @@ -102,7 +118,7 @@ + (CdocInfo*)cdocInfo:(NSString *)fullPath error:(NSError**)error { NSMutableArray *addressees = [[NSMutableArray alloc] init]; for(const libcdoc::Lock &lock: reader->getLocks()) { - if(lock.isCertificate()) { + if(lock.isCDoc1()) { NSString* concatKDFAlgorithmURI = @""; if (!lock.isRSA()) { concatKDFAlgorithmURI = [NSString stringWithStdString:lock.getString(libcdoc::Lock::CONCAT_DIGEST)]; @@ -111,7 +127,20 @@ + (CdocInfo*)cdocInfo:(NSString *)fullPath error:(NSError**)error { } else if(lock.isPKI()) { [addressees addObject:[[Addressee alloc] initWithLabel:lock.label pub:[NSData dataFromVector:lock.getBytes(libcdoc::Lock::RCPT_KEY)] concatKDFAlgorithmURI:@""]]; } else if(lock.isSymmetric()) { - [addressees addObject:[[Addressee alloc] initWithData:[NSData data] cnVal:[NSString stringWithStdString:lock.label]]]; + std::map info = libcdoc::Lock::parseLabel(lock.label); + NSString *cnVal = info.contains("label") + ? [NSString stringWithStdString:info["label"]] + : @""; + NSString *rawLockLabel = [NSString stringWithStdString:lock.label] ?: @""; + [addressees addObject:[[Addressee alloc] + initWithCnVal:cnVal + serialNumber:nil + certType:CertTypePasswordType + validTo:nil + data:[NSData data] + concatKDFAlgorithmURI:@"" + lockLabel:rawLockLabel + lockType:lockTypeName(lock.type)]]; } else { [addressees addObject:[[Addressee alloc] initWithData:[NSData data] cnVal:@"Unknown capsule"]]; } @@ -189,12 +218,27 @@ + (void)decryptFile:(NSString *)fullPath withCert:(NSData *)certData withToken:( } } crypto {password}; std::unique_ptr reader(libcdoc::CDocReader::createReader(fullPath.UTF8String, nullptr, &crypto, nullptr)); + if (!reader) { + return [NSError cryptoError:@"Failed to create CDocReader" error:error]; + } - auto idx = 0; // TODO: reader->getLockForCert(network.cert); - if(idx < 0) + int idx = -1; + const auto& locks = reader->getLocks(); + for (size_t i = 0; i < locks.size(); i++) { + if (locks[i].type == libcdoc::Lock::Type::PASSWORD) { + idx = (int)i; + break; + } + } + if (idx < 0) { return [NSError cryptoError:@"Decrypting failed" error:error]; + } std::vector fmk; - if(reader->getFMK(fmk, unsigned(idx)) != 0 || fmk.empty()) { + auto fmkResult = reader->getFMK(fmk, unsigned(idx)); + if (fmkResult == libcdoc::WRONG_KEY) { + return [NSError cryptoWrongKeyError:error]; + } + if (fmkResult != libcdoc::OK || fmk.empty()) { return [NSError cryptoError:@"Decrypting failed" error:error]; } return [self decryptReader:*reader withFMK:fmk error:error]; diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm index 7c185fd8..8e522038 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Encrypt.mm @@ -25,7 +25,8 @@ #include #include -#include +#include + @implementation Encrypt + (void)setCerts:(nullable NSArray *)certs { @@ -66,14 +67,14 @@ + (void)setProxy:(nonnull NSString *)host port:(NSInteger)port username:(nonnull encoding:NSUTF8StringEncoding] ?: @""; } -static inline NSString *NSStringFromLogLevel(libcdoc::ILogger::LogLevel level) { +static inline NSString *NSStringFromLogLevel(libcdoc::LogLevel level) { switch (level) { - case libcdoc::ILogger::LEVEL_FATAL: return @"FATAL"; - case libcdoc::ILogger::LEVEL_ERROR: return @"ERROR"; - case libcdoc::ILogger::LEVEL_WARNING: return @"WARN"; - case libcdoc::ILogger::LEVEL_INFO: return @"INFO"; - case libcdoc::ILogger::LEVEL_DEBUG: return @"DEBUG"; - case libcdoc::ILogger::LEVEL_TRACE: return @"TRACE"; + case libcdoc::LEVEL_FATAL: return @"FATAL"; + case libcdoc::LEVEL_ERROR: return @"ERROR"; + case libcdoc::LEVEL_WARNING: return @"WARN"; + case libcdoc::LEVEL_INFO: return @"INFO"; + case libcdoc::LEVEL_DEBUG: return @"DEBUG"; + case libcdoc::LEVEL_TRACE: return @"TRACE"; } return @"UNKNOWN"; } @@ -91,9 +92,9 @@ + (void)setProxy:(nonnull NSString *)host port:(NSInteger)port username:(nonnull return path; } -class ObjCLogger final : public libcdoc::ILogger { -public: - void LogMessage(libcdoc::ILogger::LogLevel level, +class ObjCLogger final : public libcdoc::Logger { +protected: + void logMessage(libcdoc::LogLevel level, std::string_view file, int line, std::string_view message) override @@ -132,8 +133,8 @@ + (void)enableLogging:(bool)enabled { // Install only once, even if enableLogging:YES is called many times static std::once_flag once; std::call_once(once, [] { - libcdoc::ILogger::setLogger(&gLogger); - gLogger.SetMinLogLevel(libcdoc::ILogger::LEVEL_TRACE); + libcdoc::setLogger(&gLogger); + gLogger.setMinLogLevel(libcdoc::LEVEL_TRACE); }); } @@ -212,12 +213,14 @@ + (void)encryptFile:(NSString *)fullPath withDataFiles:(NSArray return completion([NSError cryptoError:@"Failed to create writer"]); } - if (writer->beginEncryption() != 0) { - return completion([NSError cryptoError:@"Failed to start encryption"]); + auto passwordRecipient = libcdoc::Recipient::makeSymmetric("", 65536); + passwordRecipient.setLabelValue("label", std::string(label.UTF8String)); + if (writer->addRecipient(passwordRecipient) != 0) { + return completion([NSError cryptoError:@"Failed to create key"]); } - if (writer->addRecipient(libcdoc::Recipient::makeSymmetric(label.UTF8String, 65536))) { - return completion([NSError cryptoError:@"Failed to create key"]); + if (writer->beginEncryption() != 0) { + return completion([NSError cryptoError:@"Failed to start encryption"]); } for (CryptoDataFile *dataFile in dataFiles) { diff --git a/Modules/CryptoLib/Sources/CryptoObjC/include/Extensions.h b/Modules/CryptoLib/Sources/CryptoObjC/include/Extensions.h index 46c286fa..5c8af62d 100644 --- a/Modules/CryptoLib/Sources/CryptoObjC/include/Extensions.h +++ b/Modules/CryptoLib/Sources/CryptoObjC/include/Extensions.h @@ -22,9 +22,12 @@ #include #include +static const NSInteger CryptoLibWrongKeyErrorCode = -109; // libcdoc::WRONG_KEY + @interface NSError (CryptoLib) + (NSError*)cryptoError:(NSString*)msg; + (id)cryptoError:(NSString*)msg error:(NSError**)error; ++ (id)cryptoWrongKeyError:(NSError**)error; @end @interface NSString (std_string) @@ -77,4 +80,13 @@ } return nil; } + ++ (id)cryptoWrongKeyError:(NSError**)error { + if (error) { + *error = [[NSError alloc] initWithDomain:@"ee.ria.digidoc.CryptoLib" + code:CryptoLibWrongKeyErrorCode + userInfo:@{NSLocalizedDescriptionKey: @"Wrong password"}]; + } + return nil; +} @end diff --git a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift index da278ca7..693fea23 100644 --- a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift +++ b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/Addressee.swift @@ -30,6 +30,8 @@ import Foundation public let validTo: Date? @MainActor @objc public var concatKDFAlgorithmURI: String + @objc public let lockLabel: String + @objc public let lockType: String @objc public init( data: Data, @@ -39,7 +41,9 @@ import Foundation serialNumber: String?, certType: CertType, validTo: Date?, - concatKDFAlgorithmURI: String = "" + concatKDFAlgorithmURI: String = "", + lockLabel: String = "", + lockType: String = "" ) { self.identifier = cnVal self.data = data @@ -49,6 +53,8 @@ import Foundation self.certType = certType self.validTo = validTo self.concatKDFAlgorithmURI = concatKDFAlgorithmURI + self.lockLabel = lockLabel + self.lockType = lockType } @objc public convenience init(data: Data, cnVal: String) { @@ -69,10 +75,12 @@ import Foundation certType: CertType, validTo: Date?, data: Data, - concatKDFAlgorithmURI: String = "" + concatKDFAlgorithmURI: String = "", + lockLabel: String = "", + lockType: String = "" ) { let split = cnVal.split(separator: ",").map { String($0) } - if split.count > 1 { + if split.count >= 3 { surname = split[0] givenName = split[1] identifier = split[2] @@ -86,13 +94,15 @@ import Foundation self.validTo = validTo self.data = data self.concatKDFAlgorithmURI = concatKDFAlgorithmURI + self.lockLabel = lockLabel + self.lockType = lockType } public init(cert: Data, x509: X509Certificate?) { data = cert let cnVal = x509?.subject(oid: .commonName)?.joined(separator: ",") ?? "" let split = cnVal.split(separator: ",").map { String($0) } - if split.count > 1 { + if split.count >= 3 { surname = split[0] givenName = split[1] identifier = split[2] @@ -105,6 +115,8 @@ import Foundation certType = x509?.certType() ?? .unknownType validTo = x509?.notAfter concatKDFAlgorithmURI = "" + lockLabel = "" + lockType = "" } convenience public init(cert: Data) { diff --git a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift index 798d86c8..7b61cde3 100644 --- a/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift +++ b/Modules/CryptoLib/Sources/CryptoObjCWrapper/Domain/X509CertificateType.swift @@ -27,6 +27,7 @@ import ASN1Decoder case mobileIDType case smartIDType case eSealType + case passwordType } extension X509Certificate { diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift index 60bed90f..5b13cfdb 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainer.swift @@ -343,31 +343,41 @@ extension CryptoContainer { withCert: cert, withToken: SmartToken(card: cardCommands, pin1: pin) ) - var cryptoDataFiles: [CryptoDataFile] = [] - var urlDataFiles: [URL] = [] - cryptoDataFiles.removeAll() - for dataFile in decryptedData { - - let sanitizedName = dataFile.key.sanitized() + let urlDataFiles = try writeDecryptedFiles(decryptedData, fileManager: fileManager) - let destinationPath = try Directories.getCacheDirectory( - subfolders: [Constants.Folder.ContainerFolder, Constants.Folder.Temp], - fileManager: fileManager - ) - - let fileUrl = destinationPath.appending(path: sanitizedName, directoryHint: .notDirectory) + return try await create( + containerFile: containerFile, + dataFiles: urlDataFiles, + recipients: recipients, + isDecrypted: true, + isEncrypted: false + ) + } - cryptoDataFiles.append(CryptoDataFile(filename: dataFile.key, filePath: destinationPath.resolvedPath)) - urlDataFiles.append(fileUrl) - let isCreated = fileManager.createFile( - atPath: fileUrl.resolvedPath, contents: dataFile.value, attributes: nil - ) + private static let libcdocWrongKeyCode = -109 // libcdoc::WRONG_KEY - if !isCreated { - CryptoContainer.logger().error("Unable to create file at path: \(destinationPath.resolvedPath)") + @MainActor + public static func decryptWithPassword( + containerFile: URL, + recipients: [Addressee], + password: String, + fileManager: FileManagerProtocol = Container.shared.fileManager() + ) async throws -> CryptoContainerProtocol { + let decryptedData: [String: Data] = try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + let result = try Decrypt.decryptFile(containerFile.resolvedPath, withPassword: password) + continuation.resume(returning: result) + } catch let nsError as NSError where nsError.code == libcdocWrongKeyCode { + continuation.resume(throwing: CryptoError.wrongDecryptionKey) + } catch { + continuation.resume(throwing: error) + } } } + let urlDataFiles = try writeDecryptedFiles(decryptedData, fileManager: fileManager) + return try await create( containerFile: containerFile, dataFiles: urlDataFiles, @@ -377,6 +387,55 @@ extension CryptoContainer { ) } + private static func writeDecryptedFiles( + _ decryptedData: [String: Data], + fileManager: FileManagerProtocol + ) throws -> [URL] { + let destinationPath = try Directories.getCacheDirectory( + subfolders: [Constants.Folder.ContainerFolder, Constants.Folder.Temp], + fileManager: fileManager + ) + return try decryptedData.map { name, data in + let fileUrl = destinationPath.appending(path: name.sanitized(), directoryHint: .notDirectory) + guard fileManager.createFile(atPath: fileUrl.resolvedPath, contents: data, attributes: nil) else { + throw CryptoError.containerDataFileSavingFailed( + CryptoErrorDetail(message: "Unable to create decrypted file", userInfo: ["fileName": name]) + ) + } + return fileUrl + } + } + + @MainActor + public static func encryptWithPassword( + containerFile: URL, + dataFiles: [URL], + label: String, + password: String + ) async throws -> CryptoContainerProtocol { + if dataFiles.isEmpty { + throw CryptoError.containerCreationFailed( + CryptoErrorDetail(message: "Cannot create an empty crypto container") + ) + } + + var cryptoDataFiles: [CryptoDataFile] = [] + for dataFile in dataFiles { + cryptoDataFiles.append( + CryptoDataFile(filename: dataFile.lastPathComponent, filePath: dataFile.resolvedPath) + ) + } + + try await Encrypt.encryptFile( + containerFile.resolvedPath, + withDataFiles: cryptoDataFiles, + withLabel: label, + withPassword: password + ) + + return try await open(containerFile: containerFile) + } + @MainActor public static func encrypt( containerFile: URL, diff --git a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift index 64a6cc8c..5b88f04c 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/CryptoContainerProtocol.swift @@ -22,6 +22,7 @@ import CommonsLib import CryptoObjC import CryptoObjCWrapper +/// @mockable public protocol CryptoContainerProtocol: GeneralContainer, Sendable { func isDecrypted() async -> Bool func isEncrypted() async -> Bool diff --git a/Modules/CryptoLib/Sources/CryptoSwift/Errors/CryptoError.swift b/Modules/CryptoLib/Sources/CryptoSwift/Errors/CryptoError.swift index f353a5cc..bd311181 100644 --- a/Modules/CryptoLib/Sources/CryptoSwift/Errors/CryptoError.swift +++ b/Modules/CryptoLib/Sources/CryptoSwift/Errors/CryptoError.swift @@ -27,6 +27,7 @@ public enum CryptoError: Error { case containerSavingFailed(CryptoErrorDetail) case containerRenamingFailed(CryptoErrorDetail) case containerDataFileSavingFailed(CryptoErrorDetail) + case wrongDecryptionKey public var errorDetail: CryptoErrorDetail { switch self { @@ -38,6 +39,8 @@ public enum CryptoError: Error { .containerRenamingFailed(let errorDetail), .containerDataFileSavingFailed(let errorDetail): return errorDetail + case .wrongDecryptionKey: + return CryptoErrorDetail(message: "Wrong decryption key") } } diff --git a/Modules/CryptoLib/Tests/CryptoSwiftTests/AddresseeTests.swift b/Modules/CryptoLib/Tests/CryptoSwiftTests/AddresseeTests.swift new file mode 100644 index 00000000..9cb1949e --- /dev/null +++ b/Modules/CryptoLib/Tests/CryptoSwiftTests/AddresseeTests.swift @@ -0,0 +1,194 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import CryptoObjCWrapper +import Foundation +import Testing + +struct AddresseeTests { + + @Test + func init_defaultLockLabelAndLockTypeAreEmpty() { + let addressee = Addressee( + data: Data(), + cnVal: "SMITH,JOHN,38001010001", + givenName: "JOHN", + surname: "SMITH", + serialNumber: nil, + certType: .iDCardType, + validTo: nil + ) + + #expect(addressee.lockLabel == "") + #expect(addressee.lockType == "") + } + + @Test + func init_lockLabelAndLockTypeSetCorrectly() { + let addressee = Addressee( + data: Data(), + cnVal: "myKey", + givenName: nil, + surname: nil, + serialNumber: nil, + certType: .passwordType, + validTo: nil, + concatKDFAlgorithmURI: "", + lockLabel: "data:v=1&label=myKey&type=pw", + lockType: "PASSWORD" + ) + + #expect(addressee.lockLabel == "data:v=1&label=myKey&type=pw") + #expect(addressee.lockType == "PASSWORD") + #expect(addressee.identifier == "myKey") + #expect(addressee.certType == .passwordType) + } + + @Test + func init_passwordTypePreservesEmptyLockLabelAndLockType() { + let addressee = Addressee( + data: Data(), + cnVal: "", + givenName: nil, + surname: nil, + serialNumber: nil, + certType: .passwordType, + validTo: nil + ) + + #expect(addressee.lockLabel == "") + #expect(addressee.lockType == "") + #expect(addressee.identifier == "") + } + + @Test + func cnValInit_with3Segments_parsesSurnameGivenNameIdentifier() { + let addressee = Addressee( + cnVal: "SMITH,JOHN,38001010001", + serialNumber: nil, + certType: .iDCardType, + validTo: nil, + data: Data() + ) + + #expect(addressee.surname == "SMITH") + #expect(addressee.givenName == "JOHN") + #expect(addressee.identifier == "38001010001") + } + + @Test + func cnValInit_withMoreThan3Segments_usesFirst3() { + let addressee = Addressee( + cnVal: "SMITH,JOHN,38001010001,EXTRA", + serialNumber: nil, + certType: .iDCardType, + validTo: nil, + data: Data() + ) + + #expect(addressee.surname == "SMITH") + #expect(addressee.givenName == "JOHN") + #expect(addressee.identifier == "38001010001") + } + + @Test + func cnValInit_with2Segments_doesNotSplitAndUsesFullStringAsIdentifier() { + let addressee = Addressee( + cnVal: "ACME OÜ,12345678", + serialNumber: nil, + certType: .eSealType, + validTo: nil, + data: Data() + ) + + #expect(addressee.surname == nil) + #expect(addressee.givenName == nil) + #expect(addressee.identifier == "ACME OÜ,12345678") + } + + @Test + func cnValInit_with1Segment_usesFullStringAsIdentifier() { + let addressee = Addressee( + cnVal: "SomeCompany", + serialNumber: nil, + certType: .eSealType, + validTo: nil, + data: Data() + ) + + #expect(addressee.surname == nil) + #expect(addressee.givenName == nil) + #expect(addressee.identifier == "SomeCompany") + } + + @Test + func cnValInit_withEmptyString_usesEmptyIdentifier() { + let addressee = Addressee( + cnVal: "", + serialNumber: nil, + certType: .unknownType, + validTo: nil, + data: Data() + ) + + #expect(addressee.surname == nil) + #expect(addressee.givenName == nil) + #expect(addressee.identifier == "") + } + + @Test + func dataConvenienceInit_setsIdentifierToCnVal() { + let addressee = Addressee(data: Data([1, 2, 3]), cnVal: "TestLabel") + + #expect(addressee.identifier == "TestLabel") + #expect(addressee.certType == .unknownType) + #expect(addressee.lockLabel == "") + #expect(addressee.lockType == "") + } + + @Test + func cnValInit_lockLabelAndLockTypeSetCorrectly() { + let addressee = Addressee( + cnVal: "SMITH,JOHN,38001010001", + serialNumber: nil, + certType: .iDCardType, + validTo: nil, + data: Data(), + lockLabel: "rawLockLabel", + lockType: "PUBLIC_KEY" + ) + + #expect(addressee.lockLabel == "rawLockLabel") + #expect(addressee.lockType == "PUBLIC_KEY") + } + + @Test + func cnValInit_defaultLockLabelAndLockTypeAreEmpty() { + let addressee = Addressee( + cnVal: "SMITH,JOHN,38001010001", + serialNumber: nil, + certType: .iDCardType, + validTo: nil, + data: Data() + ) + + #expect(addressee.lockLabel == "") + #expect(addressee.lockType == "") + } +} diff --git a/Modules/CryptoLib/Tests/CryptoTests.swift b/Modules/CryptoLib/Tests/CryptoSwiftTests/CryptoTests.swift similarity index 100% rename from Modules/CryptoLib/Tests/CryptoTests.swift rename to Modules/CryptoLib/Tests/CryptoSwiftTests/CryptoTests.swift diff --git a/RIADigiDoc/Domain/Model/Crypto/RecipientDetailViewTab.swift b/RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift similarity index 89% rename from RIADigiDoc/Domain/Model/Crypto/RecipientDetailViewTab.swift rename to RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift index 29b7d7b3..973d401b 100644 --- a/RIADigiDoc/Domain/Model/Crypto/RecipientDetailViewTab.swift +++ b/RIADigiDoc/Domain/Model/Crypto/EncryptRecipientViewTab.swift @@ -17,6 +17,7 @@ * */ -enum RecipientDetailViewTab: Int, Sendable { - case recipientDetails = 0 +enum EncryptRecipientViewTab: Int, Sendable { + case recipient = 0 + case password = 1 } diff --git a/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift b/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift index 950c872c..5add02e2 100644 --- a/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift +++ b/RIADigiDoc/Domain/Model/Crypto/EncryptViewTab.swift @@ -17,7 +17,7 @@ * */ -enum EncryptViewTab: Int, Sendable { +public enum EncryptViewTab: Int, Sendable { case files = 0 case recipients = 1 } diff --git a/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift b/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift index bdced805..a4e37ba8 100644 --- a/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift +++ b/RIADigiDoc/Domain/Model/EncryptionCdocOption.swift @@ -17,7 +17,7 @@ * */ -public enum EncryptionCdocOption: Int, Sendable { +public enum EncryptionCdocOption: Int, Sendable, Hashable { case cdoc1 = 0 case cdoc2 = 1 } diff --git a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift index 0b2a7a24..1465fce3 100644 --- a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift +++ b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift @@ -30,7 +30,7 @@ public enum NavigationDestination: Hashable { extensions: [String] ) - case encryptRecipientView + case encryptRecipientView(cdocOption: EncryptionCdocOption) case signingView case signatureDetailView( @@ -51,8 +51,11 @@ public enum NavigationDestination: Hashable { ) case encryptView( - isWithEncryption: Bool + isWithEncryption: Bool, + cdocOption: EncryptionCdocOption, + selectedTab: EncryptViewTab ) + case recipientDetailView( recipient: Addressee, ) diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 03b639f8..768de6d2 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -1259,6 +1259,204 @@ } } }, + "Crypto password encryption description" : { + "comment" : "Encrypt with password tab description", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password encryption is intended for long-term storage. The password cannot be changed or recovered." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parooliga krüpteerimine on mõeldud pikaajalise säilitamise jaoks. Parooli ei ole võimalik muuta ega taastada." + } + } + } + }, + "Crypto password field label" : { + "comment" : "Password dialog — password field label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Document password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dokumendi parool" + } + } + } + }, + "Crypto password key label" : { + "comment" : "Password dialog — key label field placeholder", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Key label" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Võtme silt" + } + } + } + }, + "Crypto password key label description" : { + "comment" : "Password dialog — key label helper text shown below the field", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recipient name or ID" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adressaadi nimi või ID" + } + } + } + }, + "Crypto password length requirement" : { + "comment" : "Password dialog — length requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Length: 20 – 64 characters" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikkus: 20 – 64 tähemärki" + } + } + } + }, + "Crypto password lowercase requirement" : { + "comment" : "Password dialog — lowercase letter requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one lowercase letter" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte väiketähte" + } + } + } + }, + "Crypto password number requirement" : { + "comment" : "Password dialog — number requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one number (0 – 9)" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte numbrit (0 – 9)" + } + } + } + }, + "Crypto password repeat label" : { + "comment" : "Password dialog — repeat password field label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repeat password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korda parooli" + } + } + } + }, + "Crypto password repeat mismatch" : { + "comment" : "Password dialog — repeat password does not match error", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwords do not match" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paroolid ei kattu" + } + } + } + }, + "Crypto password save warning" : { + "comment" : "Password dialog — info box warning text", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Be sure to save the password in a secure place - without the password, you won't be able to open the file again." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvesta parool kindlasse kohta – ilma paroolita ei saa te faili enam avada." + } + } + } + }, + "Crypto password uppercase requirement" : { + "comment" : "Password dialog — uppercase letter requirement", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contains at least one uppercase letter" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sisaldab vähemalt ühte suurtähte" + } + } + } + }, "Crypto recipients description" : { "comment" : "Crypto recipients description", "extractionState" : "manual", @@ -1547,6 +1745,24 @@ } } }, + "Encrypt based on recipient" : { + "comment" : "Encrypt recipient tab title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypt based on recipient" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krüpteeri adressaadi põhjal" + } + } + } + }, "Encrypt container" : { "comment" : "Accessibility variant of Encrypt", "extractionState" : "manual", @@ -1565,6 +1781,42 @@ } } }, + "Decrypt general error" : { + "comment" : "CryptoContainer password decrypt error message", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Container decryption was unsuccessful" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ümbriku dekrüpteerimine ebaõnnestus" + } + } + } + }, + "Decrypt wrong password error" : { + "comment" : "CryptoContainer password decrypt — wrong password error", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wrong password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vale parool" + } + } + } + }, "Encrypt general error" : { "comment" : "CryptoContainer encrypt error message", "extractionState" : "manual", @@ -1583,6 +1835,24 @@ } } }, + "Encrypt with password" : { + "comment" : "Encrypt with password tab title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypt with password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krüpteeri parooliga" + } + } + } + }, "Enter current PIN code" : { "comment" : "My eID current PIN or PUK code step title", "extractionState" : "manual", @@ -5386,6 +5656,60 @@ } } }, + "Password" : { + "comment" : "Crypto recipient password type label", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parool" + } + } + } + }, + "Password range to accessibility" : { + "comment" : "Password dialog — VoiceOver word spoken in place of the en dash in ranges like 20 – 64", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "to" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "kuni" + } + } + } + }, + "Password requirements" : { + "comment" : "Password dialog — VoiceOver heading announced before the requirement list", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password requirements" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parooli nõuded" + } + } + } + }, "Person or company does not own a valid certificate" : { "comment" : "LDAP search message when no results found", "extractionState" : "manual", @@ -6212,6 +6536,24 @@ } } }, + "Lock type" : { + "comment" : "Recipient detail view — lock type label for password recipients", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lock type" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luku tüüp" + } + } + } + }, "Recipient" : { "comment" : "Home title, signer details title, accessibility recipient prefix", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index cafd2b14..83072d3a 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -19,6 +19,7 @@ import SwiftUI import FactoryKit +import CryptoSwift import CryptoObjCWrapper import CommonsLib import UtilsLib @@ -38,6 +39,9 @@ struct EncryptView: View { @Environment(\.dismiss) private var dismiss @State private var selectedTab: EncryptViewTab = .files @State private var selectedRecipient: Addressee? + @State private var cdocOption: EncryptionCdocOption + @State private var showDecryptPasswordModal = false + @State private var passwordDecryptKeyLabel = "" @State private var viewModel: EncryptViewModel @@ -173,10 +177,14 @@ struct EncryptView: View { init( isWithEncryption: Bool = false, isWithDecryption: Bool = false, + cdocOption: EncryptionCdocOption = .cdoc1, + selectedTab: EncryptViewTab = .files, nameUtil: NameUtilProtocol = Container.shared.nameUtil(), recipientUtil: RecipientUtilProtocol = Container.shared.recipientUtil(), fileUtil: FileUtilProtocol = Container.shared.fileUtil() ) { + _cdocOption = State(wrappedValue: cdocOption) + _selectedTab = State(wrappedValue: selectedTab) _viewModel = State(wrappedValue: Container.shared.encryptViewModel()) self.isWithEncryption = isWithEncryption self.isWithDecryption = isWithDecryption @@ -238,9 +246,15 @@ struct EncryptView: View { } } } else if viewModel.isDecryptButtonShown { - isWithEncryption = false - isWithDecryption = false - pathManager.navigate(to: .decryptRootView) + if let passwordRecipient = viewModel.recipients + .first(where: { $0.certType == .passwordType }) { + passwordDecryptKeyLabel = passwordRecipient.identifier + showDecryptPasswordModal = true + } else { + isWithEncryption = false + isWithDecryption = false + pathManager.navigate(to: .decryptRootView) + } } }, onSaveContainerButtonClick: { @@ -358,6 +372,7 @@ struct EncryptView: View { .environment(languageSettings) } } + .padding(.top, Dimensions.Padding.LPadding) } } } @@ -391,7 +406,7 @@ struct EncryptView: View { rightButtonAccessibilityLabel: rightButtonLabel.lowercased(), rightButtonAction: { if viewModel.isContainerUnencrypted { - pathManager.replaceLast(to: .encryptRecipientView) + pathManager.replaceLast(to: .encryptRecipientView(cdocOption: cdocOption)) } else { if encryptionButtonEnabled { encryptionButtonEnabled = false @@ -466,6 +481,14 @@ struct EncryptView: View { } ) + if showDecryptPasswordModal { + DecryptPasswordModalView( + keyLabel: passwordDecryptKeyLabel, + onDecrypt: { password in Task { await handlePasswordDecrypt(password) } }, + onCancel: { showDecryptPasswordModal = false } + ) + } + if showRenameModal { RenameModalView( containerName: containerName, @@ -545,6 +568,33 @@ struct EncryptView: View { } } + private func handlePasswordDecrypt(_ password: String) async { + guard let containerFile = viewModel.containerURL else { + Toast.show(languageSettings.localized("Decrypt general error")) + return + } + do { + let decryptedContainer = try await CryptoContainer.decryptWithPassword( + containerFile: containerFile, + recipients: viewModel.recipients, + password: password + ) + let sharedVM = Container.shared.sharedContainerViewModel() + sharedVM.removeLastContainer() + sharedVM.setCryptoContainer(decryptedContainer) + await viewModel.loadContainerData(cryptoContainer: decryptedContainer) + showDecryptPasswordModal = false + selectedTab = .files + await updateAsyncLabels() + await viewModel.updateAsyncProperties() + Toast.show(languageSettings.localized("Container successfully decrypted"), type: .success) + } catch CryptoError.wrongDecryptionKey { + Toast.show(languageSettings.localized("Decrypt wrong password error")) + } catch { + Toast.show(languageSettings.localized("Decrypt general error")) + } + } + func updateAsyncLabels() async { let containerTitle = await containerTitle() let encryptDecryptLabel = await self.encryptDecryptLabel() diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift new file mode 100644 index 00000000..8fc3da9b --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/DecryptPasswordModalView.swift @@ -0,0 +1,104 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct DecryptPasswordModalView: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + @State private var password: String = "" + + let keyLabel: String + let onDecrypt: (String) -> Void + let onCancel: () -> Void + + private var passwordFieldTitle: String { + languageSettings.localized("Crypto password field label") + } + + var body: some View { + PasswordModalCard { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + ViewThatFits(in: .vertical) { + dialogContent + + ScrollView { + dialogContent + } + } + PasswordModalButtonRow( + cancelLabel: languageSettings.localized("Cancel"), + confirmLabel: languageSettings.localized("Decrypt"), + onCancel: onCancel, + onConfirm: { onDecrypt(password) } + ) + } + } + } + + private var dialogContent: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + PasswordModalTitleView(text: languageSettings.localized("Decrypt")) + keyLabelSection + passwordSection + } + } + + private var keyLabelTitle: String { + languageSettings.localized("Crypto password key label") + } + + private var keyLabelSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.XXSPadding) { + Text(verbatim: keyLabelTitle) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .accessibilityHidden(true) + Text(verbatim: keyLabel) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurface) + .fontWeight(.bold) + .accessibilityLabel(Text(verbatim: "\(keyLabelTitle) \(keyLabel)")) + } + } + + private var passwordSection: some View { + FloatingLabelTextField( + title: passwordFieldTitle, + placeholder: passwordFieldTitle, + text: $password, + isSecure: true, + submitLabel: .done, + identifier: "decryptPasswordInput" + ) + } +} + +#Preview { + DecryptPasswordModalView( + keyLabel: "Allkirjastamata lepingud 2026", + onDecrypt: { _ in }, + onCancel: {} + ) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift new file mode 100644 index 00000000..c48d25b4 --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/EncryptPasswordModalView.swift @@ -0,0 +1,189 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct EncryptPasswordModalView: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + @State private var keyLabel: String = "" + @State private var password: String = "" + @State private var repeatPassword: String = "" + + let onEncrypt: (String, String) -> Void + let onCancel: () -> Void + + private var keyLabelTitle: String { languageSettings.localized("Crypto password key label") } + private var passwordTitle: String { languageSettings.localized("Crypto password field label") } + private var repeatTitle: String { languageSettings.localized("Crypto password repeat label") } + + private var isPasswordValid: Bool { + let len = password.count + return len >= 20 && len <= 64 + && password.contains(where: { $0.isNumber }) + && password.contains(where: { $0.isUppercase }) + && password.contains(where: { $0.isLowercase }) + } + + private var showPasswordError: Bool { !password.isEmpty && !isPasswordValid } + private var showRepeatError: Bool { !repeatPassword.isEmpty && repeatPassword != password } + + var body: some View { + PasswordModalCard { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ScrollView { + VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { + PasswordModalTitleView(text: languageSettings.localized("Encrypt with password")) + keyLabelSection + infoBox + passwordSection + repeatPasswordSection + } + .padding(.vertical, Dimensions.Padding.MSPadding) + } + PasswordModalButtonRow( + cancelLabel: languageSettings.localized("Cancel"), + confirmLabel: languageSettings.localized("Encrypt"), + isConfirmEnabled: isPasswordValid && !repeatPassword.isEmpty && password == repeatPassword, + onCancel: onCancel, + onConfirm: { onEncrypt(keyLabel, password) } + ) + } + .frame(maxHeight: .infinity) + } + } + + private var keyLabelSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { + FloatingLabelTextField( + title: keyLabelTitle, + placeholder: keyLabelTitle, + text: $keyLabel, + submitLabel: .next, + identifier: "passwordKeyLabel", + accessibilityHint: languageSettings.localized("Crypto password key label description") + ) + Text(verbatim: languageSettings.localized("Crypto password key label description")) + .font(typography.labelMedium) + .foregroundStyle(theme.onSecondaryContainer) + .padding(.top, Dimensions.Padding.XXSPadding) + .accessibilityHidden(true) + } + } + + private var infoBox: some View { + HStack(alignment: .center, spacing: Dimensions.Padding.SPadding) { + Image("ic_m3_info_48pt_wght400") + .resizable() + .scaledToFit() + .frame( + width: Dimensions.Icon.IconSizeXXS, + height: Dimensions.Icon.IconSizeXXS + ) + .foregroundStyle(theme.primary) + .accessibilityHidden(true) + + Text(verbatim: languageSettings.localized("Crypto password save warning")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurface) + .fixedSize(horizontal: false, vertical: true) + } + .padding(Dimensions.Padding.SPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.XSCornerRadius) + .fill(theme.surfaceVariant) + ) + } + + private static let requirementKeys = [ + "Crypto password length requirement", + "Crypto password number requirement", + "Crypto password uppercase requirement", + "Crypto password lowercase requirement" + ] + + private var requirementsAccessibilityLabel: String { + ([languageSettings.localized("Password requirements")] + + EncryptPasswordModalView.requirementKeys.map { languageSettings.localized($0) }) + .joined(separator: ". ") + .replacingOccurrences( + of: "–", + with: " \(languageSettings.localized("Password range to accessibility")) " + ) + } + + private var passwordSection: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.MSPadding) { + FloatingLabelTextField( + title: passwordTitle, + placeholder: passwordTitle, + text: $password, + isSecure: true, + isError: showPasswordError, + submitLabel: .next, + identifier: "passwordInput", + sortPriority: 0 + ) + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ForEach(EncryptPasswordModalView.requirementKeys, id: \.self) { key in + requirementRow(key) + } + } + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(verbatim: requirementsAccessibilityLabel)) + .accessibilitySortPriority(1) + } + .accessibilityElement(children: .contain) + } + + private var repeatPasswordSection: some View { + FloatingLabelTextField( + title: repeatTitle, + placeholder: repeatTitle, + text: $repeatPassword, + isSecure: true, + isError: showRepeatError, + errorText: showRepeatError + ? languageSettings.localized("Crypto password repeat mismatch") + : "", + submitLabel: .done, + identifier: "repeatPasswordInput" + ) + } + + private func requirementRow(_ key: String) -> some View { + Text(verbatim: "• \(languageSettings.localized(key))") + .font(typography.labelMedium) + .foregroundStyle(showPasswordError ? theme.error : theme.onSecondaryContainer) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +#Preview { + EncryptPasswordModalView( + onEncrypt: { _, _ in }, + onCancel: {} + ) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift b/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift new file mode 100644 index 00000000..4760abd2 --- /dev/null +++ b/RIADigiDoc/UI/Component/Container/Crypto/Modal/PasswordModalCard.swift @@ -0,0 +1,122 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct PasswordModalCard: View { + @AppTheme private var theme + + @State private var keyboardHeight: CGFloat = 0 + + @ViewBuilder let content: () -> Content + + var body: some View { + ZStack { + Color.black + .opacity(Dimensions.Shadow.LOpacity) + .ignoresSafeArea() + .accessibilityHidden(true) + .allowsHitTesting(true) + + content() + .padding(Dimensions.Padding.MPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.MCornerRadius) + .fill(theme.surface) + ) + .padding(.horizontal, Dimensions.Padding.MPadding) + .padding(.vertical, Dimensions.Padding.XLPadding) + .offset(y: -keyboardHeight / 2) + .animation( + .easeOut(duration: Dimensions.Duration.focusAnimation), + value: keyboardHeight + ) + .accessibilityAddTraits([.isModal]) + } + .ignoresSafeArea(.keyboard) + .onReceive( + NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification) + ) { notification in + guard let frame = notification.userInfo?[ + UIResponder.keyboardFrameEndUserInfoKey + ] as? CGRect else { return } + keyboardHeight = frame.height + } + .onReceive( + NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification) + ) { _ in + keyboardHeight = 0 + } + } +} + +struct PasswordModalTitleView: View { + @AppTheme private var theme + @AppTypography private var typography + + @AccessibilityFocusState private var isFocused: Bool + + let text: String + + var body: some View { + Text(verbatim: text) + .foregroundStyle(theme.onSurface) + .font(typography.headlineSmall) + .fixedSize(horizontal: false, vertical: true) + .minimumScaleFactor(0.5) + .accessibilityHeading(.h1) + .accessibilityAddTraits([.isHeader]) + .accessibilityFocused($isFocused) + .onAppear { + Task { + try? await Task.sleep(for: .seconds(0.3)) + isFocused = true + } + } + } +} + +struct PasswordModalButtonRow: View { + @Environment(LanguageSettings.self) private var languageSettings + @AppTheme private var theme + @AppTypography private var typography + + let cancelLabel: String + let confirmLabel: String + var isConfirmEnabled: Bool = true + let onCancel: () -> Void + let onConfirm: () -> Void + + var body: some View { + HStack(spacing: Dimensions.Padding.MPadding) { + Button(cancelLabel) { onCancel() } + .font(typography.labelLarge) + .foregroundStyle(theme.primary) + .minimumScaleFactor(0.5) + Button(confirmLabel) { onConfirm() } + .font(typography.labelLarge) + .foregroundStyle(isConfirmEnabled ? theme.primary : theme.onSurfaceVariant) + .minimumScaleFactor(0.5) + .disabled(!isConfirmEnabled) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.vertical, Dimensions.Padding.MSPadding) + } +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift index 564e9df9..58e4b773 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/Recipient/EncryptRecipientView.swift @@ -45,13 +45,17 @@ struct EncryptRecipientView: View { @State private var selectedRecipient: Addressee? @State private var showRemoveRecipientModal = false + @State private var showPasswordEncryptModal = false @State private var addedRecipients: [Addressee] = [] + @State private var selectedTab: EncryptRecipientViewTab = .recipient + @State private var cdocOption: EncryptionCdocOption @State private var viewModel: EncryptRecipientViewModel @State private var encryptViewModel: EncryptViewModel - init() { + init(cdocOption: EncryptionCdocOption) { + _cdocOption = State(wrappedValue: cdocOption) _viewModel = State(wrappedValue: Container.shared.encryptRecipientViewModel()) _encryptViewModel = State(wrappedValue: Container.shared.encryptViewModel()) } @@ -72,10 +76,22 @@ struct EncryptRecipientView: View { languageSettings.localized("Encrypt") } + var nextLabel: String { + languageSettings.localized("Next") + } + var noSearchResultsMessage: String { languageSettings.localized("Person or company does not own a valid certificate") } + private var recipientTabTitle: String { + languageSettings.localized("Encrypt based on recipient") + } + + private var passwordTabTitle: String { + languageSettings.localized("Encrypt with password") + } + private var addedRecipientsSection: some View { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { if noSearchResults { @@ -127,167 +143,239 @@ struct EncryptRecipientView: View { .listSectionSpacing(.compact) } + @ViewBuilder + private var bottomButtonBar: some View { + if cdocOption == .cdoc2 && selectedTab == .password { + UnsignedBottomBarView( + showLeftButton: false, + leftButtonIconName: "", + leftButtonLabel: "", + leftButtonAccessibilityLabel: "", + leftButtonAction: {}, + rightButtonEnabled: true, + rightButtonIconName: "ic_m3_arrow_forward_48pt_wght400", + rightButtonLabel: nextLabel, + rightButtonAccessibilityLabel: nextLabel.lowercased(), + rightButtonAction: { + showPasswordEncryptModal = true + }, + showBackground: false + ) + .accessibilityIdentifier("bottomNextButton") + } else { + HStack { + Spacer() + Button(action: { + encryptionButtonEnabled = false + pathManager.replaceLast(to: .encryptView(isWithEncryption: true, cdocOption: cdocOption, selectedTab: .files)) + }, label: { + HStack(spacing: Dimensions.Padding.XSPadding) { + Image("ic_m3_encrypted_48pt_wght400") + .resizable() + .scaledToFit() + .frame( + width: Dimensions.Icon.IconSizeXXS, + height: Dimensions.Icon.IconSizeXXS + ) + .foregroundStyle(theme.onPrimaryContainer) + Text(verbatim: encryptLabel) + .foregroundStyle(theme.onPrimaryContainer) + .font(typography.bodyLarge) + } + .accessibilityHidden(true) + }) + .contentShape(Rectangle()) + .disabled(!encryptionButtonEnabled) + .padding(Dimensions.Padding.MSPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Corner.MSCornerRadius) + .fill(theme.primaryContainer) + .shadow( + color: theme.onSurfaceVariant.opacity(Dimensions.Shadow.SOpacity), + radius: Dimensions.Shadow.radius, + x: Dimensions.Shadow.xOffset, + y: Dimensions.Shadow.yOffset + ) + ) + .padding(Dimensions.Padding.MPadding) + .accessibilityElement(children: .ignore) + .accessibilityLabel(encryptLabel.lowercased()) + .accessibilityAddTraits(.isButton) + .accessibilityIdentifier("bottomEncryptButton") + } + } + } + + @ViewBuilder + private var searchField: some View { + HStack { + Image(systemName: "magnifyingglass") + .foregroundStyle(theme.onSurfaceVariant) + .accessibilityHidden(true) + + FloatingLabelTextField( + title: "", + placeholder: languageSettings.localized("Search recipients"), + text: $viewModel.searchText, + submitLabel: .done, + identifier: "searchRecipients", + sortPriority: 1, + showBorder: false, + onDone: { + if viewModel.searchText.allSatisfy(\.isNumber) && + viewModel.searchText.count == 11 && + !PersonalCodeValidator.isPersonalCodeValid(viewModel.searchText) { + let personalCodeNotValidMessage = languageSettings.localized( + "Personal code is not valid" + ) + + Toast.show(personalCodeNotValidMessage) + + if voiceOverEnabled { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AccessibilityUtil.announceMessage( + personalCodeNotValidMessage + ) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isTitleFocused = true + } + } + } + return + } + + Task { + await viewModel.loadRecipients() + } + } + ) + .accessibilityFocused($isSearchFieldFocused) + .focused($isSearchFocused) + .onChange(of: isSearchFocused) { _, newValue in + isSearchExpanded = newValue + } + .onChange(of: viewModel.searchText) { + viewModel.handleSearchTextChange() + } + } + .padding(.horizontal, Dimensions.Padding.SPadding) + .background( + RoundedRectangle(cornerRadius: Dimensions.Padding.MPadding, style: .continuous) + .fill(Color(.systemGray6)) + ) + } + + @ViewBuilder + private var recipientsScrollView: some View { + ScrollView { + if noSearchResults && !isSearchExpanded { + VStack { + Text(languageSettings.localized("Crypto recipients description")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .multilineTextAlignment(.leading) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollDisabled(true) + .scrollContentBackground(.hidden) + } else if showNoRecipientsFoundMessage { + emptyStateView( + languageSettings.localized( + "Person or company does not own a valid certificate" + ) + ) + } else { + filteredRecipientsSection + } + + Spacer().frame(height: Dimensions.Padding.MSPadding) + + if addedRecipients.count > 0 { + addedRecipientsSection + } + } + .accessibilitySortPriority(filteredRecipients.isEmpty ? 2 : 0) + } + + @ViewBuilder + private func containerRecipientsTitle(topPadding: CGFloat) -> some View { + if !isSearchExpanded { + Text(verbatim: languageSettings.localized("Container recipients")) + .foregroundStyle(theme.onSurface) + .font(typography.headlineSmall) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, topPadding) + .minimumScaleFactor(0.5) + .accessibilityHeading(.h1) + .accessibilityAddTraits([.isHeader]) + .accessibilityFocused($isTitleFocused) + .accessibilitySortPriority(3) + .onAppear { + if noSearchResults { isTitleFocused = true } + } + } + } + var body: some View { TopBarContainer( title: nil, onLeftClick: { - pathManager.replaceLast(to: .encryptView(isWithEncryption: false)) + pathManager.replaceLast(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) }, showRightIcons: !isSearchExpanded, content: { ZStack { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { - if !isSearchExpanded { - Text(verbatim: languageSettings.localized("Container recipients")) - .foregroundStyle(theme.onSurface) - .font(typography.headlineSmall) - .padding(.top, Dimensions.Padding.SPadding) - .minimumScaleFactor(0.5) - .accessibilityHeading(.h1) - .accessibilityAddTraits([.isHeader]) - .accessibilityFocused($isTitleFocused) - .accessibilitySortPriority(3) - .onAppear { - if noSearchResults { - isTitleFocused = true + if cdocOption == .cdoc2 { + TabView(selectedTab: $selectedTab, titles: [ + recipientTabTitle, + passwordTabTitle + ]) { + if selectedTab == .recipient { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + containerRecipientsTitle(topPadding: Dimensions.Padding.MPadding) + searchField + .padding(.top, Dimensions.Padding.SPadding) + .padding(.bottom, Dimensions.Padding.SPadding) + recipientsScrollView } - } - } - HStack { - Image(systemName: "magnifyingglass") - .foregroundStyle(theme.onSurfaceVariant) - .accessibilityHidden(true) - - FloatingLabelTextField( - title: "", - placeholder: languageSettings.localized("Search recipients"), - text: $viewModel.searchText, - submitLabel: .done, - identifier: "searchRecipients", - sortPriority: 1, - showBorder: false, - onDone: { - if viewModel.searchText.allSatisfy(\.isNumber) && - viewModel.searchText.count == 11 && - !PersonalCodeValidator.isPersonalCodeValid(viewModel.searchText) { - let personalCodeNotValidMessage = languageSettings.localized( - "Personal code is not valid" - ) - - Toast.show(personalCodeNotValidMessage) - - if voiceOverEnabled { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - AccessibilityUtil.announceMessage( - personalCodeNotValidMessage - ) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - isTitleFocused = true - } - } + } else { + VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { + ScrollView { + Text(languageSettings.localized("Crypto password encryption description")) + .font(typography.bodyLarge) + .foregroundStyle(theme.onSurfaceVariant) + .multilineTextAlignment(.leading) + .padding(.top, Dimensions.Padding.MPadding) + .frame(maxWidth: .infinity, alignment: .leading) } - return - } - - Task { - await viewModel.loadRecipients() } } - ) - .accessibilityFocused($isSearchFieldFocused) - .focused($isSearchFocused) - .onChange(of: isSearchFocused) { _, newValue in - isSearchExpanded = newValue - } - .onChange(of: viewModel.searchText) { - viewModel.handleSearchTextChange() } + } else { + containerRecipientsTitle(topPadding: Dimensions.Padding.SPadding) + searchField + .padding(.top, Dimensions.Padding.LPadding) + .padding(.bottom, Dimensions.Padding.SPadding) + recipientsScrollView } - .padding(.horizontal, Dimensions.Padding.SPadding) - .background( - RoundedRectangle(cornerRadius: Dimensions.Padding.MPadding, style: .continuous) - .fill(Color(.systemGray6)) - ) - .padding(.top, Dimensions.Padding.LPadding) - .padding(.bottom, Dimensions.Padding.SPadding) - - ScrollView { - if noSearchResults && !isSearchExpanded { - VStack { - Text(languageSettings.localized("Crypto recipients description")) - .font(typography.bodyLarge) - .foregroundStyle(theme.onSurfaceVariant) - .multilineTextAlignment(.leading) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - } - .listStyle(.plain) - .scrollDisabled(true) - .scrollContentBackground(.hidden) - } else if showNoRecipientsFoundMessage { - emptyStateView( - languageSettings.localized( - "Person or company does not own a valid certificate" - ) - ) - } else { - filteredRecipientsSection - } - - Spacer().frame(height: Dimensions.Padding.MSPadding) - if addedRecipients.count > 0 { - addedRecipientsSection - } - } - .accessibilitySortPriority(filteredRecipients.isEmpty ? 2 : 0) - - HStack { - Spacer() - - Button(action: { - encryptionButtonEnabled = false - pathManager.replaceLast(to: .encryptView(isWithEncryption: true)) - }, label: { - HStack(spacing: Dimensions.Padding.XSPadding) { - Image("ic_m3_encrypted_48pt_wght400") - .resizable() - .scaledToFit() - .frame( - width: Dimensions.Icon.IconSizeXXS, - height: Dimensions.Icon.IconSizeXXS - ) - .foregroundStyle(theme.onPrimaryContainer) - - Text(verbatim: encryptLabel) - .foregroundStyle(theme.onPrimaryContainer) - .font(typography.bodyLarge) - } - .accessibilityHidden(true) - }) - .contentShape(Rectangle()) - .disabled(!encryptionButtonEnabled) - .padding(Dimensions.Padding.MSPadding) - .background( - RoundedRectangle(cornerRadius: Dimensions.Corner.MSCornerRadius) - .fill(theme.primaryContainer) - .shadow( - color: theme.onSurfaceVariant.opacity(Dimensions.Shadow.SOpacity), - radius: Dimensions.Shadow.radius, - x: Dimensions.Shadow.xOffset, - y: Dimensions.Shadow.yOffset - ) - ) - .padding(Dimensions.Padding.MPadding) - .accessibilityElement(children: .ignore) - .accessibilityLabel(encryptLabel.lowercased()) - .accessibilityAddTraits(.isButton) - .accessibilityIdentifier("bottomEncryptButton") - } + bottomButtonBar } .padding(.horizontal, Dimensions.Padding.SPadding) .accessibilityElement(children: .contain) + if showPasswordEncryptModal { + EncryptPasswordModalView( + onEncrypt: { keyLabel, password in + Task { await handlePasswordEncrypt(label: keyLabel, password: password) } + }, + onCancel: { showPasswordEncryptModal = false } + ) + } + if showRemoveRecipientModal { ConfirmModalView( title: "\(languageSettings.localized("Remove recipient"))?", @@ -343,7 +431,9 @@ struct EncryptRecipientView: View { } .onChange(of: viewModel.successMessage) { _, message in guard let message, !message.key.isEmpty else { return } - let localizedMessage = languageSettings.localized(message.key, [message.args.joined(separator: ", ")]) + let localizedMessage = languageSettings.localized( + message.key, [message.args.joined(separator: ", ")] + ) Toast.show(localizedMessage, type: .success) if voiceOverEnabled { @@ -406,6 +496,17 @@ struct EncryptRecipientView: View { .background(theme.surface) } + private func handlePasswordEncrypt(label: String, password: String) async { + do { + try await viewModel.encryptWithPassword(label: label, password: password) + showPasswordEncryptModal = false + pathManager.replaceLast(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .recipients)) + Toast.show(languageSettings.localized("Container successfully encrypted"), type: .success) + } catch { + Toast.show(languageSettings.localized("Encrypt general error")) + } + } + private func emptyStateView(_ text: String) -> some View { ContentUnavailableView { Text(verbatim: text) @@ -417,7 +518,7 @@ struct EncryptRecipientView: View { } #Preview { - EncryptRecipientView() + EncryptRecipientView(cdocOption: .cdoc1) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) .environment(NavigationPathManager()) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift index ef0748ae..80c2dfc6 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift @@ -30,42 +30,29 @@ struct RecipientDetailView: View { @Environment(LanguageSettings.self) private var languageSettings @Environment(\.openURL) var openURL - @State private var selectedTab: RecipientDetailViewTab = .recipientDetails - @State private var viewModel: SignatureDetailViewModel private let recipient: Addressee - private let nameUtil: NameUtilProtocol - var recipientDetailsTitle: String { - return languageSettings.localized("Recipient") + private var nameText: String { + if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { + return nameUtil.formatName( + surname: recipient.surname, + givenName: recipient.givenName, + identifier: recipient.identifier + ) + } else { + return nameUtil.formatCompanyName( + identifier: recipient.identifier, + serialNumber: recipient.serialNumber + ) + } } - var nameText: String { - return { - if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { - return nameUtil.formatName( - surname: recipient.surname, - givenName: recipient.givenName, - identifier: recipient.identifier - ) - } else { - return nameUtil.formatCompanyName( - identifier: recipient.identifier, - serialNumber: recipient.serialNumber - ) - } - }() - } - - var validToDate: String { + private var validToDate: String { guard let validToDate = recipient.validTo else { return "" } - - return DateUtil.getFormattedDateTime( - date: validToDate, - isUTC: false - ).date + return DateUtil.getFormattedDateTime(date: validToDate, isUTC: false).date } init( @@ -74,7 +61,6 @@ struct RecipientDetailView: View { ) { _viewModel = State(wrappedValue: Container.shared.signatureDetailViewModel()) self.recipient = recipient - self.nameUtil = nameUtil } @@ -92,73 +78,65 @@ struct RecipientDetailView: View { ) VStack(alignment: .leading) { - TabView( - selectedTab: $selectedTab, - titles: [recipientDetailsTitle], - content: { - VStack(alignment: .leading) { - if selectedTab == .recipientDetails { - let issuerName = viewModel.getIssuerName(cert: recipient.data) - if !issuerName.isEmpty { - SignerDetailView( - signatureDataItem: SignatureDataItem( - title: languageSettings.localized("Recipient certificate issuer"), - value: viewModel.getIssuerName(cert: recipient.data) - ) - ) - } - - if !nameText.isEmpty { - NavigationLink( - value: NavigationDestination - .certificateDetailView(certificate: recipient.data) - ) { - SignerDetailView( - signatureDataItem: SignatureDataItem( - title: languageSettings.localized("Recipient certificate"), - value: nameText, - extraIcon: "ic_m3_expand_content_48pt_wght400", - ) - ) - } - .buttonStyle(.plain) - } - if !recipient.concatKDFAlgorithmURI.isEmpty { - Button { - if let url = URL(string: recipient.concatKDFAlgorithmURI) { - openURL(url) - } - } label: { - SignerDetailView( - signatureDataItem: SignatureDataItem( - title: languageSettings.localized("ConcatKDF reference method"), - value: recipient.concatKDFAlgorithmURI, - extraIcon: "ic_m3_open_in_new_48pt_wght400", - ) - ) - } - .buttonStyle(.plain) - .accessibilityRemoveTraits([.isButton]) - .accessibilityAddTraits([.isLink]) - } - if !validToDate.isEmpty { - SignerDetailView( - signatureDataItem: SignatureDataItem( - title: languageSettings.localized( - "Recipient certificate expiry date" - ), - value: validToDate - ) - ) - } - } - } - }) + recipientDetails } + .padding(.top, Dimensions.Padding.LPadding) } .padding(Dimensions.Padding.SPadding) }) } + + @ViewBuilder + private var recipientDetails: some View { + if recipient.certType == .passwordType { + passwordRecipientDetails + } else { + certificateRecipientDetails + } + } + + @ViewBuilder + private var passwordRecipientDetails: some View { + detailRow("Recipient", value: recipient.lockLabel) + detailRow("Lock type", value: recipient.lockType) + } + + @ViewBuilder + private var certificateRecipientDetails: some View { + detailRow("Recipient certificate issuer", value: viewModel.getIssuerName(cert: recipient.data)) + if !nameText.isEmpty { + NavigationLink(value: NavigationDestination.certificateDetailView(certificate: recipient.data)) { + detailRow("Recipient certificate", value: nameText, extraIcon: "ic_m3_expand_content_48pt_wght400") + } + .buttonStyle(.plain) + } + if let uri = URL(string: recipient.concatKDFAlgorithmURI), !recipient.concatKDFAlgorithmURI.isEmpty { + Button { openURL(uri) } label: { + detailRow( + "ConcatKDF reference method", + value: recipient.concatKDFAlgorithmURI, + extraIcon: "ic_m3_open_in_new_48pt_wght400" + ) + } + .buttonStyle(.plain) + .accessibilityRemoveTraits([.isButton]) + .accessibilityAddTraits([.isLink]) + } + detailRow("Recipient certificate expiry date", value: validToDate) + } + + @ViewBuilder + private func detailRow(_ titleKey: String, value: String, extraIcon: String? = nil) -> some View { + if !value.isEmpty { + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized(titleKey), + value: value, + extraIcon: extraIcon + ) + ) + } + } } #Preview { @@ -172,10 +150,8 @@ struct RecipientDetailView: View { validTo: Date.distantFuture ) - RecipientDetailView( - recipient: recipient - ) - .environment(Container.shared.languageSettings()) - .environment(Container.shared.themeSettings()) - .environment(NavigationPathManager()) + RecipientDetailView(recipient: recipient) + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift index 24826a96..0cb05cc8 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientView.swift @@ -75,21 +75,23 @@ struct RecipientView: View { } } + private var isPasswordRecipient: Bool { + recipient.certType == .passwordType + } + var nameText: String { - return { - if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { - return nameUtil.formatName( - surname: recipient.surname, - givenName: recipient.givenName, - identifier: recipient.identifier - ) - } else { - return nameUtil.formatCompanyName( - identifier: recipient.identifier, - serialNumber: recipient.serialNumber - ) - } - }() + if isPasswordRecipient { return recipient.identifier } + if PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) { + return nameUtil.formatName( + surname: recipient.surname, + givenName: recipient.givenName, + identifier: recipient.identifier + ) + } + return nameUtil.formatCompanyName( + identifier: recipient.identifier, + serialNumber: recipient.serialNumber + ) } var validToDate: String { @@ -147,7 +149,7 @@ struct RecipientView: View { ) let certType = recipientUtil.getRecipientCertTypeText(certType: recipient.certType) - let validPart = validToDate.isEmpty + let validPart = (validToDate.isEmpty || isPasswordRecipient) ? "" : " " + languageSettings.localized("Valid to", [validToDate]) diff --git a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift index 5ec90543..515d224e 100644 --- a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift @@ -325,7 +325,9 @@ struct SignatureDetailView: View { ) } } + .padding(.top, Dimensions.Padding.SPadding) }) + .padding(.top, Dimensions.Padding.SPadding) } } .padding(Dimensions.Padding.SPadding) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 806f8a53..9c84800e 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -293,6 +293,7 @@ struct SigningView: View { .environment(languageSettings) } } + .padding(.top, Dimensions.Padding.LPadding) } else { VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { Text(verbatim: languageSettings.localized("Container files")) @@ -486,7 +487,10 @@ struct SigningView: View { .onChange(of: viewModel.navigateToNestedCryptoContainerView) { _, isNavigating in if isNavigating { viewModel.navigateToNestedCryptoContainerView.toggle() - pathManager.navigate(to: .encryptView(isWithEncryption: false)) + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + pathManager.navigate(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + } } } } @@ -569,8 +573,9 @@ struct SigningView: View { languageSettings.localized("Converted to crypto container"), type: .success ) + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) await MainActor.run { - pathManager.replaceLast(to: .encryptView(isWithEncryption: false)) + pathManager.replaceLast(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) } } } diff --git a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift index 25fa59f2..d6b4b1a8 100644 --- a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift +++ b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift @@ -36,6 +36,7 @@ struct UnsignedBottomBarView: View { let rightButtonLabel: String let rightButtonAccessibilityLabel: String let rightButtonAction: () -> Void + var showBackground: Bool = true var body: some View { HStack { @@ -88,7 +89,7 @@ struct UnsignedBottomBarView: View { } .padding(.vertical, Dimensions.Padding.SPadding) .padding(.horizontal, Dimensions.Padding.MPadding) - .background(theme.surfaceContainer) + .background(showBackground ? theme.surfaceContainer : Color.clear) } } diff --git a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift index aa456aa4..16770061 100644 --- a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift +++ b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift @@ -95,7 +95,9 @@ struct CryptoFileOpeningView: View { } } } else { - let localizedMessage = languageSettings.localized(errorMessage?.key ?? "General error", errorMessage?.args ?? []) + let localizedMessage = languageSettings.localized( + errorMessage?.key ?? "General error", errorMessage?.args ?? [] + ) Toast.show(localizedMessage) if voiceOverEnabled { diff --git a/RIADigiDoc/UI/Component/FileOpeningView.swift b/RIADigiDoc/UI/Component/FileOpeningView.swift index 115c5db1..28dcdcb1 100644 --- a/RIADigiDoc/UI/Component/FileOpeningView.swift +++ b/RIADigiDoc/UI/Component/FileOpeningView.swift @@ -136,7 +136,9 @@ struct FileOpeningView: View { } } } else { - let localizedMessage = languageSettings.localized(errorMessage?.key ?? "General error", errorMessage?.args ?? []) + let localizedMessage = languageSettings.localized( + errorMessage?.key ?? "General error", errorMessage?.args ?? [] + ) Toast.show(localizedMessage) if voiceOverEnabled { diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index 8c28a925..8a36ff6a 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -248,8 +248,11 @@ struct HomeView: View { }) .onChange(of: isNavigatingToEncryptView, { _, newValue in if newValue { - navigateWithVoiceOverFocusGuard(to: .encryptView(isWithEncryption: false)) - isNavigatingToEncryptView = false + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + navigateWithVoiceOverFocusGuard(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + isNavigatingToEncryptView = false + } } }) .onChange(of: isBottomSheetPresented) { oldValue, newValue in diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index a4a9aa82..7306bf23 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -130,6 +130,7 @@ struct MyEidView: View { documentExpirationStatus: viewModel .getDocumentExpirationStatus(expiryDate: idCardData.publicData.dateOfExpiry) ) + .padding(.top, Dimensions.Padding.SPadding) } else if selectedTab == .pinsAndCertificates { MyEidPinsAndCertificatesView( isPin1Blocked: $isPin1Blocked, @@ -141,8 +142,10 @@ struct MyEidView: View { signCertValidTo: idCardData.signCertNotValidDate ?? "", isPUKChangeable: idCardData.isPUKChangeable ) + .padding(.top, Dimensions.Padding.SPadding) } } + .padding(.top, Dimensions.Padding.SPadding) .accessibilityFocused($isTabFocused) .onAppear { DispatchQueue.main.async { diff --git a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift index dbb6fc58..24640949 100644 --- a/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift +++ b/RIADigiDoc/UI/Component/Recent documents/RecentDocumentsView.swift @@ -204,8 +204,11 @@ struct RecentDocumentsView: View { } .onChange(of: isNavigatingToEncryptView) { _, newValue in if newValue { - pathManager.navigate(to: .encryptView(isWithEncryption: false)) - isNavigatingToEncryptView = false + Task { @MainActor in + let cdocOption = await Container.shared.dataStore().getEncryptionCdocOption(false) + pathManager.navigate(to: .encryptView(isWithEncryption: false, cdocOption: cdocOption, selectedTab: .files)) + isNavigatingToEncryptView = false + } } } } diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index bd2cee94..95c46004 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -50,6 +50,7 @@ struct FloatingLabelTextField: View { let sortPriority: Double let spellOutCharacters: Bool let showBorder: Bool + let accessibilityHint: String let onDone: (() -> Void) init( @@ -69,6 +70,7 @@ struct FloatingLabelTextField: View { sortPriority: Double = 0, spellOutCharacters: Bool = false, showBorder: Bool = true, + accessibilityHint: String = "", onDone: @escaping (() -> Void) = {} ) { self.title = title @@ -87,6 +89,7 @@ struct FloatingLabelTextField: View { self.sortPriority = sortPriority self.spellOutCharacters = spellOutCharacters self.showBorder = showBorder + self.accessibilityHint = accessibilityHint self.onDone = onDone } @@ -174,6 +177,14 @@ struct FloatingLabelTextField: View { .joined(separator: " ") } + private var fieldAccessibilityValue: String { + if text.isEmpty { return "" } + if isSecure && !isPasswordVisible { + return String(repeating: "•", count: text.count) + } + return text + } + private var shouldShowToolbar: Bool { fieldIsFocused && (showDashButton || keyboardType.needsDoneButton) } @@ -359,7 +370,9 @@ struct FloatingLabelTextField: View { .onChange(of: errorText, { _, newValue in AccessibilityUtil.announceMessage(newValue) }) - .frame(height: Dimensions.Icon.IconSizeXXS) + .accessibilityHint(Text(verbatim: accessibilityHint)) + .accessibilityValue(Text(verbatim: fieldAccessibilityValue)) + .frame(minHeight: Dimensions.Icon.IconSizeXXS) } @ToolbarContentBuilder @@ -523,7 +536,7 @@ private extension View { .disabled(isDisabled) .keyboardType(keyboardType) .submitLabel(submitLabel) - .textContentType(.none) + .textContentType(.init(rawValue: "")) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .speechSpellsOutCharacters(spellOut) diff --git a/RIADigiDoc/UI/Component/Shared/TabView.swift b/RIADigiDoc/UI/Component/Shared/TabView.swift index 1b474713..326591b5 100644 --- a/RIADigiDoc/UI/Component/Shared/TabView.swift +++ b/RIADigiDoc/UI/Component/Shared/TabView.swift @@ -58,9 +58,9 @@ struct TabView: View where Tab.RawValue == .foregroundStyle(isSelected ? theme.primary : theme.onSurface) .multilineTextAlignment(.center) .lineLimit(nil) - .minimumScaleFactor(0.9) - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity) .padding(.horizontal, Dimensions.Padding.XSPadding) + .padding(.vertical, Dimensions.Padding.XSPadding) .accessibilityLabel(Text(verbatim: "\(title), " + "\(languageSettings.localized("Tab")) \(index + 1) / \(titles.count), " + @@ -72,22 +72,20 @@ struct TabView: View where Tab.RawValue == } .buttonStyle(.plain) .frame(maxWidth: .infinity, minHeight: tabMinHeight) - .overlay(alignment: .bottom) { + .accessibilityLabel(titles[index].lowercased()) + } + } + .fixedSize(horizontal: false, vertical: true) + .overlay(alignment: .bottom) { + HStack(spacing: Dimensions.Padding.ZeroPadding) { + ForEach(titles.indices, id: \.self) { index in Rectangle() - .fill(theme.outlineVariant) + .fill(selectedIndex.wrappedValue == index ? theme.primary : theme.outlineVariant) .frame(height: Dimensions.Height.SBorder) } - .overlay(alignment: .bottom) { - if isSelected { - Rectangle() - .fill(theme.primary) - .frame(height: Dimensions.Height.SBorder) - } - } - .accessibilityLabel(titles[index].lowercased()) } } - .padding(.top, Dimensions.Padding.LPadding) + content() } } diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift index 1b5b5c42..f6a3a0a0 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift @@ -55,6 +55,7 @@ struct SigningServicesSettingsView: View { } } ) + .padding(.top, Dimensions.Padding.MPadding) } } } diff --git a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift index 6b25bc1e..a4f2cd38 100644 --- a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift +++ b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift @@ -40,8 +40,8 @@ struct NavigationDestinations: ViewModifier { case .recentDocumentsView(let folderURL, let extensions): RecentDocumentsView(folderURL: folderURL, extensions: extensions) - case .encryptRecipientView: - EncryptRecipientView() + case .encryptRecipientView(let cdocOption): + EncryptRecipientView(cdocOption: cdocOption) case .signingView: SigningView() @@ -72,10 +72,14 @@ struct NavigationDestinations: ViewModifier { ActionMethodSelectionView(actionType: actionType, methods: methods) case .encryptView( - let isWithEncryption + let isWithEncryption, + let cdocOption, + let selectedTab ): EncryptView( isWithEncryption: isWithEncryption, + cdocOption: cdocOption, + selectedTab: selectedTab ) case .recipientDetailView( let recipient, diff --git a/RIADigiDoc/Util/Recipient/RecipientUtil.swift b/RIADigiDoc/Util/Recipient/RecipientUtil.swift index c6aed6a6..e53acf84 100644 --- a/RIADigiDoc/Util/Recipient/RecipientUtil.swift +++ b/RIADigiDoc/Util/Recipient/RecipientUtil.swift @@ -37,6 +37,8 @@ struct RecipientUtil: RecipientUtilProtocol { return "Smart-ID" case .eSealType: return "Certificate for Encryption" + case .passwordType: + return "Password" } } } diff --git a/RIADigiDoc/ViewModel/EncryptRecipientViewModel.swift b/RIADigiDoc/ViewModel/EncryptRecipientViewModel.swift index d28cfb6f..7b6c1628 100644 --- a/RIADigiDoc/ViewModel/EncryptRecipientViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptRecipientViewModel.swift @@ -36,6 +36,7 @@ class EncryptRecipientViewModel: EncryptRecipientViewModelProtocol, Loggable { private let sharedContainerViewModel: SharedContainerViewModelProtocol private let openLdap: OpenLdapProtocol + var encryptWithPasswordAction: (URL, [URL], String, String) async throws -> any CryptoContainerProtocol = CryptoContainer.encryptWithPassword init( sharedContainerViewModel: SharedContainerViewModelProtocol, @@ -129,6 +130,22 @@ class EncryptRecipientViewModel: EncryptRecipientViewModelProtocol, Loggable { } } + func encryptWithPassword(label: String, password: String) async throws { + let cryptoContainer = sharedContainerViewModel.currentContainer() as? any CryptoContainerProtocol + guard let cryptoContainer else { + EncryptRecipientViewModel.logger().error("Cannot encrypt: crypto container is nil") + throw CryptoError.containerCreationFailed(CryptoErrorDetail(message: "Container is nil")) + } + guard let containerFile = await cryptoContainer.getRawContainerFile() else { + EncryptRecipientViewModel.logger().error("Cannot encrypt: container file URL is nil") + throw CryptoError.containerCreationFailed(CryptoErrorDetail(message: "Container file URL is nil")) + } + let dataFiles = await cryptoContainer.getDataFiles() + let encryptedContainer = try await encryptWithPasswordAction(containerFile, dataFiles, label, password) + sharedContainerViewModel.clearContainers() + sharedContainerViewModel.setCryptoContainer(encryptedContainer) + } + func resetErrorMessage() { errorMessage = nil } diff --git a/RIADigiDoc/ViewModel/Protocols/EncryptRecipientViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/EncryptRecipientViewModelProtocol.swift index 93271bb9..caf67374 100644 --- a/RIADigiDoc/ViewModel/Protocols/EncryptRecipientViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/EncryptRecipientViewModelProtocol.swift @@ -30,6 +30,7 @@ public protocol EncryptRecipientViewModelProtocol: Sendable { func loadRecipients() async func getContainerRecipientList() async -> [Addressee] func deleteRecipient(_ recipient: Addressee) async + func encryptWithPassword(label: String, password: String) async throws func resetErrorMessage() func resetSuccessMessage() } diff --git a/RIADigiDocTests/ViewModel/EncryptRecipientViewModelTests.swift b/RIADigiDocTests/ViewModel/EncryptRecipientViewModelTests.swift new file mode 100644 index 00000000..8843c09e --- /dev/null +++ b/RIADigiDocTests/ViewModel/EncryptRecipientViewModelTests.swift @@ -0,0 +1,81 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import CommonsLib +import CryptoObjCWrapper +import CryptoSwift +import Foundation +import Testing + + +@MainActor +struct EncryptRecipientViewModelTests { + + private let mockSharedContainerViewModel: SharedContainerViewModelProtocolMock + private let mockOpenLdap: OpenLdapProtocolMock + private let viewModel: EncryptRecipientViewModel + + init() { + mockSharedContainerViewModel = SharedContainerViewModelProtocolMock() + mockOpenLdap = OpenLdapProtocolMock() + viewModel = EncryptRecipientViewModel( + sharedContainerViewModel: mockSharedContainerViewModel, + openLdap: mockOpenLdap + ) + } + + @Test + func encryptWithPassword_successClearsAndSetsNewContainer() async throws { + let mockContainer = CryptoContainerProtocolMock() + let containerFile = URL(fileURLWithPath: "/tmp/test.cdoc") + let dataFile = URL(fileURLWithPath: "/tmp/doc.pdf") + mockSharedContainerViewModel.currentContainerHandler = { mockContainer } + mockContainer.getRawContainerFileHandler = { containerFile } + mockContainer.getDataFilesHandler = { [dataFile] } + + let resultContainer = CryptoContainerProtocolMock() + viewModel.encryptWithPasswordAction = { _, _, _, _ in resultContainer } + + try await viewModel.encryptWithPassword(label: "testKey", password: "Abcdefgh1234567890123") + + #expect(mockSharedContainerViewModel.clearContainersCallCount == 1) + #expect(mockSharedContainerViewModel.setCryptoContainerCallCount == 1) + } + + @Test + func encryptWithPassword_throwsWhenContainerIsNil() async { + mockSharedContainerViewModel.currentContainerHandler = { nil } + + await #expect(throws: (any Error).self) { + try await viewModel.encryptWithPassword(label: "testKey", password: "Abcdefgh1234567890123") + } + } + + @Test + func encryptWithPassword_throwsWhenContainerFileIsNil() async { + let mockContainer = CryptoContainerProtocolMock() + mockSharedContainerViewModel.currentContainerHandler = { mockContainer } + mockContainer.getRawContainerFileHandler = { nil } + + await #expect(throws: (any Error).self) { + try await viewModel.encryptWithPassword(label: "testKey", password: "Abcdefgh1234567890123") + } + } + +} diff --git a/scripts/GenerateMocks.sh b/scripts/GenerateMocks.sh index e4edf449..891d3b3a 100755 --- a/scripts/GenerateMocks.sh +++ b/scripts/GenerateMocks.sh @@ -112,6 +112,11 @@ main_output_file="${main_output_dir}/${main_module_name}+Mocks.swift" mkdir -p "$main_output_dir" echo "\n\nGenerating mocks for $main_module_name...\n" -mockolo -s "$main_src_dir" -d "$main_output_file" --enable-args-history +mockolo \ + -s "$main_src_dir" \ + -s "Modules/CryptoLib/Sources/CryptoSwift" \ + -d "$main_output_file" \ + --custom-imports "CryptoSwift" "CryptoObjCWrapper" \ + --enable-args-history echo "\n\nDone\n\n"