diff --git a/.gitignore b/.gitignore index 02bce2e92c459988286bf0fede31144a407d64a2..683bcea58a3a309d3b90ee42ed144f6b3af3fd54 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,3 @@ Gopkg.lock temp/ yarn-error.log /.project - -/cpp/cmake-build* -/cpp/cmake-install* -/cpp/compile_commands.json -/cpp/.clangd -/cpp/tags - -/cpp/docs/doxy diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78e537aecaf90b57fd0de6eca8bf310b11e92cb6..c6336e75c17af7e4a7bd88867a7218453c82379b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,12 +1,10 @@ -image: registry.vereign.com/docker/docker:master -services: - - registry.vereign.com/docker/docker:dind-master - variables: DOCKER_CLI_EXPERIMENTAL: 'enabled' JOB_IMAGE: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/$CI_JOB_NAME:$CI_COMMIT_REF_NAME MANIFEST_IMAGE: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME - CI_DEBUG_TRACE: "true" + IMAGE_amd64: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/amd64:$CI_COMMIT_REF_NAME + IMAGE_ppc64le: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/ppc64le:$CI_COMMIT_REF_NAME + CI_DEBUG_TRACE: "false" stages: - build @@ -18,7 +16,7 @@ before_script: .build: script: - - docker build --build-arg CI_PROJECT_NAME=$CI_PROJECT_NAME --build-arg GITLAB_LOGIN=gitlab-ci-token --build-arg GITLAB_PASSWORD=$CI_JOB_TOKEN --pull -t $JOB_IMAGE -f Dockerfile . + - docker build --build-arg GITLAB_LOGIN=gitlab-ci-token --build-arg GITLAB_PASSWORD=$CI_JOB_TOKEN --pull -t $JOB_IMAGE -f Dockerfile . - docker push $JOB_IMAGE ppc64le: @@ -37,9 +35,6 @@ amd64: manifest: stage: manifest - variables: - IMAGE_amd64: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/amd64:$CI_COMMIT_REF_NAME - IMAGE_ppc64le: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/ppc64le:$CI_COMMIT_REF_NAME tags: - ppc64le-docker script: diff --git a/Dockerfile b/Dockerfile index a072a900e07bee18ab81b1e4794745777199af8c..d088fec85bfda919e93a4b1811f27ac3c38fb0f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.vereign.com/docker/vcl-build-base:buster as builder +FROM registry.vereign.com/docker/vcl-build-base:golang1.14.1 as builder ARG GITLAB_LOGIN ARG GITLAB_PASSWORD @@ -11,5 +11,7 @@ RUN git config --global url."https://$GITLAB_LOGIN:$GITLAB_PASSWORD@code.vereign FROM registry.vereign.com/docker/go-runtime:master COPY --from=builder /go/src/code.vereign.com/code/vcl/javascript/dist /srv/dist +COPY --from=builder /go/src/code.vereign.com/code/vcl/Gopkg.lock /srv/dist/ + ENTRYPOINT ["/bin/cp","-a","/srv/dist/.","/srv/target"] diff --git a/cpp/.clang-format b/cpp/.clang-format new file mode 100644 index 0000000000000000000000000000000000000000..4314079c156529a96bc58ba0c63a1f2b890a1ddd --- /dev/null +++ b/cpp/.clang-format @@ -0,0 +1,33 @@ +--- +Language: Cpp +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: true +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +FixNamespaceComments: true +IndentWidth: 2 +ObjCBlockIndentWidth: 2 +PointerAlignment: Left +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 2 +UseTab: Never \ No newline at end of file diff --git a/cpp/.gitignore b/cpp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..48d08e65e0c7b04f833e2cb69913b72a86acc93c --- /dev/null +++ b/cpp/.gitignore @@ -0,0 +1,6 @@ +/cmake-* +/compile_commands.json +/.clangd +/tags + +/docs/doxy diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index abf9b756454dc84725418d3d5426339b5354583f..35a77a1a47dd339cb229a2fc1ab63a90f3605997 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -9,6 +9,10 @@ option(VEREIGN_USE_PRECOMPILED_HEADERS "Use precompiled headers" OFF) option(VEREIGN_USE_TIME_TRACE "Use compilation profiler" OFF) option(VEREIGN_ENABLE_BENCHMARKING "Enable tests benchmarks" OFF) +if (UNIX AND NOT APPLE) + set(LINUX TRUE) +endif() + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "19.0.24215.1") message(FATAL_ERROR "Microsoft Visual C++ version MSVC 19.0.24215.1 required") @@ -91,6 +95,7 @@ set(_cmake_prefix_paths ${VENDOR_INSTALL_DIR} ${VENDOR_INSTALL_DIR}/grpc ${VENDOR_INSTALL_DIR}/nlohmann + ${VENDOR_INSTALL_DIR}/sqlite3 ) set(CMAKE_PREFIX_PATH ${_cmake_prefix_paths} CACHE STRING "") @@ -115,7 +120,7 @@ find_package( 1.72.0 EXACT REQUIRED - COMPONENTS regex thread system date_time + COMPONENTS regex thread system date_time filesystem ) find_package(Protobuf CONFIG REQUIRED) @@ -137,6 +142,7 @@ else() endif() find_package(nlohmann_json 3.7.3 REQUIRED) +find_package(SQLite3 REQUIRED) add_subdirectory("src") add_subdirectory("tests") @@ -176,6 +182,7 @@ message(STATUS "summary of build options: Boost libs ${Boost_LIBRARIES} gRPC ${gRPC_FOUND} [${gRPC_VERSION}] (LIBS='${_grpc_libs}') nlohmann ${nlohmann_json_FOUND} [${nlohmann_json_VERSION}] (HEADERS='${nlohmann_json_DIR}') + sqlite3 ${SQLite3_FOUND} [${SQLite3_VERSION}] (LIBS='${SQLite3_LIBRARIES}') Options: VEREIGN_USE_LLD ${VEREIGN_USE_LLD} VEREIGN_USE_PRECOMPILED_HEADERS ${VEREIGN_USE_PRECOMPILED_HEADERS} diff --git a/cpp/include/vereign/vereign.h b/cpp/include/vereign/vereign.h index c450bd819efba614e91f8c0cac23dfbe4c5503a3..f77824c16926b03dacd6b6cb9366d53a538474c6 100644 --- a/cpp/include/vereign/vereign.h +++ b/cpp/include/vereign/vereign.h @@ -1,5 +1,5 @@ -#ifndef VEREIGN_VEREIGN_H_ -#define VEREIGN_VEREIGN_H_ +#ifndef __VEREIGN_VEREIGN_H +#define __VEREIGN_VEREIGN_H #ifdef _WIN32 #ifdef WIN_EXPORT @@ -84,8 +84,12 @@ typedef struct vereign_service vereign_service; * **NOTE: On failure the `err` object must be freed with vereign_error_free method.** * * @param listen_address gRPC listen address, for example "localhost:". - * @param vereignHost Vereign restapi host. - * @param vereignPort Vereign restapi port - https, 443... + * @param vereign_host Vereign restapi host. + * @param vereign_port Vereign restapi port - https, 443... + * @param storage_path Full path to directory where the storage files will stay. + * If the `storage_path` is `nullptr`, a default will be used. Under linux this default is + * `$HOME/vereign`, and under windows it is `C:\Users\<user>\AppData\Local\vereign`. + * * @param err On failure err is initialized with the reason of the failure, * otherwise err is set to nullptr. * @returns vereign_service object if the gRPC is up and running, otherwise returns nullptr. @@ -94,8 +98,7 @@ PUBLIC_API vereign_service* vereign_service_start( const char* listen_address, const char* vereign_host, const char* vereign_port, - // FIXME: public_key must come from a storage internally. - const char* public_key, + const char* storage_path, vereign_error** err ); @@ -122,4 +125,4 @@ PUBLIC_API void vereign_service_shutdown(vereign_service* service); }; #endif -#endif // VEREIGN_VEREIGN_H_ +#endif // __VEREIGN_VEREIGN_H diff --git a/cpp/proto b/cpp/proto index e861100984116aacf6d84cb8a09dc0ef81041509..cfea384107dec09a91ae566481432bfd1b6702dc 160000 --- a/cpp/proto +++ b/cpp/proto @@ -1 +1 @@ -Subproject commit e861100984116aacf6d84cb8a09dc0ef81041509 +Subproject commit cfea384107dec09a91ae566481432bfd1b6702dc diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 555515506f7c1d62b01e54cf2eeaeeb5401e783b..205e69aee397da56add3ed4d5514932d4461c041 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -3,9 +3,11 @@ if (fmt_FOUND) endif() if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - add_definitions(-DNOGDI) + add_definitions(-DNOGDI -DNOMINMAX) endif() +add_definitions(-DBOOST_FILESYSTEM_NO_DEPRECATED) + include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src @@ -16,6 +18,8 @@ include_directories( ) file(GLOB PROTO_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/*.cc) +file(GLOB PROTO_INTERNAL_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/internal/*.cc) +list(APPEND PROTO_SRC ${PROTO_INTERNAL_SRC}) list(APPEND PROTO_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/annotations.pb.cc ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/http.pb.cc @@ -45,15 +49,63 @@ if (VEREIGN_USE_PRECOMPILED_HEADERS) endif() set(VEREIGNLIB_SRC + vereign/core/rand.cc + vereign/core/string.cc + vereign/core/time.cc + vereign/fs/util.cc + vereign/fs/operations.cc + vereign/fs/path.cc + vereign/restapi/detail/http_reader.cc vereign/restapi/client.cc - vereign/service/passport_service.cc + vereign/grpc/gen/gen.cc vereign/grpc/json/encoder.cc vereign/grpc/service_registry.cc vereign/grpc/server.cc + + vereign/sqlite/statement.cc + vereign/sqlite/connection.cc + + vereign/bytes/view_dump.cc + vereign/bytes/buffer.cc + + vereign/encoding/binary.cc + vereign/encoding/base64.cc + vereign/encoding/hex.cc + + vereign/crypto/rand.cc + vereign/crypto/aes.cc + vereign/crypto/rsa.cc + vereign/crypto/bio.cc + vereign/crypto/digest.cc + vereign/crypto/cert.cc + + vereign/kvstore/lock.cc + vereign/kvstore/detail/base_crypto_storage.cc + vereign/kvstore/sqlite_storage.cc + vereign/kvstore/crypto_storage.cc + vereign/kvstore/detail/value_encoder.cc + + vereign/event/broker.cc + vereign/identity/provider.cc + + vereign/service/identity_service.cc ) +if (LINUX) + list(APPEND VEREIGNLIB_SRC + vereign/kvstore/detail/linux_crypto_storage.cc + ) +elseif (WIN32) + list(APPEND VEREIGNLIB_SRC + vereign/ncrypt/errors.cc + vereign/ncrypt/unique_ptr.cc + vereign/ncrypt/rsa.cc + vereign/kvstore/detail/win_crypto_storage.cc + ) +endif() + file(GLOB GENERATED_SERVICES_SRC vereign/service/gen/*.cc) list(APPEND VEREIGNLIB_SRC ${GENERATED_SERVICES_SRC}) @@ -67,7 +119,11 @@ target_link_libraries(vereignlib PUBLIC fmt::fmt gRPC::grpc++_reflection gRPC::grpc++ - $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time> + Boost::filesystem + Boost::date_time + SQLite::SQLite3 + $<$<CXX_COMPILER_ID:MSVC>:ncrypt.lib> + $<$<CXX_COMPILER_ID:MSVC>:cryptui.lib> ) add_library(vereign SHARED @@ -93,9 +149,14 @@ set(csandbox_sources add_executable(csandbox ${csandbox_sources}) -target_link_libraries(csandbox - PRIVATE vereignlib - $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time> +target_link_libraries(csandbox PRIVATE + vereignlib + # OpenSSL::Crypto + # OpenSSL::SSL + profiler + # $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time> + # Boost::filesystem + # Boost::file # Boost::thread # vereign # fmt::fmt diff --git a/cpp/src/csandbox.cc b/cpp/src/csandbox.cc index 561cf2123f21bd6b5ad1a9b6858d2fb5b2e83a7d..5cbbd22b6df93c5e2eab5ddd8e5d89a49e78efc4 100644 --- a/cpp/src/csandbox.cc +++ b/cpp/src/csandbox.cc @@ -1,4 +1,57 @@ +#include "vereign/client_library/event_types.pb.h" +#include "vereign/client_library/internal/event_types.pb.h" +#include "vereign/sync/channel.hh" +#include "vereign/container/bounded_queue.hh" +#include <thread> +// #include "vereign/event/message_pool.hh" +// #include <future> +// #include <thread> +// #include <vereign/crypto/cert.hh> -auto main(int argc, char* argv[]) -> int { - return 0; +// #include <vereign/crypto/rsa.hh> +// #include <vereign/crypto/rand.hh> +// #include <vereign/crypto/bio.hh> +// #include <vereign/bytes/view_dump.hh> +// #include <vereign/fs/util.hh> +// #include <vereign/core/time.hh> +// #include <boost/date_time.hpp> +// #include <boost/pool/object_pool.hpp> + +// #include <openssl/x509v3.h> + +// #include <chrono> +// #include <iostream> +// #include <fstream> + +auto main(int argc, char** argv) -> int { + argc = 0; + argv = nullptr; + + int64_t iterations = int64_t(1000)*1000*1000*10; + + using clock = std::chrono::high_resolution_clock; + auto q = vereign::container::BoundedQueue<int>(10); + + auto start = clock::now(); + int64_t s = 0; + + for (int64_t i = 0; i < iterations; i++) { + if (q.IsFull()) { + q.PopFront(); + } + + q.PushBack(i); + } + + auto end = clock::now() - start; + + std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end).count() << std::endl; + std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(end).count()/iterations << std::endl; + + while (!q.IsEmpty()) { + s += q.Front(); + q.PopFront(); + } + + std::cout << s << std::endl; } diff --git a/cpp/src/vereign/bytes/buffer.cc b/cpp/src/vereign/bytes/buffer.cc new file mode 100644 index 0000000000000000000000000000000000000000..546a4228f372ea30a4dc914b5c66f054ce1c1527 --- /dev/null +++ b/cpp/src/vereign/bytes/buffer.cc @@ -0,0 +1,180 @@ +#include <vereign/bytes/buffer.hh> + +#include <vereign/bytes/errors.hh> + +#include <memory> + +namespace vereign::bytes { + +Buffer::Buffer() noexcept + : cap_{0}, + size_{0}, + data_{nullptr} +{ +} + +Buffer::Buffer(std::size_t cap) + : cap_{cap}, + size_{0}, + data_{nullptr} +{ + if (cap == 0) { + return; + } + + data_ = reinterpret_cast<uint8_t*>(std::malloc(cap)); + if (data_ == nullptr) { + throw std::bad_alloc{}; + } +} + +Buffer::Buffer(bytes::View src) + : cap_{src.Size()}, + size_{src.Size()}, + data_{nullptr} +{ + if (size_ == 0) { + return; + } + + data_ = reinterpret_cast<uint8_t*>(std::malloc(cap_)); + if (data_ == nullptr) { + throw std::bad_alloc{}; + } + + std::memcpy(data_, src.Data(), src.Size()); +} + +Buffer::Buffer(Buffer&& other) noexcept + : cap_{other.cap_}, + size_{other.size_}, + data_{other.data_} +{ + other.cap_ = 0; + other.size_ = 0; + other.data_ = nullptr; +} + +auto Buffer::operator=(Buffer&& other) noexcept -> Buffer& { + std::swap(cap_, other.cap_); + std::swap(size_, other.size_); + std::swap(data_, other.data_); + + return *this; +} + + +Buffer::~Buffer() { + std::free(data_); +} + + +auto Buffer::Size() const noexcept -> std::size_t { + return size_; +} + +auto Buffer::FreeCap() const noexcept -> std::size_t { + return cap_ - size_; +} + +auto Buffer::Cap() const noexcept -> std::size_t { + return cap_; +} + +auto Buffer::begin() noexcept -> uint8_t* { + return data_; +} + +auto Buffer::begin() const noexcept -> const uint8_t* { + return data_; +} + +auto Buffer::end() noexcept -> uint8_t* { + return data_ + size_; +} + +auto Buffer::end() const noexcept -> const uint8_t* { + return data_ + size_; +} + +void Buffer::Reserve(std::size_t size) { + if (cap_ == 0) { + cap_ = size; + data_ = reinterpret_cast<uint8_t*>(std::malloc(cap_)); + if (data_ == nullptr) { + throw std::bad_alloc{}; + } + + return; + } + + if (size <= cap_ - size_) { + return; + } + + auto cap = cap_ * 2; + if (size > cap - size_) { + cap = size_ + size; + } + + auto newData = reinterpret_cast<uint8_t*>(std::realloc(data_, cap)); + if (newData == nullptr) { + throw std::bad_alloc{}; + } + + data_ = newData; + cap_ = cap; +} + +void Buffer::Reset() { + size_ = 0; +} + +void Buffer::IncSize(std::size_t val) { + if (size_ + val > cap_) { + throw IncrementOutOfBounds{}; + } + + size_ += val; +} + +auto Buffer::WriteWithinCap(bytes::View src) noexcept -> std::size_t { + auto size = std::min(cap_ - size_, src.Size()); + + std::memcpy(data_ + size_, src.Data(), size); + size_ += size; + + return size; +} + +auto Buffer::Write(bytes::View src) -> std::size_t { + Reserve(src.Size()); + + return WriteWithinCap(src); +} + +auto Buffer::View() const noexcept -> bytes::View { + return bytes::View{data_, size_}; +} + +auto Buffer::View(std::size_t start) const noexcept -> bytes::View { + return bytes::View{data_, size_}.Slice(start); +} + +auto Buffer::operator[](std::size_t index) -> uint8_t& { + if (index >= cap_) { + throw IndexOutOfBounds{}; + } + + return data_[index]; +} + +auto Buffer::operator[](std::size_t index) const -> const uint8_t& { + if (index >= cap_) { + throw IndexOutOfBounds{}; + } + + return data_[index]; +} + +} // namespace vereign::bytes diff --git a/cpp/src/vereign/bytes/buffer.hh b/cpp/src/vereign/bytes/buffer.hh new file mode 100644 index 0000000000000000000000000000000000000000..6d0e5c7a8bddee458283afc810b3b79765e76968 --- /dev/null +++ b/cpp/src/vereign/bytes/buffer.hh @@ -0,0 +1,275 @@ +#ifndef __VEREIGN_BYTES_BUFFER_HH +#define __VEREIGN_BYTES_BUFFER_HH + +#include <vereign/bytes/view.hh> +#include <cstring> + +namespace vereign::bytes { + +/** + * Dynamically expandable memory buffer. + * + * The buffer is a 3-tuple - pointer, size and capacity. + * Typically used in functions for output parameters and return values. + * Provides API that is easy to use with C APIs. + * + * The buffer is move only. + */ +class Buffer { +public: + /** + * Creates empty buffer. + */ + Buffer() noexcept; + + /** + * Creates a buffer with reserved memory capacity. + * + * The size of the buffer is zero. + * + * @param cap The capacity of the buffer. + * + * @throws std::bad_alloc when memory reservation fails. + */ + Buffer(std::size_t cap); + + /** + * Creates a buffer by copying from source bytes view. + * + * @param src The source that will be copied from. + * + * @throws std::bad_alloc when memory reservation fails. + */ + Buffer(View src); + + /** + * The buffer is movable. + */ + Buffer(Buffer&& other) noexcept; + auto operator=(Buffer&& other) noexcept -> Buffer&; + + // disable copying + Buffer(const Buffer&) = delete; + auto operator=(const Buffer&) -> Buffer& = delete; + + /** + * Frees the buffer memory. + */ + ~Buffer(); + + /** + * Returns a pointer to the first byte of the buffer. + * + * Buffer::begin(), Buffer::end() pair is useful for range loops. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("foo")}; + * for (const auto& byte : buf) { + * byte = 'x'; + * } + * + * assert(buf.View().String() == "xxx"); + * @endcode + */ + auto begin() noexcept -> uint8_t*; + + /** + * Returns a pointer to the first byte of the buffer. + * + * Buffer::begin(), Buffer::end() pair is useful for range loops. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("foo bar")}; + * std::string s; + * + * for (const auto& byte : buf) { + * s += byte; + * } + * + * assert(s == "foo bar"); + * @endcode + */ + auto begin() const noexcept -> const uint8_t*; + + /** + * Returns a pointer to the byte following the last byte in the buffer. + * + * Note that this is the last byte in the range [0, size). + * It is often used when calling C APIs. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("foo bar")}; + * + * buf.Reserve(4); + * std::strncpy((char*)buf.end(), " baz", 4); + * buf.IncSize(4); + * + * assert(buf.View().String() == "foo bar baz"); + * @endcode + */ + auto end() noexcept -> uint8_t*; + + /** + * Returns a read only pointer to the byte following the last byte in the buffer. + * + * Note that this is the last byte in the range [0, size). + */ + auto end() const noexcept -> const uint8_t*; + + /** + * Access a byte in the range of [0, cap). + * + * @param index The index of the byte to access. + * + * @throws std::runtime_error when the passed index is out of bounds. + */ + auto operator[](std::size_t index) -> uint8_t&; + + /** + * Read only access a byte in the range of [0, cap). + * + * @param index The index of the byte to access. + * + * @throws std::runtime_error when the passed index is out of bounds. + */ + auto operator[](std::size_t index) const -> const uint8_t&; + + /** + * Retrieve buffer size. + * + * @returns the buffer size. + */ + auto Size() const noexcept -> std::size_t; + + /** + * Retrieve buffer capacity. + * + * @returns the buffer capacity. + */ + auto Cap() const noexcept -> std::size_t; + + /** + * Reserve memory, so that there is at least `size` free capacity. + * + * If there is already enough free capacity, no memory allocation is done. + * The allocated memory may be bigger than what is needed. + * + * If the call is successful then it is guaranteed that `this->FreeCap() >= size`. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("foo bar")}; + * + * buf.Reserve(4); // ensure there will be a free capacity for 4 bytes + * std::strncpy((char*)buf.end(), " baz", 4); // copy the bytes + * buf.IncSize(4); // update the buffer size with the newly written bytes + * + * assert(buf.View().String() == "foo bar baz"); + * @endcode + * + * @param size The desired free capacity. + * + * @throws std::bad_alloc when memory allocation fails. + */ + void Reserve(std::size_t size); + + /** + * Sets the buffer size to zero. + * + * This does not free any memory, so the capacity stays the intact, and the buffer can be reused. + */ + void Reset(); + + /** + * Increments the size of the buffer. + * + * It is typically used after some function has written bytes to the end of the buffer. + * + * @param val The value that will be added to the current size. + */ + void IncSize(std::size_t val); + + /** + * Retrieve the buffer free capacity. + * + * This is equal to `this->Cap() - this->Size()`. + */ + auto FreeCap() const noexcept -> std::size_t; + + /** + * Adds bytes up to the currently available buffer capacity. + * + * After the operation succeeds, the buffer size will be incremented with the number of bytes + * that have been copied. + * + * Example: + * @code + * auto buf = bytes::Buffer{3}; + * buf.WriteWithinCap(bytes::View("foo bar")); + * + * // only 3 bytes are written + * assert(buf.View.String() == "foo"); + * @endcode + * + * @param src The source that will be appended to the buffer. + * @returns The amount of bytes that were actually copied into the buffer. + */ + auto WriteWithinCap(bytes::View src) noexcept -> std::size_t; + + /** + * Adds a source view of bytes to the buffer. + * + * If the buffer does not have enough capacity, it will be expanded. + * + * Example: + * @code + * auto buf = bytes::Buffer{3}; + * buf.WriteWithinCap(bytes::View("foo bar")); + * + * // all bytes are written + * assert(buf.View.String() == "foo bar"); + * @endcode + * + * @param The source that will be appended to the buffer. + * @returns The amount of bytes that were copied into the buffer. That is equal to src.Size(). + */ + auto Write(bytes::View src) -> std::size_t; + + /** + * Retrieve a read only view of the buffer. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("123")}; + * assert(buf.View().String() == "123"); + * @endcode + * + * @returns a read only view range [0, this->Size()). + */ + auto View() const noexcept -> bytes::View; + + /** + * Retrieve a read only view of the buffer staring from a given offset. + * + * Example: + * @code + * auto buf = bytes::Buffer{bytes::View("123")}; + * assert(buf.View(1).String() == "23"); + * @endcode + * + * @returns a read only view range [start, this->Size()). + */ + auto View(std::size_t start) const noexcept -> bytes::View; + +private: + std::size_t cap_; + std::size_t size_; + uint8_t* data_; +}; + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_BUFFER_HH diff --git a/cpp/src/vereign/bytes/bytes.hh b/cpp/src/vereign/bytes/bytes.hh new file mode 100644 index 0000000000000000000000000000000000000000..437a692f7d9b5cd753836a6bfc9d1c591fa5844a --- /dev/null +++ b/cpp/src/vereign/bytes/bytes.hh @@ -0,0 +1,45 @@ +#ifndef __VEREIGN_BYTES_BYTES_HH +#define __VEREIGN_BYTES_BYTES_HH + +#include <vereign/bytes/view.hh> +#include <cstring> + +namespace vereign::bytes { + +class Bytes { +public: + Bytes(uint8_t* data, std::size_t size) + : size_{size}, + data_{data} + {} + + Bytes(const Bytes&) = default; + auto operator=(const Bytes&) -> Bytes& = default; + + auto Data() const -> uint8_t* { + return data_; + } + + auto CharData() const -> unsigned char* { + return data_; + } + + auto Size() const -> std::size_t { + return size_; + } + +private: + std::size_t size_; + uint8_t* data_; +}; + +inline auto Copy(Bytes dst, View src) noexcept -> std::size_t { + auto size = std::min(src.Size(), dst.Size()); + std::memcpy(dst.Data(), src.Data(), size); + + return size; +} + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_BYTES_HH diff --git a/cpp/src/vereign/bytes/errors.hh b/cpp/src/vereign/bytes/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..ac7e9e66a800a30f340e6b3f0de9c9cbf959cabd --- /dev/null +++ b/cpp/src/vereign/bytes/errors.hh @@ -0,0 +1,35 @@ +#ifndef __VEREIGN_BYTES_ERRORS_HH +#define __VEREIGN_BYTES_ERRORS_HH + +#include <stdexcept> + +namespace vereign::bytes { + +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error{what} + { + + } +}; + +class IndexOutOfBounds : public Error { +public: + IndexOutOfBounds() + : Error{"index out of bounds"} + { + } +}; + +class IncrementOutOfBounds : public Error { +public: + IncrementOutOfBounds() + : Error{"cannot increment size pass the capacity"} + { + } +}; + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_ERRORS_HH diff --git a/cpp/src/vereign/bytes/view.hh b/cpp/src/vereign/bytes/view.hh new file mode 100644 index 0000000000000000000000000000000000000000..4520934c927170ed79682ab95d9f9ed5e430dee8 --- /dev/null +++ b/cpp/src/vereign/bytes/view.hh @@ -0,0 +1,211 @@ +#ifndef __VEREIGN_BYTES_VIEW_HH +#define __VEREIGN_BYTES_VIEW_HH + +#include <vereign/bytes/errors.hh> + +#include <cstring> +#include <string> +#include <string_view> +#include <stdexcept> +#include <algorithm> + +namespace vereign::bytes { + +/** + * Bytes view represents a read only access to a range of bytes. + * + * The View is a 2-tuple with pointer and size. + * Typically used in functions for input parameters. + * + * **NOTE: The View does not own the memory that it references.** + */ +class View { +public: + /** + * Creates empty view. + */ + View() = default; + + /** + * Creates a view from raw pointer and a size. + * + * @param data Pointer to the memory. + * @param size The size of the memory. + */ + View(const uint8_t* data, std::size_t size) noexcept + : size_{size}, + data_{data} + { + } + + /** + * Create a view from a string view. + * + * @param str The input string view. + */ + View(std::string_view str) noexcept + : size_{str.length()}, + data_{str.length() > 0 ? reinterpret_cast<const uint8_t*>(str.data()): nullptr} + { + } + + /** + * Creates a view from wide string view. + * + * @param str The input string. + */ + View(std::wstring_view str) noexcept + : size_{str.length() * sizeof(wchar_t)}, + data_{size_ > 0 ? reinterpret_cast<const uint8_t*>(str.data()): nullptr} + { + } + + /** + * Creates a view from void pointer and a size. + * + * @param data Pointer to the memory. + * @param size The size of the memory. + */ + View(const void* ptr, std::size_t size) noexcept + : size_{size}, + data_{static_cast<const uint8_t*>(ptr)} + { + } + + // default copyable. + View(const View&) = default; + auto operator=(const View&) -> View& = default; + + /** + * Slice returns a new view in the interval [start, size). + * + * If the start is bigger than the size of the slice it returns empty view. + * + * @param start The beginning of the new View. + * @returns a new view in the interval [start, size). + */ + auto Slice(std::size_t start) const -> View { + if (start >= size_) { + return View(data_, 0); + } + + return View(data_ + start, size_ - start); + } + + /** + * Slice returns a new view in the interval [start, end). + * + * If the start is bigger than the size of the slice it returns empty view. + * If the end is bigger than the size of the slice it returns [start, size). + * + * @param start The beginning of the new View. + * @returns a new view in the interval [start, size). + */ + auto Slice(std::size_t start, std::size_t end) const -> View { + if (start >= size_) { + return View(data_, 0); + } + + return View(data_ + start, std::min(size_, end) - start); + } + + /** + * Retrieve a pointer to the data. + * + * @returns a pointer to the data. + */ + auto Data() const noexcept -> const uint8_t* { + return data_; + } + + /** + * Retrieve a char pointer to the data. + * + * @returns a char pointer to the data. + */ + auto CharData() const noexcept -> const char* { + return reinterpret_cast<const char*>(data_); + } + + /** + * Retrieve a wide char pointer to the data. + * + * @returns a wide char pointer to the data. + */ + auto WideCharData() const noexcept -> const wchar_t* { + return reinterpret_cast<const wchar_t*>(data_); + } + + /** + * Retrieve view size. + * + * @returns view size. + */ + auto Size() const noexcept -> std::size_t { + return size_; + } + + /** + * Retrieve a string view of the data. + * + * @returns a string view of the data. + */ + auto String() const noexcept -> std::string_view { + return std::string_view{CharData(), size_}; + } + + /** + * Retrieve a wide string view of the data. + * + * @returns a wide string view of the data. + */ + auto WideString() const noexcept -> std::wstring_view { + return std::wstring_view{WideCharData(), size_/sizeof(wchar_t)}; + } + + /** + * Binary compare the contents of two views. + * + * @returns true if the views are of the same size and if all the bytes in the two views are equal. + */ + auto operator==(View other) const noexcept -> bool { + if (size_ != other.size_) { + return false; + } + + return std::memcmp(data_, other.data_, size_) == 0; + } + + /** + * Binary compare the contents of two views. + * + * @returns true if the views are of different size or if the bytes in the two views are not equal. + */ + auto operator!=(View other) const noexcept -> bool { + return !(*this == other); + } + + /** + * Access a single byte in the view. + * + * @param index The index of the byte that will be returned. + * @returns the byte at the specified index. + * + * @throws bytes::IndexOutOfBounds when the index is out of bounds. + */ + auto operator[](std::size_t index) const -> const uint8_t& { + if (index >= size_ ) { + throw IndexOutOfBounds{}; + } + + return data_[index]; + } + +private: + std::size_t size_ = 0; + const uint8_t* data_ = nullptr; +}; + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_VIEW_HH diff --git a/cpp/src/vereign/bytes/view_dump.cc b/cpp/src/vereign/bytes/view_dump.cc new file mode 100644 index 0000000000000000000000000000000000000000..7d892bb9e350710109b6b34d3472e7334fd3690e --- /dev/null +++ b/cpp/src/vereign/bytes/view_dump.cc @@ -0,0 +1,64 @@ +#include <vereign/bytes/view_dump.hh> + +#include <iomanip> + +namespace vereign::bytes { + +namespace detail { + +auto operator<<(std::ostream& os, const ViewDump& vd) -> std::ostream& { + if (vd.buf_.Size() == 0) { + os << "empty buffer\n"; + return os; + } + + core::IosFlagsLock os_flags(os); + + os << std::hex; + + for (std::size_t i = 0; i < vd.buf_.Size(); ++i) { + if (i % 16 == 0) { + os << "0x" << std::setfill('0') << std::setw(4) << i << ": "; + } + + if (i % 2 == 0) { + os << " "; + } + + os << std::setfill('0') << std::setw(2) << (unsigned int) vd.buf_.Data()[i]; + + if ((i != 0 && (i + 1) % 16 == 0) || i == vd.buf_.Size() - 1) { + int bytes = (i + 1) % 16 == 0 ? 0 : (16 - (i + 1) % 16); + os << " " << std::setfill(' ') << std::setw(2 * bytes + bytes / 2) << ""; + vd.printChars(os, i - i % 16); + os << "\n"; + } + } + + return os; +} + +void ViewDump::printChars(std::ostream& os, std::size_t offset) const { + auto seq = buf_.Slice(offset, offset + 16); + for (int i = 0; i < (int) seq.Size(); ++i) { + auto ch = seq.Data()[i]; + if (std::isprint(ch) != 0) { + os << ch; + } else { + os << "."; + } + } +} + +} // namespace detail + + +auto dump(View buf) -> detail::ViewDump { + return detail::ViewDump{buf}; +} + +auto dump(const Buffer& buf) -> detail::ViewDump { + return detail::ViewDump{buf.View()}; +} + +} // namespace vereign::bytes diff --git a/cpp/src/vereign/bytes/view_dump.hh b/cpp/src/vereign/bytes/view_dump.hh new file mode 100644 index 0000000000000000000000000000000000000000..47625f49016bee9fd8181dbc66746d54b7b3131f --- /dev/null +++ b/cpp/src/vereign/bytes/view_dump.hh @@ -0,0 +1,92 @@ +#ifndef __VEREIGN_BYTES_VIEW_DUMP_HH +#define __VEREIGN_BYTES_VIEW_DUMP_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/buffer.hh> +#include <vereign/core/io_flags_lock.hh> +#include <iostream> + +namespace vereign::bytes { + +namespace detail { +class ViewDump; +} + +/** + * Returns a view dump object that can write the view dump into std::ostream. + * + * The dump is the good old hexadecimal format. + * + * Example: + * @code + * std::string input{"foo bar"}; + * std::cout << bytes::dump(bytes::View(input)) << std::endl; + * + * // Output: + * // 0x0000: 666f 6f20 6261 72 foo bar + * + * auto buf = crypto::Rand(32); + * std::cout << bytes::dump(buf.View()) << std::endl; + * + * // Output: + * // 0x0000: 1666 855a 650a 7549 ed0f ec01 4d87 09bf .f.Ze.uI....M... + * // 0x0010: e644 dbc8 7943 37b4 185c dbab 4977 ff3f .D..yC7..\..Iw.? + * + * @endcode + */ +auto dump(View buf) -> detail::ViewDump; + +/** + * Returns a dump object that can write the buffer dump into std::ostream. + * + * The dump is the good old hexadecimal format. + * + * Example: + * @code + auto buf = bytes::Buffer{bytes::View("foo bar")}; + std::cout << bytes::dump(buf) << std::endl; + * + * // Output: + * // 0x0000: 666f 6f20 6261 72 foo bar + * + * auto buf = crypto::Rand(32); + * std::cout << bytes::dump(buf) << std::endl; + * + * // Output: + * // 0x0000: 1666 855a 650a 7549 ed0f ec01 4d87 09bf .f.Ze.uI....M... + * // 0x0010: e644 dbc8 7943 37b4 185c dbab 4977 ff3f .D..yC7..\..Iw.? + * + * @endcode + */ +auto dump(const Buffer& buf) -> detail::ViewDump; + +namespace detail { + +/** + * Helper for dumping a view to a std::ostream. + * + * Users typically will use the bytes::dump functions. + */ +class ViewDump { +public: + auto operator=(const ViewDump&) -> ViewDump& = delete; + + explicit ViewDump(View buf) + : buf_(buf) + { + } + + friend auto operator<<(std::ostream& os, const ViewDump& vd) -> std::ostream&; + +private: + void printChars(std::ostream& os, std::size_t offset) const; + +private: + View buf_; +}; + +} // namespace detail + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_VIEW_DUMP_HH diff --git a/cpp/src/vereign/container/bounded_queue.hh b/cpp/src/vereign/container/bounded_queue.hh new file mode 100644 index 0000000000000000000000000000000000000000..fbb2ab64b0110e3dd9d5ca0219c9902ab9ee06df --- /dev/null +++ b/cpp/src/vereign/container/bounded_queue.hh @@ -0,0 +1,139 @@ +#ifndef __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH +#define __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH + +#include <vector> + +namespace vereign::container { + +/** + * A queue (FIFO) data structure with fixed size. + * + * Being a fixed in its size, the BoundedQueue has the opportunity to internally allocate only once + * upon construction a single continues block of memory for holding the queue elements. + */ +template <typename T> +class BoundedQueue { +public: + /** + * The type of the values in the queue. + */ + using ValueType = T; + + // Helper type that is properly aligned so that it can act as a storage for the ValueType. + using StorageType = typename std::aligned_storage_t<sizeof(T), alignof(T)>; + + /** + * Creates a queue with given capacity. + * + * The capacity cannot be changed after construction. + */ + BoundedQueue(std::size_t capacity) + : begin_{0}, + size_{0}, + cap_{capacity}, + queue_{new StorageType[capacity]} + { + } + + /** + * Destroy the elements if any, and free all the memory. + */ + ~BoundedQueue() { + while (!IsEmpty()) { + PopFront(); + } + + delete [] queue_; + } + + // disable copying + BoundedQueue(const BoundedQueue&) = delete; + auto operator=(const BoundedQueue&) -> BoundedQueue& = delete; + + /** + * Retrieve the number of elements in the queue. + * + * @returns the number of elements in the queue. + */ + auto Size() -> std::size_t { + return size_; + } + + /** + * Check if the queue is empty. + * + * @returns true when the queue is empty, false otherwise. + */ + auto IsEmpty() -> bool { + return size_ == 0; + } + + /** + * Check if the queue is full up to its capacity. + * + * @returns true when the queue is full, false otherwise. + */ + auto IsFull() -> bool { + return size_ == cap_; + } + + /** + * Moves a value to the end of the queue. + * + * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour. + */ + void PushBack(ValueType&& value) { + auto pos = (begin_ + size_) % cap_; + + new(&queue_[pos]) T(std::move(value)); + + size_++; + } + + /** + * Copies a value to the end of the queue. + * + * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour. + */ + void PushBack(const ValueType& value) { + auto pos = (begin_ + size_) % cap_; + + new(&queue_[pos]) T(value); + + size_++; + } + + /** + * Access the first value in the queue. + * + * @note Calling BoundedQueue::Front on empty queue is undefined behaviour. + */ + auto Front() -> ValueType& { + return reinterpret_cast<ValueType&>(queue_[begin_]); + } + + /** + * Remove the fist item from the queue. + * + * @note Calling BoundedQueue::PopFront on empty queue is undefined behaviour. + */ + void PopFront() { + reinterpret_cast<ValueType*>(&queue_[begin_])->~ValueType(); + + size_--; + begin_ = (begin_ == cap_ - 1) ? 0 : begin_ + 1; + } + +private: + std::size_t begin_; + + std::size_t size_; + + std::size_t cap_; + + StorageType* queue_; +}; + +} // namespace vereign::container + +#endif // __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH diff --git a/cpp/src/vereign/core/hex.hh b/cpp/src/vereign/core/hex.hh deleted file mode 100644 index a370a6e98f39594c98578e1c455a8e390881b69e..0000000000000000000000000000000000000000 --- a/cpp/src/vereign/core/hex.hh +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef __VEREIGN_CORE_HEX_HH -#define __VEREIGN_CORE_HEX_HH - -#include <string> - -namespace vereign { -namespace core { -namespace detail { - -inline auto charToInt(char ch) -> int { - if (ch >= '0' && ch <= '9') { - return ch - '0'; - } - - if (ch >= 'A' && ch <= 'F') { - return ch - 'A' + 10; - } - - if (ch >= 'a' && ch <= 'f') { - return ch - 'a' + 10; - } - - return 0; -} - -} // namespace detail - -inline auto BinToHex(const unsigned char* src, std::size_t size) -> std::string { - static const char* nibbles = { "0123456789abcdef" }; - - std::string result; - result.reserve(size * 2); - - for (std::size_t i = 0; i < size; ++i) { - result.push_back(nibbles[src[i] >> 4]); - result.push_back(nibbles[src[i] & 0x0F]); - } - - return result; -} - -inline auto BinToHex(const std::string& src) -> std::string { - return BinToHex((unsigned char*) src.c_str(), src.size()); -} - -inline void hex_to_bin(const std::string& src, unsigned char* dst) { - for (int i = 0, len = (int) src.size() - 1; i < len; i += 2) { - dst[i/2] = detail::charToInt(src[i]) * 16 + detail::charToInt(src[i + 1]); - } -} - -} // namespace core -} // namespace vereign - -#endif // __VEREIGN_CORE_HEX_HH diff --git a/cpp/src/vereign/core/io_flags_lock.hh b/cpp/src/vereign/core/io_flags_lock.hh new file mode 100644 index 0000000000000000000000000000000000000000..8a6feda70736c794162f434650c8cbbb34815691 --- /dev/null +++ b/cpp/src/vereign/core/io_flags_lock.hh @@ -0,0 +1,37 @@ +#ifndef __VEREIGN_CORE_IOS_FLAGS_LOCK_HH +#define __VEREIGN_CORE_IOS_FLAGS_LOCK_HH + +#include <ios> + +namespace vereign::core { + +/** + * Restore the ios flags of a stream when is it self destroyed. + */ +class IosFlagsLock { +public: + IosFlagsLock(const IosFlagsLock&) = delete; + auto operator=(const IosFlagsLock&) -> IosFlagsLock& = delete; + + IosFlagsLock(std::ios_base& str) + : str_(str), + flags_(str.flags()) + { + } + + ~IosFlagsLock() { + restore(); + } + + void restore() { + str_.flags(flags_); + } + +private: + std::ios_base& str_; + std::ios_base::fmtflags flags_; +}; + +} // namespace vereign::core + +#endif // __TURBO_CORE_IOS_FLAGS_LOCK_HH diff --git a/cpp/src/vereign/core/lock_guard.hh b/cpp/src/vereign/core/lock_guard.hh new file mode 100644 index 0000000000000000000000000000000000000000..9e310b21ca53ce5c53fea581a483ecaa568004f4 --- /dev/null +++ b/cpp/src/vereign/core/lock_guard.hh @@ -0,0 +1,28 @@ +#ifndef __VEREIGN_LOCK_GUARD_HH +#define __VEREIGN_LOCK_GUARD_HH + +namespace vereign::core { + +template <class Lockable> +class LockGuard { +public: + explicit LockGuard(Lockable& lock) + : lock_{lock} + { + lock.Lock(); + } + + ~LockGuard() noexcept { + lock_.Unlock(); + } + + LockGuard(const LockGuard&) = delete; + auto operator=(const LockGuard&) -> LockGuard& = delete; + +private: + Lockable& lock_; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_LOCK_GUARD_HH diff --git a/cpp/src/vereign/core/rand.cc b/cpp/src/vereign/core/rand.cc new file mode 100644 index 0000000000000000000000000000000000000000..7e65a25ae0c2e45a79e34328edd05d54e53c9df1 --- /dev/null +++ b/cpp/src/vereign/core/rand.cc @@ -0,0 +1,30 @@ +#include <vereign/core/rand.hh> + +#include <random> + +namespace vereign::core { + +auto RandNumber(int n) -> int { + static std::random_device dev; + static std::mt19937 rng(dev()); + using Dist = std::uniform_int_distribution<std::mt19937::result_type>; + static Dist dist{}; + + return dist(dev, Dist::param_type{0, static_cast<unsigned long>(n) - 1} ); +} + +auto RandLowerAlpha(int len) -> std::string { + using namespace std::string_view_literals; + static constexpr std::string_view chars = "0123456789abcdefghijklmnopqrstuvwxyz"sv; + + std::string result; + result.resize(len); + + for (int i = 0; i < len; i++) { + result[i] = chars[RandNumber(chars.size())]; + } + + return result; +} + +} // namespace vereign::core diff --git a/cpp/src/vereign/core/rand.hh b/cpp/src/vereign/core/rand.hh new file mode 100644 index 0000000000000000000000000000000000000000..a39eae5e551a259259717e33bc6ceba2395e8a8d --- /dev/null +++ b/cpp/src/vereign/core/rand.hh @@ -0,0 +1,26 @@ +#ifndef __VEREIGN_CORE_RAND_HH +#define __VEREIGN_CORE_RAND_HH + +#include <random> + +namespace vereign::core { + +/** + * Generated random number in the half closed interval [0, n). + * + * @param n Upper bound of the generated random number. + * @returns a random number in the half closed interval [0, n). + */ +auto RandNumber(int n) -> int; + +/** + * Generates random string with alpha lower case characters. + * + * @param len The length of the desired random string. + * @returns a random string of size len consisting of numbers 0-9 and lower case latin chars a-z. + */ +auto RandLowerAlpha(int len) -> std::string; + +} // namespace vereign::core + +#endif // __VEREIGN_CORE_RAND_HH diff --git a/cpp/src/vereign/core/string.cc b/cpp/src/vereign/core/string.cc new file mode 100644 index 0000000000000000000000000000000000000000..bf2340fcb9ebafd0b6c8ec5ccdec511ee24a5ecd --- /dev/null +++ b/cpp/src/vereign/core/string.cc @@ -0,0 +1,94 @@ +#include <vereign/core/string.hh> + +#include <iostream> + +#ifdef _WIN32 +# include <windows.h> +# include <stringapiset.h> +#endif + +namespace vereign::string { + +#ifdef _WIN32 + +auto widen(std::string_view utf8_str) -> std::wstring { + if (utf8_str.empty()) { + return L""; + } + + int num_chars = MultiByteToWideChar(CP_UTF8 , 0, utf8_str.data(), utf8_str.length(), nullptr, 0); + if (num_chars == 0) { + return std::wstring{}; + } + + std::wstring result; + result.resize(num_chars); + num_chars = MultiByteToWideChar( + CP_UTF8, + 0, + utf8_str.data(), + utf8_str.length(), + result.data(), + num_chars + ); + + if (num_chars == 0) { + return L""; + } + + return result; +} + +auto narrow(std::wstring_view utf16_str) -> std::string { + if (utf16_str.empty()) { + return ""; + } + + int num_chars = WideCharToMultiByte( + CP_UTF8, + 0, + utf16_str.data(), + utf16_str.length(), + nullptr, + 0, + nullptr, + nullptr + ); + + if (num_chars == 0) { + return ""; + } + + std::string result; + result.resize(num_chars); + int new_size = WideCharToMultiByte( + CP_UTF8, + 0, + utf16_str.data(), + utf16_str.length(), + result.data(), + num_chars, + nullptr, + nullptr + ); + + if (new_size == 0) { + return ""; + } + + result.resize(new_size); + + return result; +} + +#endif + +auto tail(const std::string& src, std::size_t length) -> std::string { + if (length >= src.size()) { + return src; + } + + return src.substr(src.size() - length); +} + +} // vereign::string diff --git a/cpp/src/vereign/core/string.hh b/cpp/src/vereign/core/string.hh new file mode 100644 index 0000000000000000000000000000000000000000..62429b28a0d77feaf77575edd8b630372b571f87 --- /dev/null +++ b/cpp/src/vereign/core/string.hh @@ -0,0 +1,32 @@ +#ifndef __VEREIGN_CORE_STRING_HH +#define __VEREIGN_CORE_STRING_HH + +#include <string> +#include <codecvt> + +namespace vereign::string { + +#ifdef _WIN32 + +auto widen(std::string_view utf8_str) -> std::wstring; +auto narrow(std::wstring_view utf16_str) -> std::string; + +#endif + +/** + * Retrieve string tail. + * + * @code + * std::string str = "foobar"; + * std::string tail = vereign::string::tail(str, 3); + * assert(tail == "bar"); + * @endcode + * + * @param src Source string. + * @param length How many characters from the string tail to return. + */ +auto tail(const std::string& src, std::size_t length) -> std::string; + +} // vereign::string + +#endif // __VEREIGN_CORE_STRING_HH diff --git a/cpp/src/vereign/core/time.cc b/cpp/src/vereign/core/time.cc new file mode 100644 index 0000000000000000000000000000000000000000..33030082a682d2bc3091602a24d5e39c22da4b96 --- /dev/null +++ b/cpp/src/vereign/core/time.cc @@ -0,0 +1,73 @@ +#include <vereign/core/time.hh> + +namespace vereign::time { + +auto Epoch() -> boost::posix_time::ptime { + static const auto epoch = boost::posix_time::ptime{boost::gregorian::date{1970, 1, 1}}; + + return epoch; +} + +auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t { + return time_t((t - Epoch()).total_seconds()); +} + +auto MakePosixTime( + int year, + int month, + int day, + int hours, + int minutes, + int seconds +) -> boost::posix_time::ptime { + using namespace boost::posix_time; + using namespace boost::gregorian; + + return ptime( + date{ + static_cast<date::year_type>(year), + static_cast<date::month_type>(month), + static_cast<date::day_type>(day) + }, + time_duration(hours, minutes, seconds) + ); +} + +auto MakeTimeUTC( + int year, + int month, + int day, + int hours, + int seconds, + int milliseconds +) -> time_t { + using namespace boost::posix_time; + using namespace boost::gregorian; + + return PosixTimeToTime( + ptime( + date{ + static_cast<date::year_type>(year), + static_cast<date::month_type>(month), + static_cast<date::day_type>(day) + }, + time_duration(hours, seconds, milliseconds) + ) + ); +} + +auto MakeTimeUTCFromString(const std::string& str) -> time_t { + using namespace boost::posix_time; + using namespace boost::gregorian; + + return PosixTimeToTime(boost::posix_time::time_from_string(str)); +} + +auto MakeTimeUTCFromISO(const std::string& str) -> time_t { + using namespace boost::posix_time; + using namespace boost::gregorian; + + return PosixTimeToTime(from_iso_string(str)); +} + +} // vereign::time diff --git a/cpp/src/vereign/core/time.hh b/cpp/src/vereign/core/time.hh new file mode 100644 index 0000000000000000000000000000000000000000..5b7d775606d25a7c2ac264d543bbbdeac833088c --- /dev/null +++ b/cpp/src/vereign/core/time.hh @@ -0,0 +1,72 @@ +#ifndef __VEREIGN_CORE_TIME_HH +#define __VEREIGN_CORE_TIME_HH + +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace vereign::time { + +/** + * Returns the gregorian epoch boost posix time - 1970:01:01. + */ +auto Epoch() -> boost::posix_time::ptime; + +/** + * Converts boost posix time to time_t timestamp. + * + * @param t The source boost posix time. + * @returns timestamp. + */ +auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t; + +/** + * Creates boost posix timetamp. + * + * @param year Year component. + * @param month Month component 1-12. + * @param day Day component 1-31. + * @param hours The hours component 0-23. + * @param minutes The minutes component 0-59. + * @param seconds The minutes component 0-59. + * @returns posix time. + */ +auto MakePosixTime( + int year, + int month, + int day, + int hours, + int minutes, + int seconds +) -> boost::posix_time::ptime; + +/** + * Creates UTC timestamp. + * + * @param year Year component. + * @param month Month component 1-12. + * @param day Day component 1-31. + * @param hours The hours component 0-23. + * @param minutes The minutes component 0-59. + * @param seconds The minutes component 0-59. + * @returns timestamp. + */ +auto MakeTimeUTC(int year, int month, int day, int hours, int seconds, int milliseconds) -> time_t; + +/** + * Creates UTC timestamp from a string. + * + * @param str Time string representation, example: "2002-01-20 23:59:59.000". + * @returns timestamp. + */ +auto MakeTimeUTCFromString(const std::string& str) -> time_t; + +/** + * Creates UTC timestamp from ISO formatted string. + * + * @param str Time string representation, example: "20020120T235959". + * @returns timestamp. + */ +auto MakeTimeUTCFromISO(const std::string& str) -> time_t; + +} // vereign::time + +#endif // __VEREIGN_CORE_TIME_HH diff --git a/cpp/src/vereign/crypto/aes.cc b/cpp/src/vereign/crypto/aes.cc new file mode 100644 index 0000000000000000000000000000000000000000..f9bcefab4365b17f31e960ffb802d662692695d2 --- /dev/null +++ b/cpp/src/vereign/crypto/aes.cc @@ -0,0 +1,128 @@ +#include <openssl/err.h> +#include <vereign/crypto/aes.hh> + +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/errors.hh> + +#include <openssl/base.h> +#include <openssl/evp.h> + +namespace { + constexpr int gcmIVSizeBytes = 12; + constexpr int gcmTagSizeBytes = 16; + constexpr int aes256BlockSizeBytes = 32; +} + +namespace vereign::crypto::aes { + +void GCM256Encrypt( + bytes::View src, + bytes::View key, + bytes::Buffer& iv, + bytes::Buffer& tag, + bytes::Buffer& encrypted +) { + iv.Reserve(gcmIVSizeBytes); + crypto::Rand(iv, gcmIVSizeBytes); + + encrypted.Reserve(src.Size() + aes256BlockSizeBytes); + + bssl::UniquePtr<EVP_CIPHER_CTX> ctx{EVP_CIPHER_CTX_new()}; + if (!ctx) { + throw OpenSSLError("evp cipher context cannot be created"); + } + + auto r = EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr); + if (r != 1) { + throw OpenSSLError("AES GCM encrypt init failed"); + } + + r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.Size(), nullptr); + if (r != 1) { + throw OpenSSLError("iv size init failed"); + } + + r = EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key.Data(), iv.View().Data()); + if (r != 1) { + throw OpenSSLError("key and iv init failed"); + } + + int bytes_written; + r = EVP_EncryptUpdate(ctx.get(), encrypted.end(), &bytes_written, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("encrypt failed"); + } + encrypted.IncSize(bytes_written); + + r = EVP_EncryptFinal_ex(ctx.get(), encrypted.end(), &bytes_written); + if (r != 1) { + throw OpenSSLError("finalize encryption failed"); + } + encrypted.IncSize(bytes_written); + + tag.Reserve(gcmTagSizeBytes); + r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, gcmTagSizeBytes, tag.end()); + if (r != 1) { + throw OpenSSLError("getting GCM tag failed"); + } + tag.IncSize(gcmTagSizeBytes); + + r = EVP_CIPHER_CTX_cleanup(ctx.get()); + if (r != 1) { + ERR_clear_error(); + } +} + +void GCM256Decrypt( + bytes::View src, + bytes::View key, + bytes::View iv, + bytes::View tag, + bytes::Buffer& decrypted +) { + bssl::UniquePtr<EVP_CIPHER_CTX> ctx{EVP_CIPHER_CTX_new()}; + if (!ctx) { + throw OpenSSLError("evp cipher context cannot be created"); + } + + auto r = EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr); + if (r != 1) { + throw OpenSSLError("AES GCM decrypt init failed"); + } + + r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.Size(), nullptr); + if (r != 1) { + throw OpenSSLError("iv size init failed"); + } + + r = EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.Data(), iv.Data()); + if (r != 1) { + throw OpenSSLError("key and iv init failed"); + } + + decrypted.Reserve(src.Size()); + + int bytes_written; + r = EVP_DecryptUpdate(ctx.get(), decrypted.end(), &bytes_written, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("decrypt failed"); + } + decrypted.IncSize(bytes_written); + + r = EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_SET_TAG, + tag.Size(), + const_cast<uint8_t*>(tag.Data()) + ); + if (r != 1) { + throw OpenSSLError("setting GCM tag failed"); + } + + r = EVP_DecryptFinal_ex(ctx.get(), decrypted.end(), &bytes_written); + if (r != 1) { + throw OpenSSLError("verification failed"); + } +} + +} // vereign::crypto::aes diff --git a/cpp/src/vereign/crypto/aes.hh b/cpp/src/vereign/crypto/aes.hh new file mode 100644 index 0000000000000000000000000000000000000000..09be668b964bddd8f31aecc93e874ae35834eb3b --- /dev/null +++ b/cpp/src/vereign/crypto/aes.hh @@ -0,0 +1,80 @@ +#ifndef __VEREIGN_CRYPTO_AES_HH +#define __VEREIGN_CRYPTO_AES_HH + +#include <vereign/bytes/buffer.hh> + +/** + * Provides utilities for AES encryption/decryption. + */ +namespace vereign::crypto::aes { + +/** + * Encrypt given bytes with AES256-GCM. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto key = crypto::Rand(32); // 256 bits + * + * bytes::Buffer iv; + * bytes::Buffer tag; + * bytes::Buffer encrypted; + * + * crypto::aes::GCM256Encrypt(bytes::View(input), key.View(), iv, tag, encrypted); + * @endcode + * + * @param src The bytes that will be encrypted. + * @param key The AES 256 bit key. + * @param iv The initialization vector that was used during the encryption. + * @param tag The authentication tag that was produced during the encryption. + * @param encrypted The result of the encryption. + * + * @throws crypto::OpenSSLError on failure. + */ +void GCM256Encrypt( + bytes::View src, + bytes::View key, + bytes::Buffer& iv, + bytes::Buffer& tag, + bytes::Buffer& encrypted +); + +/** + * Decrypts given bytes with AES256-GCM. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto key = crypto::Rand(32); // 256 bits + * + * bytes::Buffer iv; + * bytes::Buffer tag; + * bytes::Buffer encrypted; + * + * crypto::aes::GCM256Encrypt(bytes::View(input), key.View(), iv, tag, encrypted); + * + * bytes::Buffer decrypted; + * crypto::aes::GCM256Decrypt(encrypted.View(), key.View(), iv.View(), tag.View(), decrypted); + * + * assert(input == decrypted.View().String()); + * @endcode + * + * @param src The bytes that will be decrypted. + * @param key The AES 256 bit key. + * @param iv The initialization vector that was used during the encryption. + * @param tag The authentication tag that was produced during the encryption. + * @param encrypted The result of the decryption. + * + * @throws crypto::OpenSSLError on failure. + */ +void GCM256Decrypt( + bytes::View src, + bytes::View key, + bytes::View iv, + bytes::View tag, + bytes::Buffer& decrypted +); + +} // vereign::crypto::aes + +#endif // __VEREIGN_CRYPTO_AES_HH diff --git a/cpp/src/vereign/crypto/bio.cc b/cpp/src/vereign/crypto/bio.cc new file mode 100644 index 0000000000000000000000000000000000000000..5a630f6eedcd17a51dddb79c826a992827485e2e --- /dev/null +++ b/cpp/src/vereign/crypto/bio.cc @@ -0,0 +1,18 @@ +#include <vereign/crypto/bio.hh> + +#include <vereign/crypto/errors.hh> + +namespace vereign::crypto::bio { + +auto View(const BIO* bio) -> bytes::View { + const uint8_t* data; + std::size_t size; + auto r = BIO_mem_contents(bio, &data, &size); + if (r != 1) { + throw OpenSSLError("fetching bio mem contents failed"); + } + + return bytes::View(data, size); +} + +} // vereign::crypto::bio diff --git a/cpp/src/vereign/crypto/bio.hh b/cpp/src/vereign/crypto/bio.hh new file mode 100644 index 0000000000000000000000000000000000000000..2bd5cedacee0b6c94c81a0f2af50e448982b6d3f --- /dev/null +++ b/cpp/src/vereign/crypto/bio.hh @@ -0,0 +1,19 @@ +#ifndef __VEREIGN_CRYPTO_BIO_HH +#define __VEREIGN_CRYPTO_BIO_HH + +#include <vereign/bytes/view.hh> +#include <openssl/bio.h> + +namespace vereign::crypto::bio { + +/** + * Creates a bytes::View for given OpenSSL BIO. + * + * @param bio The input BIO. + * @returns a bytes view of OpenSSL BIO. + */ +auto View(const BIO* bio) -> bytes::View; + +} // vereign::crypto::bio + +#endif // __VEREIGN_CRYPTO_BIO_HH diff --git a/cpp/src/vereign/crypto/cert.cc b/cpp/src/vereign/crypto/cert.cc new file mode 100644 index 0000000000000000000000000000000000000000..bc6f84d2d6f74904056f6b5bfe7c4c9d5402d2a4 --- /dev/null +++ b/cpp/src/vereign/crypto/cert.cc @@ -0,0 +1,428 @@ +#include <vereign/crypto/cert.hh> + +#include <vereign/crypto/bio.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/encoding/base64.hh> + +#include <openssl/x509v3.h> +#include <openssl/digest.h> +#include <openssl/pem.h> +#include <sstream> + +namespace { + constexpr const char* certDefaultHashAlg = "SHA256"; + constexpr const int certVersion = 2; +} + +namespace vereign::crypto::cert { + +static auto addCertExtension( + const X509* issuer_cert, + X509* cert, + int nid, + const std::string& value +) -> int { + X509V3_CTX ctx; + + // This sets the 'context' of the extensions. + // No configuration database + X509V3_set_ctx_nodb(&ctx); + + // Issuer and subject certs: both the target since it is self signed, + // no request and no CRL + X509V3_set_ctx(&ctx, const_cast<X509*>(issuer_cert), cert, nullptr, nullptr, 0); + bssl::UniquePtr<X509_EXTENSION> ex{X509V3_EXT_nconf_nid( + nullptr, + &ctx, + nid, + const_cast<std::string&>(value).data() + )}; + if (!ex) { + return 0; + } + + return X509_add_ext(cert, ex.get(), -1); +} + +static void addSubjAltNameExt(X509* cert, const std::string& email, const std::string& url) { + auto gens = bssl::UniquePtr<GENERAL_NAMES>(sk_GENERAL_NAME_new_null()); + if (!gens) { + throw OpenSSLError("creating GENERAL_NAMES stack failed"); + } + + if (!email.empty()) { + auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new()); + if (!gen) { + throw OpenSSLError("creating GENERAL_NAME failed"); + } + + auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new()); + if (!ia5) { + throw OpenSSLError("creating ASN1_IA5STRING failed"); + } + + auto r = ASN1_STRING_set(ia5.get(), email.data(), -1); + if (r != 1) { + throw OpenSSLError("set certificate alternative name email part failed"); + } + + GENERAL_NAME_set0_value(gen.get(), GEN_EMAIL, ia5.release()); + r = sk_GENERAL_NAME_push(gens.get(), gen.release()); + if (r == 0) { + throw OpenSSLError("pushing email to certificate subject alternative name failed"); + } + } + + if (!url.empty()) { + auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new()); + if (!gen) { + throw OpenSSLError("creating GENERAL_NAME failed"); + } + + auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new()); + if (!ia5) { + throw OpenSSLError("creating ASN1_IA5STRING failed"); + } + + auto r = ASN1_STRING_set(ia5.get(), url.data(), -1); + if (r != 1) { + throw OpenSSLError("set certificate alternative name URL part failed"); + } + + GENERAL_NAME_set0_value(gen.get(), GEN_DNS, ia5.release()); + r = sk_GENERAL_NAME_push(gens.get(), gen.release()); + if (r == 0) { + throw OpenSSLError("pushing URL to certificate subject alternative name failed"); + } + } + + auto r = X509_add1_ext_i2d(cert, NID_subject_alt_name, gens.get(), 0, 0); + if (r != 1) { + throw OpenSSLError("set certificate alternative name failed"); + } +} + +static void setCertSubject(const CertData& cert_data, X509* cert) { + const CertSubject& subject = cert_data.Subject; + auto subject_name = X509_get_subject_name(cert); + int r = 0; + + if (!subject.CommonName.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_commonName, + MBSTRING_ASC, + (unsigned char*) subject.CommonName.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Common Name failed"); + } + } + + if (!subject.Country.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_countryName, + MBSTRING_ASC, + (unsigned char*) subject.Country.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Country Name failed"); + } + } + + if (!subject.Locality.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_localityName, + MBSTRING_ASC, + (unsigned char*) subject.Locality.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Locality Name failed"); + } + } + + if (!subject.State.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_stateOrProvinceName, + MBSTRING_ASC, + (unsigned char*) subject.State.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject State Name failed"); + } + } + + if (!subject.Organization.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_organizationName, + MBSTRING_ASC, + (unsigned char*) subject.Organization.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Organization Name failed"); + } + } + + if (!subject.OrganizationUnit.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_organizationalUnitName, + MBSTRING_ASC, + (unsigned char*) subject.OrganizationUnit.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Organizational Unit Name failed"); + } + } + + + if (!cert_data.Email.empty()) { + r = X509_NAME_add_entry_by_NID( + subject_name, + NID_pkcs9_emailAddress, + MBSTRING_ASC, + (unsigned char*) cert_data.Email.data(), + -1, + -1, + 0 + ); + if (r != 1) { + throw OpenSSLError("set certificate subject Email Address failed"); + } + } +} + +static void addCertExtensions(const CertData& cert_data, const X509* issuer_cert, X509* cert) { + // basic constraints + const std::string basic_constraints = cert_data.IsCA ? "critical,CA:TRUE" : "critical,CA:FALSE"; + auto r = addCertExtension(issuer_cert, cert, NID_basic_constraints, basic_constraints); + if (r != 1) { + throw OpenSSLError("set certificate basic constraints failed"); + } + + // key usage + const std::string key_usage = cert_data.IsCA + ? "critical,digitalSignature,keyCertSign,cRLSign" + : "critical,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment"; + r = addCertExtension(issuer_cert, cert, NID_key_usage, key_usage); + if (r != 1) { + throw OpenSSLError("set certificate key usage failed"); + } + + // extended key usage + if (!cert_data.IsCA && !cert_data.Email.empty()) { + r = addCertExtension(issuer_cert, cert, NID_ext_key_usage, "emailProtection"); + if (r != 1) { + throw OpenSSLError("set certificate extended key usage failed"); + } + } + + // subject alternative name + if (!cert_data.Email.empty() || !cert_data.Url.empty()) { + addSubjAltNameExt(cert, cert_data.Email, cert_data.Url); + } + + // subject key identifier + r = addCertExtension(issuer_cert, cert, NID_subject_key_identifier, "hash"); + if (r != 1) { + throw OpenSSLError("set certificate key usage failed"); + } + + // authority key identifier + r = addCertExtension(issuer_cert, cert, NID_authority_key_identifier, "keyid,issuer:always"); + if (r != 1) { + throw OpenSSLError("set certificate key usage failed"); + } +} + +static void setCertValidity(const CertData& cert_data, X509* cert) { + using clock = std::chrono::system_clock; + + long not_before_adj = 0; + time_t not_before = cert_data.Validity.NotBefore; + + if (not_before == 0) { + auto now = clock::now(); + not_before = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count(); + not_before_adj = -(not_before % (24 * 60 * 60)); + } + + auto timeret = X509_time_adj(X509_get_notBefore(cert), not_before_adj, ¬_before); + if (timeret == nullptr) { + throw OpenSSLError("adjusting certificate not before validity failed"); + } + + long not_after_adj = 0; + time_t not_after = cert_data.Validity.NotAfter; + + if (not_after == 0) { + not_after = not_before; + + int valid_years = cert_data.Validity.ValidYears; + if (valid_years == 0) { + valid_years = 1; + } + + not_after_adj = not_before_adj + valid_years * 365 * 24 * 60 * 60; + } + + timeret = X509_time_adj(X509_get_notAfter(cert), not_after_adj, ¬_after); + if (timeret == nullptr) { + throw OpenSSLError("adjusting certificate not after validity failed"); + } +} + +static auto createCert( + const CertData& cert_data, + const X509* issuer_cert, + const EVP_PKEY* issuer_pkey, + const EVP_PKEY* pkey +) -> bssl::UniquePtr<X509> { + const EVP_MD* hash_alg = nullptr; + if (cert_data.Algorithms.HashAlg.empty()) { + hash_alg = EVP_get_digestbyname(certDefaultHashAlg); + } else { + hash_alg = EVP_get_digestbyname(cert_data.Algorithms.HashAlg.data()); + } + + if (hash_alg == nullptr) { + throw crypto::OpenSSLError("cannot find hash algorithm"); + } + + auto cert = bssl::UniquePtr<X509>(X509_new()); + + if (issuer_cert == nullptr || issuer_pkey == nullptr) { + issuer_cert = cert.get(); + issuer_pkey = pkey; + } + + // set public key + auto r = X509_set_pubkey(cert.get(), const_cast<EVP_PKEY*>(pkey)); + if (r != 1) { + throw OpenSSLError("set public key to certificate failed"); + } + + // set certificate version + r = X509_set_version(cert.get(), certVersion); + if (r != 1) { + throw OpenSSLError("set version to certificate failed"); + } + + // set serial number + auto serial_number = cert_data.SerialNumber; + if (serial_number == 0) { + serial_number = crypto::RandUint64(); + } + r = ASN1_INTEGER_set_uint64(X509_get_serialNumber(cert.get()), serial_number); + if (r != 1) { + throw OpenSSLError("set certificate serial number"); + } + + // set subject + setCertSubject(cert_data, cert.get()); + + // set issuer name + auto issuer_name = X509_get_subject_name(const_cast<X509*>(issuer_cert)); + r = X509_set_issuer_name(cert.get(), issuer_name); + if (r != 1) { + throw OpenSSLError("set certificate issuer name failed"); + } + + // set validity + setCertValidity(cert_data, cert.get()); + + // add extensions + addCertExtensions(cert_data, issuer_cert, cert.get()); + + r = X509_sign(cert.get(), const_cast<EVP_PKEY*>(issuer_pkey), hash_alg); + if (r == 0) { + throw OpenSSLError("certificate sign failed"); + } + + return cert; +} + +auto CreateCert( + const CertData& cert_data, + const X509* issuer_cert, + const EVP_PKEY* issuer_pkey, + const EVP_PKEY* pkey +) -> bssl::UniquePtr<X509> { + + return createCert(cert_data, issuer_cert, issuer_pkey, pkey); +} + +auto CreateSelfSignedCert( + const CertData& cert_data, + const EVP_PKEY* pkey +) -> bssl::UniquePtr<X509> { + + return createCert(cert_data, nullptr, nullptr, pkey); +} + +auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO> { + bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem())); + if (!mem) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto r = PEM_write_bio_X509(mem.get(), const_cast<X509*>(cert)); + if (r != 1) { + throw OpenSSLError("exporting certificate to PEM failed"); + } + + return mem; +} + +auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509> { + bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size())); + if (mem == nullptr) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto cert = bssl::UniquePtr<X509>(PEM_read_bio_X509(mem.get(), nullptr, nullptr, nullptr)); + if (cert == nullptr) { + throw OpenSSLError("importing certificate from PEM failed"); + } + + return cert; +} + +void PrintCert(std::ostream& os, const X509* cert) { + bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem())); + if (!mem) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto r = X509_print(mem.get(), const_cast<X509*>(cert)); + if (r != 1) { + throw OpenSSLError("printing certificate failed"); + } + + os << bio::View(mem.get()).String(); +} + +} // vereign::crypto::cert diff --git a/cpp/src/vereign/crypto/cert.hh b/cpp/src/vereign/crypto/cert.hh new file mode 100644 index 0000000000000000000000000000000000000000..6a5ed4e010a68dce886223936afbb6f537ded563 --- /dev/null +++ b/cpp/src/vereign/crypto/cert.hh @@ -0,0 +1,170 @@ +#ifndef __VEREIGN_CRYPTO_CERT_HH +#define __VEREIGN_CRYPTO_CERT_HH + +#include <vereign/bytes/buffer.hh> + +#include <openssl/base.h> +#include <openssl/x509.h> +#include <chrono> + +namespace vereign::crypto::cert { + +/** + * Certificate algorithms configuration used for creating certificates. + * + * Used in CertData struct. + */ +struct CertAlgorithms { + /** + * The hash algorithm, if it is empty the default SHA256 will be used. + */ + std::string HashAlg; + + // FIXME: SignAlg is unused + std::string SignAlg; + // FIXME: KeyLength is unused + int KeyLength; +}; + +/** + * Certificate subject configuration used for creating certificates. + * + * All fields are optional for leaf, recommended for CA. + * + * Used in CertData struct. + */ +struct CertSubject { + std::string CommonName; + std::string Country; + std::string Locality; + std::string State; + std::string Organization; + std::string OrganizationUnit; +}; + +/** + * Certificate validity configuration used for creating certificates. + * + * Used in CertData struct. + */ +struct CertValidity { + /** + * If 0, the default is today at 00:00:00. + */ + time_t NotBefore; + + /** + * If 0, the default is NotBefore + ValidYears at 23:59:59. + */ + time_t NotAfter; + + /** + * If 0, the default is 1. + */ + int ValidYears; +}; + +/** + * Certificate configuration used for creating certificates. + * + * @see CreateCert + * @see CreateSelfSignedCert + */ +struct CertData { + CertAlgorithms Algorithms; + CertSubject Subject; + CertValidity Validity; + + /** + * If 0, the default is the current milliseconds timestamp - the number of milliseconds since epoch. + */ + uint64_t SerialNumber; + + /** + * Added to DN and Subject Alternative Name extension. + * Optional for CA. Mandatory for leaf certificate, used for email protection. + */ + std::string Email; + + /** + * Optional, recommended for CA, added to Subject Alternative Name extension. + */ + std::string Url; + + /** + * Denotes if the created certificate is CA or leaf certificate. The default is false. + */ + bool IsCA; +}; + +/** + * Creates and signs a certificate. + * + * @param cert_data Certificate configuration. + * @param issuer_cert The issuer certificate. + * @param issuer_pkey The issuer private key. + * @param pkey The private key for the newly created certificate. + * @returns new certificate signed by the provided issuer. + * + * @throws crypto::OpenSSLError on failure. + */ +auto CreateCert( + const CertData& cert_data, + const X509* issuer_cert, + const EVP_PKEY* issuer_pkey, + const EVP_PKEY* pkey +) -> bssl::UniquePtr<X509>; + +/** + * Creates self signed certificate. + * + * @param cert_data Certificate configuration. + * @param pkey The private key for the newly created certificate. + * @returns the new certificate signed by its own private key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto CreateSelfSignedCert( + const CertData& cert_data, + const EVP_PKEY* pkey +) -> bssl::UniquePtr<X509>; + +/** + * Exports certificate to PEM format. + * + * @param cert The certificate to export. + * @returns a memory BIO with the exported certificate. + */ +auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO>; + +/** + * Exports certificate to PEM format into string. + * + * @param cert The certificate to export. + * @returns a string with the exported certificate. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ExportCertToPEMString(const X509* cert) -> std::string; + +/** + * Imports certificate from PEM format. + * + * @param pem PEM encoded certificate. + * @returns imported X509 certificate. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509>; + +/** + * Print textual representation of the certificate into output stream. + * + * @param os The stream where to write the textual representation of the certificate. + * @param cert The certificate. + */ +void PrintCert(std::ostream& os, const X509* cert); + +} // vereign::crypto::cert + +#endif // __VEREIGN_CRYPTO_CERT_HH diff --git a/cpp/src/vereign/crypto/digest.cc b/cpp/src/vereign/crypto/digest.cc new file mode 100644 index 0000000000000000000000000000000000000000..ab9ed2567189b0a1cb68ab092c5afd2725408901 --- /dev/null +++ b/cpp/src/vereign/crypto/digest.cc @@ -0,0 +1,33 @@ +#include <vereign/crypto/digest.hh> + +#include <vereign/crypto/errors.hh> + +#include <openssl/base.h> +#include <openssl/evp.h> + +namespace { + constexpr int sha1SizeBytes = 20; +} + +namespace vereign::crypto::digest { + +void sha1(bytes::View src, bytes::Buffer& result) { + bssl::UniquePtr<EVP_MD_CTX> ctx(EVP_MD_CTX_new()); + if (!ctx) { + throw OpenSSLError("evp digest context cannot be created"); + } + + auto r = EVP_DigestInit_ex(ctx.get(), EVP_sha1(), nullptr); + if (r != 1) { + throw OpenSSLError("sha1 init failed"); + } + + result.Reserve(sha1SizeBytes); + + EVP_DigestUpdate(ctx.get(), src.Data(), src.Size()); + EVP_DigestFinal_ex(ctx.get(), result.end(), nullptr); + + result.IncSize(sha1SizeBytes); +} + +} // vereign::crypto::digest diff --git a/cpp/src/vereign/crypto/digest.hh b/cpp/src/vereign/crypto/digest.hh new file mode 100644 index 0000000000000000000000000000000000000000..365d45e9af3fa5fd3be617d539aaa823ac210af0 --- /dev/null +++ b/cpp/src/vereign/crypto/digest.hh @@ -0,0 +1,28 @@ +#ifndef __VEREIGN_CRYPTO_DIGEST_HH +#define __VEREIGN_CRYPTO_DIGEST_HH + +#include <vereign/bytes/buffer.hh> + +namespace vereign::crypto::digest { + +/** + * Creates a SHA1 hash of given bytes. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * + * bytes::Buffer hash; + * crypto::digest::sha1(bytes::View(data), hash); + * @endcode + * + * @param src The input bytes. + * @param result The SHA1 hash of the input. + * + * @throws crypto::OpenSSLError on failure. + */ +void sha1(bytes::View src, bytes::Buffer& result); + +} // vereign::crypto::digest + +#endif // __VEREIGN_CRYPTO_DIGEST_HH diff --git a/cpp/src/vereign/crypto/errors.hh b/cpp/src/vereign/crypto/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..825af7e98baf03a468ace2ced84cea76960b5c4f --- /dev/null +++ b/cpp/src/vereign/crypto/errors.hh @@ -0,0 +1,43 @@ +#ifndef __VEREIGN_CRYPTO_ERRORS_HH +#define __VEREIGN_CRYPTO_ERRORS_HH + +#include <stdexcept> +#include <openssl/err.h> + +namespace vereign::crypto { + +/** + * The base error type for the namespace vereign::crypto. + */ +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error(what) + { + } +}; + +/** + * An error thrown by the crypto functions. + */ +class OpenSSLError : public Error { +public: + OpenSSLError(const std::string& what) + : Error(getOpenSSLErrorString(what)) + { + } + +private: + static auto getOpenSSLErrorString(const std::string& what) -> std::string { + auto error = ERR_get_error(); + if (error == 0) { + return what + ": no openssl error in the error queue"; + } + + return what + ": " + ERR_reason_error_string(error); + } +}; + +} // vereign::crypto + +#endif // __VEREIGN_CRYPTO_ERRORS_HH diff --git a/cpp/src/vereign/crypto/rand.cc b/cpp/src/vereign/crypto/rand.cc new file mode 100644 index 0000000000000000000000000000000000000000..f61274887d5a826a22e0db178c86564ae2adcad8 --- /dev/null +++ b/cpp/src/vereign/crypto/rand.cc @@ -0,0 +1,37 @@ +#include <vereign/crypto/rand.hh> + +#include <openssl/rand.h> +#include <vereign/crypto/errors.hh> + +namespace vereign::crypto { + +void Rand(bytes::Buffer& buf, std::size_t size) { + buf.Reserve(size); + int result = RAND_bytes(buf.end(), size); + if (result == 0) { + ERR_clear_error(); + throw Error("crypto rand failed"); + } + + buf.IncSize(size); +} + +auto Rand(std::size_t size) -> bytes::Buffer { + bytes::Buffer buf{size}; + Rand(buf, size); + + return buf; +} + +auto RandUint64() -> uint64_t { + uint64_t x = 0; + int result = RAND_bytes((uint8_t*) &x, sizeof(x)); + if (result == 0) { + ERR_clear_error(); + throw Error("crypto rand failed"); + } + + return x; +} + +} // vereign::crypto diff --git a/cpp/src/vereign/crypto/rand.hh b/cpp/src/vereign/crypto/rand.hh new file mode 100644 index 0000000000000000000000000000000000000000..e377ac88ce42544b57f337ef82f8ba7c9b24f71c --- /dev/null +++ b/cpp/src/vereign/crypto/rand.hh @@ -0,0 +1,57 @@ +#ifndef __VEREIGN_CRYPTO_RAND_HH +#define __VEREIGN_CRYPTO_RAND_HH + +#include <vereign/bytes/buffer.hh> + +namespace vereign::crypto { + +/** + * Appends a random bytes to a buffer. + * + * After the operation is finished, the passed `buf` will be with incremented size of the newly + * added random bytes. + * + * Example: + * @code + * bytes::Bytes buf; + * crypto::Rand(buf, 16); + * + * assert(buf.Size() == 16); + * @endcode + * + * @param buf The buffer that will be filled with random bytes. + * @param size The number of the random bytes. + * + * @throws crypto::Error on failure. + */ +void Rand(bytes::Buffer& buf, std::size_t size); + +/** + * Generate random bytes. + * + * Example: + * @code + * auto buf = crypto::Rand(16); + * + * assert(buf.Size() == 16); + * @endcode + * + * @param size The number of the random bytes. + * @returns buffer with the generated random bytes. + * + * @throws crypto::Error on failure. + */ +auto Rand(std::size_t size) -> bytes::Buffer; + +/** + * Generates random uint64_t. + * + * @returns random unsigned 64 bit integer. + * + * @throws crypto::Error on failure. + */ +auto RandUint64() -> uint64_t; + +} // vereign::crypto + +#endif // __VEREIGN_CRYPTO_RAND_HH diff --git a/cpp/src/vereign/crypto/rsa.cc b/cpp/src/vereign/crypto/rsa.cc new file mode 100644 index 0000000000000000000000000000000000000000..2c89dfcb8078be5b4f232a33eb9c23781690b8cc --- /dev/null +++ b/cpp/src/vereign/crypto/rsa.cc @@ -0,0 +1,174 @@ +#include <vereign/crypto/rsa.hh> + +#include <vereign/bytes/view.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/bytes/buffer.hh> +#include <vereign/crypto/bio.hh> + +#include <openssl/base.h> +#include <openssl/bn.h> +#include <openssl/rsa.h> +#include <openssl/mem.h> +#include <openssl/evp.h> +#include <openssl/err.h> +#include <openssl/pem.h> + +namespace vereign::crypto::rsa { + +auto GenerateKey(int bits) -> bssl::UniquePtr<EVP_PKEY> { + bssl::UniquePtr<BIGNUM> bn{BN_new()}; + if (!bn) { + throw OpenSSLError("rsa key generation failed"); + } + + auto r = BN_set_word(bn.get(), RSA_F4); + if (r != 1) { + throw OpenSSLError("rsa key generation failed"); + } + + bssl::UniquePtr<RSA> rsa{RSA_new()}; + r = RSA_generate_key_ex(rsa.get(), bits, bn.get(), nullptr); + if (r != 1) { + throw OpenSSLError("rsa key generation failed"); + } + + bssl::UniquePtr<EVP_PKEY> key(EVP_PKEY_new()); + if (key == nullptr) { + throw OpenSSLError("creating key failed"); + } + + r = EVP_PKEY_assign_RSA(key.get(), rsa.release()); + if (r != 1) { + throw OpenSSLError("rsa key assign to evp failed"); + } + + return key; +} + +void PublicKeyEncrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& encrypted) { + bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(key, nullptr)); + if (!ctx) { + throw OpenSSLError("creating evp ctx failed"); + } + + auto r = EVP_PKEY_encrypt_init(ctx.get()); + if (r != 1) { + throw OpenSSLError("encrypt init failed"); + } + + r = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING); + if (r != 1) { + throw OpenSSLError("pkey padding init failed"); + } + + std::size_t outlen = 0; + r = EVP_PKEY_encrypt(ctx.get(), nullptr, &outlen, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("determining ciphertext size failed"); + } + + encrypted.Reserve(outlen); + + r = EVP_PKEY_encrypt(ctx.get(), encrypted.end(), &outlen, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("encrypting failed"); + } + + encrypted.IncSize(outlen); +} + +void PrivateKeyDecrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& decrypted) { + bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(key, nullptr)); + if (!ctx) { + throw OpenSSLError("creating evp ctx failed"); + } + + auto r = EVP_PKEY_decrypt_init(ctx.get()); + if (r != 1) { + throw OpenSSLError("decrypt init failed"); + } + + r = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING); + if (r != 1) { + throw OpenSSLError("init pkey padding failed"); + } + + std::size_t outlen = 0; + r = EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("determining decrypted buffer size failed"); + } + + decrypted.Reserve(outlen); + + r = EVP_PKEY_decrypt(ctx.get(), decrypted.end(), &outlen, src.Data(), src.Size()); + if (r != 1) { + throw OpenSSLError("decrypting failed"); + } + + decrypted.IncSize(outlen); +} + +auto ExportPublicKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO> { + bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem())); + if (!mem) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto r = PEM_write_bio_PUBKEY(mem.get(), key); + if (r != 1) { + throw OpenSSLError("exporting public key to PEM failed"); + } + + return mem; +} + +auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> { + bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size())); + if (mem == nullptr) { + throw OpenSSLError("creating memory buffer failed"); + } + + bssl::UniquePtr<EVP_PKEY> key(PEM_read_bio_PUBKEY(mem.get(), nullptr, nullptr, nullptr)); + if (key == nullptr) { + throw OpenSSLError("importing public key from PEM failed"); + } + + return key; +} + +auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO> { + bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem())); + if (!mem) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto r = PEM_write_bio_PrivateKey(mem.get(), key, nullptr, nullptr, 0, nullptr, nullptr); + if (r != 1) { + throw OpenSSLError("exporting private key to PEM failed"); + } + + return mem; +} + +auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string { + auto key_bio = ExportPublicKeyToPEM(key); + + return std::string{crypto::bio::View(key_bio.get()).String()}; +} + +auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> { + bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size())); + if (mem == nullptr) { + throw OpenSSLError("creating memory buffer failed"); + } + + auto key_ptr = PEM_read_bio_PrivateKey(mem.get(), nullptr, nullptr, nullptr); + if (key_ptr == nullptr) { + throw OpenSSLError("importing private key from PEM failed"); + } + + return bssl::UniquePtr<EVP_PKEY>(key_ptr); +} + +} // vereign::crypto::rsa diff --git a/cpp/src/vereign/crypto/rsa.hh b/cpp/src/vereign/crypto/rsa.hh new file mode 100644 index 0000000000000000000000000000000000000000..03a6c136503f3b0bfdda1b1df80d3a8a640b80f1 --- /dev/null +++ b/cpp/src/vereign/crypto/rsa.hh @@ -0,0 +1,164 @@ +#ifndef __VEREIGN_CRYPTO_RSA_HH +#define __VEREIGN_CRYPTO_RSA_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/buffer.hh> + +#include <openssl/base.h> +#include <openssl/evp.h> + +namespace vereign::crypto::rsa { + +/** + * Generates new RSA key. + * + * Example: + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * @endcode + * + * @param bits The length of the key. + * @returns the newly generated key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto GenerateKey(int bits) -> bssl::UniquePtr<EVP_PKEY>; + +/** + * Encrypts given bytes with RSA public key. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto key = crypto::rsa::GenerateKey(2048); + * bytes::Buffer encrypted; + * + * crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted); + * + * bytes::Buffer decrypted; + * crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + * + * assert(decrypted.View() == bytes.View(input)); + * @endcode + * + * @param key The RSA key. + * @param src The bytes that will be encrypted. + * @param encrypted The result of the encryption. + * + * @throws crypto::OpenSSLError on failure. + */ +void PublicKeyEncrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& encrypted); + +/** + * Decrypts given bytes with RSA private key. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto key = crypto::rsa::GenerateKey(2048); + * bytes::Buffer encrypted; + * + * crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted); + * + * bytes::Buffer decrypted; + * crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + * + * assert(decrypted.View() == bytes.View(input)); + * @endcode + * + * @param key The RSA key. + * @param src The bytes that will be decrypted. + * @param decrypted The result of the decryption. + * + * @throws crypto::OpenSSLError on failure. + */ +void PrivateKeyDecrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& decrypted); + +/** + * Exports a public key part to PEM format. + * + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * + * auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get()); + * std::cout << crypto::bio::View(bio.get()).String() << std::endl; + * @endcode + * + * @param key The key to export. + * @returns a memory BIO with the exported key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ExportPublicKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>; + +/** + * Import public key from PEM format. + * + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * + * auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get()); + * auto imported_key = crypto::rsa::ImportPublicKeyFromPEM(crypto::bio::View(bio.get())); + * @endcode + * + * @param pem PEM encoded key. + * @returns imported key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>; + +/** + * Export private key from PEM format. + * + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * + * auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get()); + * std::cout << crypto::bio::View(bio.get()).String() << std::endl; + * @endcode + * + * @param key The key to export. + * @returns a memory BIO with the exported key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>; + +/** + * Export private key from PEM format. + * + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * + * auto pem = crypto::rsa::ExportPrivateKeyToPEMString(key.get()); + * std::cout << pem << std::endl; + * @endcode + * + * @param key The key to export. + * @returns the exported key in PEM format. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string; + +/** + * Import private key from PEM format. + * + * @code + * auto key = crypto::rsa::GenerateKey(2048); + * + * auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get()); + * auto imported_key = crypto::rsa::ImportPrivateKeyFromPEM(crypto::bio::View(bio.get())); + * @endcode + * + * @param pem PEM encoded key. + * @returns imported key. + * + * @throws crypto::OpenSSLError on failure. + */ +auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>; + +} // vereign::crypto::rsa + +#endif // __VEREIGN_CRYPTO_RSA_HH diff --git a/cpp/src/vereign/encoding/base64.cc b/cpp/src/vereign/encoding/base64.cc new file mode 100644 index 0000000000000000000000000000000000000000..88e1e539f83ac5c8cb960a500a7b605fde093fcc --- /dev/null +++ b/cpp/src/vereign/encoding/base64.cc @@ -0,0 +1,40 @@ +#include <vereign/encoding/base64.hh> + +#include <boost/beast/core/detail/base64.hpp> + +namespace vereign::encoding::base64 { + +void Encode(bytes::View src, bytes::Buffer& encoded) { + if (src.Size() == 0) { + return; + } + + encoded.Reserve(boost::beast::detail::base64::encoded_size(src.Size())); + auto written = boost::beast::detail::base64::encode(encoded.end(), src.Data(), src.Size()); + encoded.IncSize(written); +} + +auto EncodeToString(bytes::View src) -> std::string { + if (src.Size() == 0) { + return ""; + } + + std::string encoded; + encoded.resize(boost::beast::detail::base64::encoded_size(src.Size())); + auto written = boost::beast::detail::base64::encode(encoded.data(), src.Data(), src.Size()); + encoded.resize(written); + + return encoded; +} + +void Decode(bytes::View src, bytes::Buffer& decoded) { + if (src.Size() == 0) { + return ; + } + + decoded.Reserve(boost::beast::detail::base64::decoded_size(src.Size())); + auto written = boost::beast::detail::base64::decode(decoded.end(), src.CharData(), src.Size()); + decoded.IncSize(written.first); +} + +} // vereign::encoding::base64 diff --git a/cpp/src/vereign/encoding/base64.hh b/cpp/src/vereign/encoding/base64.hh new file mode 100644 index 0000000000000000000000000000000000000000..d3ee3ce4b13a22628dca0c1fe76653e762c3aacd --- /dev/null +++ b/cpp/src/vereign/encoding/base64.hh @@ -0,0 +1,65 @@ +#ifndef __VEREIGN_ENCODING_BASE64_HH +#define __VEREIGN_ENCODING_BASE64_HH + +#include <vereign/bytes/buffer.hh> +#include <string> + +namespace vereign::encoding::base64 { + +/** + * Encodes source bytes into base64 encoding. + * + * No new lines are inserted. + * + * Example: + * @code + * std::string s{"foob"}; + * bytes::Buffer encoded; + * encoding::base64::Encode(bytes::View(s), encoded); + * + * assert(encoded.View().String() == "Zm9vYg==") + * @endcode + * + * @param src The source bytes that will be encoded. + * @param encoded The encoded bytes. + */ +void Encode(bytes::View src, bytes::Buffer& encoded); + +/** + * Encodes source bytes into base64 encoding. + * + * No new lines are inserted. + * + * Example: + * @code + * std::string s{"foob"}; + * auto encoded = encoding::base64::Encode(bytes::View(s)); + * + * assert(encoded == "Zm9vYg==") + * @endcode + * + * @param src The source bytes that will be encoded. + * @returns the encoded string. + */ +auto EncodeToString(bytes::View src) -> std::string; + +/** + * Decodes base64 encoded bytes. + * + * Example: + * @code + * std::string s{"Zm9vYg=="}; + * bytes::Buffer decoded; + * encoding::base64::Decode(bytes::View(s), decoded); + * + * assert(decoded.View().String() == "foob"); + * @endcode + * + * @param src The base64 encoded bytes that will be decoded. + * @param decoded The decoded bytes. + */ +void Decode(bytes::View src, bytes::Buffer& decoded); + +} // vereign::encoding::base64 + +#endif // __VEREIGN_ENCODING_BASE64_HH diff --git a/cpp/src/vereign/encoding/binary.cc b/cpp/src/vereign/encoding/binary.cc new file mode 100644 index 0000000000000000000000000000000000000000..3c25446ccf77e123d6c19ec71578a38ab895c34e --- /dev/null +++ b/cpp/src/vereign/encoding/binary.cc @@ -0,0 +1,73 @@ +#include <vereign/encoding/binary.hh> + +#include <vereign/encoding/errors.hh> + +namespace vereign::encoding::binary { + +void EncodeUint8(bytes::Buffer& out, uint8_t v) { + out.Reserve(1); + out.end()[0] = v; + out.IncSize(1); +} + +auto DecodeUint8(bytes::View& b) -> uint8_t { + if (b.Size() < 1) { + throw encoding::Error("decoding failed the input size is less than 1 bytes"); + } + + return b.Data()[0]; +} + +void EncodeUint64(uint8_t* out, uint64_t v) { + out[0] = uint8_t(v); + out[1] = uint8_t(v >> 8); + out[2] = uint8_t(v >> 16); + out[3] = uint8_t(v >> 24); + out[4] = uint8_t(v >> 32); + out[5] = uint8_t(v >> 40); + out[6] = uint8_t(v >> 48); + out[7] = uint8_t(v >> 56); +} + +void EncodeUint64(bytes::Buffer& out, uint64_t v) { + out.Reserve(8); + EncodeUint64(out.end(), v); + out.IncSize(8); +} + +auto DecodeUint64(const uint8_t* b) -> uint64_t { + return uint64_t(b[0]) + | uint64_t(b[1]) << 8 + | uint64_t(b[2]) << 16 + | uint64_t(b[3]) << 24 + | uint64_t(b[4]) << 32 + | uint64_t(b[5]) << 40 + | uint64_t(b[6]) << 48 + | uint64_t(b[7]) << 56; +} + +auto DecodeUint64(bytes::View b) -> uint64_t { + if (b.Size() < 8) { + throw encoding::Error("decoding failed the input size is less than 8 bytes"); + } + + return DecodeUint64(b.Data()); +} + +void EncodeBytes(bytes::Buffer& out, bytes::View bytes) { + EncodeUint64(out, uint64_t(bytes.Size())); + out.Write(bytes); +} + +auto DecodeBytes(bytes::View b, bytes::View& out) -> std::size_t { + auto size = DecodeUint64(b); + if (size < 0 || size + 8 > b.Size()) { + throw encoding::Error("decode bytes failed: invalid size"); + } + + out = b.Slice(8, size + 8); + + return 8 + size; +} + +} // namespace vereign::encoding::binary diff --git a/cpp/src/vereign/encoding/binary.hh b/cpp/src/vereign/encoding/binary.hh new file mode 100644 index 0000000000000000000000000000000000000000..8be71c31f3a76f1c4c36c7e631f8731253edd64d --- /dev/null +++ b/cpp/src/vereign/encoding/binary.hh @@ -0,0 +1,101 @@ +#ifndef __VEREIGN_ENCODING_BINARY_HH +#define __VEREIGN_ENCODING_BINARY_HH + +#include <stdexcept> +#include <vereign/bytes/buffer.hh> + +/** + * Binary encoding of integers, bytes. + * + * The integers are encoded in little endian. + */ +namespace vereign::encoding::binary { + +/** + * Encodes a byte. + * + * On success the out buffer size will be incremented with 1. + * + * @param out buffer where the byte will be written. + * @param v the byte that will be encoded. + */ +void EncodeUint8(bytes::Buffer& out, uint8_t v); + +/** + * Decodes a byte. + * + * @param b the source bytes from where the single byte will be decoded. + */ +auto DecodeUint8(bytes::View& b) -> uint8_t; + +/** + * Encodes a uint64. + * + * The number is encoded in little endian. + * + * @param out where the value will be written. + * @param v the number that will be encoded. + */ +void EncodeUint64(uint8_t* out, uint64_t v); + +/** + * Encodes a uint64. + * + * On success the out buffer size will be incremented with 8. + * The number is encoded in little endian. + * + * @param out where the value will be written. + * @param v the number that will be encoded. + */ +void EncodeUint64(bytes::Buffer& out, uint64_t v); + +/** + * Decodes a uint64. + * + * The number is expected to be encoded in little endian. + * + * @param b the source number. + * @returns the decoded number. + */ +auto DecodeUint64(const uint8_t* b) -> uint64_t; + +/** + * Decodes a uint64. + * + * The number is expected to be encoded in little endian. + * + * @param b the source bytes where the encoded number will be read from. + * @returns the decoded number. + * + * @throws encoding::Error when the source bytes size is less than 8. + */ +auto DecodeUint64(bytes::View b) -> uint64_t; + +/** + * Encodes bytes. + * + * The result is 8 bytes for the size, followed by the bytes them self. + * The out buffer size is incremented with out.Size() + 8. + * + * @param out the where the encoded value will be written. + * @param bytes the source bytes that will be encoded. + */ +void EncodeBytes(bytes::Buffer& out, bytes::View bytes); + +/** + * Decode bytes. + * + * Note that the decoded bytes reference to the source buffer. + * In other words it is only a view, there is no copying involved. + * + * @param b the source with the encoded bytes. + * @param out the decoded bytes. + * @returns the bytes read during the decoding (8 + out.Size()). + * + * @throws encoding::Error when the decoding fails. + */ +auto DecodeBytes(bytes::View b, bytes::View& out) -> std::size_t; + +} // namespace vereign::encoding::binary + +#endif // __VEREIGN_ENCODING_BINARY_HH diff --git a/cpp/src/vereign/encoding/errors.hh b/cpp/src/vereign/encoding/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..d9d012210165fb645f600dc60a989dfb5ffcb392 --- /dev/null +++ b/cpp/src/vereign/encoding/errors.hh @@ -0,0 +1,21 @@ +#ifndef __VEREIGN_ENCODING_ERRORS_HH +#define __VEREIGN_ENCODING_ERRORS_HH + +#include <stdexcept> + +namespace vereign::encoding { + +/** + * Base error for namespace vereign::encoding. + */ +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error(what) + { + } +}; + +} // namespace vereign::encoding::binary + +#endif // __VEREIGN_ENCODING_ERRORS_HH diff --git a/cpp/src/vereign/encoding/hex.cc b/cpp/src/vereign/encoding/hex.cc new file mode 100644 index 0000000000000000000000000000000000000000..46ed21564dbd5e8cc363e43e04123a9c6a5bddaf --- /dev/null +++ b/cpp/src/vereign/encoding/hex.cc @@ -0,0 +1,71 @@ +#include <vereign/encoding/hex.hh> + +namespace vereign::encoding::hex { + +namespace detail { + +auto charToInt(char ch) -> int { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + + if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + + if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + + return 0; +} + +} // namespace detail + +void Encode(bytes::View src, bytes::Buffer& encoded) { + if (src.Size() == 0) { + return; + } + + static const char* nibbles = { "0123456789abcdef" }; + + encoded.Reserve(src.Size() * 2); + + for (std::size_t i = 0; i < src.Size(); ++i) { + encoded[i*2] = nibbles[src[i] >> 4]; + encoded[i*2 + 1] = nibbles[src[i] & 0x0F]; + } + + encoded.IncSize(src.Size() * 2); +} + +void Decode(bytes::View src, bytes::Buffer& decoded) { + if (src.Size() == 0) { + return ; + } + + decoded.Reserve(src.Size() / 2); + for (int i = 0, len = (int) src.Size() - 1; i < len; i += 2) { + decoded[i/2] = detail::charToInt(src[i]) * 16 + detail::charToInt(src[i + 1]); + } + decoded.IncSize(src.Size() / 2); +} + +void EncodeReverse(bytes::View src, bytes::Buffer& encoded) { + if (src.Size() == 0) { + return; + } + + static const char* nibbles = { "0123456789abcdef" }; + + encoded.Reserve(src.Size() * 2); + + for (std::size_t i = 0; i < src.Size(); ++i) { + encoded[i*2] = nibbles[src[src.Size() - 1 - i] >> 4]; + encoded[i*2 + 1] = nibbles[src[src.Size() - 1 - i] & 0x0F]; + } + + encoded.IncSize(src.Size() * 2); +} + +} // vereign::encoding::base64 diff --git a/cpp/src/vereign/encoding/hex.hh b/cpp/src/vereign/encoding/hex.hh new file mode 100644 index 0000000000000000000000000000000000000000..6dec0f13270bae1d163d7f9114e56276f082515d --- /dev/null +++ b/cpp/src/vereign/encoding/hex.hh @@ -0,0 +1,59 @@ +#ifndef __VEREIGN_ENCODING_HEX_HH +#define __VEREIGN_ENCODING_HEX_HH + +#include <vereign/bytes/buffer.hh> +#include <string> + +namespace vereign::encoding::hex { + +/** + * Encodes bytes in hexadecimal. + * + * Example: + * @code + * bytes::Buffer encoded; + * encoding::hex::Decode(bytes::View("foobar"), encoded); + * + * assert(encoded.View().String() == "666f6f626172"); + * @endcode + * + * @param src The bytes that will be encoded. + * @param encoded The buffer where the encoded bytes will be written. + */ +void Encode(bytes::View src, bytes::Buffer& encoded); + +/** + * Decodes hexadecimal encoded bytes. + * + * Example: + * @code + * bytes::Buffer decoded; + * encoding::hex::Decode(bytes::View("666f6f626172"), decoded); + * + * assert(decoded.View().String() == "foobar"); + * @endcode + * + * @param src The bytes that will be decoded. + * @param encoded The buffer where the decoded bytes will be written. + */ +void Decode(bytes::View src, bytes::Buffer& decoded); + +/** + * Encodes bytes in hexadecimal in reverse order. + * + * Example: + * @code + * bytes::Buffer encoded; + * encoding::hex::Decode(bytes::View("foobar"), encoded); + * + * assert(encoded.View().String() == "7261626f6f66"); + * @endcode + * + * @param src The bytes that will be encoded. + * @param encoded The buffer where the encoded bytes will be written. + */ +void EncodeReverse(bytes::View src, bytes::Buffer& encoded); + +} // vereign::encoding::hex + +#endif // __VEREIGN_ENCODING_HEX_HH diff --git a/cpp/src/vereign/event/broker.cc b/cpp/src/vereign/event/broker.cc new file mode 100644 index 0000000000000000000000000000000000000000..f86e3e3932ec44b7d2cd0e943e5d1f2bcc499bab --- /dev/null +++ b/cpp/src/vereign/event/broker.cc @@ -0,0 +1,266 @@ +#include <vereign/event/broker.hh> + +#include <vereign/restapi/client_session.hh> +#include <vereign/identity/provider.hh> + +namespace { + +const auto pollInterval = std::chrono::seconds(1); + +const auto eventsModeEntity = std::string{"entity"}; +const auto eventsModeDeviceKey = std::string{"devicekey"}; + +const auto getNewEventsWithoutSessionPath = std::string{"/event/getNewEventsWithoutSession"}; +const auto getNewEventsPath = std::string{"/event/getNewEvents"}; + +const auto updateLastViewedWithoutSessionPath = std::string{"/event/updateLastViewedWithoutSession"}; +const auto updateLastViewedPath = std::string{"/event/updateLastViewed"}; + +auto eventBatchTime(vereign::client_library::GetNewEventsFormResponse& resp) -> int64_t { + int64_t max_time = 0; + for (auto& event : resp.data()) { + max_time = std::max(max_time, event.stamp()); + } + + return max_time; +} + +} + +namespace vereign::event { + +Broker::Broker(restapi::ClientSession& client_session) + : client_session_{client_session} +{ + entity_req_.set_mode(eventsModeEntity); + device_req_.set_mode(eventsModeDeviceKey); + + update_entity_req_.set_mode(eventsModeEntity); + update_device_req_.set_mode(eventsModeDeviceKey); + + dispatch_thread_ = std::thread([this] { + pollAndDispatch(); + }); +} + +auto Broker::Subscribe() -> Subscription { + auto Subscribe() -> Subscription; + auto channel = std::make_shared<sync::Channel<EventTypeSharedPtr>>(10); + + uint64_t id = 0; + + { + std::lock_guard<std::mutex> lock{mu_}; + + id = ++next_subscription_id_; + subscriptions_[id] = Subscription{}; + subscriptions_[id].ID = id; + subscriptions_[id].EventsChannel = channel; + } + + cv_.notify_one(); + + return subscriptions_[id]; +} + +void Broker::Unsubscribe(const Subscription& subscription) { + std::unique_lock<std::mutex> lock{mu_}; + + auto it = subscriptions_.find(subscription.ID); + if (it == subscriptions_.end()) { + return; + } + + it->second.EventsChannel->Close(); + + subscriptions_.erase(it); +} + +auto Broker::dispatchEvents(EventTypeSharedPtr events) -> int { + std::unique_lock<std::mutex> lock{mu_}; + cv_.wait(lock, [this] { + return subscriptions_.size() > 0; + }); + + int dispatched_cnt = 0; + + for (auto& s : subscriptions_) { + auto channel = s.second.EventsChannel; + + auto r = channel->TryAdd(events); + if (r.IsOk()) { + dispatched_cnt++; + } + } + + return dispatched_cnt; +} + +// NOTE: it is assumed that the mutex is already locked. +void Broker::closeAllSubscriptions() { + for (auto& s: subscriptions_) { + s.second.EventsChannel->Close(); + } +} + +void Broker::pollEventsWithoutSession() { + if (!client_session_.HasIdentity()) { + return; + } + + auto resp = std::make_shared<client_library::GetNewEventsFormResponse>(); + + auto result = client_session_.PublicPost( + getNewEventsWithoutSessionPath, + &device_req_, + resp.get() + ); + + result.wait(); + + // do not dispatch successful poll, when there are no new events + if (resp->code() == "200" && resp->data().size() == 0) { + return; + } + + auto cnt = dispatchEvents(resp); + // if nothing is dispatched, do not update the last viewed events + if (cnt == 0) { + return; + } + + auto time = eventBatchTime(*resp); + if (time == 0) { + return; + } + + update_device_req_.set_lastviewed(std::to_string(time)); + auto update_result = client_session_.PublicPost( + updateLastViewedWithoutSessionPath, + &update_device_req_, + &update_device_resp_ + ); + + update_result.wait(); + + // TODO: Add debug log here on failure. +} + +void Broker::pollEntityEvents() { + auto resp = std::make_shared<client_library::GetNewEventsFormResponse>(); + + auto result = client_session_.Post(getNewEventsPath, &entity_req_, resp.get()); + + result.wait(); + + // do not dispatch successful poll, when there are no new events + if (resp->code() == "200" && resp->data().size() == 0) { + return; + } + + auto cnt = dispatchEvents(resp); + // if nothing is dispatched, do not update the last viewed events + if (cnt == 0) { + return; + } + + auto time = eventBatchTime(*resp); + if (time == 0) { + return; + } + + update_entity_req_.set_lastviewed(std::to_string(time)); + auto update_result = client_session_.Post( + updateLastViewedPath, + &update_entity_req_, + &update_entity_resp_ + ); + + update_result.wait(); + + // TODO: Add debug log here on failure. +} + +void Broker::pollDeviceEvents() { + auto resp = std::make_shared<client_library::GetNewEventsFormResponse>(); + + auto result = client_session_.Post(getNewEventsPath, &device_req_, resp.get()); + + result.wait(); + + // do not dispatch successful poll, when there are no new events + if (resp->code() == "200" && resp->data().size() == 0) { + return; + } + + auto cnt = dispatchEvents(resp); + // if nothing is dispatched, do not update the last viewed events + if (cnt == 0) { + return; + } + + auto time = eventBatchTime(*resp); + if (time == 0) { + return; + } + + update_device_req_.set_lastviewed(std::to_string(time)); + auto update_result = client_session_.Post( + updateLastViewedPath, + &update_device_req_, + &update_device_resp_ + ); + + update_result.wait(); + + // TODO: Add debug log here on failure. +} + +void Broker::pollAndDispatch() { + for (;;) { + { + std::unique_lock<std::mutex> lock{mu_}; + cv_.wait(lock, [this] { + return subscriptions_.size() > 0 || stopped_ == true; + }); + + if (stopped_) { + closeAllSubscriptions(); + break; + } + } + + if (!client_session_.HasSession()) { + pollEventsWithoutSession(); + } else { + pollDeviceEvents(); + pollEntityEvents(); + } + + std::this_thread::sleep_for(pollInterval); + } +} + +void Broker::Shutdown() { + { + std::lock_guard<std::mutex> lock{mu_}; + + if (stopped_) { + return; + } + + stopped_ = true; + } + + cv_.notify_one(); + + if (dispatch_thread_.joinable()) { + dispatch_thread_.join(); + } +} + +Broker::~Broker() { + Shutdown(); +} + +} // namespace vereign::event diff --git a/cpp/src/vereign/event/broker.hh b/cpp/src/vereign/event/broker.hh new file mode 100644 index 0000000000000000000000000000000000000000..f8842dc209a989cb1d14f118dbc5ac63aa22a5e6 --- /dev/null +++ b/cpp/src/vereign/event/broker.hh @@ -0,0 +1,131 @@ +#ifndef __VEREIGN_EVENT_BROKER_HH +#define __VEREIGN_EVENT_BROKER_HH + +#include <vereign/sync/channel.hh> +#include <vereign/client_library/event_types.pb.h> +#include <vereign/client_library/internal/event_types.pb.h> +#include <vereign/client_library/common_types.pb.h> + +#include <thread> +#include <unordered_map> + +// forward declarations +namespace vereign::restapi { + +class ClientSession; + +} // namespace vereign + +namespace vereign::event { + +using EventType = client_library::GetNewEventsFormResponse; +using EventTypeSharedPtr = std::shared_ptr<EventType>; +using EventsChannelSharedPtr = std::shared_ptr<sync::Channel<EventTypeSharedPtr>>; + +/** + * Subscription that is returned, when calling Broker::Subscribe. + * + * Once the consumer is done consuming, it must call Broker::Unsubscribe, passing the + * Subscription object. + */ +struct Subscription { + /** + * Subscription id. + */ + uint64_t ID; + + /** + * Channel that the subscribers can use to receive dispatched events. + */ + EventsChannelSharedPtr EventsChannel; +}; + +/** + * Broker polls restful API for events and dispatches them to a set of subscribers. + * + * All public methods are thread safe. + */ +class Broker { +public: + /** + * Creates a broker. + * + * @param client_session The client session used for polling the restful API for new events. + */ + Broker(restapi::ClientSession& client_session); + + /** + * Shutdown the broker. + */ + ~Broker(); + + // disable copying + Broker(const Broker&) = delete; + auto operator=(const Broker&) -> Broker = delete; + + /** + * Subscribe for events. + * + * Once the consumer is done with the returned Subscription, it must call + * Broker::Unsubscribe, passing the Subscription object. + * + * @returns a subscription. + */ + auto Subscribe() -> Subscription; + + /** + * Remove a subscription. + * + * The broker stops to dispatch events to the `subscription` channel. + */ + void Unsubscribe(const Subscription& subscription); + + /** + * Shutdown the broker. + * + * All subscription channels will be closed. + * + * Shutdown is idempotent, meaning that it can be safely called more than once. + */ + void Shutdown(); + +private: + void pollEventsWithoutSession(); + void pollDeviceEvents(); + void pollEntityEvents(); + void pollAndDispatch(); + auto dispatchEvents(EventTypeSharedPtr events) -> int; + void closeAllSubscriptions(); + +private: + restapi::ClientSession& client_session_; + + std::thread dispatch_thread_; + + client_library::internal::GetNewEventsForm entity_req_; + + client_library::internal::GetNewEventsForm device_req_; + + client_library::internal::UpdateLastViewedForm update_entity_req_; + + client_library::internal::UpdateLastViewedForm update_device_req_; + + client_library::EmptyResponse update_entity_resp_; + + client_library::EmptyResponse update_device_resp_; + + // protects the fields that follow + std::mutex mu_; + + std::condition_variable cv_; + + bool stopped_ = false; + + int64_t next_subscription_id_ = 0; + + std::unordered_map<int64_t, Subscription> subscriptions_; +}; + +} // namespace vereign::event + +#endif // __VEREIGN_EVENT_BROKER_HH diff --git a/cpp/src/vereign/fs/errors.hh b/cpp/src/vereign/fs/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..ef72c8c00655fd544e4cca926ca1493f94161d73 --- /dev/null +++ b/cpp/src/vereign/fs/errors.hh @@ -0,0 +1,35 @@ +#ifndef __VEREIGN_FS_ERRORS_HH +#define __VEREIGN_FS_ERRORS_HH + +#include <string> +#include <stdexcept> + +namespace vereign::fs { + +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error{what} + { + } +}; + +class HomeNotFoundError : public Error { +public: + HomeNotFoundError() + : Error{"cannot find user's home dir"} + { + } +}; + +class TempDirNotFoundError : public Error { +public: + TempDirNotFoundError() + : Error{"cannot find user's temp dir"} + { + } +}; + +} // namespace vereign::fs + +#endif // __VEREIGN_FS_ERRORS_HH diff --git a/cpp/src/vereign/fs/operations.cc b/cpp/src/vereign/fs/operations.cc new file mode 100644 index 0000000000000000000000000000000000000000..9f33ed2d31d3322c2748ee32ba91d4b9c477f53f --- /dev/null +++ b/cpp/src/vereign/fs/operations.cc @@ -0,0 +1,29 @@ +#include <vereign/fs/operations.hh> + +#include <vereign/fs/util.hh> +#include <vereign/core/string.hh> + +#include <boost/filesystem/directory.hpp> +#include <boost/filesystem/operations.hpp> +#include <fstream> + +namespace vereign::fs { + +auto Exists(const std::string& path) -> bool { +#ifdef _WIN32 + std::ifstream f{string::widen(path)}; +#else + std::ifstream f{path}; +#endif + return f.good(); +} + +auto IsDir(const std::string& path) -> bool { + return boost::filesystem::is_directory(fs::detail::StringToPath(path)); +} + +auto CreateDir(const std::string& path) -> bool { + return boost::filesystem::create_directory(fs::detail::StringToPath(path)); +} + +} // namespace vereign::fs diff --git a/cpp/src/vereign/fs/operations.hh b/cpp/src/vereign/fs/operations.hh new file mode 100644 index 0000000000000000000000000000000000000000..e5f60c773a1eb9de85d76b32100e0c11d033c5bf --- /dev/null +++ b/cpp/src/vereign/fs/operations.hh @@ -0,0 +1,29 @@ +#ifndef __VEREIGN_FS_OPERATIONS_HH +#define __VEREIGN_FS_OPERATIONS_HH + +#include <string> + +namespace vereign::fs { + +/** + * Check if file exists and is readable. + */ +auto Exists(const std::string& path) -> bool; + +/** + * Check if given path is a directory. + * + * @param path Path to check. + */ +auto IsDir(const std::string& path) -> bool; + +/** + * Creates the last directory of the provided path. + * + * @param path Path of the directory that will be created. + */ +auto CreateDir(const std::string& path) -> bool; + +} // namespace vereign::fs + +#endif // __VEREIGN_FS_OPERATIONS_HH diff --git a/cpp/src/vereign/fs/path.cc b/cpp/src/vereign/fs/path.cc new file mode 100644 index 0000000000000000000000000000000000000000..fc92ce61945f17a34df5f3575b4605303733a978 --- /dev/null +++ b/cpp/src/vereign/fs/path.cc @@ -0,0 +1,37 @@ +#include <vereign/fs/path.hh> + +namespace vereign::fs::path { + +/** + * Convert relative path from host OS to unix format. + * + * It replaces the host path separator with the unix path separator. + * For example "foo\\bar" on windows becomes "foo/bar". + * + * @param path Source path. + */ +auto to_unix(std::string path) -> std::string { + if (sep[0] != unix_sep[0]) { + std::replace(path.begin(), path.end(), sep[0], unix_sep[0]); + } + + return path; +} + +/** + * Convert relative path from unix format to native OS format. + * + * It replaces the unix path separator with the native path separator. + * For example "foo/bar" on windows becomes "foo\\bar". + * + * @param path Source path. + */ +auto to_native(std::string path) -> std::string { + if (sep[0] != unix_sep[0]) { + std::replace(path.begin(), path.end(), unix_sep[0], sep[0]); + } + + return path; +} + +} // namespace vereign::fs::path diff --git a/cpp/src/vereign/fs/path.hh b/cpp/src/vereign/fs/path.hh new file mode 100644 index 0000000000000000000000000000000000000000..87d58adc00845b30d16ed155010a1ea23ca4b244 --- /dev/null +++ b/cpp/src/vereign/fs/path.hh @@ -0,0 +1,60 @@ +#ifndef __VEREIGN_FS_PATH_HH +#define __VEREIGN_FS_PATH_HH + +#include <boost/core/ignore_unused.hpp> +#include <algorithm> +#include <string> + +namespace vereign::fs::path { + +#if defined(_WIN32) + constexpr inline const char* unix_sep = "/"; + constexpr inline const char* sep = "\\"; + constexpr inline const char* prefix = "\\\\?\\"; + constexpr inline const char* drive_sep = ":\\"; + constexpr inline const char invalid_escape_char = '_'; +#else + constexpr inline const char* unix_sep = "/"; + constexpr inline const char* sep = "/"; + constexpr inline const char* prefix = "/"; + constexpr inline const char* drive_sep = ""; + constexpr inline const char invalid_escape_char = '_'; +#endif + + +/** + * Join path fragments with the file system path separator. + */ +template <typename... Args> +auto Join(const Args&... args) -> std::string { + std::string path; + int unpack[] { 0, (path = path + sep + args, 0)... }; + boost::ignore_unused(unpack); + path.erase(0, 1); + + return path; +} + +/** + * Convert relative path from host OS to unix format. + * + * It replaces the host path separator with the unix path separator. + * For example "foo\\bar" on windows becomes "foo/bar". + * + * @param path Source path. + */ +auto to_unix(std::string path) -> std::string; + +/** + * Convert relative path from unix format to native OS format. + * + * It replaces the unix path separator with the native path separator. + * For example "foo/bar" on windows becomes "foo\\bar". + * + * @param path Source path. + */ +auto to_native(std::string path) -> std::string; + +} // namespace vereign::fs::path + +#endif // __VEREIGN_FS_PATH_HH diff --git a/cpp/src/vereign/fs/util.cc b/cpp/src/vereign/fs/util.cc new file mode 100644 index 0000000000000000000000000000000000000000..8d60e3e827c1228e2f5db2a00444ebab66674db2 --- /dev/null +++ b/cpp/src/vereign/fs/util.cc @@ -0,0 +1,153 @@ +#include <vereign/fs/util.hh> + +#include <vereign/fs/operations.hh> +#include <vereign/fs/path.hh> +#include <vereign/fs/errors.hh> + +#include <vereign/core/rand.hh> +#include <vereign/core/string.hh> +#include <boost/filesystem/operations.hpp> +#include <boost/filesystem/fstream.hpp> + +// FIXME: remove +#include <iostream> + +#ifdef _WIN32 +# include <shlobj_core.h> +#endif + +namespace vereign::fs { + +namespace detail { + +auto PathToString(const boost::filesystem::path& path) -> std::string { +#ifdef _WIN32 + return string::narrow(path.wstring()); +#else + return path.string(); +#endif +} + +auto StringToPath(std::string_view path) -> boost::filesystem::path { +#ifdef _WIN32 + return boost::filesystem::path{string::widen(path)}; +#else + return boost::filesystem::path{std::string{path}}; +#endif +} + +} + +RemoveFileGuard::RemoveFileGuard(std::string path) + : path_{std::move(path)} +{ +} + +RemoveFileGuard::~RemoveFileGuard() { + boost::filesystem::remove(detail::StringToPath(path_)); +} + +RemoveAllGuard::RemoveAllGuard(std::string path) + : path_{std::move(path)} +{ +} + +RemoveAllGuard::~RemoveAllGuard() { + boost::filesystem::remove_all(detail::StringToPath(path_)); +} + +auto TempFilePath(std::string_view dir, std::string_view prefix) -> std::string { + return path::Join(std::string(dir), std::string(prefix) + core::RandLowerAlpha(10)); +} + +auto TempFilePath(std::string_view prefix) -> std::string { + return TempFilePath(detail::PathToString(boost::filesystem::temp_directory_path()), prefix); +} + +auto TempDir(std::string_view prefix) -> std::string { + auto dir = TempFilePath(prefix); + + CreateDir(dir); + + return dir; +} + +auto HomePath() -> std::string { +#ifdef _WIN32 + PWSTR home; + SHGetKnownFolderPath( + FOLDERID_LocalAppData, + 0, + nullptr, + &home + ); + + std::wstring wpath; + try { + wpath = home; + CoTaskMemFree(home); + } catch (...) { + CoTaskMemFree(home); + throw; + } + + return ""; + + auto path = boost::filesystem::path(wpath); + if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) { + return detail::PathToString(path); + } + + throw HomeNotFoundError{}; +#else + auto home = std::getenv("HOME"); + if (home == nullptr) { + throw HomeNotFoundError{}; + } + + auto path = boost::filesystem::path{home}; + if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) { + return detail::PathToString(path); + } + + throw HomeNotFoundError{}; +#endif +} + +auto ReadFile(std::string_view path) -> bytes::Buffer { + boost::filesystem::ifstream file{ + detail::StringToPath(path), + boost::filesystem::fstream::binary | boost::filesystem::fstream::ate, + }; + + if (!file.is_open()) { + throw fs::Error("open file for reading failed"); + } + + auto size = file.tellg(); + bytes::Buffer result{static_cast<size_t>(size)}; + file.seekg(0); + + file.read((char*) result.end(), size); + result.IncSize(file.gcount()); + + file.close(); + + return result; +} + +void WriteFile(std::string_view path, bytes::View data) { + boost::filesystem::ofstream file{ + detail::StringToPath(path), + boost::filesystem::fstream::binary | boost::filesystem::fstream::trunc, + }; + + if (!file.is_open()) { + throw fs::Error("open file for writing failed"); + } + + file.write(data.CharData(), data.Size()); + file.close(); +} + +} // namespace vereign::fs diff --git a/cpp/src/vereign/fs/util.hh b/cpp/src/vereign/fs/util.hh new file mode 100644 index 0000000000000000000000000000000000000000..d665902476cd8f2760a7729fe3649027463c13cc --- /dev/null +++ b/cpp/src/vereign/fs/util.hh @@ -0,0 +1,117 @@ +#ifndef __VEREIGN_FS_UTIL_HH +#define __VEREIGN_FS_UTIL_HH + +#include <vereign/bytes/buffer.hh> +#include <boost/filesystem/path.hpp> +#include <string> +#include <string_view> + +namespace vereign::fs { + +namespace detail { + +auto PathToString(const boost::filesystem::path& path) -> std::string; +auto StringToPath(std::string_view path) -> boost::filesystem::path; + +} // namespace detail + +/** + * A RAII guard that deletes a file upon destruction. + */ +class RemoveFileGuard { +public: + RemoveFileGuard(std::string path); + ~RemoveFileGuard(); + + RemoveFileGuard(const RemoveFileGuard&) = delete; + auto operator=(const RemoveFileGuard&) -> RemoveFileGuard& = delete; +private: + std::string path_; +}; + +/** + * A RAII guard that deletes all files and directories recursively. + */ +class RemoveAllGuard { +public: + RemoveAllGuard(std::string path); + ~RemoveAllGuard(); + + RemoveAllGuard(const RemoveAllGuard&) = delete; + auto operator=(const RemoveAllGuard&) -> RemoveAllGuard& = delete; +private: + std::string path_; +}; + +/** + * Generates a file path usable as temporary file. + * + * For a temp file path under the system tmp dir use the other TempFilePath overload. + * + * @code + * auto path = fs::TempFilePath("/tmp/foo", "test_db_"); + * std::cout << path << std::endl; + * + * // Output: + * // /tmp/foo/test_db_bh0vcr0jbz + * @endcode + * + * @param dir The directory of the temp file path. + * @param prefix A prefix to prepend to the temp file name. + * @returns a file path usable as temporary file. + */ +auto TempFilePath(std::string_view dir, std::string_view prefix) -> std::string; + +/** + * Generates a file path usable as temporary file under the system temporary directory. + * + * It tries to detect the correct temp dir under different operation systems. + * For Linux this is `/tmp` and for Windows this is `C:\Users\<username>\AppData\Local\Temp`. + * + * @code + * auto path = fs::TempFilePath("test_db_"); + * std::cout << path << std::endl; + * + * // Output: + * // /tmp/test_db_bh0vcr0jbz + * @endcode + * + * @param prefix A prefix to prepend to the temp file name. + * @returns a file path usable as temporary file. + */ +auto TempFilePath(std::string_view prefix) -> std::string; + +/** + * Creates a temporary sub directory under the system temporary directory. + * + * It tries to detect the correct temp dir under different operation systems. + * For Linux this is `/tmp` and for Windows this is `C:\Users\<username>\AppData\Local\Temp`. + * + * @code + * auto path = fs::TempDir("test_db_"); + * std::cout << path << std::endl; + * + * // Output: + * // /tmp/test_db_bh0vcr0jbz + * @endcode + * + * @param prefix A prefix to prepend to the temp dir name. + * @returns a the path of the created temporary directory. + */ +auto TempDir(std::string_view prefix) -> std::string; + +/** + * Returns current user home directory. + * + * On Windows this is `C:\Users\<username>\AppData\Local`. + * + * @returns the user's home directory full path. + */ +auto HomePath() -> std::string; + +auto ReadFile(std::string_view path) -> bytes::Buffer; +void WriteFile(std::string_view path, bytes::View data); + +} // namespace vereign::fs + +#endif // __VEREIGN_FS_UTIL_HH diff --git a/cpp/src/vereign/grpc/error_code.hh b/cpp/src/vereign/grpc/error_code.hh new file mode 100644 index 0000000000000000000000000000000000000000..2c5c26e9d6ac060ac6351405aab90f0b4b0b5010 --- /dev/null +++ b/cpp/src/vereign/grpc/error_code.hh @@ -0,0 +1,38 @@ +#ifndef __VEREIGN_GRPC_ERROR_CODES_HH +#define __VEREIGN_GRPC_ERROR_CODES_HH + +#include <cstdint> +#include <string> + +namespace vereign::grpc { + +static constexpr const char* ClientErrorStatus = "Vereign Client Library Error"; + +/** + * Error codes returned into the gRPC API response `code` field on various failures. + * + * These are errors that happen inside the Vereign Client Library. + * The errors that happen inside the Vereign Restful API are in the standard HTTP status code range + * below 600. + */ +enum class ErrorCode : uint64_t { + ClientError = 1000, + UnexpectedError = 1001, + DeviceNotRegistered = 1002, + InvalidPinCode = 1003, + InvalidIdentity = 1004 +}; + +/** + * Convert the error code to string. + * + * @param ec The error code. + * @returns the error code integer as string. + */ +inline auto ErrorCodeAsString(ErrorCode ec) -> std::string { + return std::to_string(uint64_t(ec)); +} + +} + +#endif // __VEREIGN_GRPC_ERROR_CODES_HH diff --git a/cpp/src/vereign/grpc/event_api.hh b/cpp/src/vereign/grpc/event_api.hh new file mode 100644 index 0000000000000000000000000000000000000000..42afa02396fec2ce80a280924e9d1d897f978be6 --- /dev/null +++ b/cpp/src/vereign/grpc/event_api.hh @@ -0,0 +1,86 @@ +#ifndef __VEREIGN_GRPC_EVENT_API_HH +#define __VEREIGN_GRPC_EVENT_API_HH + +#include <vereign/grpc/gen/event_api.hh> + +#include <vereign/event/broker.hh> +#include <vereign/core/scope_guard.hh> + +// #include <vereign/grpc/error_code.hh> +// #include <vereign/kvstore/errors.hh> +// #include <vereign/client_library/common_types.pb.h> +// #include <vereign/client_library/identity_types.pb.h> +#include <boost/core/ignore_unused.hpp> + +#include <thread> + +namespace vereign::grpc { + +/** + * Implementation of the gRPC `vereign::client_library::EventAPI::Service` service. + * + * Inherits all the API implementations from the generated gen::EventAPI and adds some + * additional implementations. + */ +template <class VereignService> +class EventAPI final : public gen::EventAPI<VereignService> { +public: + // API service name. + static constexpr const char* Name = gen::EventAPI<VereignService>::Name; + + using VereignServiceType = VereignService; + using VereignServicePtr = std::unique_ptr<VereignService>; + + /** + * Constructs EventAPI instance. + * + * @param service The client library Event service. + */ + EventAPI(VereignServicePtr&& service, event::Broker& events_broker) + : gen::EventAPI<VereignService>{std::move(service)}, + events_broker_{events_broker} + {} + + // disable copying + EventAPI(const EventAPI&) = delete; + auto operator=(const EventAPI&) -> EventAPI& = delete; + + /** + * Returns to the gRPC client a gRPC stream for receiving last available events. + */ + auto GetNewEvents( + ::grpc::ServerContext* ctx, + const client_library::GetNewEventsForm* req, + ::grpc::ServerWriter<client_library::GetNewEventsFormResponse>* resp_stream + ) -> ::grpc::Status override { + boost::ignore_unused(ctx); + boost::ignore_unused(req); + + auto subscription = events_broker_.Subscribe(); + auto unsubscribe = core::MakeScopeGuard([this, &subscription] { + events_broker_.Unsubscribe(subscription); + }); + auto channel = subscription.EventsChannel; + + for (;;) { + auto msg = channel->Get(); + if (!msg) { + break; + } + + auto ok = resp_stream->Write(*msg.Value()); + if (!ok) { + break; + } + } + + return ::grpc::Status::OK; + } + +private: + event::Broker& events_broker_; +}; + +} // namespace vereign::grpc + +#endif // __VEREIGN_GRPC_IDENTITY_API_HH diff --git a/cpp/src/vereign/grpc/identity_api.hh b/cpp/src/vereign/grpc/identity_api.hh new file mode 100644 index 0000000000000000000000000000000000000000..6083548eb488823dafedf3991c9c704af929b91f --- /dev/null +++ b/cpp/src/vereign/grpc/identity_api.hh @@ -0,0 +1,150 @@ +#ifndef __VEREIGN_GRPC_IDENTITY_API_HH +#define __VEREIGN_GRPC_IDENTITY_API_HH + +#include <vereign/grpc/gen/identity_api.hh> + +#include <vereign/grpc/error_code.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/client_library/common_types.pb.h> +#include <vereign/client_library/identity_types.pb.h> +#include <boost/core/ignore_unused.hpp> + +namespace vereign::grpc { + +/** + * Implementation of the gRPC `vereign::client_library::IdentityAPI::Service` service. + * + * Inherits all the API implementations from the generated gen::IdentityAPI and adds some + * additional implementations. + * + * IdentityAPI is a thin layer on top of the service::IdentityService. + */ +template <class VereignService> +class IdentityAPI final : public gen::IdentityAPI<VereignService> { +public: + // API service name. + static constexpr const char* Name = gen::IdentityAPI<VereignService>::Name; + + using VereignServiceType = VereignService; + using VereignServicePtr = std::unique_ptr<VereignService>; + + /** + * Constructs IdentityAPI instance. + * + * @param service The client library Identity service. + */ + IdentityAPI(VereignServicePtr&& service) + : gen::IdentityAPI<VereignService>{std::move(service)} + {} + + // disable copying + IdentityAPI(const IdentityAPI&) = delete; + auto operator=(const IdentityAPI&) -> IdentityAPI& = delete; + + /** + * Registers a new device. + * + * req.pin is required only under Linux. + * + * Under windows the system cypto storage is used. + * When the device is registered a master key is created and the user will be asked for his + * consent by showing a dialog window. + * + * Unexpected error codes: + * - ErrorCode::ClientError Error that happen inside the Vereign Client Library + * - ErrorCode::UnexpectedError Should never happen. + * + * Error codes of interest: + * - ErrorCode::InvalidPinCode The pin code is invalid, currently during the registration an empty + * pin code is considered invalid. + */ + auto LoginWithNewDevice( + ::grpc::ServerContext* ctx, + const client_library::LoginFormNewDevice* req, + client_library::LoginFormNewDeviceResponse* resp + ) -> ::grpc::Status override { + boost::ignore_unused(ctx); + + try { + this->service_->LoginWithNewDevice(req, resp); + + } catch (const kvstore::InvalidPinCodeError& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::InvalidPinCode)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (const std::exception& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::ClientError)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (...) { + resp->set_code(ErrorCodeAsString(ErrorCode::UnexpectedError)); + resp->set_status(ClientErrorStatus); + resp->set_error(ClientErrorStatus); + } + + return ::grpc::Status::OK; + } + + /** + * Login with already registered device. + * + * req.pin is required only under Linux. + * + * Under windows the system cypto storage is used. + * When the device is registered a master key is created and the user will be asked for his + * consent by showing a dialog window. + * + * Unexpected error codes: + * - ErrorCode::ClientError Error that happen inside the Vereign Client Library + * - ErrorCode::UnexpectedError Should never happen. + * + * Error codes of interest: + * - ErrorCode::DeviceNotRegistered The device is not registered. + * - ErrorCode::InvalidPinCode The pin code is invalid and the crypto storage cannot be unlocked. + * - ErrorCode::InvalidIdentity Under windows if for some reason the RSA master key has been changed. + */ + auto LoginWithPreviouslyAddedDevice( + ::grpc::ServerContext* ctx, + const client_library::LoginFormPreviousAddedDevice* req, + client_library::EmptyResponse* resp + ) -> ::grpc::Status override { + boost::ignore_unused(ctx); + + try { + this->service_->LoginWithPreviouslyAddedDevice(req, resp); + + } catch (const kvstore::StorageNotInitializedError& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::DeviceNotRegistered)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (const kvstore::InvalidPinCodeError& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::InvalidPinCode)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (const kvstore::InvalidIdentityError& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::InvalidIdentity)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (const std::exception& e) { + resp->set_code(ErrorCodeAsString(ErrorCode::ClientError)); + resp->set_status(ClientErrorStatus); + resp->set_error(e.what()); + + } catch (...) { + resp->set_code(ErrorCodeAsString(ErrorCode::UnexpectedError)); + resp->set_status(ClientErrorStatus); + resp->set_error(ClientErrorStatus); + } + + return ::grpc::Status::OK; + } +}; + +} // namespace vereign::grpc + +#endif // __VEREIGN_GRPC_IDENTITY_API_HH diff --git a/cpp/src/vereign/grpc/passport_api.hh b/cpp/src/vereign/grpc/passport_api.hh deleted file mode 100644 index 02eb2b36c07dfe12e4d7f46c9bf67d5a5ffac6bf..0000000000000000000000000000000000000000 --- a/cpp/src/vereign/grpc/passport_api.hh +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef __VEREIGN_GRPC_PASSPORT_API_HH -#define __VEREIGN_GRPC_PASSPORT_API_HH - -#include <vereign/grpc/gen/passport_api.hh> - -namespace vereign { -namespace grpc { - -template <class VereignService> -class PassportAPI final : public gen::PassportAPI<VereignService> { -public: - static constexpr const char* Name = gen::PassportAPI<VereignService>::Name; - - using VereignServiceType = VereignService; - using VereignServicePtr = std::unique_ptr<VereignService>; - - PassportAPI(VereignServicePtr&& service) - : gen::PassportAPI<VereignService>{std::move(service)} - {} - - PassportAPI(const PassportAPI&) = delete; - PassportAPI& operator=(const PassportAPI&) = delete; - - ::grpc::Status ListPassportsManually( - ::grpc::ServerContext*, - const client_library::ListPassportsForm* request, - client_library::ListPassportsFormResponse* response - ) override { - auto result_future = this->service_->ListPassports(request, response); - - try { - result_future.get(); - } catch (const std::exception& e) { - response->set_code("500"); - response->set_status("Internal Service Error"); - response->set_error(e.what()); - } catch (...) { - response->set_code("500"); - response->set_status("Internal Service Error"); - response->set_error("Internal Service Error"); - } - - return ::grpc::Status::OK; - } -}; - -} // namespace grpc -} // namespace vereign - -#endif // __VEREIGN_GRPC_PASSPORT_API_HH diff --git a/cpp/src/vereign/grpc/server.cc b/cpp/src/vereign/grpc/server.cc index fd3c911aae22ff35bb513772a27b7d8d3555b985..5e1e899f139a340fae45b06ca97874cee9f90ab4 100644 --- a/cpp/src/vereign/grpc/server.cc +++ b/cpp/src/vereign/grpc/server.cc @@ -1,13 +1,23 @@ #include <vereign/grpc/server.hh> +#include <vereign/fs/path.hh> +#include <vereign/fs/operations.hh> +#include <vereign/fs/util.hh> + +#include <vereign/kvstore/crypto_storage.hh> +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/kvstore/storage.hh> + #include <vereign/restapi/client.hh> #include <vereign/restapi/client_session.hh> #include <vereign/service/gen/gen.hh> #include <vereign/grpc/gen/gen.hh> #include <vereign/grpc/service_registry.hh> -#include <vereign/service/passport_service.hh> -#include <vereign/grpc/passport_api.hh> +// manually written api +#include <vereign/service/identity_service.hh> +#include <vereign/grpc/identity_api.hh> +#include <vereign/grpc/event_api.hh> #include <grpcpp/server.h> #include <boost/asio/io_context.hpp> @@ -19,42 +29,67 @@ #include <boost/asio/executor_work_guard.hpp> -namespace vereign { -namespace grpc { +namespace { + +constexpr auto shutdownTimeout = std::chrono::seconds(2); + +} + +namespace vereign::grpc { namespace asio = boost::asio; class Server::Impl { public: Impl( - const std::string& listenAddress, - const std::string& vereignHost, - const std::string& vereignPort, - // FIXME: the public key must come from a storage - const std::string& publicKey + const std::string& listen_address, + const std::string& vereign_host, + const std::string& vereign_port, + std::string storage_path ) : selected_port_{0}, work_guard_{asio::make_work_guard(ioc_)}, ssl_context_{asio::ssl::context::tlsv12_client}, client_{std::make_unique<restapi::Client>( - ioc_, ssl_context_, vereignHost, vereignPort - )}, - client_session_{std::make_unique<restapi::ClientSession>( - *client_, publicKey + ioc_, ssl_context_, vereign_host, vereign_port )}, + client_session_{std::make_unique<restapi::ClientSession>(*client_)}, + event_broker_{std::make_unique<event::Broker>(*client_session_)}, + kvstorage_{nullptr}, + crypto_storage_{nullptr}, + identity_provider_{nullptr}, server_{nullptr} { + if (storage_path == "") { + storage_path = fs::path::Join(fs::HomePath(), "vereign"); + fs::CreateDir(storage_path); + } + + storage_path = fs::path::Join(storage_path, "db"); + + kvstorage_ = std::make_unique<kvstore::SqliteStorage>(storage_path); + crypto_storage_ = std::make_unique<kvstore::CryptoStorage>(*kvstorage_); + identity_provider_ = std::make_unique<identity::Provider>(*client_session_, *crypto_storage_); // FIXME: Verify the remote server's certificate // ssl_context.set_verify_mode(ssl::verify_peer); ::grpc::ServerBuilder builder; builder.AddListeningPort( - listenAddress, + listen_address, ::grpc::InsecureServerCredentials(), &selected_port_ ); // register manually written services - services_registry_.RegisterIfNotExist<PassportAPI<service::PassportService>>(*client_session_); + services_registry_.RegisterIfNotExist<IdentityAPI<service::IdentityService>>( + *client_session_, + *identity_provider_ + ); + services_registry_.RegisterIfNotExist( + std::make_unique<EventAPI<service::gen::EventService>>( + std::make_unique<service::gen::EventService>(*client_session_), + *event_broker_ + ) + ); // register all generated services grpc::gen::RegisterAll(*client_session_, services_registry_); @@ -76,9 +111,17 @@ public: } void Shutdown() { + if (stopped_.test_and_set()) { + return; + } + client_session_->Close(); - server_->Shutdown(); + event_broker_->Shutdown(); + + auto deadline = std::chrono::system_clock::now() + shutdownTimeout; + + server_->Shutdown(deadline); if (server_thread_.joinable()) { server_thread_.join(); } @@ -100,20 +143,24 @@ private: asio::ssl::context ssl_context_; std::unique_ptr<vereign::restapi::Client> client_; std::unique_ptr<vereign::restapi::ClientSession> client_session_; + std::unique_ptr<vereign::event::Broker> event_broker_; ServiceRegistry services_registry_; + std::unique_ptr<kvstore::Storage> kvstorage_; + std::unique_ptr<kvstore::CryptoStorage> crypto_storage_; + std::unique_ptr<identity::Provider> identity_provider_; std::unique_ptr<::grpc::Server> server_; std::thread server_thread_; std::thread service_thread_; + std::atomic_flag stopped_ = ATOMIC_FLAG_INIT; }; Server::Server( const std::string& listenAddress, const std::string& vereignHost, const std::string& vereignPort, - // FIXME: the public key must come from a storage - const std::string& publicKey + const std::string& storage_path ) - : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort, publicKey)} + : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort, storage_path)} { } @@ -129,5 +176,4 @@ auto Server::SelectedPort() const -> int { return impl_->SelectedPort(); } -} // namespace grpc } // namespace vereign diff --git a/cpp/src/vereign/grpc/server.hh b/cpp/src/vereign/grpc/server.hh index bcadb25458827207c43b6a0050c92c5dda9cd0fa..ef3e8903c76490ded00529f1cfc9af0e5bd09ac5 100644 --- a/cpp/src/vereign/grpc/server.hh +++ b/cpp/src/vereign/grpc/server.hh @@ -4,13 +4,12 @@ #include <string> #include <memory> -namespace vereign { -namespace grpc { +namespace vereign::grpc { /** * BindError is thrown when the Server::Server could not start listening. */ -class BindError: public virtual std::exception { +class BindError: public std::exception { public: auto what() const noexcept -> const char* override { return "gRPC listen failed"; @@ -34,14 +33,17 @@ public: * @param listenAddress gRPC listen address, for example "localhost:". * @param vereignHost Vereign restapi host. * @param vereignPort Vereign restapi port - https, 443... + * @param storage_path Path to directory that will be used for the crypto storage. + * If storage_path is empty string, a default is used. Under linux this default is + * `$HOME/vereign`, and under windows it is `C:\Users\<user>\AppData\Local\vereign`. + * * @throws BindError when the gRPC server could not start listening. */ explicit Server( const std::string& listenAddress, const std::string& vereignHost, const std::string& vereignPort, - // FIXME: the public key must come from a storage - const std::string& publicKey + const std::string& storage_path = "" ); /** @@ -77,7 +79,6 @@ private: std::unique_ptr<Impl> impl_; }; -} // namespace grpc } // namespace vereign #endif diff --git a/cpp/src/vereign/grpc/service_registry.hh b/cpp/src/vereign/grpc/service_registry.hh index cddc32a89d1a323761bcaa6fa54bb4262a613a89..cc60016be9b268e3ad3572242736a73ce72334e6 100644 --- a/cpp/src/vereign/grpc/service_registry.hh +++ b/cpp/src/vereign/grpc/service_registry.hh @@ -3,18 +3,30 @@ #include <grpcpp/server_builder.h> #include <vereign/restapi/client_session.hh> +#include <vereign/identity/provider.hh> #include <unordered_map> -namespace vereign { -namespace grpc { +namespace vereign::grpc { class ServiceRegistry { public: void RegisterIntoBuilder(::grpc::ServerBuilder& builder); template <class API> - bool RegisterIfNotExist(restapi::ClientSession& client_session) { + auto RegisterIfNotExist(std::unique_ptr<API>&& api) -> bool { + auto it = services_.find(API::Name); + if (it != services_.end()) { + return false; + } + + services_[API::Name] = std::move(api); + + return true; + } + + template <class API> + auto RegisterIfNotExist(restapi::ClientSession& client_session) -> bool { auto it = services_.find(API::Name); if (it != services_.end()) { return false; @@ -26,11 +38,28 @@ public: return true; } + template <class API> + auto RegisterIfNotExist( + restapi::ClientSession& client_session, + identity::Provider& identity_provider + ) -> bool { + auto it = services_.find(API::Name); + if (it != services_.end()) { + return false; + } + + using Service = typename API::VereignServiceType; + services_[API::Name] = std::make_unique<API>( + std::make_unique<Service>(client_session, identity_provider) + ); + + return true; + } + private: std::unordered_map<std::string, std::unique_ptr<::grpc::Service>> services_; }; -} // namespace grpc -} // namespace vereign +} // namespace vereign::grpc #endif // __VEREIGN_GRPC_SERVICE_REGISTRY_HH diff --git a/cpp/src/vereign/identity/errors.hh b/cpp/src/vereign/identity/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..ffc59e9f9564d081c8c3467747cab9055e5a8598 --- /dev/null +++ b/cpp/src/vereign/identity/errors.hh @@ -0,0 +1,37 @@ +#ifndef __VEREIGN_IDENTITY_ERRORS_HH +#define __VEREIGN_IDENTITY_ERRORS_HH + +#include <stdexcept> + +namespace vereign::identity { + +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error(what) + { + } +}; + +class ServerSignCertificateError : public Error { +public: + std::string Code; + std::string Status; + std::string Error; + + ServerSignCertificateError( + std::string code, + std::string status, + std::string error + ) + : identity::Error(error), + Code{std::move(code)}, + Status{std::move(status)}, + Error{std::move(error)} + { + } +}; + +} // namespace vereign::identity + +#endif // __VEREIGN_IDENTITY_ERRORS_HH diff --git a/cpp/src/vereign/identity/provider.cc b/cpp/src/vereign/identity/provider.cc new file mode 100644 index 0000000000000000000000000000000000000000..6e96552f319de93e66201bd4423752ce1868fb05 --- /dev/null +++ b/cpp/src/vereign/identity/provider.cc @@ -0,0 +1,245 @@ +#include <vereign/identity/provider.hh> + +#include <vereign/identity/errors.hh> + +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/bio.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/cert.hh> +#include <vereign/encoding/base64.hh> + +#include <vereign/client_library/sign_types.pb.h> + +namespace { + +constexpr int rsaKeySizeBits = 2048; +constexpr const auto profileCertCountry = std::string_view{"CH"}; +constexpr const auto profileCertState = std::string_view{"Zug"}; +constexpr const auto profileCertLocality = std::string_view{"Zug"}; +constexpr const auto profileCertOrganization = std::string_view{"Vereign AG"}; +constexpr const auto profileCertOrganizationUnit = std::string_view{"Business Dep"}; +constexpr const int profileCertValidityYears = 5; + +const auto signCertificatePath = std::string{"/sign/signCertificate"}; + +} + +namespace vereign::identity { + +static auto makeProfileCertificate( + bytes::View key_pem, + const client_library::SignCertificateFormResponsePayload& msg +) -> std::unique_ptr<ProfileCertificate> { + + auto profile_cert = std::make_unique<ProfileCertificate>(); + + profile_cert->PrivateKeyPEM = bytes::Buffer{key_pem}; + encoding::base64::Decode(bytes::View(msg.signedcertificate()), profile_cert->CertificatePEM); + profile_cert->CertificateUUID = msg.certificateuuid(); + + for (auto& cert : msg.chain()) { + bytes::Buffer buf; + encoding::base64::Decode(bytes::View(cert), buf); + profile_cert->Chain.push_back(std::move(buf)); + } + + return profile_cert; +} + +Provider::Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage) + : client_session_{client_session}, + storage_{storage} +{} + +Provider::~Provider() = default; + +auto Provider::RecreateIdentity(const std::string& pin) -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + storage_.Recreate(pin); + + auto rsa = crypto::rsa::GenerateKey(rsaKeySizeBits); + + auto private_key = crypto::rsa::ExportPrivateKeyToPEM(rsa.get()); + storage_.PutBytes("identity_private_key", crypto::bio::View(private_key.get())); + + auto public_key = crypto::rsa::ExportPublicKeyToPEM(rsa.get()); + storage_.PutBytes("identity_public_key", crypto::bio::View(public_key.get())); + + bytes::Buffer encoded; + encoding::base64::Encode(crypto::bio::View(public_key.get()), encoded); + + has_identity_ = true; + + return std::string{encoded.View().String()}; +} + +auto Provider::LoadIdentity(const std::string& pin) -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + storage_.Open(pin); + + bytes::Buffer public_key; + storage_.GetBytes("identity_public_key", public_key); + + bytes::Buffer encoded; + encoding::base64::Encode(public_key.View(), encoded); + + has_identity_ = true; + + return std::string(encoded.View().String()); +} + +auto Provider::GetIdentityPublicKeyBase64() -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + bytes::Buffer public_key; + storage_.GetBytes("identity_public_key", public_key); + + bytes::Buffer encoded; + encoding::base64::Encode(public_key.View(), encoded); + + return std::string(encoded.View().String()); +} + +auto Provider::GetDeviceHash() -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + bytes::Buffer public_key; + storage_.GetBytes("identity_public_key", public_key); + + bytes::Buffer hash; + crypto::digest::sha1(public_key.View(), hash); + + bytes::Buffer encoded; + encoding::base64::Encode(hash.View(), encoded); + + return std::string(encoded.View().String()); +} + +auto Provider::getProfileCertificateFromStorage( + const std::string& profile_uuid +) -> std::unique_ptr<ProfileCertificate> { + + std::lock_guard<std::mutex> l{mu_}; + + auto key_pem = bytes::Buffer{}; + auto cert_binary = bytes::Buffer{}; + + storage_.GetBytes("profile-key-" + profile_uuid, key_pem); + storage_.GetBytes("profile-cert-" + profile_uuid, cert_binary); + + if (key_pem.Size() == 0 || cert_binary.Size() == 0) { + return nullptr; + } + + auto cert_msg = client_library::SignCertificateFormResponsePayload{}; + auto result = cert_msg.ParseFromArray(cert_binary.View().Data(), cert_binary.View().Size()); + if (!result) { + return nullptr; + } + + return makeProfileCertificate(key_pem.View(), cert_msg); +} + +auto Provider::GetProfileCertificate( + const std::string& profile_uuid +) -> std::unique_ptr<ProfileCertificate> { + auto profile_cert = getProfileCertificateFromStorage(profile_uuid); + if (profile_cert) { + return profile_cert; + } + + auto key = crypto::rsa::GenerateKey(rsaKeySizeBits); + + crypto::cert::CertData cert_data{}; + cert_data.Subject.CommonName = profile_uuid + "-userdevice"; + cert_data.Subject.Country = profileCertCountry; + cert_data.Subject.State = profileCertState; + cert_data.Subject.Locality = profileCertLocality; + cert_data.Subject.Organization = profileCertOrganization; + cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit; + + // cert_data.Email = "ca@vereign.com"; // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection + // cert_data.Url = "www.vereign.com"; // optional url, recommended for CA, added to Subject Alternative Name extension + + cert_data.Validity.ValidYears = profileCertValidityYears; + cert_data.IsCA = true; + + auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get()); + auto cert_pem_bio = crypto::cert::ExportCertToPEM(cert.get()); + auto cert_pem = encoding::base64::EncodeToString(crypto::bio::View(cert_pem_bio.get())); + + // sign the new profile certificate + auto req = client_library::SignCertificateForm{}; + req.set_passportuuid(profile_uuid); + req.set_certificate(cert_pem); + auto resp = client_library::SignCertificateFormResponse{}; + + auto result = client_session_.Post(signCertificatePath, &req, &resp); + result.wait(); + + if (resp.code() != "200") { + throw ServerSignCertificateError(resp.code(), resp.status(), resp.error()); + } + + // serialize cert data + auto payload_size = resp.data().ByteSizeLong(); + auto cert_binary = bytes::Buffer{payload_size}; + resp.data().SerializeToArray(cert_binary.end(), payload_size); + cert_binary.IncSize(payload_size); + + auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get()); + + { + std::lock_guard<std::mutex> l{mu_}; + + storage_.PutBytes("profile-key-" + profile_uuid, crypto::bio::View(key_bio.get())); + storage_.PutBytes("profile-cert-" + profile_uuid, cert_binary.View()); + } + + return makeProfileCertificate(crypto::bio::View(key_bio.get()), resp.data()); +} + +auto Provider::GetProfileOneTimeCertificate( + const std::string& profile_uuid, + const std::string& email +) -> std::unique_ptr<ProfileCertificate> { + auto profile_cert = GetProfileCertificate(profile_uuid); + + auto key = crypto::rsa::GenerateKey(rsaKeySizeBits); + + crypto::cert::CertData cert_data{}; + cert_data.Subject.CommonName = profile_uuid + "-onetime"; + cert_data.Subject.Country = profileCertCountry; + cert_data.Subject.State = profileCertState; + cert_data.Subject.Locality = profileCertLocality; + cert_data.Subject.Organization = profileCertOrganization; + cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit; + cert_data.Email = email; + + cert_data.Validity.ValidYears = profileCertValidityYears; + cert_data.IsCA = false; + + auto issuer_cert = crypto::cert::ImportCertFromPEM(profile_cert->CertificatePEM.View()); + auto issuer_pkey = crypto::rsa::ImportPrivateKeyFromPEM(profile_cert->PrivateKeyPEM.View()); + + auto cert = crypto::cert::CreateCert(cert_data, issuer_cert.get(), issuer_pkey.get(), key.get()); + auto cert_bio = crypto::cert::ExportCertToPEM(cert.get()); + + auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get()); + + auto result = std::make_unique<ProfileCertificate>(); + result->PrivateKeyPEM = crypto::bio::View(key_bio.get()); + result->CertificatePEM = crypto::bio::View(cert_bio.get()); + + result->Chain.emplace_back(profile_cert->CertificatePEM.View()); + + for (const auto& chain_cert : profile_cert->Chain) { + result->Chain.emplace_back(chain_cert.View()); + } + + return result; +} + +} // namespace vereign::identity diff --git a/cpp/src/vereign/identity/provider.hh b/cpp/src/vereign/identity/provider.hh new file mode 100644 index 0000000000000000000000000000000000000000..c20763d835204009ea56b2c541147f5b8e91a4c3 --- /dev/null +++ b/cpp/src/vereign/identity/provider.hh @@ -0,0 +1,150 @@ +#ifndef __VEREIGN_IDENTITY_PROVIDER_HH +#define __VEREIGN_IDENTITY_PROVIDER_HH + +#include <vereign/restapi/client_session.hh> +#include <vereign/kvstore/crypto_storage.hh> + +#include <mutex> + +namespace vereign::identity { + +/** + * A DTO representing a profile/onetime certificate with its private key, UUID and certificate chain. + * + * @see Provider::GetProfileCertificate + * @see Provider::GetProfileOneTimeCertificate + */ +struct ProfileCertificate { + bytes::Buffer PrivateKeyPEM; + bytes::Buffer CertificatePEM; + std::string CertificateUUID; + std::vector<bytes::Buffer> Chain; +}; + +/** + * Identity provider that manages the locally stored user identity. + * + * All public methods are thread safe. + */ +class Provider { +public: + /** + * Creates Provider instance. + * + * @param storage The crypto storage used for read/write identity properties. + */ + Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage); + + /** + * Default constructor. + * + * Does nothing. + */ + ~Provider(); + + // disable copying + Provider(const kvstore::Storage&) = delete; + auto operator=(const kvstore::Storage&) -> Provider& = delete; + + /** + * Recreates the current identity. + * + * @param pin Required only under Linux. The pin code used for derivation of the crypto storage + * master key. + * + * @returns The base64 encoded PEM encoded identity public key. + */ + auto RecreateIdentity(const std::string& pin) -> std::string; + + /** + * Loads the local identity. + * + * @param pin Required only under Linux. The pin code used for derivation of the crypto storage + * master key. + * + * @returns The base64 encoded PEM encoded identity public key. + */ + auto LoadIdentity(const std::string& pin) -> std::string; + + /** + * Retrieve identity public key. + * + * @returns The base64 encoded PEM encoded identity public key. + */ + auto GetIdentityPublicKeyBase64() -> std::string; + + /** + * @returns base64 encoded SHA1 hash of the identity public key. + */ + auto GetDeviceHash() -> std::string; + + /** + * Retrieve profile certificate. + * + * When the profile certificate does not exist in the crypto storage just yet, a new private key + * and certificate is created and signed by Vereign server. Then the private key, the certificate + * and the issuers certificate chain are stored into the crypto storage. + * + * This means that the profile certificate is reused for the lifetime of the device registration. + * + * The profile certificate is a CA certificate and is used for signing one-time certificates. + * + * The `Common Name` of the certificate is derived from the + * `profile_uuid` - "<profile_uuid>-userdevice". + * + * Currently the profile certificate is part of the following certificate chain: + * 1. profile-userdevice + * 2. profile-serverside + * 3. vereign + * + * @param profile_uuid The profile UUID. + * @returns the profile certificate, its private key and the issuers certificate chain. + */ + auto GetProfileCertificate(const std::string& profile_uuid) -> std::unique_ptr<ProfileCertificate>; + + /** + * Creates and returns a one-time certificate for given profile. + * + * The one-time certificate is signed by the user-device profile certificate. + * + * When the profile certificate does not exist in the crypto storage just yet, a new private key + * and certificate is created and signed by Vereign server. Then the private key, the certificate + * and the issuers certificate chain are stored into the crypto storage. + * + * This means that the profile certificate is reused for the lifetime of the device registration. + * + * The one-time certificate is a leaf certificate and must be used one only sign operation. + * + * The `Common Name` of the certificate is derived from the + * `profile_uuid` - "<profile_uuid>-onetime". + * + * Currently the one-time certificate is part of the following certificate chain: + * 1. profile-onetime + * 2. profile-userdevice + * 3. profile-serverside + * 4. vereign + * + */ + auto GetProfileOneTimeCertificate( + const std::string& profile_uuid, + const std::string& email + ) -> std::unique_ptr<ProfileCertificate>; + +private: + auto getProfileCertificateFromStorage( + const std::string& profile_uuid + ) -> std::unique_ptr<ProfileCertificate>; + +private: + restapi::ClientSession& client_session_; + + std::mutex mu_; + + kvstore::CryptoStorage& storage_; + + bool has_identity_ = false; +}; + +} // namespace vereign::identity + +#endif // __VEREIGN_IDENTITY_PROVIDER_HH diff --git a/cpp/src/vereign/kvstore/README.md b/cpp/src/vereign/kvstore/README.md new file mode 100644 index 0000000000000000000000000000000000000000..62a67dc5f3d413eb11c04a66e68c26bb62cca1ce --- /dev/null +++ b/cpp/src/vereign/kvstore/README.md @@ -0,0 +1,71 @@ +# Vereign C++ Client Library Storage Spec {#vcl_storage_spec} + +## Overview + +The Vereign C++ Library Storage is used for securely store locally user's identity and profile +certificates. + +The storage is a key/value storage where the values are encrypted with `master key` using +`AES256-GCM` cypher. + +The current backend of the storage is sqlite3, where the database file is normally located at +`${HOME}/vereign/db`. + +The `master key` key is provided in different way depending on the operating system. + +## windows master key + +Under windows [CNG](https://docs.microsoft.com/en-us/windows/win32/seccng/about-cng) is used for +generating a persistent RSA key pair 2048 bits long. The name of the key in the windows storage by +default is `vereign_key`. This key is used to encrypt a randomly generated AES256 key, and the +encrypted key is saved in the storage under a special key `__master_key`. + +## linux master key + +Under linux the user must provide a password (pin code). +This password along with randomly generated salt is used to derive a master key using +`PKCS5_PBKDF2_HMAC` with `SHA256` where the salt is 16 bytes and the iterations are 2^18. +The salt and the iterations are stored in the storage under special keys `__master_key_salt` and +`master_key_iterations`. Note that these are stored in plain form without encryption. + +## locking and storage tag + +Every time the storage is going to be modified the whole storage must be locked, which with the +sqlite3 backend is achieved with so called exclusive transaction. +This is needed in order to have consistency when more then a single application use the same storage. + +But locking is not enough to achieve consistency. If an application A already had opened the storage, +and application B reset the identity (changing the master key), and after some time A writes to the +storage it will overwrite values written by B leaving the storage in a state where different values +are encrypted with different keys. + +In order to prevent this, when the master key is created for the first time a 16 byte long random +tag is generated and stored encrypted with the master key into a special key `__tag`. +Then whenever a value must be written after the storage is locked additionally the `__tag` is read +and decrypted. If the tag retrieval or decryption fails, and error is triggered and the write is +cancelled. + +## encrypted values encoding + +As already mentioned the values are encrypted with `AES256-GCM`. + +The encrypted values are encoded according to the BNF grammars described below. + +The primitives used are: +* `uint8` - a single byte. +* `uint64` - unsigned 64 bit integer in Little Endian. +* `bytes` - a sequence of bytes. + +### encoding version 1 + +``` +Bytes = Size Data + Size = uint64 + Data = bytes + +EncryptedValue = Version IV Tag EncryptedData + Version = uint8 + IV = bytes + Tag = bytes + EncryptedData = bytes +``` diff --git a/cpp/src/vereign/kvstore/crypto_storage.cc b/cpp/src/vereign/kvstore/crypto_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..1990c7f86211005f96ed3618757b1eeacf1af8ba --- /dev/null +++ b/cpp/src/vereign/kvstore/crypto_storage.cc @@ -0,0 +1,33 @@ +#include <vereign/kvstore/crypto_storage.hh> + +#if defined(_WIN32) +# include <vereign/kvstore/detail/win_crypto_storage.hh> +#else +# include <vereign/kvstore/detail/linux_crypto_storage.hh> +#endif + +namespace vereign::kvstore { + +CryptoStorage::CryptoStorage(Storage& storage, bool disable_key_protection) + : impl_{std::make_unique<detail::CryptoStorageImpl>(storage, disable_key_protection)} +{} + +CryptoStorage::~CryptoStorage() = default; + +void CryptoStorage::Recreate(const std::string& pin) { + impl_->Recreate(pin); +} + +void CryptoStorage::Open(const std::string& pin) { + impl_->Open(pin); +} + +void CryptoStorage::PutBytes(const std::string& key, bytes::View value) { + impl_->PutBytes(key, value); +} + +auto CryptoStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool { + return impl_->GetBytes(key, value); +} + +} // namespace vereign::identity diff --git a/cpp/src/vereign/kvstore/crypto_storage.hh b/cpp/src/vereign/kvstore/crypto_storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..23f0e2fa5af3858d3d0677d012e00e6a3eed02f8 --- /dev/null +++ b/cpp/src/vereign/kvstore/crypto_storage.hh @@ -0,0 +1,111 @@ +#ifndef __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH +#define __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH + +#include <vereign/kvstore/storage.hh> +#include <memory> + +namespace vereign::kvstore { + +namespace detail { + +class CryptoStorageImpl; + +} + +/** + * Crypto storage is used for securely store sensitive user's data. + * + * This includes user's device private/public key, profile certificates etc. + * + * The CryptoStorage has different implementations per operating system. + * Check detail/linux_crypto_storage.cc and detail/win_crypto_storage.cc. + * + * For more information about the crypto storage design check the [storage spec](@ref vcl_storage_spec) + * in the README.md. + * + */ +class CryptoStorage { +public: + /** + * Creates CryptoStorage. + * + * @param storage The underlying key/value storage. + * @param disable_key_protection Used only under Windows. If false when the master RSA key + * is created and loaded, the user will be asked for his consent by showing a dialog window + * with text information specified in the UI policy. + */ + CryptoStorage(Storage& storage, bool disable_key_protection = false); + + /** + * Default destructor - does nothing. + */ + ~CryptoStorage(); + + // disable copying + CryptoStorage(const CryptoStorage&) = delete; + auto operator=(const CryptoStorage&) -> CryptoStorage& = delete; + + /** + * Reinitializes the storage, by effectively initializing a new empty storage. + * + * Although the resulting storage is empty it will include some internal data needed for the + * master key management. + * + * @param pin Used only under Linux. A pin code that will be used for derivation of the master key. + * Under windows ncrypt is used for generating a RSA key which is used for encryption of the + * master key, and thus the pin is not used. + * + * @throws Error on failure. + */ + void Recreate(const std::string& pin); + + /** + * Open the storage. + * + * @param pin Used only under Linux. A pin code that will be used for derivation of the master key. + * Under windows ncrypt is used for generating a RSA key which is used for encryption of the + * master key, and thus the pin is not used. + * + * @throws StorageNotInitializedError when storage is not initialized yet. One must use + * CryptoStorage::Reset to initialize the storage prior calling CryptoStorage::Open. + * + * @throws InvalidPinCodeError (only under Linux) when the pin code is empty or does not match + * the pin code used with CryptoStorage::Reset. + * + * @throws Error when the key derivation under Linux fails. + * + * @throws InvalidIdentityError when the master key could not be decrypted under Windows. + * This possibly means that the RSA key was changed. + */ + void Open(const std::string& pin); + + /** + * Encrypt and store bytes value into the storage. + * + * @param key The key under which the value will be stored. + * @param value The bytes that will be encrypted and stored. + * + * @throws StorageNotInitializedError when the storage is not initialized. + * @throws InvalidIdentityError when another application had reset the storage. + */ + void PutBytes(const std::string& key, bytes::View value); + + /** + * Retrieve and decrypt bytes value from the storage. + * + * @param key The key of the value that will be retrieved. + * @param value Buffer where the value will be returned. + * + * @throws StorageNotInitializedError when the storage is not initialized. + * @throws encoding::Error when the encrypted value cannot be decoded. + * @throws crypto::OpenSSLError when the value cannot be decrypted. + */ + auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool; + +private: + std::unique_ptr<detail::CryptoStorageImpl> impl_; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..44243c88497a01759a559f8e6ece6d430e5a237e --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc @@ -0,0 +1,102 @@ +#include <vereign/kvstore/detail/base_crypto_storage.hh> + +#include <vereign/kvstore/detail/value_encoder.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> + +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/aes.hh> +#include <vereign/crypto/bio.hh> + +#include <boost/core/ignore_unused.hpp> +#include <chrono> + +namespace { + + // FIXME: should these be injected and provided by the integrator + constexpr int tagSizeBytes = 16; + constexpr int lockRetryCount = 10; + constexpr auto lockRetrySleep = std::chrono::milliseconds{1000}; +} + +namespace vereign::kvstore::detail { + +BaseCryptoStorageImpl::BaseCryptoStorageImpl(kvstore::Storage& storage) + : storage_{storage} +{ +} + +void BaseCryptoStorageImpl::encryptBytes(const std::string& key, bytes::View value) { + if (key_.Size() == 0) { + throw StorageNotInitializedError{"key not initialized"}; + } + + bytes::Buffer iv; + bytes::Buffer tag; + bytes::Buffer encrypted; + + crypto::aes::GCM256Encrypt(value, key_.View(), iv, tag, encrypted); + + bytes::Buffer encoded_value; + EncodeEncryptedValue(encoded_value, iv.View(), tag.View(), encrypted.View()); + + storage_.PutBytes(key, encoded_value.View()); +} + +void BaseCryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + if (!isTagValid()) { + throw InvalidIdentityError{}; + } + + encryptBytes(key, value); +} + +auto BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) const -> bool { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + if (key_.Size() == 0) { + throw StorageNotInitializedError{"key not initialized"}; + } + + bytes::Buffer encoded; + auto found = storage_.GetBytes(key, encoded); + if (!found) { + return false; + } + + bytes::View iv; + bytes::View tag; + bytes::View encrypted; + DecodeEncryptedValue(encoded.View(), iv, tag, encrypted); + + crypto::aes::GCM256Decrypt(encrypted, key_.View(), iv, tag, value); + + return true; +} + +void BaseCryptoStorageImpl::initKey(bytes::Buffer&& key) { + key_ = std::move(key); +} + +auto BaseCryptoStorageImpl::isTagValid() const -> bool { + bytes::Buffer tag; + try { + return GetBytes("__tag", tag); + + } catch (const crypto::Error&) { + return false; + } +} + +void BaseCryptoStorageImpl::updateTag() { + auto tag = crypto::Rand(tagSizeBytes); + + encryptBytes("__tag", tag.View()); +} + +} // namespace vereign::kvstore::detail diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..3039b8fe0fbb82f9207689dc9faa213795fab5c5 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh @@ -0,0 +1,37 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH +#define __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH + +#include <vereign/kvstore/storage.hh> + +namespace vereign::kvstore::detail { + +class BaseCryptoStorageImpl { +public: + BaseCryptoStorageImpl(kvstore::Storage& storage); + + // disable copying + BaseCryptoStorageImpl(const BaseCryptoStorageImpl&) = delete; + auto operator=(const BaseCryptoStorageImpl&) -> BaseCryptoStorageImpl& = delete; + + void PutBytes(const std::string& key, bytes::View value); + auto GetBytes(const std::string& key, bytes::Buffer& value) const -> bool; + +protected: + void initKey(bytes::Buffer&& key); + auto isTagValid() const -> bool; + void updateTag(); + +private: + void encryptBytes(const std::string& key, bytes::View value); + +protected: + kvstore::Storage& storage_; + +private: + bytes::Buffer key_; +}; + +} // namespace vereign::kvstore::detail + + +#endif // __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..eb49ca7171ff49c3009c6bd5fbbf7cb9a9303e76 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc @@ -0,0 +1,120 @@ +#include <vereign/kvstore/detail/linux_crypto_storage.hh> + +#include <vereign/kvstore/detail/value_encoder.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> + +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/aes.hh> +#include <vereign/crypto/bio.hh> + +#include <boost/core/ignore_unused.hpp> +#include <chrono> + +namespace { + +// FIXME: should these be injected and provided by the integrator +constexpr int iterations = 1 << 18; +constexpr int saltSizeBytes = 16; +constexpr int aesKeySizeBytes = 32; + +constexpr int lockRetryCount = 10; +constexpr auto lockRetrySleep = std::chrono::milliseconds{1000}; + +} + +namespace vereign::kvstore::detail { + +CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection) + : BaseCryptoStorageImpl{storage} +{ + boost::ignore_unused(disable_key_protection); +} + +void CryptoStorageImpl::Open(const std::string& pin) { + if (pin.empty()) { + throw InvalidPinCodeError{}; + } + + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + bytes::Buffer salt; + int64_t iterations = 0; + + try { + storage_.GetInt64("__master_key_iterations", iterations); + storage_.GetBytes("__master_key_salt", salt); + } catch (const std::exception& e) { + throw StorageNotInitializedError{e.what()}; + } + + if (iterations == 0 || salt.Size() == 0) { + throw StorageNotInitializedError{"iterations cannot be zero"}; + } + + bytes::Buffer key{aesKeySizeBytes}; + + int result = PKCS5_PBKDF2_HMAC( + pin.data(), + pin.length(), + salt.View().Data(), + salt.View().Size(), + iterations, + EVP_sha256(), + aesKeySizeBytes, + key.end() + ); + if (result == 0) { + throw Error("key derivation failed"); + } + + key.IncSize(aesKeySizeBytes); + + initKey(std::move(key)); + if (!isTagValid()) { + throw InvalidPinCodeError{}; + } +} + +void CryptoStorageImpl::Recreate(const std::string& pin) { + if (pin.empty()) { + throw InvalidPinCodeError{}; + } + + auto salt = crypto::Rand(saltSizeBytes); + + bytes::Buffer key{aesKeySizeBytes}; + + int result = PKCS5_PBKDF2_HMAC( + pin.data(), + pin.length(), + salt.View().Data(), + salt.View().Size(), + iterations, + EVP_sha256(), + aesKeySizeBytes, + key.end() + ); + if (result == 0) { + throw Error("key derivation failed"); + } + + key.IncSize(aesKeySizeBytes); + initKey(std::move(key)); + + { + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + storage_.DeleteAll(); + + updateTag(); + + storage_.PutInt64("__master_key_iterations", iterations); + storage_.PutBytes("__master_key_salt", salt.View()); + } +} + +} // namespace vereign::kvstore::detail diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..fcc3e1fc0b67664ede137622c8ac79a7c765a6f6 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh @@ -0,0 +1,24 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH +#define __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH + +#include <vereign/kvstore/storage.hh> +#include <vereign/kvstore/detail/base_crypto_storage.hh> + +namespace vereign::kvstore::detail { + +class CryptoStorageImpl : public BaseCryptoStorageImpl { +public: + CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection); + + // disable copying + CryptoStorageImpl(const CryptoStorageImpl&) = delete; + auto operator=(const CryptoStorageImpl&) -> CryptoStorageImpl& = delete; + + void Recreate(const std::string& pin); + void Open(const std::string& pin); +}; + +} // namespace vereign::kvstore::detail + + +#endif // __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH diff --git a/cpp/src/vereign/kvstore/detail/value_encoder.cc b/cpp/src/vereign/kvstore/detail/value_encoder.cc new file mode 100644 index 0000000000000000000000000000000000000000..6144bebbdffdee83f9797cf063d9729b34d96c35 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/value_encoder.cc @@ -0,0 +1,42 @@ +#include <vereign/kvstore/detail/value_encoder.hh> + +#include <vereign/kvstore/errors.hh> +#include <vereign/encoding/binary.hh> + +namespace vereign::kvstore::detail { + +void EncodeEncryptedValue( + bytes::Buffer& out, + bytes::View iv, + bytes::View tag, + bytes::View encrypted +) { + // 25 = buffers size numbers (3 int64) plus 1 byte for the version + out.Reserve(iv.Size() + tag.Size() + encrypted.Size() + 25); + // encode version + encoding::binary::EncodeUint8(out, 1); + + // encode data + encoding::binary::EncodeBytes(out, iv); + encoding::binary::EncodeBytes(out, tag); + encoding::binary::EncodeBytes(out, encrypted); +} + +void DecodeEncryptedValue( + bytes::View encoded, + bytes::View& iv, + bytes::View& tag, + bytes::View& encrypted +) { + auto version = encoding::binary::DecodeUint8(encoded); + if (version != 1) { + throw Error("decoding encrypted value failed: invalid version"); + } + + auto offset = 1; + offset += encoding::binary::DecodeBytes(encoded.Slice(offset), iv); + offset += encoding::binary::DecodeBytes(encoded.Slice(offset), tag); + offset += encoding::binary::DecodeBytes(encoded.Slice(offset), encrypted); +} + +} // namespace vereign::kvstore::detail diff --git a/cpp/src/vereign/kvstore/detail/value_encoder.hh b/cpp/src/vereign/kvstore/detail/value_encoder.hh new file mode 100644 index 0000000000000000000000000000000000000000..3c9e532853a853febaadf44b39c7941c1f0f28e1 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/value_encoder.hh @@ -0,0 +1,46 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH +#define __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/buffer.hh> + +namespace vereign::kvstore::detail { + +/** + * Encodes value encrypted with AES in GCM mode. + * + * @param out The encoded result. + * @param iv The IV of the encrypted value. + * @param tag The tag of the encrypted value. + * @param encrypted The encrypted bytes. + */ +void EncodeEncryptedValue( + bytes::Buffer& out, + bytes::View iv, + bytes::View tag, + bytes::View encrypted +); + +/** + * Decode value encrypted with AES in GCM mode. + * + * Note that iv, tag and encrypted are decoded in-place, meaning that they point to the same memory + * of the `encoded` input. + * + * @param out The encoded value. + * @param iv The decoded IV of the encrypted value. + * @param tag The decoded tag of the encrypted value. + * @param encrypted The decoded encrypted bytes. + * + * @throws kvstore::Error on failure. + */ +void DecodeEncryptedValue( + bytes::View encoded, + bytes::View& iv, + bytes::View& tag, + bytes::View& encrypted +); + +} // namespace vereign::kvstore::detail + +#endif // __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH diff --git a/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..1e78842bec9d48ad965f2486df9836da57d266d0 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc @@ -0,0 +1,110 @@ +#include <vereign/kvstore/detail/win_crypto_storage.hh> + +#include <vereign/kvstore/detail/value_encoder.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> + +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/aes.hh> +#include <vereign/crypto/bio.hh> + +#include <boost/core/ignore_unused.hpp> +#include <vereign/ncrypt/rsa.hh> +#include <vereign/ncrypt/errors.hh> + +#include <chrono> + +namespace { + +// FIXME: should these be injected and provided by the integrator +constexpr int keySizeBits = 2048; +constexpr int aesKeySizeBytes = 32; + +constexpr int lockRetryCount = 10; +constexpr auto lockRetrySleep = std::chrono::milliseconds{1000}; + +// FIXME: ask business for these values +constexpr const auto vereignKeyCreationTitle = std::string_view{"Vereign Client"}; +constexpr const auto vereignKeyDescription = std::string_view{ + "Vereign Client will use this key to authenticate with the Vereign Services" +}; +constexpr const auto vereignKeyFriendlyName = std::string_view{"Vereign Client Identity Key"}; + +} + +namespace vereign::kvstore::detail { + +CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection) + : BaseCryptoStorageImpl{storage}, + disable_key_protection_{disable_key_protection} +{} + +void CryptoStorageImpl::Open(const std::string& pin) { + boost::ignore_unused(pin); + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + auto provider = ncrypt::rsa::OpenStorageProvider(); + auto rsa_key = ncrypt::rsa::LoadKey(provider.Get(), std::string(VereignKeyName)); + if (!rsa_key) { + throw StorageNotInitializedError{"key not initialized"}; + } + + bytes::Buffer key; + try { + bytes::Buffer encrypted_key; + storage_.GetBytes("__master_key", encrypted_key); + + ncrypt::rsa::PrivateKeyDecrypt(rsa_key.Get(), encrypted_key.View(), key); + } catch(const std::exception&) { + throw InvalidIdentityError{}; + } + + initKey(std::move(key)); + // FIXME: write tests for tampering and regular identity change + if (!isTagValid()) { + throw InvalidIdentityError{}; + } +} + +void CryptoStorageImpl::Recreate(const std::string& pin) { + boost::ignore_unused(pin); + kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep}; + + auto provider = ncrypt::rsa::OpenStorageProvider(); + auto old_key = ncrypt::rsa::LoadKey(provider.Get(), std::string(VereignKeyName)); + if (old_key) { + ncrypt::rsa::DeleteKey(old_key.Get()); + } + + std::optional<ncrypt::rsa::KeyUIPolicy> ui_policy; + if (!disable_key_protection_) { + ui_policy = ncrypt::rsa::KeyUIPolicy{ + vereignKeyCreationTitle, + vereignKeyDescription, + vereignKeyFriendlyName + }; + } + + auto rsa_key = ncrypt::rsa::CreateKey( + provider.Get(), + keySizeBits, + std::string(VereignKeyName), + ui_policy + ); + + storage_.DeleteAll(); + + auto key = crypto::Rand(aesKeySizeBytes); + + bytes::Buffer encrypted_key; + ncrypt::rsa::PublicKeyEncrypt(rsa_key.Get(), key.View(), encrypted_key); + storage_.PutBytes("__master_key", encrypted_key.View()); + + initKey(std::move(key)); + updateTag(); +} + +} diff --git a/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..48150ac4ff6068fbe79b3abffd257f508b89e907 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh @@ -0,0 +1,27 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH +#define __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH + +#include <vereign/kvstore/storage.hh> +#include <vereign/kvstore/detail/base_crypto_storage.hh> + +namespace vereign::kvstore::detail { + +class CryptoStorageImpl : public BaseCryptoStorageImpl { +public: + CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection); + + // disable copying + CryptoStorageImpl(const CryptoStorageImpl&) = delete; + auto operator=(const CryptoStorageImpl&) -> CryptoStorageImpl& = delete; + + void Recreate(const std::string& pin); + void Open(const std::string& pin); + +private: + bool disable_key_protection_ = false; +}; + +} // namespace vereign::kvstore::detail + + +#endif // __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH diff --git a/cpp/src/vereign/kvstore/errors.hh b/cpp/src/vereign/kvstore/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..50ec7271532c6b55cc6b8fc3adc7a1ddb1a569db --- /dev/null +++ b/cpp/src/vereign/kvstore/errors.hh @@ -0,0 +1,50 @@ +#ifndef __VEREIGN_KVSTORE_ERRORS_HH +#define __VEREIGN_KVSTORE_ERRORS_HH + +#include <stdexcept> + +namespace vereign::kvstore { + +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error(what) + { + } +}; + +class StorageNotInitializedError : public Error { +public: + StorageNotInitializedError(const std::string& reason) + : Error{"storage is not initialized: " + reason} + { + } +}; + +class LockError : public Error { +public: + LockError() + : Error{"cannot acquire storage lock"} + { + } +}; + +class InvalidPinCodeError : public Error { +public: + InvalidPinCodeError() + : Error{"invalid pin code"} + { + } +}; + +class InvalidIdentityError : public Error { +public: + InvalidIdentityError() + : Error{"invalid identity"} + { + } +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_ERRORS_HH diff --git a/cpp/src/vereign/kvstore/lock.cc b/cpp/src/vereign/kvstore/lock.cc new file mode 100644 index 0000000000000000000000000000000000000000..5a0863e967ea6de6243652a7bbb1752c8a286480 --- /dev/null +++ b/cpp/src/vereign/kvstore/lock.cc @@ -0,0 +1,39 @@ +#include <vereign/kvstore/lock.hh> + +#include <vereign/kvstore/errors.hh> + +#include <thread> +#include <sqlite3.h> + +namespace vereign::kvstore { + +Lock::Lock(Storage& storage) + : storage_{storage} +{ + storage.Lock(); +} + +Lock::Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval) + : storage_{storage} +{ + for (int i = 0; i < retry_count; i++) { + try { + storage.Lock(); + + return; + } catch (const LockError&) { + std::this_thread::sleep_for(sleep_interval); + + continue; + } + } + + throw LockError{}; +} + +Lock::~Lock() noexcept { + storage_.Unlock(); +} + + +} // namespace vereign::kvstore diff --git a/cpp/src/vereign/kvstore/lock.hh b/cpp/src/vereign/kvstore/lock.hh new file mode 100644 index 0000000000000000000000000000000000000000..1989b1dd0376353417224edfd29c249c1fae4108 --- /dev/null +++ b/cpp/src/vereign/kvstore/lock.hh @@ -0,0 +1,53 @@ +#ifndef __VEREIGN_KVSTORE_LOCK_HH +#define __VEREIGN_KVSTORE_LOCK_HH + +#include <vereign/kvstore/storage.hh> +#include <chrono> + +namespace vereign::kvstore { + +/** + * Lock guard used for lock/unlock Storage within a scope. + * + * When the Lock is constructed it locks the Storage, and when it is destroyed it unlock it. + */ +class Lock { +public: + /** + * Creates a Lock and locks the Storage. + * + * @param storage The storage to lock. + * @throws LockError when the lock is held by another process. + */ + explicit Lock(Storage& storage); + + /** + * Creates a Lock and locks the Storage. + * + * If the lock is not possible it retries `retry_count` and sleeps between retries `sleep_interval`. + * + * @param storage The storage to lock. + * @param retry_count How many times to retry if the lock is held by another process. + * @param sleep_interval How many time to sleep between retries. + * + * @throws LockError If the lock could not be held after `retry_count` retries. + */ + Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval); + + /** + * Unlocks the storage. + */ + ~Lock() noexcept; + + // copying is disabled. + Lock(const Lock&) = delete; + auto operator=(const Lock&) -> Lock& = delete; + +private: + Storage& storage_; +}; + + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_LOCK_HH diff --git a/cpp/src/vereign/kvstore/sqlite_storage.cc b/cpp/src/vereign/kvstore/sqlite_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..21371363d40fc84cb497c283b869b999baed8ab3 --- /dev/null +++ b/cpp/src/vereign/kvstore/sqlite_storage.cc @@ -0,0 +1,135 @@ +#include <vereign/kvstore/sqlite_storage.hh> + +#include <vereign/kvstore/errors.hh> +#include <vereign/kvstore/lock.hh> + +#include <vereign/encoding/binary.hh> +#include <boost/optional.hpp> +#include <vereign/core/lock_guard.hh> +#include <vereign/sqlite/errors.hh> +#include <sqlite3.h> + +#include <array> + +namespace { +constexpr int createTableRetryCount = 10; +constexpr auto createTableRetrySleep = std::chrono::milliseconds{1000}; +} + +namespace vereign::kvstore { + +SqliteStorage::SqliteStorage(const std::string& db_path) + : db_{db_path} +{ + kvstore::Lock l{*this, createTableRetryCount, createTableRetrySleep}; + + db_.Execute(R"( +CREATE TABLE IF NOT EXISTS storage ( + key TEXT PRIMARY KEY NOT NULL, + value BLOB +); + )"); +} + +SqliteStorage::~SqliteStorage() { + if (lock_count_ != 0) { + db_.Commit(); + } +} + +void SqliteStorage::Lock() { + if (lock_count_ != 0) { + lock_count_++; + + return; + } + + try { + db_.BeginExplicitTransaction(); + lock_count_++; + } catch (const sqlite::Error& err) { + if (err.code() == SQLITE_BUSY) { + throw LockError{}; + } + + throw; + } +} + +void SqliteStorage::Unlock() { + if (lock_count_ == 0) { + throw Error{"unexpected call Unlock with non existent lock"}; + } + + lock_count_--; + if (lock_count_ == 0) { + db_.Commit(); + } +} + + +void SqliteStorage::DeleteAll() { + kvstore::Lock l{*this}; + + db_.Execute("DELETE FROM storage;"); +} + +void SqliteStorage::PutBytes(const std::string& key, bytes::View value) { + kvstore::Lock l{*this}; + + auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);"); + stmt.BindText(1, key); + stmt.BindBlob(2, value); + stmt.Step(); +} + +auto SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool { + kvstore::Lock l{*this}; + + auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); + stmt.BindText(1, key); + auto end = stmt.Step(); + + if (end) { + return false; + } + + value.Write(stmt.GetColumnBlob(0)); + + return true; +} + +void SqliteStorage::PutInt64(const std::string& key, int64_t value) { + kvstore::Lock l{*this}; + + std::array<uint8_t, 8> encoded; + encoding::binary::EncodeUint64(encoded.data(), value); + + auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);"); + stmt.BindText(1, key); + stmt.BindBlob(2, bytes::View(encoded.data(), 8)); + stmt.Step(); +} + +auto SqliteStorage::GetInt64(const std::string& key, int64_t& value) -> bool { + kvstore::Lock l{*this}; + + auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); + stmt.BindText(1, key); + auto end = stmt.Step(); + + if (end) { + return false; + } + + auto buf = stmt.GetColumnBlob(0); + if (buf.Size() != 8) { + throw Error("cannot decode int64 value"); + } + + value = encoding::binary::DecodeUint64(buf); + + return true; +} + +} // namespace vereign::kvstore diff --git a/cpp/src/vereign/kvstore/sqlite_storage.hh b/cpp/src/vereign/kvstore/sqlite_storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..6fdfce9d8e996351f99c85f0f9dc1f7e6c2525c8 --- /dev/null +++ b/cpp/src/vereign/kvstore/sqlite_storage.hh @@ -0,0 +1,106 @@ +#ifndef __VEREIGN_KVSTORE_SQLITE_STORAGE_HH +#define __VEREIGN_KVSTORE_SQLITE_STORAGE_HH + +#include <vereign/kvstore/storage.hh> +#include <vereign/sqlite/connection.hh> + +namespace vereign::kvstore { + +/** + * Sqlite implementation of the kvstore::Storage interface. + */ +class SqliteStorage : public Storage { +public: + /** + * Creates SqliteStorage instance. + * + * @param db_path Full path to the sqlite database file. + */ + SqliteStorage(const std::string& db_path); + + /** + * Closes the connection with the database. + */ + ~SqliteStorage() override; + + // disable copying + SqliteStorage(const SqliteStorage&) = delete; + auto operator=(const SqliteStorage&) -> SqliteStorage& = delete; + + /** + * Locks the storage. + * + * The lock must be recursive, meaning that may be called multiple times. + * Storage::Unlock must be called the same number of times the Storage::Lock was called. + * + * Use kvstore::Lock for lock guard and lock with retrials. + * + * @throws LockError when the lock is held by another process. + */ + void Lock() override; + + /** + * Locks the storage. + * + * The lock must be recursive, meaning that may be called multiple times. + * Storage::Unlock must be called the same number of times the Storage::Lock was called. + * + * Use kvstore::Lock for lock guard and lock with retrials. + */ + void Unlock() override; + + /** + * Deletes all the values in the storage. + * + * @throws sqlite::Error on failure. + */ + void DeleteAll() override; + + /** + * Store bytes value into the storage. + * + * @param key The key under which the value will be stored. + * @param value The bytes that will be stored. + * + * @throws sqlite::Error on failure. + */ + void PutBytes(const std::string& key, bytes::View value) override; + + /** + * Retrieve bytes value from the storage. + * + * @param key The key of the value that will be retrieved. + * @param value Buffer where the value will be returned. + * + * @throws sqlite::Error on failure. + */ + auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool override; + + /** + * Store int64_t value into the storage. + * + * @param key The key under which the value will be stored. + * @param value The value that will be stored. + * + * @throws sqlite::Error on failure. + */ + void PutInt64(const std::string& key, int64_t value) override; + + /** + * Retrieve int64_t value from the storage. + * + * @param key The key of the value that will be retrieved. + * @param value Buffer where the value will be returned. + * + * @throws sqlite::Error on failure. + */ + auto GetInt64(const std::string& key, int64_t& value) -> bool override; + +private: + sqlite::Connection db_; + int lock_count_ = 0; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_SQLITE_STORAGE_HH diff --git a/cpp/src/vereign/kvstore/storage.hh b/cpp/src/vereign/kvstore/storage.hh new file mode 100644 index 0000000000000000000000000000000000000000..ccb809468144a940243ee0e7d4769726056b8673 --- /dev/null +++ b/cpp/src/vereign/kvstore/storage.hh @@ -0,0 +1,83 @@ +#ifndef __VEREIGN_KVSTORE_STORAGE_HH +#define __VEREIGN_KVSTORE_STORAGE_HH + +#include <vereign/bytes/buffer.hh> + +namespace vereign::kvstore { + +// The vereign RSA master key name. +constexpr const auto VereignKeyName = std::string_view{"vereign_key"}; + +/** + * Key/value storage interface. + */ +class Storage { +public: + /** + * Locks the storage. + * + * The lock must be recursive, meaning that may be called multiple times. + * Storage::Unlock must be called the same number of times the Storage::Lock was called. + * + * Use kvstore::Lock for lock guard and lock with retrials. + * + * @throws LockError when the lock is held by another process. + */ + virtual void Lock() = 0; + + /** + * Locks the storage. + * + * The lock must be recursive, meaning that may be called multiple times. + * Storage::Unlock must be called the same number of times the Storage::Lock was called. + * + * Use kvstore::Lock for lock guard and lock with retrials. + */ + virtual void Unlock() = 0; + + /** + * Deletes all the values in the storage. + */ + virtual void DeleteAll() = 0; + + /** + * Store bytes value into the storage. + * + * @param key The key under which the value will be stored. + * @param value The bytes that will be stored. + */ + virtual void PutBytes(const std::string& key, bytes::View value) = 0; + + /** + * Retrieve bytes value from the storage. + * + * @param key The key of the value that will be retrieved. + * @param value Buffer where the value will be returned. + */ + virtual auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool = 0; + + /** + * Store int64_t value into the storage. + * + * @param key The key under which the value will be stored. + * @param value The value that will be stored. + */ + virtual void PutInt64(const std::string& key, int64_t value) = 0; + + /** + * Retrieve int64_t value from the storage. + * + * @param key The key of the value that will be retrieved. + * @param value Buffer where the value will be returned. + */ + virtual auto GetInt64(const std::string& key, int64_t& value) -> bool = 0; + + /** + * Destroy and cleanup. + */ + virtual ~Storage() = default; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_STORAGE_HH diff --git a/cpp/src/vereign/ncrypt/errors.cc b/cpp/src/vereign/ncrypt/errors.cc new file mode 100644 index 0000000000000000000000000000000000000000..fedb516591b3709bad2cbc244caabc0f4f9a3e79 --- /dev/null +++ b/cpp/src/vereign/ncrypt/errors.cc @@ -0,0 +1,45 @@ +#include <vereign/ncrypt/errors.hh> +#include <vereign/bytes/buffer.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/encoding/hex.hh> +#include <vereign/encoding/binary.hh> + +#include <string> + +namespace vereign::ncrypt { + +auto SecurityStatusToString(SECURITY_STATUS status) -> std::string { + switch (status) { + case NTE_FAIL: + return "NTE_FAIL"; + case NTE_INVALID_PARAMETER: + return "NTE_INVALID_PARAMETER"; + case NTE_BUFFER_TOO_SMALL: + return "NTE_BUFFER_TOO_SMALL"; + case NTE_EXISTS: + return "NTE_EXISTS"; + case NTE_BAD_KEYSET: + return "NTE_BAD_KEYSET"; + case NTE_INVALID_HANDLE: + return "NTE_INVALID_HANDLE"; + case NTE_NOT_SUPPORTED: + return "NTE_NOT_SUPPORTED"; + case NTE_NULL_REFERENCE_POINTER: + return "NTE_NULL_REFERENCE_POINTER"; + default: + bytes::Buffer encoded; + encoding::hex::EncodeReverse(bytes::View(&status, sizeof(status)), encoded); + return std::string(encoded.View().String()); + } +} + +Error::Error(SECURITY_STATUS status, const std::string& msg) + : std::runtime_error{msg + ": " + SecurityStatusToString(status)}, + status_{status} +{} + +auto Error::SecurityStatus() const -> SECURITY_STATUS { + return status_; +} + +} // vereign::ncrypt diff --git a/cpp/src/vereign/ncrypt/errors.hh b/cpp/src/vereign/ncrypt/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..fe6920132b3e3cc1f1596c68af9671c3cfa2764f --- /dev/null +++ b/cpp/src/vereign/ncrypt/errors.hh @@ -0,0 +1,49 @@ +#ifndef __VEREIGN_NCRYPT_ERRORS_HH +#define __VEREIGN_NCRYPT_ERRORS_HH + +#include <windows.h> +#include <ncrypt.h> +#include <stdexcept> + +namespace vereign::ncrypt { + +// windows returns this error when there is unexpected NULL input +constexpr SECURITY_STATUS NTE_NULL_REFERENCE_POINTER = 0x800706f4; + +/** + * Returns a string representation of the SECURITY_STATUS errors. + * + * @param status The SECURITY_STATUS error. + * @returns a string representation of the SECURITY_STATUS error. + */ +auto SecurityStatusToString(SECURITY_STATUS status) -> std::string; + +/** + * The base error type for the namespace vereign::ncrypto. + */ +class Error : public std::runtime_error { +public: + /** + * Creates Error with status and additional message. + * + * Example: + * @code + * auto err = Error{NTE_FAIL, "operation failed"}; + * assert(err.what() == "operation failed: NTE_FAIL"); + * assert(err.SecurityStatus() == NTE_FAIL); + * @endcode + */ + Error(SECURITY_STATUS status, const std::string& msg); + + /** + * Returns the SECURITY_STATUS associated with this error. + */ + auto SecurityStatus() const -> SECURITY_STATUS; + +private: + SECURITY_STATUS status_; +}; + +} // vereign::ncrypt + +#endif // __VEREIGN_NCRYPT_ERRORS_HH diff --git a/cpp/src/vereign/ncrypt/rsa.cc b/cpp/src/vereign/ncrypt/rsa.cc new file mode 100644 index 0000000000000000000000000000000000000000..a9e351d4a3e7f15f513167b05d154716a5d2d188 --- /dev/null +++ b/cpp/src/vereign/ncrypt/rsa.cc @@ -0,0 +1,182 @@ +#include <vereign/ncrypt/rsa.hh> + +#include <vereign/ncrypt/errors.hh> +#include <vereign/core/string.hh> + +namespace vereign::ncrypt::rsa { + +auto OpenStorageProvider() -> UniquePtr { + UniquePtr provider{}; + auto status = NCryptOpenStorageProvider(provider.Ref(), MS_KEY_STORAGE_PROVIDER, 0); + if (status != ERROR_SUCCESS) { + throw Error{status, "open crypto store failed"}; + } + + return provider; +} + +auto LoadKey(NCRYPT_PROV_HANDLE provider, const std::string& key_name) -> UniquePtr { + UniquePtr key{}; + auto wkey_name = string::widen(key_name); + auto status = NCryptOpenKey(provider, key.Ref(), wkey_name.data(), 0, 0); + if (status != ERROR_SUCCESS && status != NTE_BAD_KEYSET) { + throw Error{status, "open key failed"}; + } + + return key; +} + +auto CreateKey( + NCRYPT_PROV_HANDLE provider, + int bits, + const std::string& key_name, + std::optional<KeyUIPolicy> ui_policy +) -> UniquePtr { + UniquePtr key{}; + auto wkey_name = string::widen(key_name); + auto status = NCryptCreatePersistedKey( + provider, + key.Ref(), + BCRYPT_RSA_ALGORITHM, + wkey_name.data(), + 0, + 0 + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "creating rsa key failed"}; + } + + auto key_len = DWORD(bits); + status = NCryptSetProperty( + key.Get(), + NCRYPT_LENGTH_PROPERTY, + (PBYTE)&key_len, + sizeof(key_len), + NCRYPT_PERSIST_FLAG + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "setup rsa key length failed"}; + } + + if (ui_policy) { + auto creation_title = vereign::string::widen(ui_policy->CreationTitle); + auto description = vereign::string::widen(ui_policy->Description); + auto friendly_name = vereign::string::widen(ui_policy->FriendlyName); + + NCRYPT_UI_POLICY ui_policy_prop{}; + ui_policy_prop.dwVersion = 1; + ui_policy_prop.dwFlags = NCRYPT_UI_PROTECT_KEY_FLAG; + ui_policy_prop.pszCreationTitle = creation_title.data(); + ui_policy_prop.pszDescription = description.data(); + ui_policy_prop.pszFriendlyName = friendly_name.data(); + + status = NCryptSetProperty( + key.Get(), + NCRYPT_UI_POLICY_PROPERTY, + (PBYTE)&ui_policy_prop, + sizeof(ui_policy_prop), + NCRYPT_PERSIST_FLAG + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "configure key ui policy failed"}; + } + } + + status = NCryptFinalizeKey(key.Get(), 0); + if (status != ERROR_SUCCESS) { + throw Error{status, "finalizing rsa key failed"}; + } + + return key; +} + +void DeleteKey(NCRYPT_KEY_HANDLE key) { + auto status = NCryptDeleteKey(key, 0); + if (status != ERROR_SUCCESS) { + throw Error{status, "deleting key failed"}; + } +} + +void PublicKeyEncrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& encrypted) { + BCRYPT_OAEP_PADDING_INFO pad; + int flags = NCRYPT_PAD_OAEP_FLAG; + pad.pszAlgId = BCRYPT_SHA1_ALGORITHM; + pad.pbLabel = nullptr; + pad.cbLabel = 0; + DWORD size; + + auto status = NCryptEncrypt( + key, + (PBYTE)src.Data(), + (DWORD)src.Size(), + &pad, + nullptr, + 0, + &size, + flags + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "encryption failed"}; + } + + encrypted.Reserve(size); + + status = NCryptEncrypt( + key, + (PBYTE)src.Data(), + (DWORD)src.Size(), + &pad, + (PBYTE)encrypted.end(), + size, + &size, + flags + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "encryption failed"}; + } + + encrypted.IncSize(size); +} + +void PrivateKeyDecrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& decrypted) { + BCRYPT_OAEP_PADDING_INFO pad; + int flags = NCRYPT_PAD_OAEP_FLAG; + pad.pszAlgId = BCRYPT_SHA1_ALGORITHM; + pad.pbLabel = nullptr; + pad.cbLabel = 0; + DWORD size; + + auto status = NCryptDecrypt( + key, + (PBYTE)src.Data(), + (DWORD)src.Size(), + &pad, + nullptr, + 0, + &size, + flags + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "decryption failed"}; + } + + decrypted.Reserve(size); + + status = NCryptDecrypt( + key, + (PBYTE)src.Data(), + (DWORD)src.Size(), + &pad, + (PBYTE)decrypted.end(), + size, + &size, + flags + ); + if (status != ERROR_SUCCESS) { + throw Error{status, "decryption failed"}; + } + + decrypted.IncSize(size); +} + +} // vereign::ncrypt::rsa diff --git a/cpp/src/vereign/ncrypt/rsa.hh b/cpp/src/vereign/ncrypt/rsa.hh new file mode 100644 index 0000000000000000000000000000000000000000..06d3f05d2cb1892b6a43956c6e6940f6d6b3d0a0 --- /dev/null +++ b/cpp/src/vereign/ncrypt/rsa.hh @@ -0,0 +1,150 @@ +#ifndef __VEREIGN_NCRYPT_RSA_HH +#define __VEREIGN_NCRYPT_RSA_HH + +#include <vereign/ncrypt/unique_ptr.hh> +#include <vereign/bytes/buffer.hh> + +#include <windows.h> +#include <ncrypt.h> +#include <string> +#include <optional> + +namespace vereign::ncrypt::rsa { + +/** + * KeyUIPolicy is used to specify some UI captions shown to the user in a dialog when a key is + * created or opened for the first time in a process. + */ +struct KeyUIPolicy { + std::string_view CreationTitle; + std::string_view Description; + std::string_view FriendlyName; +}; + +/** + * Opens the default ncrypt storage provider. + * + * @returns the storage provider handle. + * + * @throws ncrypt::Error on failure. + */ +auto OpenStorageProvider() -> UniquePtr; + +/** + * Loads a key from a ncrypt storage provider. + * + * Example: + * @code + * auto provider = ncrypt::rsa::OpenStorageProvider(); + * auto key = ncrypt::rsa::LoadKey(provider.Get(), "test_key"); + * assert(key.Get() != 0); + * @endcode + * + * @param provider The ncrypt storage provider. + * @param key_name The name of the key that will be loaded. + * @returns the loaded key if the key exists, or null ncrypt::UniquePtr if the key does not exists. + * + * @throws ncrypt::Error on failure. + */ +auto LoadKey(NCRYPT_PROV_HANDLE provider, const std::string& key_name) -> UniquePtr; + +/** + * Creates persistent RSA key into a given storage provider. + * + * Example: + * @code + * auto provider = ncrypt::rsa::OpenStorageProvider(); + * // create key without UI policy + * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {}); + * assert(key.Get() != 0); + * @endcode + * + * @param provider The ncrypt storage provider. + * @param bits The length of the key in bits. + * @param key_name The new key name. + * @param ui_policy The UI policy. If the UI policy is not provided, the key will not have any + * protection. If the UI policy is provided when the key is created and loaded the user will be + * asked for his consent by showing a dialog window with text information specified in the UI policy. + * + * @returns the created key on success. + * + * @throws ncrypt::Error on failure. + */ +auto CreateKey( + NCRYPT_PROV_HANDLE provider, + int bits, + const std::string& key_name, + std::optional<KeyUIPolicy> ui_policy +) -> UniquePtr; + +/** + * Deletes a key. + * + * Example: + * @code + * auto provider = ncrypt::rsa::OpenStorageProvider(); + * auto key = ncrypt::rsa::LoadKey(provider.Get(), "test_key"); + * if (key) { + * ncryp::rsa::DeleteKey(key.Get()); + * } + * @endcode + * + * @param key The key to delete. + * + * @throws ncrypt::Error on failure. + */ +void DeleteKey(NCRYPT_KEY_HANDLE key); + +/** + * Encrypts given bytes with RSA public key. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto provider = ncrypt::rsa::OpenStorageProvider(); + * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {}); + * + * bytes::Buffer encrypted; + * ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted); + * + * bytes::Buffer decrypted; + * ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted); + * assert(decrypted.View() == encrypted.View()); + * @endcode + * + * @param key The RSA key. + * @param src The bytes that will be encrypted. + * @param encrypted The result of the encryption. + * + * @throws ncrypt::Error on failure. + */ +void PublicKeyEncrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& encrypted); + +/** + * Decrypts given bytes with RSA private key. + * + * Example: + * @code + * const std::string input{"foo bar"}; + * auto provider = ncrypt::rsa::OpenStorageProvider(); + * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {}); + * + * bytes::Buffer encrypted; + * ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted); + * + * bytes::Buffer decrypted; + * ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted); + * assert(decrypted.View() == encrypted.View()); + * @endcode + * + * @param key The RSA key. + * @param src The bytes that will be decrypted. + * @param decrypted The result of the decryption. + * + * @throws ncrypt::Error on failure. + */ +void PrivateKeyDecrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& decrypted); + +} // vereign::ncrypt::rsa + +#endif // __VEREIGN_NCRYPT_RSA_HH diff --git a/cpp/src/vereign/ncrypt/unique_ptr.cc b/cpp/src/vereign/ncrypt/unique_ptr.cc new file mode 100644 index 0000000000000000000000000000000000000000..a177839f4ec6c2a12780a5e204db45e92965ecd1 --- /dev/null +++ b/cpp/src/vereign/ncrypt/unique_ptr.cc @@ -0,0 +1,82 @@ +#include <vereign/ncrypt/unique_ptr.hh> + +#include <vereign/ncrypt/errors.hh> +#include <utility> +#include <iostream> + +namespace vereign::ncrypt { + +UniquePtr::UniquePtr() noexcept + : ptr_{0} +{ +} + +UniquePtr::UniquePtr(ULONG_PTR handle) noexcept + : ptr_{handle} +{ +} + +UniquePtr::UniquePtr(UniquePtr&& other) noexcept + : ptr_{other.ptr_} +{ + other.ptr_ = 0; +} + +auto UniquePtr::operator=(UniquePtr&& other) noexcept -> UniquePtr& { + std::swap(ptr_, other.ptr_); + + return *this; +} + +UniquePtr::~UniquePtr() noexcept { + if (ptr_ == 0) { + return; + } + + NCryptFreeObject(ptr_); +} + +auto UniquePtr::Get() const noexcept -> ULONG_PTR { + return ptr_; +} + +auto UniquePtr::Ref() noexcept -> ULONG_PTR* { + return &ptr_; +} + +void UniquePtr::Reset() { + if (ptr_ == 0) { + return; + } + + auto status = NCryptFreeObject(ptr_); + if (status == NTE_INVALID_HANDLE) { + throw Error(status, "free object failed"); + } + + ptr_ = 0; +} + +auto UniquePtr::Release() -> ULONG_PTR { + if (ptr_ == 0) { + return 0; + } + + auto result = ptr_; + + auto status = NCryptFreeObject(ptr_); + if (status == NTE_INVALID_HANDLE) { + throw Error(status, "free object failed"); + } + + ptr_ = 0; + + return result; +} + + +UniquePtr::operator bool() const noexcept { + return ptr_ != 0; +} + +} // vereign::ncrypt diff --git a/cpp/src/vereign/ncrypt/unique_ptr.hh b/cpp/src/vereign/ncrypt/unique_ptr.hh new file mode 100644 index 0000000000000000000000000000000000000000..ae4475300c7399f136810082988e4c19d3495666 --- /dev/null +++ b/cpp/src/vereign/ncrypt/unique_ptr.hh @@ -0,0 +1,98 @@ +#ifndef __VEREIGN_NCRYPT_UNIQUE_PTR_HH +#define __VEREIGN_NCRYPT_UNIQUE_PTR_HH + +#include <windows.h> + +namespace vereign::ncrypt { + +/** + * Holds ownership of ncrypt handles like NCRYPT_PROV_HANDLE, NCRYPT_PROV_HANDLE. + * + * The UniquePtr should be used any time a ncrypt API returns a handle in order to ensure exception + * safety and resource cleanup. + * + * The owned handler is typically cleaned up in the UniquePtr::~UniquePtr() by using + * NCryptFreeObject function. + */ +class UniquePtr { +public: + /** + * Constructs a default empty object. + */ + UniquePtr() noexcept; + + /** + * Constructs a UniquePtr that becomes an owner of the passed handle. + * + * @param handle The handle that becomes owned by the crated UniquePtr. + */ + explicit UniquePtr(ULONG_PTR handle) noexcept; + + /** + * Move constructor. + */ + UniquePtr(UniquePtr&& other) noexcept; + + /** + * Move assignment operator. + */ + auto operator=(UniquePtr&& other) noexcept -> UniquePtr&; + + // Disable copying + UniquePtr(const UniquePtr&) = delete; + auto operator=(const UniquePtr&) -> UniquePtr& = delete; + + /** + * Upon destruction the owned handle if any is freed with NCryptFreeObject function. + */ + ~UniquePtr() noexcept; + + /** + * Retrieve the owned handle. + */ + auto Get() const noexcept -> ULONG_PTR; + + /** + * Retrieve a pointer to the owned handle. + * + * Example: + * @code + * UniquePtr key{}; + * NCryptOpenKey(provider, key.Ref(), L"test_key", 0, 0); + * assert(bool(key) == true); + * @endcode + * + * This makes it possible for a ncrypt API to inject its output handle directly into the + * UniquePtr object. + */ + auto Ref() noexcept -> ULONG_PTR*; + + /** + * Free the owned handler. + * + * If the owned handler is not null, it frees the handle by calling NCryptFreeObject, and sets + * the owned handler to null. + */ + void Reset(); + + /** + * Releases the ownership of the owned handler. + * + * @returns the owned handler, before releasing the ownership. + */ + auto Release() -> ULONG_PTR; + + /** + * Checks if *this owns a non-null handler. + * + * @returns true if *this owns a handler, false otherwise. + */ + operator bool() const noexcept; + +private: + ULONG_PTR ptr_; +}; + +} // vereign::ncrypt + +#endif // __VEREIGN_NCRYPT_UNIQUE_PTR_HH diff --git a/cpp/src/vereign/restapi/client.cc b/cpp/src/vereign/restapi/client.cc index 1dd1b8cc9e874ce972ff6725af6ce3768759a4c5..38f023c008b9a9c357fc430efa95fdc04083cebf 100644 --- a/cpp/src/vereign/restapi/client.cc +++ b/cpp/src/vereign/restapi/client.cc @@ -1,5 +1,3 @@ -#include "vereign/restapi/detail/post_task.hh" -#include <chrono> #include <vereign/restapi/client.hh> #include <vereign/restapi/detail/http_reader.hh> @@ -10,23 +8,27 @@ #include <boost/beast/version.hpp> #include <boost/asio/dispatch.hpp> -namespace vereign { -namespace restapi { +namespace { + constexpr std::string_view httpUserAgent = "Vereign Client Library"; +} + +namespace vereign::restapi { constexpr auto defaultTimeout = std::chrono::seconds(30); Client::Client( boost::asio::io_context& ioc, boost::asio::ssl::context& ssl_ctx, - const std::string& host, - const std::string& port + std::string host, + std::string port ) - : executor_{asio::make_strand(ioc)}, + : user_agent_{httpUserAgent}, + executor_{asio::make_strand(ioc)}, ssl_ctx_{ssl_ctx}, resolver_{ioc}, reader_{nullptr}, - host_{host}, - port_{port}, + host_{std::move(host)}, + port_{std::move(port)}, expiry_time_{defaultTimeout}, connecting_{false}, writing_{false}, @@ -38,16 +40,17 @@ Client::Client( Client::Client( boost::asio::io_context& ioc, boost::asio::ssl::context& ssl_ctx, - const std::string& host, - const std::string& port, + std::string host, + std::string port, std::chrono::nanoseconds expiry_time ) - : executor_{asio::make_strand(ioc)}, + : user_agent_{httpUserAgent}, + executor_{asio::make_strand(ioc)}, ssl_ctx_{ssl_ctx}, resolver_{ioc}, reader_{nullptr}, - host_{host}, - port_{port}, + host_{std::move(host)}, + port_{std::move(port)}, expiry_time_{expiry_time}, connecting_{false}, writing_{false}, @@ -60,6 +63,10 @@ Client::~Client() noexcept { Close(); } +auto Client::UserAgent() const -> const std::string& { + return user_agent_; +} + void Client::Close() { asio::post( executor_, @@ -77,7 +84,7 @@ void Client::Close() { ); } -const asio::executor& Client::GetExecutor() const { +auto Client::GetExecutor() const -> const asio::executor& { return executor_; } @@ -153,7 +160,9 @@ void Client::connect(tcp::resolver::results_type results) { auto stream = stream_; beast::get_lowest_layer(*stream_).async_connect( results, - [this, stream](beast::error_code ec, endpoint_t) { + [this, stream](beast::error_code ec, endpoint_t endpoint) { + boost::ignore_unused(endpoint); + if (ec) { connecting_ = false; handleError(ec.message()); @@ -213,7 +222,7 @@ void Client::doPost() { req_->version(11); req_->target(task->Path()); req_->set(beast::http::field::host, host_); - req_->set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req_->set(beast::http::field::user_agent, user_agent_); req_->set(beast::http::field::content_type, "application/json"); auto err = task->EncodeRequest(*req_); @@ -278,5 +287,4 @@ void Client::readResponse() { }); } -} -} +} // namespace vereign::restapi diff --git a/cpp/src/vereign/restapi/client.hh b/cpp/src/vereign/restapi/client.hh index 1bb1821f37dacbffdac972e6a5d3e75fe3415be0..c03304c3eb97b650b1635cf2cbbbe49e496baa2f 100644 --- a/cpp/src/vereign/restapi/client.hh +++ b/cpp/src/vereign/restapi/client.hh @@ -1,8 +1,6 @@ #ifndef __VEREIGN_RESTAPI_CLIENT_HH #define __VEREIGN_RESTAPI_CLIENT_HH -#include <chrono> -#include <type_traits> #include <vereign/restapi/detail/post_task.hh> #include <vereign/restapi/http_header.hh> @@ -16,9 +14,10 @@ #include <string> #include <future> #include <deque> +#include <chrono> +#include <type_traits> -namespace vereign { -namespace restapi { +namespace vereign::restapi { namespace beast = boost::beast; namespace asio = boost::asio; @@ -69,8 +68,8 @@ public: Client( asio::io_context& ioc, asio::ssl::context& ssl_ctx, - const std::string& host, - const std::string& port + std::string host, + std::string port ); /** @@ -86,8 +85,8 @@ public: Client( asio::io_context& ioc, asio::ssl::context& ssl_ctx, - const std::string& host, - const std::string& port, + std::string host, + std::string port, std::chrono::nanoseconds expiry_time ); @@ -100,7 +99,16 @@ public: // Disable copying Client(const Client&) = delete; - Client& operator=(const Client&) = delete; + auto operator=(const Client&) -> Client& = delete; + + /** + * Retrieve client http user agent string. + * + * This is what the client puts in the requests as `User-Agent` header. + * + * @returns the client http user agent string. + */ + auto UserAgent() const -> const std::string&; /** * Close closes the connection. @@ -117,7 +125,7 @@ public: * * @returns the client's strand executor. */ - const asio::executor& GetExecutor() const; + auto GetExecutor() const -> const asio::executor&; /** * Post makes a blocking post request. @@ -310,6 +318,9 @@ private: void cancelAllTasks(const detail::PostError& err); private: + // http user agent. + std::string user_agent_; + // a strand that calls all completion handlers in single thread. asio::executor executor_; @@ -353,7 +364,6 @@ private: bool closed_; }; -} // namespace restapi -} // namespace vereign +} // namespace vereign::restapi #endif // __VEREIGN_RESTAPI_CLIENT_HH diff --git a/cpp/src/vereign/restapi/client_session.hh b/cpp/src/vereign/restapi/client_session.hh index 28e848a7e270c52f09e083132a862c51e08caded..55ee67962299230e8a280016a8e5631c69f0a676 100644 --- a/cpp/src/vereign/restapi/client_session.hh +++ b/cpp/src/vereign/restapi/client_session.hh @@ -3,15 +3,12 @@ #include <vereign/restapi/client.hh> #include <vereign/restapi/http_header.hh> - +#include <vereign/client_library/common_types.pb.h> #include <vereign/client_library/identity_types.pb.h> -#include <boost/none.hpp> -#include <vector> -#include <future> +#include <boost/none.hpp> -namespace vereign { -namespace restapi { +namespace vereign::restapi { namespace detail { struct AuthErrorType { @@ -29,9 +26,9 @@ struct AuthErrorType { using AuthError = boost::optional<AuthErrorType>; -inline AuthError MakeAuthError( +inline auto MakeAuthError( const client_library::LoginFormPreviousAddedDeviceResponse& resp -) { +) -> AuthError { if (resp.code() == "200") { return boost::none; } @@ -43,18 +40,26 @@ inline AuthError MakeAuthError( }; } -inline AuthError NoAuthError() { +inline auto NoAuthError() -> AuthError { return boost::none; } +struct Session { + std::string PublicKey; + std::string DeviceHash; + std::string Token; + std::string UUID; +}; + } // namespace detail /** * ClientSession is a Client decorator, that maintains authenticated session. + * + * All public methods are thread safe. */ class ClientSession { public: - // FIXME: the key should come from a storage /** * Constructs ClientSession instance with default API base path. * @@ -63,13 +68,15 @@ public: * @param client Restapi client. * @returns ClientSession instance. */ - ClientSession(Client& client, const std::string& pub_key) + ClientSession(Client& client) : client_{client}, - pub_key_{pub_key}, - base_path_{"/api"} + base_path_{"/api"}, + uuid_{""}, + session_token_{""}, + pub_key_{""}, + device_hash_{""} {} - // FIXME: the key should come from a storage /** * Constructs ClientSession instance. * @@ -77,18 +84,18 @@ public: * @param base_path API base path, for example `/api` * @returns ClientSession instance. */ - ClientSession( - Client& client, - const std::string& base_path, - const std::string& pub_key - ) : client_{client}, - pub_key_{pub_key}, - base_path_{base_path} + ClientSession(Client& client, std::string base_path) + : client_{client}, + base_path_{std::move(base_path)}, + uuid_{""}, + session_token_{""}, + pub_key_{""}, + device_hash_{""} {} // Disable copying. ClientSession(const ClientSession&) = delete; - ClientSession& operator=(const ClientSession&) = delete; + auto operator=(const ClientSession&) -> ClientSession& = delete; /** * Closes the connection. @@ -97,6 +104,40 @@ public: Close(); } + /** + * Set public key used for authentication. + * + * This effectively resets the current session (if any). + * + * @param key The public key that will be used for authentication. + */ + void SetPubKey(std::string key, std::string hash) { + std::lock_guard<std::mutex> l{mu_}; + + pub_key_ = std::move(key); + device_hash_ = std::move(hash); + session_token_ = ""; + uuid_ = ""; + } + + /** + * Checks if the client has identity. + * + * @returns true when there device identity exits, false otherwise. + */ + auto HasIdentity() -> bool { + std::lock_guard<std::mutex> l{mu_}; + + return pub_key_.size() != 0; + } + + /** + * Retrieve the underlying http client. + */ + auto GetClient() -> Client& { + return client_; + } + /** * Close closes the connection. * @@ -110,7 +151,7 @@ public: } /** - * Post makes a blocking post request. + * Post makes an authenticated blocking post request. * * The passed `req` and `resp` parameters are moved in and returned back once, * the returned future is resolved. @@ -152,7 +193,7 @@ public: } /** - * PostAsync makes non blocking post request. + * PostAsync makes an authenticated non blocking post request. * * The passed `req` and `resp` parameters are moved in and returned back once * the CompletionFunc is called. @@ -196,8 +237,180 @@ public: ); } + /** + * PublicPostAsync makes a non authenticated non blocking post request. + * + * The request is without session, only the device identity - public key, device hash are used + * for this request. + * It is suitable for the public API. + * + * The passed `req` and `resp` parameters are moved in and returned back once + * the CompletionFunc is called. + * + * @tparam CompletionFunc A completion functor - `void (Result&&)`, where + * the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where + * RequestPtr is `std::remove_reference<RequestPtrType>::type` and + * ResponsePtr is `std::remove_reference<RequestPtrType>::type`. + * + * @param path HTTP path, for example `/passport/listPassports`. + * @param req Request object that will be serialized as JSON body. + * It can be a const pointer or std::unique_ptr to protobuf message. + * + * @param resp Response object that will be used to decode the response. + * It can be a pointer or std::unique_ptr to protobuf message. + * + * @param func Completion func, that will be called when the post request + * is finished. + */ + template <class RequestPtrType, class ResponsePtrType, class CompletionFunc> + void PublicPostAsync( + const std::string& path, + RequestPtrType&& req, + ResponsePtrType&& resp, + CompletionFunc&& cf + ) { + + auto session = getSession(); + + client_.PostAsync( + base_path_ + path, + std::move(req), + std::move(resp), + std::vector<vereign::restapi::HttpHeader>{ + {"publicKey", session.PublicKey}, + {"deviceHash", session.DeviceHash} + }, + std::move(cf) + ); + } + + /** + * PublicPost makes a non authenticated blocking post request. + * + * The request is without session, only the device identity - public key, device hash are used + * for this request. + * It is suitable for the public API. + * + * The passed `req` and `resp` parameters are moved in and returned back once, + * the returned future is resolved. + * + * @param path HTTP path, for example `/passport/listPassports`. + * The path will be added to the ClientSession base path. + * @param req Request object that will be serialized as JSON body. + * It can be a const pointer or std::unique_ptr to protobuf message. + * + * @param resp Response object that will be used to decode the response. + * It can be a pointer or std::unique_ptr to protobuf message. + * + * @returns future that will be resolved with a result containing both + * the request and response objects originally passed to the `Post` call. + */ + template <class RequestPtrType, class ResponsePtrType> + auto PublicPost( + const std::string& path, + RequestPtrType&& req, + ResponsePtrType&& resp + ) { + auto session = getSession(); + + return client_.Post( + base_path_ + path, + std::move(req), + std::move(resp), + std::vector<vereign::restapi::HttpHeader>{ + {"publicKey", session.PublicKey}, + {"deviceHash", session.DeviceHash} + } + ); + } + + /** + * Initiate authentication. + * + * This method is blocking. + * + * @param pub_key The public key to authenticate with. + * @param resp Authentication response. + */ + auto Authenticate( + const std::string& pub_key, + const std::string& device_hash, + client_library::EmptyResponse* resp + ) { + std::promise<void> promise; + auto future = promise.get_future(); + + { + std::lock_guard<std::mutex> l{mu_}; + pub_key_ = pub_key; + device_hash_ = device_hash; + session_token_ = ""; + uuid_ = ""; + } + + withAuthentication( + [resp, promise = std::move(promise)](const detail::AuthError& err) mutable { + if (err) { + err->AssignTo(*resp); + } else { + resp->set_code("200"); + resp->set_status("OK"); + } + + promise.set_value(); + } + ); + + future.wait(); + } + + /** + * Checks if the client is authenticated. + * + * @returns true when there is successfully authenticated session, false otherwise. + */ + auto HasSession() -> bool { + std::lock_guard<std::mutex> l{mu_}; + + return session_token_.size() != 0; + } + + /** + * Retrieve the restful-api base path. + * + * @returns the base path used for http requests, for example: `api/`. + */ + auto BasePath() const -> const std::string& { + return base_path_; + } + private: + void destroySession() { + std::lock_guard<std::mutex> l{mu_}; + + session_token_ = ""; + uuid_ = ""; + } + + void updateSession(const std::string& token, const std::string& uuid) { + std::lock_guard<std::mutex> l{mu_}; + + session_token_ = token; + uuid_ = uuid; + } + + auto getSession() -> detail::Session { + std::lock_guard<std::mutex> l{mu_}; + + return detail::Session{ + pub_key_, + device_hash_, + session_token_, + uuid_ + }; + } + template <class RequestPtr, class ResponsePtr, class CompletionFunc> void authPostAsync( const detail::AuthError& err, @@ -216,15 +429,17 @@ private: } using namespace std::placeholders; + auto session = getSession(); client_.PostAsync( path, std::move(req), std::move(resp), std::vector<vereign::restapi::HttpHeader>{ - {"publicKey", pub_key_}, - {"token", session_token_}, - {"uuid", uuid_} + {"publicKey", session.PublicKey}, + {"deviceHash", session.DeviceHash}, + {"token", session.Token}, + {"uuid", session.UUID} }, std::bind( &ClientSession::handlePostResult<RequestPtr, ResponsePtr, CompletionFunc>, @@ -254,15 +469,17 @@ private: } using namespace std::placeholders; + auto session = getSession(); client_.PostAsync( path, std::move(req), std::move(resp), std::vector<vereign::restapi::HttpHeader>{ - {"publicKey", pub_key_}, - {"token", session_token_}, - {"uuid", uuid_} + {"publicKey", session.PublicKey}, + {"deviceHash", session.DeviceHash}, + {"token", session.Token}, + {"uuid", session.UUID} }, std::move(cf) ); @@ -276,7 +493,10 @@ private: ) { using namespace std::placeholders; auto& resp = result.Response; + if (resp->code() == "400" && resp->status() == "Bad session") { + destroySession(); + withAuthentication( std::bind( &ClientSession::authPostRetryAsync<RequestPtr, ResponsePtr, CompletionFunc>, @@ -300,7 +520,8 @@ private: asio::post( client_.GetExecutor(), [this, cf = std::move(cf)]() mutable { - if (session_token_.size() != 0) { + auto session = getSession(); + if (!session.Token.empty()) { cf(detail::NoAuthError()); return; } @@ -314,7 +535,8 @@ private: std::make_unique<client_library::EmptyRequest>(), std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>(), std::vector<vereign::restapi::HttpHeader>{ - {"publicKey", pub_key_} + {"publicKey", session.PublicKey}, + {"deviceHash", session.DeviceHash} }, [this, cf = std::move(cf)] (ResultType&& result) mutable { if (result.Response->code() != "200") { @@ -335,8 +557,7 @@ private: return; } - session_token_ = result.Response->data().session(); - uuid_ = result.Response->data().uuid(); + updateSession(result.Response->data().session(), result.Response->data().uuid()); cf(detail::MakeAuthError(*result.Response)); } @@ -346,23 +567,28 @@ private: } private: - // http client. + // http client Client& client_; - // public key used for creating the authenticated sessions. - std::string pub_key_; - // http base path of the api calls, for example `/api` std::string base_path_; - // session uuid. + // protects the data that follows + std::mutex mu_; + + // session uuid std::string uuid_; - // session token. + // session token std::string session_token_; + + // public key used for creating the authenticated sessions + std::string pub_key_; + + // public key hash + std::string device_hash_; }; -} // namespace restapi } // namespace vereign #endif // __VEREIGN_RESTAPI_CLIENT_SESSION_HH diff --git a/cpp/src/vereign/restapi/detail/post_task.hh b/cpp/src/vereign/restapi/detail/post_task.hh index 74f1ea70ae57b105207529f64c86aa27aaaa48bc..88b84794f1329bf69743cf4d69c6bab4472fe104 100644 --- a/cpp/src/vereign/restapi/detail/post_task.hh +++ b/cpp/src/vereign/restapi/detail/post_task.hh @@ -12,8 +12,7 @@ #include <google/protobuf/util/json_util.h> #include <fmt/core.h> -namespace vereign { -namespace restapi { +namespace vereign::restapi { namespace beast = boost::beast; @@ -109,6 +108,8 @@ public: void Complete(const PostError& err) override { if (err) { + resp_->set_code("500"); + resp_->set_status("http client error"); resp_->set_error(err.value()); } @@ -125,6 +126,14 @@ public: private: void decodeErrorResponse(const HttpResponse& httpResp) { + if (httpResp.result() == beast::http::status::not_found) { + resp_->set_code("404"); + resp_->set_status(httpResp.body()); + resp_->set_error(httpResp.body()); + + return; + } + google::protobuf::util::JsonParseOptions options; client_library::ErrorStringResponse stringResp; @@ -168,7 +177,6 @@ private: }; } // namespace detail -} // namespace restapi } // namespace vereign #endif // __VEREIGN_RESTAPI_DETAIL_POST_TASK_HH diff --git a/cpp/src/vereign/service/identity_service.cc b/cpp/src/vereign/service/identity_service.cc new file mode 100644 index 0000000000000000000000000000000000000000..9a23b616b7b843afaf409989f530c6fd5a68faeb --- /dev/null +++ b/cpp/src/vereign/service/identity_service.cc @@ -0,0 +1,66 @@ +#include <vereign/service/identity_service.hh> + +#include <vereign/service/gen/identity_service.hh> +#include <vereign/client_library/common_types.pb.h> +#include <vereign/restapi/http_header.hh> +#include <vereign/restapi/client_session.hh> +#include <vereign/encoding/base64.hh> + +#include <memory> + +namespace { + const std::string loginWithPreviouslyAddedDevicePath = "loginWithPreviouslyAddedDevice"; + const std::string loginWithNewDevicePath = "loginWithNewDevice"; +} + +namespace vereign::service { + +IdentityService::IdentityService( + restapi::ClientSession& client_session, + identity::Provider& identity_provider +) + : gen::IdentityService{client_session}, + client_session_{client_session}, + identity_provider_{identity_provider} +{} + +void IdentityService::LoginWithExistingPubKey( + const client_library::LoginWithExistingPubKeyForm* req, + client_library::EmptyResponse* resp +) { + client_session_.Authenticate(req->pubkey(), "", resp); +} + +void IdentityService::LoginWithNewDevice( + const client_library::LoginFormNewDevice* req, + client_library::LoginFormNewDeviceResponse* resp +) { + auto public_key = identity_provider_.RecreateIdentity(req->pin()); + + auto result = client_session_.GetClient().Post( + client_session_.BasePath() + gen::IdentityService::ServicePath + loginWithNewDevicePath, + std::make_unique<client_library::EmptyRequest>(), + resp, + std::vector<vereign::restapi::HttpHeader>{ + {"publicKey", public_key}, + {"deviceHash", identity_provider_.GetDeviceHash()} + } + ); + + result.wait(); + + if (resp->code() == "200") { + client_session_.SetPubKey(public_key, identity_provider_.GetDeviceHash()); + } +} + + +void IdentityService::LoginWithPreviouslyAddedDevice( + const client_library::LoginFormPreviousAddedDevice* req, + client_library::EmptyResponse* resp +) { + auto public_key = identity_provider_.LoadIdentity(req->pin()); + client_session_.Authenticate(public_key, identity_provider_.GetDeviceHash(), resp); +} + +} // namespace vereign diff --git a/cpp/src/vereign/service/identity_service.hh b/cpp/src/vereign/service/identity_service.hh new file mode 100644 index 0000000000000000000000000000000000000000..11de3e0f5318535d3d7307d126aa19d590141ea6 --- /dev/null +++ b/cpp/src/vereign/service/identity_service.hh @@ -0,0 +1,107 @@ +#ifndef __VEREIGN_SERVICE_IDENITY_SERVICE_HH +#define __VEREIGN_SERVICE_IDENITY_SERVICE_HH + +#include <vereign/identity/provider.hh> +#include <vereign/client_library/common_types.pb.h> +#include <vereign/client_library/identity_types.pb.h> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/restapi/post_result.hh> +#include <vereign/service/gen/identity_service.hh> + +#include <future> + +namespace vereign { + +namespace restapi { + class ClientSession; +} + +namespace service { + +template <class Request, class Response> +using Result = restapi::PostResult<Request, Response>; + +class IdentityService : public gen::IdentityService { +public: + /** + * Creates IdentityService instance. + * + * @param client_session HTTP client used for communicating with the Vereign Restful API. + * @param identity_provider Local identity provider (manager). + */ + IdentityService( + restapi::ClientSession& client_session, + identity::Provider& identity_provider + ); + + // disable copying + IdentityService(const IdentityService&) = delete; + auto operator=(const IdentityService&) -> IdentityService& = delete; + + /** + * Login with existing identity public key. + * + * This API is for test purposes only. It is not exposed under the gRPC API, and thus is not + * accessible by the integrators. + * + * **WARN: do not use this in production code** + */ + void LoginWithExistingPubKey( + const client_library::LoginWithExistingPubKeyForm* req, + client_library::EmptyResponse* resp + ); + + /** + * Registers a new device. + * + * req.pin is required only under Linux. + * + * Under windows the system cypto storage is used. + * When the device is registered a master key is created and the user will be asked for his + * consent by showing a dialog window. + * + * @param req Login request. + * @param resp Operation response. + * + * @throws kvstore::InvalidPinCodeError Only under Linux. Thrown when the provided pin is invalid, + * currently that is when the pin is empty. + */ + void LoginWithNewDevice( + const client_library::LoginFormNewDevice* req, + client_library::LoginFormNewDeviceResponse* resp + ); + + /** + * Login with already registered device. + * + * req.pin is required only under Linux. + * + * Under windows the system cypto storage is used. + * When the device is registered a master key is created and the user will be asked for his + * consent by showing a dialog window. + * + * @param req Login request. + * @param resp Operation response. + * + * @throws kvstore::StorageNotInitializedError when the crypto storage is empty, meaning that + * the device is not registered. + * @throws kvstore::InvalidPinCodeError under Linux, when the provided pin is invalid, meaning + * that the pin does not match the pin used during the registration. + * @throws kvstore::InvalidIdentityError under windows, when for some reason the RSA master key + * has been changed. + */ + void LoginWithPreviouslyAddedDevice( + const client_library::LoginFormPreviousAddedDevice* req, + client_library::EmptyResponse* resp + ); + +private: + restapi::ClientSession& client_session_; + identity::Provider& identity_provider_; +}; + +} // namespace service +} // namespace vereign + + +#endif // __VEREIGN_SERVICE_IDENITY_SERVICE_HH diff --git a/cpp/src/vereign/service/passport_service.cc b/cpp/src/vereign/service/passport_service.cc deleted file mode 100644 index e6cf88372cecd5c47c002b893fa035dfb5a8da8f..0000000000000000000000000000000000000000 --- a/cpp/src/vereign/service/passport_service.cc +++ /dev/null @@ -1,32 +0,0 @@ -#include "vereign/service/gen/passport_service.hh" -#include <vereign/service/passport_service.hh> -#include <vereign/restapi/client_session.hh> - -namespace { - const std::string listPassportsPath = "listPassports"; -} - -namespace vereign { -namespace service { - -PassportService::PassportService(restapi::ClientSession& client_session) - : gen::PassportService{client_session}, - client_session_{client_session} -{} - - -std::future<PassportService::ListPassportsResult> -PassportService::ListPassportsManually( - const client_library::ListPassportsForm* req, - client_library::ListPassportsFormResponse* resp -) { - return client_session_.Post( - gen::PassportService::ServicePath + listPassportsPath, - req, - resp - ); -} - - -} // namespace service -} // namespace vereign diff --git a/cpp/src/vereign/service/passport_service.hh b/cpp/src/vereign/service/passport_service.hh deleted file mode 100644 index f59b4b27290676e1c3ae9c5cd2b1a7714cce31b0..0000000000000000000000000000000000000000 --- a/cpp/src/vereign/service/passport_service.hh +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef __VEREIGN_SERVICE_PASSPORT_SERVICE_HH -#define __VEREIGN_SERVICE_PASSPORT_SERVICE_HH - -#include <vereign/restapi/client_session.hh> -#include <vereign/restapi/post_result.hh> -#include <vereign/client_library/types.gen.pb.h> -#include <vereign/service/gen/passport_service.hh> - -#include <future> - -namespace vereign { - -namespace restapi { - class ClientSession; -} - -namespace service { - -template <class Request, class Response> -using Result = restapi::PostResult<Request, Response>; - -class PassportService : public gen::PassportService { -public: - explicit PassportService(restapi::ClientSession& client_session); - - PassportService(const PassportService&) = delete; - PassportService& operator=(const PassportService&) = delete; - - using ListPassportsResult = Result< - const client_library::ListPassportsForm*, - client_library::ListPassportsFormResponse*>; - - std::future<ListPassportsResult> ListPassportsManually( - const client_library::ListPassportsForm* req, - client_library::ListPassportsFormResponse* resp - ); - -private: - restapi::ClientSession& client_session_; -}; - -} // namespace service -} // namespace vereign - -#endif // __VEREIGN_SERVICE_PASSPORT_SERVICE_HH diff --git a/cpp/src/vereign/sqlite/connection.cc b/cpp/src/vereign/sqlite/connection.cc new file mode 100644 index 0000000000000000000000000000000000000000..eefe6a7de2ff004c777f88deb81eb3cee64f9271 --- /dev/null +++ b/cpp/src/vereign/sqlite/connection.cc @@ -0,0 +1,75 @@ +#include <vereign/sqlite/connection.hh> + +#include <vereign/sqlite/errors.hh> +#include <vereign/core/scope_guard.hh> + +#include <fmt/format.h> +#include <sqlite3.h> +#include <iostream> + +namespace vereign::sqlite { + +Connection::Connection(const std::string& path) + : db_{nullptr} +{ + auto rc = sqlite3_open(path.data(), &db_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("open db failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +Connection::~Connection() noexcept { + sqlite3_close(db_); +} + +void Connection::BeginExplicitTransaction() { + char* errMsg = nullptr; + auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); + auto rc = sqlite3_exec(db_, "BEGIN EXCLUSIVE", nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) { + throw Error(rc, fmt::format("starting transaction failed, err: {}", errMsg)); + } +} + +void Connection::Commit() { + char* errMsg = nullptr; + auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); + + auto rc = sqlite3_exec(db_, "COMMIT", nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) { + throw Error(rc, fmt::format("commit transaction failed, err: {}", errMsg)); + } +} + +void Connection::Rollback() { + char* errMsg = nullptr; + auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); + + auto rc = sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) { + throw Error(rc, fmt::format("rollback transaction failed, err: {}", errMsg)); + } +} + +void Connection::Execute(const std::string& sql) { + char* errMsg = nullptr; + auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); }); + + auto rc = sqlite3_exec(db_, sql.data(), nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) { + throw Error(rc, fmt::format("query execution failed, err: {}", errMsg)); + } +} + +auto Connection::Prepare(const std::string& sql) -> Statement { + sqlite3_stmt *stmt = nullptr; + + auto rc = sqlite3_prepare_v2(db_, sql.data(), -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + throw Error(rc, fmt::format("preparing statement failed, err: {}", sqlite3_errmsg(db_))); + } + + return Statement{db_, stmt}; +} + +} // namespace vereign::sqlite diff --git a/cpp/src/vereign/sqlite/connection.hh b/cpp/src/vereign/sqlite/connection.hh new file mode 100644 index 0000000000000000000000000000000000000000..d1134045dc4c2434ff20ad5b2841e58a51c5c253 --- /dev/null +++ b/cpp/src/vereign/sqlite/connection.hh @@ -0,0 +1,32 @@ +#ifndef __VEREIGN_SQLITE_CONNECTION_HH +#define __VEREIGN_SQLITE_CONNECTION_HH + +#include <vereign/sqlite/statement.hh> + +struct sqlite3; +struct sqlite3_stmt; + +namespace vereign::sqlite { + +class Connection { +public: + Connection(const std::string& path); + ~Connection() noexcept; + + Connection(const Connection&) = delete; + auto operator=(const Connection&) -> Connection& = delete; + + void BeginExplicitTransaction(); + void Commit(); + void Rollback(); + + void Execute(const std::string& sql); + auto Prepare(const std::string& sql) -> Statement; + +private: + sqlite3* db_; +}; + +} // namespace vereign::sqlite + +#endif // __VEREIGN_SQLITE_CONNECTION_HH diff --git a/cpp/src/vereign/sqlite/errors.hh b/cpp/src/vereign/sqlite/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..00622311b42921a41500d7d7b3cc1f5b0e54f9a2 --- /dev/null +++ b/cpp/src/vereign/sqlite/errors.hh @@ -0,0 +1,26 @@ +#ifndef __VEREIGN_SQLITE_ERROR_HH +#define __VEREIGN_SQLITE_ERROR_HH + +#include <stdexcept> + +namespace vereign::sqlite { + +class Error : public std::runtime_error { +public: + Error(int code, const std::string& msg) + : std::runtime_error(msg), + code_{code} + { + } + + auto code() const noexcept -> int { + return code_; + } + +private: + int code_; +}; + +} // namespace vereign::sqlite + +#endif // __VEREIGN_SQLITE_CONNECTION_HH diff --git a/cpp/src/vereign/sqlite/statement.cc b/cpp/src/vereign/sqlite/statement.cc new file mode 100644 index 0000000000000000000000000000000000000000..0a3d00795383e4498580c1a56265b1ce55d87918 --- /dev/null +++ b/cpp/src/vereign/sqlite/statement.cc @@ -0,0 +1,78 @@ +#include <vereign/sqlite/statement.hh> +#include <vereign/sqlite/errors.hh> + +#include <fmt/format.h> +#include <sqlite3.h> + +namespace vereign::sqlite { + +Statement::Statement(sqlite3* db, sqlite3_stmt* stmt) + : db_{db}, + stmt_{stmt} +{} + +void Statement::Finalize() { + sqlite3_finalize(stmt_); +} + +Statement::~Statement() { + sqlite3_finalize(stmt_); +} + +void Statement::BindBlob(int index, bytes::View blob) { + auto rc = sqlite3_bind_blob64(stmt_, index, blob.CharData(), blob.Size(), SQLITE_STATIC); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("bind blob parameter failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +void Statement::BindText(int index, const std::string& text) { + auto rc = sqlite3_bind_text(stmt_, index, text.data(), text.size(), SQLITE_STATIC); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("bind text parameter failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +auto Statement::Step() -> bool { + auto rc = sqlite3_step(stmt_); + switch (rc) { + case SQLITE_DONE: + return true; + case SQLITE_ROW: + return false; + default: + throw Error{rc, fmt::format("executing statement failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +auto Statement::GetColumnBlob(int index) -> bytes::View { + auto size = sqlite3_column_bytes(stmt_, index); + auto blob = sqlite3_column_blob(stmt_, index); + + return bytes::View{blob, static_cast<size_t>(size)}; +} + +auto Statement::GetColumnText(int index) -> std::string_view { + std::size_t size = sqlite3_column_bytes(stmt_, index); + auto blob = sqlite3_column_text(stmt_, index); + + return std::string_view{reinterpret_cast<const char*>(blob), size}; +} + +void Statement::ResetAndClearBindings() { + auto rc = sqlite3_clear_bindings(stmt_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("statement reset bindings failed, err: {}", sqlite3_errmsg(db_))}; + } + + Reset(); +} + +void Statement::Reset() { + auto rc = sqlite3_reset(stmt_); + if (rc != SQLITE_OK) { + throw Error{rc, fmt::format("statement reset failed, err: {}", sqlite3_errmsg(db_))}; + } +} + +} // namespace vereign::sqlite diff --git a/cpp/src/vereign/sqlite/statement.hh b/cpp/src/vereign/sqlite/statement.hh new file mode 100644 index 0000000000000000000000000000000000000000..2b8c336d735a5ec33eb673dffa8df0ef87cce676 --- /dev/null +++ b/cpp/src/vereign/sqlite/statement.hh @@ -0,0 +1,42 @@ +#ifndef __VEREIGN_SQLITE_STATEMENT_HH +#define __VEREIGN_SQLITE_STATEMENT_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/bytes.hh> + +#include <string> + +struct sqlite3; +struct sqlite3_stmt; + +namespace vereign::sqlite { + +class Statement { +private: + friend class Connection; + + explicit Statement(sqlite3* db, sqlite3_stmt* stmt); + +public: + ~Statement(); + + void BindBlob(int index, bytes::View blob); + void BindText(int index, const std::string& text); + + auto Step() -> bool; + + auto GetColumnBlob(int index) -> bytes::View; + auto GetColumnText(int index) -> std::string_view; + + void Reset(); + void ResetAndClearBindings(); + void Finalize(); + +private: + sqlite3* db_; + sqlite3_stmt* stmt_; +}; + +} // namespace vereign::sqlite + +#endif // __VEREIGN_SQLITE_STATEMENT_HH diff --git a/cpp/src/vereign/sync/channel.hh b/cpp/src/vereign/sync/channel.hh index ff6ff439de10fa058ee1a80460cfc8feb1e69dd2..0e1544b29957820a0bd9218b2a8736618c3b9d27 100644 --- a/cpp/src/vereign/sync/channel.hh +++ b/cpp/src/vereign/sync/channel.hh @@ -1,13 +1,12 @@ #ifndef __VEREIGN_SYNC_CHANNEL_HH #define __VEREIGN_SYNC_CHANNEL_HH -#include <deque> +#include <vereign/container/bounded_queue.hh> + #include <condition_variable> #include <boost/optional.hpp> -#include <iostream> -namespace vereign { -namespace sync { +namespace vereign::sync { /** * ChannelAddResult is used as return value by Channel TryAdd methods. @@ -20,7 +19,8 @@ public: /** * Default constructor - the result is ok. */ - ChannelAddResult(): closed_(false), full_(false) {} + ChannelAddResult() = default; + /** * Creates ChannelAddResult. * @@ -34,7 +34,7 @@ public: * * @returns true when the add operation was successful. */ - bool IsOk() const noexcept { + auto IsOk() const noexcept -> bool { return !closed_ && !full_; } @@ -52,7 +52,7 @@ public: * * @returns true when the channel was closed. */ - bool IsClosed() const noexcept { + auto IsClosed() const noexcept -> bool { return closed_; } @@ -63,13 +63,13 @@ public: * * @returns true when the channel was full. */ - bool IsFull() const noexcept { + auto IsFull() const noexcept -> bool { return full_; } private: - bool closed_; - bool full_; + bool closed_ = false; + bool full_ = false; }; /** @@ -102,31 +102,31 @@ public: // The Channel Value is only move constructible and move assignable. ChannelValue(ChannelValue&&) = default; - ChannelValue& operator=(ChannelValue&&) = default; + auto operator=(ChannelValue&&) -> ChannelValue& = default; ChannelValue(const ChannelValue&) = delete; - ChannelValue& operator=(const ChannelValue&) = delete; + auto operator=(const ChannelValue&) -> ChannelValue& = delete; operator bool() const noexcept { return HasValue(); } - bool HasValue() const noexcept { + auto HasValue() const noexcept -> bool { return value_.has_value(); } - const ValueType& Value() const { + auto Value() const -> const ValueType& { return value_.value(); } - ValueType& Value() { + auto Value() -> ValueType& { return value_.value(); } - bool IsClosed() const noexcept { + auto IsClosed() const noexcept -> bool { return closed_; } - bool IsEmpty() const noexcept { + auto IsEmpty() const noexcept -> bool { return empty_; } @@ -164,7 +164,7 @@ public: */ explicit Channel(std::size_t capacity) : closed_{false}, - capacity_{capacity} + queue_{capacity} { } @@ -177,17 +177,17 @@ public: * @param value The value is moved into the new element. * @returns false when the channel is closed, and the value could not be pushed. */ - bool Add(ValueType&& value) { + auto Add(ValueType&& value) -> bool { std::unique_lock<std::mutex> lock(mu_); writers_cv_.wait(lock, [this]() { - return queue_.size() < capacity_ || closed_; + return !queue_.IsFull() || closed_; }); if (closed_) { return false; } - queue_.push_back(std::move(value)); + queue_.PushBack(std::move(value)); lock.unlock(); readers_cv_.notify_one(); @@ -204,17 +204,17 @@ public: * @param value The value is copied and added to the channel. * @returns false when the channel is closed, and the value could not be pushed. */ - bool Add(const ValueType& value) { + auto Add(const ValueType& value) -> bool { std::unique_lock<std::mutex> lock(mu_); writers_cv_.wait(lock, [this]() { - return queue_.size() < capacity_ || closed_; + return !queue_.IsFull() || closed_; }); if (closed_) { return false; } - queue_.push_back(value); + queue_.PushBack(value); lock.unlock(); readers_cv_.notify_one(); @@ -233,16 +233,16 @@ public: * @returns a add result, that can be used to check if the operation was * successful, and what was the channel state - closed, full. */ - ChannelAddResult TryAdd(ValueType&& value) { + auto TryAdd(ValueType&& value) -> ChannelAddResult { ChannelAddResult result; { std::lock_guard<std::mutex> lock(mu_); - result = ChannelAddResult{closed_, queue_.size() >= capacity_}; + result = ChannelAddResult{closed_, queue_.IsFull()}; if (!result) { return result; } - queue_.push_back(std::move(value)); + queue_.PushBack(std::move(value)); } readers_cv_.notify_one(); @@ -261,16 +261,16 @@ public: * @returns a add result, that can be used to check if the operation was * successful, and what was the channel state - closed, full. */ - ChannelAddResult TryAdd(const ValueType& value) { + auto TryAdd(const ValueType& value) -> ChannelAddResult { ChannelAddResult result; { std::lock_guard<std::mutex> lock(mu_); - result = ChannelAddResult{closed_, queue_.size() >= capacity_}; + result = ChannelAddResult{closed_, queue_.IsFull()}; if (!result) { return result; } - queue_.push_back(value); + queue_.PushBack(value); } readers_cv_.notify_one(); @@ -280,30 +280,31 @@ public: /** * Get retrieves a value from the channel. - * If the channel is empty, this call blocks until there is something added - * into the channel or the channel is empty but closed. + * + * If the channel is empty, this call blocks until there is something added into the channel or + * the channel is empty but closed. * * @returns the retrieved value. * The retrieved value has optional semantics. * One must check if the value exists before using it. * See the ChannelValue class docs. */ - ChannelValue<ValueType> Get() { + auto Get() -> ChannelValue<ValueType> { std::unique_lock<std::mutex> lock(mu_); readers_cv_.wait(lock, [this]() { - return queue_.size() > 0 || closed_; + return !queue_.IsEmpty() || closed_; }); - if (queue_.size() == 0) { - return ChannelValue<ValueType>{closed_, queue_.size() == 0}; + if (queue_.IsEmpty()) { + return ChannelValue<ValueType>{closed_, true}; } auto result = ChannelValue<ValueType>{ - std::move(queue_.front()), + std::move(queue_.Front()), closed_, - queue_.size() == 0, + queue_.IsEmpty(), }; - queue_.pop_front(); + queue_.PopFront(); lock.unlock(); writers_cv_.notify_one(); @@ -339,16 +340,10 @@ private: // signify if the channel is closed. bool closed_; - // maximum number of elements buffered inside the channel. - std::size_t capacity_; - // the internal queue used by the channel. - // TODO: replace the deque with circular buffer since the channel's capacity - // is fixed and known during the construction time. - std::deque<ValueType> queue_; + container::BoundedQueue<ValueType> queue_; }; -} -} +} // namespace vereign::sync -#endif +#endif // __VEREIGN_SYNC_CHANNEL_HH diff --git a/cpp/src/vereign/vereign.cc b/cpp/src/vereign/vereign.cc index 5bbc7e934fe7b9ff917e4b4b75a4a236c8b671f7..6095a55db57e77a6c11feeae3ba8f08217d223f8 100644 --- a/cpp/src/vereign/vereign.cc +++ b/cpp/src/vereign/vereign.cc @@ -27,8 +27,7 @@ auto vereign_service_start( const char* listen_address, const char* vereign_host, const char* vereign_port, - // FIXME: public_key must come from a storage internally. - const char* public_key, + const char* storage_path, vereign_error** err ) -> vereign_service* { if (err != nullptr) { @@ -42,7 +41,7 @@ auto vereign_service_start( listen_address, vereign_host, vereign_port, - public_key + storage_path != nullptr ? storage_path : "" ); return new vereign_service{std::move(serviceImpl)}; @@ -59,7 +58,6 @@ auto vereign_service_start( return nullptr; } - auto vereign_service_selected_port(vereign_service* service) -> int { return service->impl->SelectedPort(); } @@ -72,4 +70,3 @@ void vereign_service_shutdown(vereign_service* service) { service->impl->Shutdown(); delete service; } - diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 62bd6f48b027be1b1605a1f083be7c52d0ecaf00..c7bbbfcf2f6008b9cefbca1edce83566eed8f885 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -1,7 +1,20 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} - "${VENDOR_INSTALL_DIR}/boost/include" +) + +set(TESTUTIL_SRC + experiment/array.cc + + testutil/protobuf.cc + testutil/golden.cc +) +add_library(testutil STATIC ${TESTUTIL_SRC}) +target_link_libraries(testutil PRIVATE + vereignlib +) +target_include_directories(testutil PRIVATE + ${CMAKE_SOURCE_DIR}/src ) add_subdirectory("protobuf") diff --git a/cpp/tests/integration/CMakeLists.txt b/cpp/tests/integration/CMakeLists.txt index 9efb1023b1885e94c729c2d0259cd1963e188273..885f35651888dbf67bbe1dfbd30a00ef915eaabc 100644 --- a/cpp/tests/integration/CMakeLists.txt +++ b/cpp/tests/integration/CMakeLists.txt @@ -1,7 +1,6 @@ include_directories( ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/include - # ${VENDOR_INSTALL_DIR}/include ${CMAKE_SOURCE_DIR}/proto/cpp ) @@ -24,3 +23,18 @@ add_test( NAME integration_test COMMAND integration_test ) + +list(APPEND INIT_INTEGRATION_TEST_STORAGE_SRC + ../vereign/test/device.cc + ../vereign/test/service_context.cc + + init_integration_test_storage.cc +) + +add_executable(init_integration_test_storage ${INIT_INTEGRATION_TEST_STORAGE_SRC}) + +target_link_libraries(init_integration_test_storage + vereignlib + Threads::Threads +) + diff --git a/cpp/tests/integration/init_integration_test_storage.cc b/cpp/tests/integration/init_integration_test_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..ff234034cd50f576d43b70263a48f724013b4292 --- /dev/null +++ b/cpp/tests/integration/init_integration_test_storage.cc @@ -0,0 +1,34 @@ +#include <vereign/fs/util.hh> +#include <vereign/fs/path.hh> +#include <vereign/fs/operations.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> +#include <util/env.hh> + +using namespace vereign; + +/** + * The init_integration_test_storage tool is used for creating a new device with + * initialized storage at ${HOME}/vereign_integration_test dir. + * + * FIXME: create a command line utility for this and other commands like starting a gRPC + * server for integration tests. + */ +auto main(int argc, char** argv) -> int { + boost::ignore_unused(argc); + boost::ignore_unused(argv); + + auto public_key = test::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = test::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto storage_path = test::GetEnv("TEST_VEREIGN_INTEGRATION_STORAGE", ""); + auto pin = test::GetEnv("TEST_VEREIGN_PIN", "foo"); + + if (storage_path == "") { + storage_path = fs::path::Join(fs::HomePath(), "vereign_integration_test"); + } + + fs::CreateDir(storage_path); + + test::PrepareNewDevice(host, port, public_key, pin, storage_path); +} diff --git a/cpp/tests/integration/integration_test.cc b/cpp/tests/integration/integration_test.cc index ef42c7201cd946c0b613616ab45813340c351dd8..482e13d76750910dcf20cf7a3c6e23142142d27a 100644 --- a/cpp/tests/integration/integration_test.cc +++ b/cpp/tests/integration/integration_test.cc @@ -1,16 +1,20 @@ #include <vereign/vereign.h> -#include <util/env.hh> + #include <vereign/client_library/types.gen.pb.h> #include <vereign/client_library/passport_api.gen.grpc.pb.h> +#include <vereign/client_library/identity_api.gen.grpc.pb.h> #include <vereign/core/scope_guard.hh> -#include <grpcpp/create_channel.h> +#include <util/env.hh> +#include <grpcpp/create_channel.h> #include <catch2/catch.hpp> TEST_CASE("C API integration", "[.integration]") { - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); + // NOTE: use the init_integration_storage tool to create a new test storage + auto storage_path = vereign::test::RequireEnv("TEST_VEREIGN_INTEGRATION_STORAGE"); auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto pin = vereign::test::GetEnv("TEST_VEREIGN_PIN", "foo"); // start the service vereign_error* err = nullptr; @@ -18,7 +22,7 @@ TEST_CASE("C API integration", "[.integration]") { "localhost:", host.data(), port.data(), - publicKey.data(), + storage_path.data(), &err ); CHECK(service != nullptr); @@ -35,28 +39,24 @@ TEST_CASE("C API integration", "[.integration]") { ::grpc::InsecureChannelCredentials() ); + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + login_req.set_pin(pin); + auto login_resp = vereign::client_library::EmptyResponse{}; + ::grpc::ClientContext login_ctx; + auto status = identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + REQUIRE(status.error_message() == ""); + REQUIRE(status.ok() == true); + REQUIRE(login_resp.error() == ""); + CHECK(login_resp.status() == "OK"); + CHECK(login_resp.code() == "200"); + auto client = vereign::client_library::PassportAPI::NewStub(channel); vereign::client_library::ListPassportsForm req; vereign::client_library::ListPassportsFormResponse resp; ::grpc::ClientContext ctx; - auto status = client->ListPassports(&ctx, req, &resp); - - // std::cout << vereign::test::ProtobufToJson(resp) << std::endl; - - REQUIRE(status.error_message() == ""); - REQUIRE(resp.error() == ""); - CHECK(resp.status() == "OK"); - CHECK(resp.code() == "200"); - CHECK(resp.data().size() > 0); - for (auto& passport : resp.data()) { - CHECK(passport.uuid().size() == 36); - } - - req.Clear(); - resp.Clear(); - ::grpc::ClientContext manually_ctx; - status = client->ListPassportsManually(&manually_ctx, req, &resp); + status = client->ListPassports(&ctx, req, &resp); // std::cout << vereign::test::ProtobufToJson(resp) << std::endl; @@ -85,8 +85,8 @@ TEST_CASE("C API integration", "[.integration]") { // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl; - grpc_shutdown(); - google::protobuf::ShutdownProtobufLibrary(); + // grpc_shutdown(); + // google::protobuf::ShutdownProtobufLibrary(); } TEST_CASE("vereign_service_start") { diff --git a/cpp/tests/testutil/env.hh b/cpp/tests/testutil/env.hh new file mode 100644 index 0000000000000000000000000000000000000000..fdb3d27635542fc84c448408fdd2ab152872f11e --- /dev/null +++ b/cpp/tests/testutil/env.hh @@ -0,0 +1,29 @@ +#ifndef __VEREIGN_TESTUTIL_ENV_HH +#define __VEREIGN_TESTUTIL_ENV_HH + +#include <string> +#include <fmt/core.h> + +namespace vereign::testutil { + +inline auto RequireEnv(const std::string& name) -> std::string { + auto var = std::getenv(name.c_str()); + if (var == nullptr) { + throw std::runtime_error{fmt::format("{} env variable is required", name)}; + } + + return var; +} + +inline auto GetEnv(const std::string& name, const std::string& default_) -> std::string { + auto var = std::getenv(name.c_str()); + if (var == nullptr) { + return default_; + } + + return var; +} + +} // namespace vereign::testutil + +#endif // __VEREIGN_TESTUTIL_ENV_HH diff --git a/cpp/tests/testutil/error.hh b/cpp/tests/testutil/error.hh new file mode 100644 index 0000000000000000000000000000000000000000..90a1fdb1beead8a790005b22c57baefc8cfcf08f --- /dev/null +++ b/cpp/tests/testutil/error.hh @@ -0,0 +1,22 @@ +#ifndef __VEREIGN_TESTUTIL_ERROR_HH +#define __VEREIGN_TESTUTIL_ERROR_HH + +#include <optional> + +namespace vereign::testutil { + +template <class Error, class Fun> +auto CatchError(Fun fn) -> std::optional<Error> { + try { + fn(); + + return {}; + } catch (const Error& err) { + + return err; + } +} + +} // namespace vereign::testutil + +#endif // __VEREIGN_TESTUTIL_ERROR_HH diff --git a/cpp/tests/testutil/golden.cc b/cpp/tests/testutil/golden.cc new file mode 100644 index 0000000000000000000000000000000000000000..e5a011263d8cf9211b0082da94c5b67c898e8204 --- /dev/null +++ b/cpp/tests/testutil/golden.cc @@ -0,0 +1,26 @@ +#include <testutil/golden.hh> + +#include <vereign/fs/util.hh> +#include <testutil/env.hh> + +#include <catch2/catch.hpp> + +namespace { + constexpr const char* GoldenUpdateEnvVar = "GOLDEN_UPDATE"; +} + +namespace vereign::testutil::golden { + +void Assert(const std::string& golden_file_path, bytes::View src) { + auto update = vereign::testutil::GetEnv(GoldenUpdateEnvVar, ""); + if (!update.empty()) { + fs::WriteFile(golden_file_path, src); + } + + auto data = fs::ReadFile(golden_file_path); + + // TODO: create nice patch like diff on failure + CHECK(data.View().String() == src.String()); +} + +} // namespace vereign::test diff --git a/cpp/tests/testutil/golden.hh b/cpp/tests/testutil/golden.hh new file mode 100644 index 0000000000000000000000000000000000000000..2fb33bc97c6a742d35a6f569fa026431ece91a11 --- /dev/null +++ b/cpp/tests/testutil/golden.hh @@ -0,0 +1,13 @@ +#ifndef __VEREIGN_TESTUTIL_GOLDEN_HH +#define __VEREIGN_TESTUTIL_GOLDEN_HH + +#include <vereign/bytes/view.hh> +#include <string> + +namespace vereign::testutil::golden { + +void Assert(const std::string& golden_file, bytes::View src); + +} // namespace vereign::test + +#endif // __VEREIGN_TESTUTIL_GOLDEN_HH diff --git a/cpp/tests/util/protobuf.cc b/cpp/tests/testutil/protobuf.cc similarity index 74% rename from cpp/tests/util/protobuf.cc rename to cpp/tests/testutil/protobuf.cc index 05e1c791a91878f1d09a4c0068227de89e7937e1..c6ecf9991f052196651cb71af76dbf7598b1ac94 100644 --- a/cpp/tests/util/protobuf.cc +++ b/cpp/tests/testutil/protobuf.cc @@ -1,12 +1,11 @@ #include <vereign/grpc/json/encoder.hh> -#include <util/protobuf.hh> +#include <testutil/protobuf.hh> #include <google/protobuf/util/json_util.h> #include <catch2/catch.hpp> -namespace vereign { -namespace test { +namespace vereign::testutil { auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string { std::string result; @@ -15,5 +14,4 @@ auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string { return result; } -} // namespace vereign -} // namespace test +} // namespace vereign::testutil diff --git a/cpp/tests/testutil/protobuf.hh b/cpp/tests/testutil/protobuf.hh new file mode 100644 index 0000000000000000000000000000000000000000..264f7f6e51011c8678e68e0d943176ab0217e497 --- /dev/null +++ b/cpp/tests/testutil/protobuf.hh @@ -0,0 +1,13 @@ +#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH +#define __VEREIGN_TEST_UTIL_PROTOBUF_HH + +#include <string> +#include <google/protobuf/message.h> + +namespace vereign::testutil { + +auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string; + +} // namespace vereign::testutil + +#endif // __VEREIGN_TESTUTIL_PROTOBUF_HH diff --git a/cpp/tests/util/env.hh b/cpp/tests/util/env.hh deleted file mode 100644 index cb01a8c1de742a59e95935f5342de7e68fbf900c..0000000000000000000000000000000000000000 --- a/cpp/tests/util/env.hh +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef __VEREIGN_TEST_UTIL_ENV_HH -#define __VEREIGN_TEST_UTIL_ENV_HH - -#include <string> - -#include <catch2/catch.hpp> -#include <fmt/core.h> - -namespace vereign { -namespace test { - -inline std::string RequireEnv(const std::string& name) { - auto var = std::getenv(name.c_str()); - if (var == nullptr) { - FAIL(fmt::format("{} env variable is required", name)); - return ""; - } - - return var; -} - -inline std::string GetEnv(const std::string& name, const std::string& default_) { - auto var = std::getenv(name.c_str()); - if (var == nullptr) { - return default_; - } - - return var; -} - -} // namespace vereign -} // namespace test - -#endif // __VEREIGN_TEST_UTIL_ENV_HH diff --git a/cpp/tests/util/protobuf.hh b/cpp/tests/util/protobuf.hh deleted file mode 100644 index 09ca9be1f1b36d9d1444160333d0017ffc396b8e..0000000000000000000000000000000000000000 --- a/cpp/tests/util/protobuf.hh +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH -#define __VEREIGN_TEST_UTIL_PROTOBUF_HH - -#include <string> -#include <google/protobuf/message.h> - -namespace vereign { -namespace test { - -std::string ProtobufToJson(const google::protobuf::Message& msg); - -} // namespace vereign -} // namespace test - -#endif // __VEREIGN_TEST_UTIL_PROTOBUF_HH diff --git a/cpp/tests/vereign/CMakeLists.txt b/cpp/tests/vereign/CMakeLists.txt index 2c2b6fce316e7721ed05a80e53330200081a4486..913e3148a3e61291ac1214e1bf07aa0bfdb7a10d 100644 --- a/cpp/tests/vereign/CMakeLists.txt +++ b/cpp/tests/vereign/CMakeLists.txt @@ -1,6 +1,6 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - add_definitions(-DNOGDI) + add_definitions(-DNOGDI -DNOMINMAX) endif() include_directories( @@ -10,19 +10,52 @@ include_directories( ${Boost_INCLUDE_DIRS} ) -list(APPEND tests_src +list(APPEND TESTS_SRC init_tests.cc - ../util/protobuf.cc - ../experiment/array.cc + test/device.cc + test/service_context.cc + core/time_test.cc sync/channel_test.cc + container/bounded_queue_test.cc + + encoding/base64_test.cc + encoding/hex_test.cc + + bytes/buffer_test.cc + bytes/view_test.cc + + crypto/errors_test.cc + crypto/aes_test.cc + crypto/rsa_test.cc + crypto/digest_test.cc + crypto/cert_test.cc + restapi/client_test.cc restapi/client_session_test.cc + + kvstore/lock_test.cc + kvstore/sqlite_storage_test.cc + kvstore/crypto_storage_test.cc + + identity/provider_test.cc + service/gen/passport_service_test.cc + service/identity_service_test.cc + grpc/server_test.cc grpc/json/encoder_test.cc + grpc/identity_api_test.cc + grpc/event_api_test.cc ) -add_executable(tests ${tests_src}) + +if (WIN32) + list(APPEND TESTS_SRC + ncrypt/rsa_test.cc + ) +endif() + +add_executable(tests ${TESTS_SRC}) target_proto_generate( TARGET tests @@ -34,7 +67,7 @@ target_proto_generate( target_link_libraries(tests vereignlib - Threads::Threads + testutil ) if (VEREIGN_ENABLE_BENCHMARKING) diff --git a/cpp/tests/vereign/bytes/buffer_test.cc b/cpp/tests/vereign/bytes/buffer_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..710c3c184d2111aae21dc02c9b29a9939850842f --- /dev/null +++ b/cpp/tests/vereign/bytes/buffer_test.cc @@ -0,0 +1,491 @@ +#include <vereign/bytes/buffer.hh> + +#include <vereign/bytes/errors.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("bytes::Buffer::Buffer", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.Size() == 0); + CHECK(buf.Cap() == 0); + CHECK(buf.FreeCap() == 0); + } + + SECTION("buffer with predefined capacity") { + auto buf = bytes::Buffer{16}; + + CHECK(buf.Size() == 0); + CHECK(buf.Cap() == 16); + CHECK(buf.FreeCap() == 16); + } + + SECTION("buffer from a view") { + auto buf = bytes::Buffer{bytes::View{"foo bar"}}; + + CHECK(buf.Size() == 7); + CHECK(buf.Cap() == 7); + CHECK(buf.FreeCap() == 0); + CHECK(buf.View().String() == "foo bar"); + } + + SECTION("move construct") { + auto old_buf = bytes::Buffer{bytes::View{"foo bar"}}; + auto buf = std::move(old_buf); + + CHECK(old_buf.Size() == 0); + CHECK(old_buf.Cap() == 0); + CHECK(old_buf.FreeCap() == 0); + + CHECK(buf.Size() == 7); + CHECK(buf.Cap() == 7); + CHECK(buf.FreeCap() == 0); + CHECK(buf.View().String() == "foo bar"); + + auto empty = bytes::Buffer{}; + auto new_empty = std::move(empty); + + CHECK(empty.Size() == 0); + CHECK(empty.Cap() == 0); + CHECK(empty.FreeCap() == 0); + + CHECK(new_empty.Size() == 0); + CHECK(new_empty.Cap() == 0); + CHECK(new_empty.FreeCap() == 0); + } + + SECTION("move assign") { + auto foo = bytes::Buffer{bytes::View{"foo"}}; + auto bar = bytes::Buffer{bytes::View{"bar"}}; + + foo = std::move(bar); + + CHECK(foo.Size() == 3); + CHECK(foo.Cap() == 3); + CHECK(foo.FreeCap() == 0); + CHECK(foo.View().String() == "bar"); + + CHECK(bar.Size() == 3); + CHECK(bar.Cap() == 3); + CHECK(bar.FreeCap() == 0); + CHECK(bar.View().String() == "foo"); + + auto empty = bytes::Buffer{}; + foo = std::move(empty); + + CHECK(foo.Size() == 0); + CHECK(foo.Cap() == 0); + CHECK(foo.FreeCap() == 0); + CHECK(foo.View().String() == ""); + + CHECK(empty.Size() == 3); + CHECK(empty.Cap() == 3); + CHECK(empty.FreeCap() == 0); + CHECK(empty.View().String() == "bar"); + + empty = std::move(foo); + + CHECK(empty.Size() == 0); + CHECK(empty.Cap() == 0); + CHECK(empty.FreeCap() == 0); + CHECK(empty.View().String() == ""); + + CHECK(foo.Size() == 3); + CHECK(foo.Cap() == 3); + CHECK(foo.FreeCap() == 0); + CHECK(foo.View().String() == "bar"); + } +} + +TEST_CASE("bytes::Buffer::begin", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.begin() == nullptr); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo bar")}; + + CHECK(buf.begin() != nullptr); + + std::string s; + for (auto& it : buf) { + s += it; + } + + CHECK(s == "foo bar"); + + for (auto& it : buf) { + it = 'x'; + } + + CHECK(buf.View().String() == "xxxxxxx"); + } +} + +TEST_CASE("bytes::Buffer::end", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.end() == nullptr); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo bar")}; + + CHECK(buf.end() != nullptr); + + std::string s; + for (const auto& it : buf) { + s += it; + } + + CHECK(s == "foo bar"); + + for (auto& it : buf) { + it = 'x'; + } + + CHECK(buf.View().String() == "xxxxxxx"); + } + + SECTION("append to the end") { + auto buf = bytes::Buffer{bytes::View("foo bar")}; + + buf.Reserve(4); + std::strncpy((char*)buf.end(), " baz", 4); + buf.IncSize(4); + + CHECK(buf.Size() == 11); + CHECK(buf.View().String() == "foo bar baz"); + } +} + +TEST_CASE("bytes::Buffer::operator[]", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK_THROWS_AS( + buf[0], + bytes::IndexOutOfBounds + ); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + buf.Reserve(16); + + CHECK(buf.Cap() == 19); + CHECK(buf[0] == 'f'); + CHECK(buf[1] == 'o'); + CHECK(buf[2] == 'o'); + + CHECK_THROWS_AS( + buf[19], + bytes::IndexOutOfBounds + ); + } + + SECTION("modify buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + buf[0] = 'b'; + buf[1] = 'a'; + buf[2] = 'r'; + + CHECK_THROWS_AS( + buf[3] = 'x', + bytes::IndexOutOfBounds + ); + + CHECK(buf.View().String() == "bar"); + } +} + +TEST_CASE("bytes::Buffer::Size", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.Size() == 0); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Size() == 3); + } + + SECTION("append to buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + buf.Write(bytes::View(" bar")); + + CHECK(buf.Size() == 7); + } +} + +TEST_CASE("bytes::Buffer::Cap", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.Cap() == 0); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + } + + SECTION("append to buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + // capacity is doubled + buf.Write(bytes::View("b")); + + CHECK(buf.Cap() == 6); + } +} + +TEST_CASE("bytes::Buffer::FreeCap", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + CHECK(buf.FreeCap() == 0); + } + + SECTION("non empty buffer") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + CHECK(buf.FreeCap() == 0); + + buf.Reserve(1); + + CHECK(buf.Cap() - buf.Size() == buf.FreeCap()); + } +} + +TEST_CASE("bytes::Buffer::Reserve", "[vereign/bytes]") { + SECTION("empty buffer") { + auto buf = bytes::Buffer{}; + + buf.Reserve(8); + CHECK(buf.Cap() == 8); + CHECK(buf.Size() == 0); + } + + SECTION("when the required reservation is less than the old capacity*2, it reserves 2*capacity") { + { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + CHECK(buf.Size() == 3); + + // capacity is doubled + buf.Reserve(1); + + CHECK(buf.Cap() == 6); + CHECK(buf.FreeCap() == 3); + CHECK(buf.Size() == 3); + } + + { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + CHECK(buf.Size() == 3); + + // capacity is doubled + buf.Reserve(2); + + CHECK(buf.Cap() == 6); + CHECK(buf.FreeCap() == 3); + CHECK(buf.Size() == 3); + } + + { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + CHECK(buf.Size() == 3); + + // capacity is doubled + buf.Reserve(3); + + CHECK(buf.Cap() == 6); + CHECK(buf.FreeCap() == 3); + CHECK(buf.Size() == 3); + } + } + + SECTION("when the required reservation is greater than the old capacity*2, " + "it reserves capacity+required size") { + auto buf = bytes::Buffer{bytes::View("foo")}; + + CHECK(buf.Cap() == 3); + CHECK(buf.Size() == 3); + + buf.Reserve(4); + + CHECK(buf.Cap() == 7); + CHECK(buf.FreeCap() == 4); + CHECK(buf.Size() == 3); + } +} + +TEST_CASE("bytes::Buffer::IncSize", "[vereign/bytes]") { + SECTION("when IncSize is called on empty buffer, it throws bytes::IncrementOutOfBounds") { + auto buf = bytes::Buffer{}; + + CHECK_THROWS_AS( + buf.IncSize(10), + bytes::IncrementOutOfBounds + ); + } + + SECTION("when IncSize is called with value bigger than the free cap, " + "it throws bytes::IncrementOutOfBounds") { + + auto buf = bytes::Buffer{3}; + + std::strncpy((char*)buf.end(), "bar", 3); + + CHECK_THROWS_AS( + buf.IncSize(4), + bytes::IncrementOutOfBounds + ); + } + + SECTION("when IncSize is called with zero, it does nothing") { + bytes::Buffer buf; + buf.IncSize(0); + + CHECK(buf.Size() == 0); + } + + SECTION("success") { + auto buf = bytes::Buffer{3}; + + std::strncpy((char*)buf.end(), "bar", 3); + + buf.IncSize(3); + + CHECK(buf.Size() == 3); + CHECK(buf.View().String() == "bar"); + } +} + +TEST_CASE("bytes::Buffer::WriteWithinCap", "[vereign/bytes]") { + SECTION("when WriteWithinCap is called on empty buffer, it does nothing") { + auto buf = bytes::Buffer{}; + buf.WriteWithinCap(bytes::View("foo")); + + CHECK(buf.Size() == 0); + CHECK(buf.Cap() == 0); + } + + SECTION("when WriteWithinCap is called with empty source, it does nothing") { + auto buf = bytes::Buffer{3}; + buf.WriteWithinCap(bytes::View()); + + CHECK(buf.Size() == 0); + CHECK(buf.Cap() == 3); + } + + SECTION("when WriteWithinCap is called with source bigger than the free cap, " + "it copies only free cap amount of bytes") { + + auto buf = bytes::Buffer{3}; + buf.WriteWithinCap(bytes::View("foo bar")); + + CHECK(buf.Size() == 3); + CHECK(buf.View().String() == "foo"); + } + + SECTION("when WriteWithinCap is called with source smaller than the free cap, " + "it copies the full source of bytes") { + + auto buf = bytes::Buffer{7}; + buf.WriteWithinCap(bytes::View("foo bar")); + + CHECK(buf.Size() == 7); + CHECK(buf.View().String() == "foo bar"); + } +} + +TEST_CASE("bytes::Buffer::Write", "[vereign/bytes]") { + SECTION("when Write is called on empty buffer, " + "it expands the buffer and writes all the source bytes") { + auto buf = bytes::Buffer{}; + buf.Write(bytes::View("foo")); + + CHECK(buf.Size() == 3); + CHECK(buf.Cap() == 3); + CHECK(buf.View().String() == "foo"); + } + + SECTION("when Write is called with empty source, it does nothing") { + auto buf = bytes::Buffer{3}; + buf.Write(bytes::View()); + + CHECK(buf.Size() == 0); + CHECK(buf.Cap() == 3); + } + + SECTION("when Write is called with source bigger than the free cap, " + "it expands the buffer and writes all the source bytes") { + + auto buf = bytes::Buffer{3}; + buf.Write(bytes::View("foo bar")); + + CHECK(buf.Size() == 7); + CHECK(buf.View().String() == "foo bar"); + } + + SECTION("when Write is called with source smaller than the free cap, " + "it copies the full source of bytes") { + + auto buf = bytes::Buffer{7}; + buf.Write(bytes::View("foo bar")); + + CHECK(buf.Size() == 7); + CHECK(buf.View().String() == "foo bar"); + } +} + +TEST_CASE("bytes::Buffer::View", "[vereign/bytes]") { + SECTION("when the buffer is empty, it returns empty view") { + auto buf = bytes::Buffer{}; + + auto v = buf.View(); + CHECK(v.Size() == 0); + } + + SECTION("when the buffer is not empty, it returns a view of the buffer") { + auto buf = bytes::Buffer{bytes::View("123")}; + + auto v = buf.View(); + CHECK(v.Size() == 3); + CHECK(v.String() == "123"); + + v = buf.View(1); + CHECK(v.Size() == 2); + CHECK(v.String() == "23"); + + v = buf.View(2); + CHECK(v.Size() == 1); + CHECK(v.String() == "3"); + + v = buf.View(3); + CHECK(v.Size() == 0); + CHECK(v.String() == ""); + + v = buf.View(4); + CHECK(v.Size() == 0); + CHECK(v.String() == ""); + } +} diff --git a/cpp/tests/vereign/bytes/view_test.cc b/cpp/tests/vereign/bytes/view_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..1f29ab454d9d40225c45c89adddf1dce7f06036d --- /dev/null +++ b/cpp/tests/vereign/bytes/view_test.cc @@ -0,0 +1,236 @@ +#include <vereign/bytes/view.hh> + +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/errors.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("bytes::View::View", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.Size() == 0); + } + + SECTION("view from a pointer and a size") { + const uint8_t* data = (uint8_t*)"foo"; + auto v = bytes::View(data, 3); + + CHECK(v.Size() == 3); + CHECK(v.String() == "foo"); + } + + SECTION("view from a void pointer and a size") { + const void* data = (void*)"foo"; + auto v = bytes::View(data, 3); + + CHECK(v.Size() == 3); + CHECK(v.String() == "foo"); + } + + SECTION("view from a string") { + std::string data = "foo"; + auto v = bytes::View(data); + + CHECK(v.Size() == 3); + CHECK(v.String() == "foo"); + } + + SECTION("view from a wide string") { + std::wstring data = L"foo"; + auto v = bytes::View(data); + + CHECK(v.Size() == sizeof(wchar_t)*3); + CHECK(v.WideString() == L"foo"); + } +} + +TEST_CASE("bytes::View::Slice", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.Slice(1).Size() == 0); + } + + SECTION("non-empty view") { + std::string s = "123"; + auto v = bytes::View(s); + + CHECK(v.Slice(0).Size() == 3); + CHECK(v.Slice(0).String() == "123"); + + CHECK(v.Slice(1).Size() == 2); + CHECK(v.Slice(1).String() == "23"); + + CHECK(v.Slice(1, 2).Size() == 1); + CHECK(v.Slice(1, 2).String() == "2"); + + CHECK(v.Slice(1, 3).Size() == 2); + CHECK(v.Slice(1, 3).String() == "23"); + + CHECK(v.Slice(1, 4).Size() == 2); + CHECK(v.Slice(1, 4).String() == "23"); + } + + SECTION("start/end out of bounds") { + std::string s = "123"; + auto v = bytes::View(s); + + CHECK(v.Slice(0).Size() == 3); + CHECK(v.Slice(0).String() == "123"); + + CHECK(v.Slice(3).Size() == 0); + CHECK(v.Slice(3).String() == ""); + + CHECK(v.Slice(4).Size() == 0); + CHECK(v.Slice(4).String() == ""); + + CHECK(v.Slice(1, 4).Size() == 2); + CHECK(v.Slice(1, 4).String() == "23"); + } +} + +TEST_CASE("bytes::View::Data", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.Data() == nullptr); + } + + SECTION("non-empty view") { + std::string s = "123"; + auto v = bytes::View(s); + + CHECK(v.Data()[0] == '1'); + CHECK(v.Data()[1] == '2'); + } +} + +TEST_CASE("bytes::View::CharData", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.CharData() == nullptr); + } + + SECTION("non-empty view") { + std::string s = "123"; + auto v = bytes::View(s); + + const char* data = v.CharData(); + + CHECK(data[0] == '1'); + CHECK(data[1] == '2'); + } +} + +TEST_CASE("bytes::View::WideCharData", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.WideCharData() == nullptr); + } + + SECTION("non-empty view") { + std::wstring s = L"123"; + auto v = bytes::View(s); + + const wchar_t* data = v.WideCharData(); + + CHECK(data[0] == wchar_t('1')); + CHECK(data[1] == wchar_t('2')); + } +} + +TEST_CASE("bytes::View::String", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.String() == ""); + } + + SECTION("non-empty view") { + std::string s = "123"; + auto v = bytes::View(s); + + CHECK(v.String() == "123"); + } +} + +TEST_CASE("bytes::View::WideString", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK(v.WideString() == L""); + } + + SECTION("non-empty view") { + std::wstring s = L"123"; + auto v = bytes::View(s); + + CHECK(v.WideString() == L"123"); + } +} + +TEST_CASE("bytes::View::operator==", "[vereign/bytes]") { + auto s1 = std::string{"foo"}; + auto s2 = std::string{"foo"}; + + CHECK(bytes::View(s1) == bytes::View(s2)); + + s2 = "bar"; + CHECK((bytes::View(s1) == bytes::View(s2)) == false); + + s2 = ""; + CHECK((bytes::View(s1) == bytes::View(s2)) == false); + + auto b1 = crypto::Rand(16); + auto b2 = bytes::Buffer{b1.View()}; + + CHECK(bytes::View(b1.begin(), 16) == bytes::View(b2.begin(), 16)); +} + +TEST_CASE("bytes::View::operator!=", "[vereign/bytes]") { + auto s1 = std::string{"foo"}; + auto s2 = std::string{"foo"}; + + CHECK((bytes::View(s1) != bytes::View(s2)) == false); + + s2 = "bar"; + CHECK(bytes::View(s1) != bytes::View(s2)); + + s2 = ""; + CHECK(bytes::View(s1) != bytes::View(s2)); + + auto b1 = crypto::Rand(16); + auto b2 = bytes::Buffer{b1.View()}; + + CHECK((bytes::View(b1.begin(), 16) != bytes::View(b2.begin(), 16)) == false); +} + +TEST_CASE("bytes::View::operator[]", "[vereign/bytes]") { + SECTION("empty view") { + auto v = bytes::View{}; + + CHECK_THROWS_AS( + v[0], + bytes::IndexOutOfBounds + ); + } + + SECTION("non-empty view") { + auto s = std::string{"123"}; + auto v = bytes::View(s); + + CHECK(v[0] == '1'); + CHECK(v[1] == '2'); + CHECK(v[2] == '3'); + + CHECK_THROWS_AS( + v[3], + bytes::IndexOutOfBounds + ); + } +} diff --git a/cpp/tests/vereign/container/bounded_queue_test.cc b/cpp/tests/vereign/container/bounded_queue_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..c4aff6ddeb20233180c8e05bdb26b45b94e692b8 --- /dev/null +++ b/cpp/tests/vereign/container/bounded_queue_test.cc @@ -0,0 +1,81 @@ +#include <vereign/container/bounded_queue.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("container::BoundedQueue", "[vereign/container]") { + // a test type, that can confirm that the queue properly calls its destructor and there + // are no memory leaks + class Value { + public: + Value(int v) : v_{std::make_unique<int>(v)} {} + + auto Get() -> int { + return *v_; + } + private: + std::unique_ptr<int> v_; + }; + + auto q = container::BoundedQueue<Value>{3}; + + CHECK(q.Size() == 0); + CHECK(q.IsEmpty() == true); + CHECK(q.IsFull() == false); + + q.PushBack(1); + CHECK(q.Size() == 1); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + q.PushBack(2); + CHECK(q.Size() == 2); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + q.PushBack(3); + CHECK(q.Size() == 3); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == true); + + auto v = std::move(q.Front()); + q.PopFront(); + CHECK(v.Get() == 1); + CHECK(q.Size() == 2); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + v = std::move(q.Front()); + q.PopFront(); + CHECK(v.Get() == 2); + CHECK(q.Size() == 1); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + q.PushBack(4); + CHECK(q.Size() == 2); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + v = std::move(q.Front()); + q.PopFront(); + CHECK(v.Get() == 3); + CHECK(q.Size() == 1); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); + + v = std::move(q.Front()); + q.PopFront(); + CHECK(v.Get() == 4); + CHECK(q.Size() == 0); + CHECK(q.IsEmpty() == true); + CHECK(q.IsFull() == false); + + // leave something in the queue, for memory sanitizer check of the queue destruction + q.PushBack(5); + CHECK(q.Size() == 1); + CHECK(q.IsEmpty() == false); + CHECK(q.IsFull() == false); +} + diff --git a/cpp/tests/vereign/core/time_test.cc b/cpp/tests/vereign/core/time_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..09637b9fb892cc8a86a27281587d003326c15642 --- /dev/null +++ b/cpp/tests/vereign/core/time_test.cc @@ -0,0 +1,42 @@ +#include <vereign/core/time.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("time::MakePosixTime", "[vereign/time][vereign/core]") { + auto ptime = time::MakePosixTime(2020, 1, 1, 0, 1, 1); + + CHECK(ptime.date().year() == 2020); + CHECK(ptime.date().month() == boost::gregorian::Jan); + CHECK(ptime.date().day() == 1); + CHECK(ptime.time_of_day().hours() == 0); + CHECK(ptime.time_of_day().minutes() == 1); + CHECK(ptime.time_of_day().seconds() == 1); + + CHECK(time::PosixTimeToTime(ptime) == 1577836861); + CHECK(time::MakeTimeUTCFromISO("20200101T000101") == time::PosixTimeToTime(ptime)); + + ptime = time::MakePosixTime(2020, 1, 1, 24, 60, 60); + CHECK(boost::posix_time::to_iso_string(ptime) == "20200102T010100"); +} + +TEST_CASE("time::MakeTimeUTC", "[vereign/time][vereign/core]") { + auto timestamp = time::MakeTimeUTC(2020, 1, 1, 0, 1, 1); + + CHECK(timestamp == 1577836861); + CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp); +} + +TEST_CASE("time::MakeTimeUTCFromString", "[vereign/time][vereign/core]") { + auto timestamp = time::MakeTimeUTCFromString("2020-1-1 0:1:1"); + + CHECK(timestamp == 1577836861); + CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp); +} + +TEST_CASE("time::MakeTimeUTCFromISO", "[vereign/time][vereign/core]") { + auto timestamp = time::MakeTimeUTCFromISO("20200101T000101"); + + CHECK(timestamp == 1577836861); +} diff --git a/cpp/tests/vereign/crypto/aes_test.cc b/cpp/tests/vereign/crypto/aes_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..b9f4c41bd0334cd29d4a75e839572bf1f4ea8ba5 --- /dev/null +++ b/cpp/tests/vereign/crypto/aes_test.cc @@ -0,0 +1,25 @@ +#include <vereign/crypto/aes.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> + +#include <catch2/catch.hpp> + +#include <iostream> + +using namespace vereign; + +TEST_CASE("crypto::aes::GCM256Encrypt", "[vereign/crypto/aes][vereign/crypto]") { + const std::string data{"foo bar"}; + auto key = crypto::Rand(32); + + bytes::Buffer iv; + bytes::Buffer tag; + bytes::Buffer encrypted; + + crypto::aes::GCM256Encrypt(bytes::View(data), key.View(), iv, tag, encrypted); + + bytes::Buffer decrypted; + crypto::aes::GCM256Decrypt(encrypted.View(), key.View(), iv.View(), tag.View(), decrypted); + + CHECK(decrypted.View().String() == data); +} diff --git a/cpp/tests/vereign/crypto/cert_test.cc b/cpp/tests/vereign/crypto/cert_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..da6bec076b79072fff0f925e9f20965b3ec84927 --- /dev/null +++ b/cpp/tests/vereign/crypto/cert_test.cc @@ -0,0 +1,84 @@ +#include <vereign/crypto/cert.hh> + +#include <vereign/core/time.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/bio.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/fs/path.hh> +#include <vereign/fs/util.hh> +#include <testutil/golden.hh> + +#include <catch2/catch.hpp> + +#include <iostream> +#include <fstream> + +using namespace vereign; + +namespace { + const std::string testDataPath = fs::path::Join("..", "tests", "vereign", "crypto", "test_data"); +} + +TEST_CASE("crypto::cert::CreateSelfSignedCert", "[vereign/crypto/cert][vereign/crypto]") { + auto key = crypto::rsa::ImportPrivateKeyFromPEM( + fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View() + ); + + crypto::cert::CertData cert_data{}; + cert_data.Email = "ca@example.com"; + cert_data.Subject.CommonName = "ca-cn.example.com"; + cert_data.Subject.Country = "BG"; + cert_data.Subject.State = "Sofia"; + cert_data.Subject.Locality = "Sofia"; + cert_data.Subject.Organization = "Vereign AG"; + cert_data.Subject.OrganizationUnit = "CA"; + cert_data.Url = "ca.example.com"; + cert_data.Validity.ValidYears = 5; + cert_data.SerialNumber = 42; + cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-01 00:00:00"); + cert_data.IsCA = true; + + auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get()); + + std::stringstream ss; + crypto::cert::PrintCert(ss, cert.get()); + testutil::golden::Assert(fs::path::Join(testDataPath, "ca_cert.txt"), bytes::View(ss.str())); + + auto pem = crypto::cert::ExportCertToPEM(cert.get()); + testutil::golden::Assert(fs::path::Join(testDataPath, "ca.crt"), crypto::bio::View(pem.get())); +} + +TEST_CASE("crypto::cert::CreateCert", "[vereign/crypto/cert][vereign/crypto]") { + auto ca_key = crypto::rsa::ImportPrivateKeyFromPEM( + fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View() + ); + + auto ca_cert = crypto::cert::ImportCertFromPEM( + fs::ReadFile(fs::path::Join(testDataPath, "ca.crt")).View() + ); + + auto key = crypto::rsa::ImportPrivateKeyFromPEM( + fs::ReadFile(fs::path::Join(testDataPath, "leaf_key.pem")).View() + ); + + crypto::cert::CertData cert_data{}; + cert_data.Email = "test@example.com"; + cert_data.Subject.CommonName = "example.com"; + cert_data.Subject.Country = "BG"; + cert_data.Subject.State = "Sofia"; + cert_data.Subject.Locality = "Sofia"; + cert_data.Subject.Organization = "Vereign AG"; + cert_data.Subject.OrganizationUnit = "Engineering"; + cert_data.Url = "api.example.com"; + cert_data.Validity.ValidYears = 5; + cert_data.SerialNumber = 73; + cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-02 00:00:00"); + + auto cert = crypto::cert::CreateCert(cert_data, ca_cert.get(), ca_key.get(), key.get()); + + std::stringstream ss; + crypto::cert::PrintCert(ss, cert.get()); + + testutil::golden::Assert(fs::path::Join(testDataPath, "leaf_cert.txt"), bytes::View(ss.str())); +} diff --git a/cpp/tests/vereign/crypto/digest_test.cc b/cpp/tests/vereign/crypto/digest_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..cea92371b19c2290900f0047a587a54522ad537b --- /dev/null +++ b/cpp/tests/vereign/crypto/digest_test.cc @@ -0,0 +1,22 @@ +#include <vereign/crypto/digest.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/encoding/hex.hh> + +#include <catch2/catch.hpp> + +#include <iostream> + +using namespace vereign; + +TEST_CASE("crypto::digest::sha1", "[vereign/crypto/digest][vereign/crypto]") { + const std::string data{"foo bar"}; + + bytes::Buffer hash; + crypto::digest::sha1(bytes::View(data), hash); + + bytes::Buffer encoded; + encoding::hex::Encode(hash.View(), encoded); + + CHECK(encoded.View().String() == "3773dea65156909838fa6c22825cafe090ff8030"); +} diff --git a/cpp/tests/vereign/crypto/errors_test.cc b/cpp/tests/vereign/crypto/errors_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..1b9f32a3523e27b14a9985047ccb0caca2f1dd46 --- /dev/null +++ b/cpp/tests/vereign/crypto/errors_test.cc @@ -0,0 +1,24 @@ +#include <vereign/crypto/errors.hh> +#include <openssl/evp.h> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("crypto::OpenSSLError", "[vereign/crypto]") { + SECTION("it must return error with message that included the last OpenSSL error") { + EVP_PKEY_CTX* ctx = nullptr; + auto r = EVP_PKEY_encrypt_init(ctx); + CHECK(r == 0); + + auto err = crypto::OpenSSLError{"pkey encrypt failed"}; + CHECK(std::string(err.what()) == "pkey encrypt failed: OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE"); + } + + SECTION("when there was no OpenSSL error," + " it must create an error with `no openssl error in the error queue`") { + auto err = crypto::OpenSSLError{"test error"}; + + CHECK(std::string(err.what()) == "test error: no openssl error in the error queue"); + } +} diff --git a/cpp/tests/vereign/crypto/rsa_test.cc b/cpp/tests/vereign/crypto/rsa_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..f9f27acb9b3a20cf0621dd886144a6a7f2b7306b --- /dev/null +++ b/cpp/tests/vereign/crypto/rsa_test.cc @@ -0,0 +1,100 @@ +#include <vereign/crypto/rsa.hh> + +#include <vereign/crypto/rand.hh> +#include <vereign/crypto/bio.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/bytes/buffer.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("crypto::rsa PublicKeyEncrypt/PrivateKeyDecrypt", "[vereign/crypto/rsa][vereign/crypto]") { + SECTION("small input") { + auto key = crypto::rsa::GenerateKey(2048); + + const std::string input{"foo bar"}; + bytes::Buffer encrypted; + + crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted); + + bytes::Buffer decrypted; + crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == bytes::View(input)); + } + + SECTION("zero input") { + auto key = crypto::rsa::GenerateKey(2048); + + const std::string input; + bytes::Buffer encrypted; + + crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted); + + bytes::Buffer decrypted; + crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == bytes::View(input)); + } + + SECTION("max size input") { + auto key = crypto::rsa::GenerateKey(2048); + + auto input = crypto::Rand(214); + bytes::Buffer encrypted; + + crypto::rsa::PublicKeyEncrypt(key.get(), input.View(), encrypted); + + bytes::Buffer decrypted; + crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == input.View()); + } + + SECTION("invalid big input") { + auto key = crypto::rsa::GenerateKey(2048); + + auto input = crypto::Rand(215); + bytes::Buffer encrypted; + + CHECK_THROWS_WITH( + crypto::rsa::PublicKeyEncrypt(key.get(), input.View(), encrypted), + "encrypting failed: DATA_TOO_LARGE_FOR_KEY_SIZE" + ); + + CHECK(encrypted.Size() == 0); + } +} + +TEST_CASE("crypto::rsa::ExportPublicKeyToPEM", "[vereign/crypto/rsa][vereign/crypto]") { + const std::string input{"foo bar"}; + auto key = crypto::rsa::GenerateKey(2048); + + auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get()); + auto imported_key = crypto::rsa::ImportPublicKeyFromPEM(crypto::bio::View(bio.get())); + + bytes::Buffer encrypted; + crypto::rsa::PublicKeyEncrypt(imported_key.get(), bytes::View(input), encrypted); + + bytes::Buffer decrypted; + crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == bytes::View(input)); +} + +TEST_CASE("crypto::rsa::ExportPrivateKeyToPEM", "[vereign/crypto/rsa][vereign/crypto]") { + const std::string input{"foo bar"}; + auto key = crypto::rsa::GenerateKey(2048); + + auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get()); + auto imported_key = crypto::rsa::ImportPrivateKeyFromPEM(crypto::bio::View(bio.get())); + + bytes::Buffer encrypted; + crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted); + + bytes::Buffer decrypted; + crypto::rsa::PrivateKeyDecrypt(imported_key.get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == bytes::View(input)); +} diff --git a/cpp/tests/vereign/crypto/test_data/.gitattributes b/cpp/tests/vereign/crypto/test_data/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..fa1385d99a319b43c06f5309d1aae9fdd3adea46 --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/.gitattributes @@ -0,0 +1 @@ +* -text diff --git a/cpp/tests/vereign/crypto/test_data/ca.crt b/cpp/tests/vereign/crypto/test_data/ca.crt new file mode 100644 index 0000000000000000000000000000000000000000..affc0044449d58fa031c69a13afa931baf587d3e --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/ca.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEvDCCA6SgAwIBAgIBKjANBgkqhkiG9w0BAQsFADCBijEaMBgGA1UEAwwRY2Et +Y24uZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwG +A1UECAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0w +GwYJKoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbTAeFw0yMDA3MDEwMDAwMDBaFw0y +NTA2MzAwMDAwMDBaMIGKMRowGAYDVQQDDBFjYS1jbi5leGFtcGxlLmNvbTELMAkG +A1UEBhMCQkcxDjAMBgNVBAcMBVNvZmlhMQ4wDAYDVQQIDAVTb2ZpYTETMBEGA1UE +CgwKVmVyZWlnbiBBRzELMAkGA1UECwwCQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4 +YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxINphs0i +1HrDcvsG1mUnNrcnwhyitN5ZX8FDgtk+r5RXNk+zRvmlDz+kKq/UNXgIH5v1LbRi +Vcmh9njnLqh4uPW2Khkxna3yjnWF3NqV780uOCkUEZp0CC2B2RRXCjmxF3wBtggb +FjBSoh5nhYUtJZkeDgY+AaFlfBKHWWQpzLJWxdHFW/Ej33MubilPTKOaP2Nh9U9x +bxiwAHS3ijnhpW2Xh6uwf1XMQVwwsbPtEd81BBU86KIeQzOQrYkUEl81nqOzt9Ct +67fUJs0yPPTtoEekWAYCdFD0hrVLxz1ilQtYF+shXL/6GyrKz0putQhO2wOqINXV +rHUv1SgEMwD/TwIDAQABo4IBKTCCASUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAYYwKQYDVR0RBCIwIIEOY2FAZXhhbXBsZS5jb22CDmNhLmV4YW1wbGUu +Y29tMB0GA1UdDgQWBBR+ivGqlvajtAUhWQthghHP6zZrdjCBtwYDVR0jBIGvMIGs +gBR+ivGqlvajtAUhWQthghHP6zZrdqGBkKSBjTCBijEaMBgGA1UEAwwRY2EtY24u +ZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwGA1UE +CAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0wGwYJ +KoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbYIBKjANBgkqhkiG9w0BAQsFAAOCAQEA +wHKy0Gmj5Gs+DglsqzOfmUeqy8EsmaS6ufxV084umpnNmaQrdBSQFH0P8sQJROpH +1bgibRLQFwQLKvAC6dxCvnH6njW0lCZ/53OkrNyky53eZ6I6DHrT0kjkC9KqShEf ++MhHBniVCX2w8HxFgZgMKGiwL+D8qo5QzHmR+AzXAZGlKKZReKuPUehfO02cVWPH +uz/dSl2gd7xdAz3lwayVPiNNCu4r0/gwvEiHuwCvjIQPIxsUBlm5BwB0ga0OlMJu +nGVv3dNkS2KjIEBms4jvXxk9w51OQqrcVUhuj8qHenTtM6ho5QTtDi2z/NbM0oW1 +zAmLUoPW97IgXtp4ghOQ7w== +-----END CERTIFICATE----- diff --git a/cpp/tests/vereign/crypto/test_data/ca_cert.txt b/cpp/tests/vereign/crypto/test_data/ca_cert.txt new file mode 100644 index 0000000000000000000000000000000000000000..9ed51e64c82cf5d0d352d31b632f74c18daa62e8 --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/ca_cert.txt @@ -0,0 +1,63 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 42 (0x2a) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com + Validity + Not Before: Jul 1 00:00:00 2020 GMT + Not After : Jun 30 00:00:00 2025 GMT + Subject: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:c4:83:69:86:cd:22:d4:7a:c3:72:fb:06:d6:65: + 27:36:b7:27:c2:1c:a2:b4:de:59:5f:c1:43:82:d9: + 3e:af:94:57:36:4f:b3:46:f9:a5:0f:3f:a4:2a:af: + d4:35:78:08:1f:9b:f5:2d:b4:62:55:c9:a1:f6:78: + e7:2e:a8:78:b8:f5:b6:2a:19:31:9d:ad:f2:8e:75: + 85:dc:da:95:ef:cd:2e:38:29:14:11:9a:74:08:2d: + 81:d9:14:57:0a:39:b1:17:7c:01:b6:08:1b:16:30: + 52:a2:1e:67:85:85:2d:25:99:1e:0e:06:3e:01:a1: + 65:7c:12:87:59:64:29:cc:b2:56:c5:d1:c5:5b:f1: + 23:df:73:2e:6e:29:4f:4c:a3:9a:3f:63:61:f5:4f: + 71:6f:18:b0:00:74:b7:8a:39:e1:a5:6d:97:87:ab: + b0:7f:55:cc:41:5c:30:b1:b3:ed:11:df:35:04:15: + 3c:e8:a2:1e:43:33:90:ad:89:14:12:5f:35:9e:a3: + b3:b7:d0:ad:eb:b7:d4:26:cd:32:3c:f4:ed:a0:47: + a4:58:06:02:74:50:f4:86:b5:4b:c7:3d:62:95:0b: + 58:17:eb:21:5c:bf:fa:1b:2a:ca:cf:4a:6e:b5:08: + 4e:db:03:aa:20:d5:d5:ac:75:2f:d5:28:04:33:00: + ff:4f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Subject Alternative Name: + email:ca@example.com, DNS:ca.example.com + X509v3 Subject Key Identifier: + 7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76 + X509v3 Authority Key Identifier: + keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76 + DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com + serial:2A + + Signature Algorithm: sha256WithRSAEncryption + c0:72:b2:d0:69:a3:e4:6b:3e:0e:09:6c:ab:33:9f:99:47:aa: + cb:c1:2c:99:a4:ba:b9:fc:55:d3:ce:2e:9a:99:cd:99:a4:2b: + 74:14:90:14:7d:0f:f2:c4:09:44:ea:47:d5:b8:22:6d:12:d0: + 17:04:0b:2a:f0:02:e9:dc:42:be:71:fa:9e:35:b4:94:26:7f: + e7:73:a4:ac:dc:a4:cb:9d:de:67:a2:3a:0c:7a:d3:d2:48:e4: + 0b:d2:aa:4a:11:1f:f8:c8:47:06:78:95:09:7d:b0:f0:7c:45: + 81:98:0c:28:68:b0:2f:e0:fc:aa:8e:50:cc:79:91:f8:0c:d7: + 01:91:a5:28:a6:51:78:ab:8f:51:e8:5f:3b:4d:9c:55:63:c7: + bb:3f:dd:4a:5d:a0:77:bc:5d:03:3d:e5:c1:ac:95:3e:23:4d: + 0a:ee:2b:d3:f8:30:bc:48:87:bb:00:af:8c:84:0f:23:1b:14: + 06:59:b9:07:00:74:81:ad:0e:94:c2:6e:9c:65:6f:dd:d3:64: + 4b:62:a3:20:40:66:b3:88:ef:5f:19:3d:c3:9d:4e:42:aa:dc: + 55:48:6e:8f:ca:87:7a:74:ed:33:a8:68:e5:04:ed:0e:2d:b3: + fc:d6:cc:d2:85:b5:cc:09:8b:52:83:d6:f7:b2:20:5e:da:78: + 82:13:90:ef diff --git a/cpp/tests/vereign/crypto/test_data/ca_key.pem b/cpp/tests/vereign/crypto/test_data/ca_key.pem new file mode 100644 index 0000000000000000000000000000000000000000..2c73e6ead4478b7c4d32a2ad3b3031c7eff054e0 --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/ca_key.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDEg2mGzSLUesNy ++wbWZSc2tyfCHKK03llfwUOC2T6vlFc2T7NG+aUPP6Qqr9Q1eAgfm/UttGJVyaH2 +eOcuqHi49bYqGTGdrfKOdYXc2pXvzS44KRQRmnQILYHZFFcKObEXfAG2CBsWMFKi +HmeFhS0lmR4OBj4BoWV8EodZZCnMslbF0cVb8SPfcy5uKU9Mo5o/Y2H1T3FvGLAA +dLeKOeGlbZeHq7B/VcxBXDCxs+0R3zUEFTzooh5DM5CtiRQSXzWeo7O30K3rt9Qm +zTI89O2gR6RYBgJ0UPSGtUvHPWKVC1gX6yFcv/obKsrPSm61CE7bA6og1dWsdS/V +KAQzAP9PAgMBAAECggEAOBj9hGZF9ZsP9WBUrNpWbeeuGVscVX6Ny+h7UbybiPrT +RKVO28mDRY2Y2zizXwojY4adYI04bx3uttH/yNF+GOrHtE/Z2pXgAqvo6Uma+Mg9 +U/niCT5qtjTrduE4Eaqzc61KPcElnllwFWiRi8ufPjbuHfnJNKTyuA8ABVQzHIQi +Z+Rhw/IASS2aMuD1A4XicneZ43Vo83mz7xfiebVW7WRyIIcxlWruHQM+BYCTR+y5 +vGdq+P0e4ZjAdwudxB6MfmF5lO16M8CiH5SAJghmBkbEpwdkZuMX6YQLGnw2+C6m +NHRRb0WSGX12PEJcxA4mvM4L2+m7ihZTSJg5fYSLAQKBgQDmV77TXc6xAfq1O3es +AV69a5rNgtJ3I7wnv42ZLpQLVSVQ1eqRzhaMIItbX3hwb9STWagIuuGbTG590k00 +lsHRD0/Z4mapyk43b9Zs90ug1L0ojqrVfmjb5uJDzj4fbHO1QtJ2Dt2llMveGCaE +j5LYz+AKdBjZdxxU6NDws8W1AQKBgQDaZwQjMAx7usp+prVVe33QchTxeziaFh+L +q53zimoCsXjTSz040dkdk+yhj0DOT3z4Of41+PkWmbLoZh1ar9yFSQPC3fVvz4Sx +mY5gCIy1D9a3SSPYy6+V6PiCSkYB3JsGONKARl1oxNswewGcBVSxEYMH0INGccoc +Aqv0WYokTwKBgE/QtdHd3oIdUnc8HPKgIuj2AVUW8MDRxB/t0y3yIuBuZ5jEFxzg +0ZVKrZ9CzKQBTCKm3X/w6b37VQoKNjBz0MMVUDEKr1JERKgW+7EUW8NmFAmarf1+ +aO1R6rNeIs6WsN1tQyofapQ2pBHYQniKm3K/TT2yDu5DxLBzX/sX//kBAoGAfNsL +uHoNiOZ+QO1ZGSdiA7pUUsDY2O3w1s4jnEjy1dtTcae/ti3xa0FXs/zLVaaHkPjG +ZWCSNeNkLgvCmEeVT/Uv20U8vNDr0QQyKqB4oEIPw/RiM9wt4wuulf2UDBhr36C4 +ZnDTkcOIcyy+/MznX9rdB6iig7Elk+HLIAPYP20CgYAL+/AizILmdI7lBvderLfw +2oslABhYJCjaFX5XWTvOJpPdZPZSqZUgSvzgBM57f1gtRmn6NS1oM6qagoYlOpF4 +rjIcuz86j8tFTp45TjMz1Sf3afyi+iiFhdjTeDTdyjcWK0sBghyljc4jq5scQsYH +SUOdYY70Kenpzx6iOaWe/g== +-----END PRIVATE KEY----- + diff --git a/cpp/tests/vereign/crypto/test_data/leaf_cert.txt b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt new file mode 100644 index 0000000000000000000000000000000000000000..fb718f515d1240b1ea27e79859552b2d87d7c06e --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt @@ -0,0 +1,65 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 73 (0x49) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com + Validity + Not Before: Jul 2 00:00:00 2020 GMT + Not After : Jul 1 00:00:00 2025 GMT + Subject: CN=example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=Engineering/emailAddress=test@example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:be:ab:51:25:32:3c:75:bb:49:25:1b:b9:9f:6e: + af:ff:38:07:f0:2c:49:e7:3b:c8:3a:19:02:f1:aa: + 96:8d:9d:bd:37:b0:97:2a:11:05:71:65:94:ee:32: + 6b:4b:d7:94:43:a1:8c:bb:ed:fe:94:d6:93:f8:7b: + ca:48:7d:fc:99:00:88:d5:d1:73:55:31:ca:eb:69: + 5f:0d:1b:6a:24:39:db:b9:f1:7a:34:c7:1f:8e:a4: + 12:ab:1d:e8:29:b6:aa:05:d3:fb:7d:bb:7e:2b:db: + c8:18:15:ba:8e:6d:b4:df:98:3c:34:6c:50:07:e9: + 7a:7a:57:99:8b:cf:6d:d7:c3:e1:c5:90:ae:d3:94: + dc:b9:fc:90:20:ec:63:24:e5:8b:42:d3:bb:4d:e6: + e2:f0:b6:48:a9:c0:27:2e:89:bc:45:db:b6:25:be: + 7d:55:40:6d:72:e4:5b:49:81:d4:aa:e0:3a:33:93: + f9:4b:b2:82:b4:45:e1:5e:0c:14:b0:6e:78:f9:1a: + 98:72:8c:ef:c5:10:ab:7f:9d:e7:9b:68:17:30:21: + f0:95:bf:ab:c6:45:bb:3c:cf:f1:96:1f:8d:22:88: + 72:b3:52:ff:20:c0:40:5b:11:0d:62:24:ec:5c:53: + 81:fa:ca:ec:a5:00:b8:7c:1e:e2:a8:b7:01:42:5e: + 1d:01 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment + X509v3 Extended Key Usage: + E-mail Protection + X509v3 Subject Alternative Name: + email:test@example.com, DNS:api.example.com + X509v3 Subject Key Identifier: + A5:BD:8A:35:84:98:B6:6C:13:51:A2:4A:5D:C9:51:C6:61:7E:5D:AE + X509v3 Authority Key Identifier: + keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76 + DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com + serial:2A + + Signature Algorithm: sha256WithRSAEncryption + 8d:90:e7:a7:a6:57:ec:b3:db:38:37:87:bc:1e:0b:e2:bd:c4: + 88:e3:95:f0:35:01:a7:fe:51:92:b1:4c:37:d7:21:cf:80:77: + 51:98:62:61:2c:99:58:60:89:01:35:3e:56:8c:dd:07:21:ad: + a1:e3:e5:02:3c:e5:cf:c8:ee:46:11:07:16:f5:37:8c:75:22: + e0:af:85:e2:85:31:21:a2:f3:e4:b5:1b:de:ef:31:86:88:84: + 18:11:bc:67:5a:a7:0b:d1:16:fc:5c:12:51:8d:1c:26:d5:45: + f8:55:28:7f:85:a2:c7:a7:e4:bc:6b:98:b4:c4:d9:3e:f6:72: + a8:f9:24:f5:9b:38:bf:aa:76:9c:68:c8:67:4f:fa:79:9c:7c: + 52:f2:45:6c:05:26:4f:64:4c:ea:52:d0:c2:75:45:21:e5:8e: + d7:60:0a:b2:6c:d2:f8:1c:01:9b:b1:72:1d:4e:95:79:18:34: + b6:1c:bc:ea:5d:12:1f:33:90:63:dc:01:b9:9b:33:dd:62:31: + 69:c8:b2:1d:ce:3d:38:57:60:c5:75:7d:84:95:3d:db:36:0a: + a3:77:7c:67:b6:e3:7a:c0:b4:a5:51:05:78:6b:29:28:43:b6: + 74:d8:74:18:8e:4f:44:62:55:49:bb:f9:f3:1b:37:63:49:ff: + c9:2e:73:a2 diff --git a/cpp/tests/vereign/crypto/test_data/leaf_key.pem b/cpp/tests/vereign/crypto/test_data/leaf_key.pem new file mode 100644 index 0000000000000000000000000000000000000000..5d83ade2c640588ac9df74b38aa7dd62dc23600c --- /dev/null +++ b/cpp/tests/vereign/crypto/test_data/leaf_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+q1ElMjx1u0kl +G7mfbq//OAfwLEnnO8g6GQLxqpaNnb03sJcqEQVxZZTuMmtL15RDoYy77f6U1pP4 +e8pIffyZAIjV0XNVMcrraV8NG2okOdu58Xo0xx+OpBKrHegptqoF0/t9u34r28gY +FbqObbTfmDw0bFAH6Xp6V5mLz23Xw+HFkK7TlNy5/JAg7GMk5YtC07tN5uLwtkip +wCcuibxF27Ylvn1VQG1y5FtJgdSq4Dozk/lLsoK0ReFeDBSwbnj5GphyjO/FEKt/ +neebaBcwIfCVv6vGRbs8z/GWH40iiHKzUv8gwEBbEQ1iJOxcU4H6yuylALh8HuKo +twFCXh0BAgMBAAECggEAAuEIMLktYl4MLh2DpBrng5bMwI1mpfFy5fUD5J1zDbCJ ++iSLj8oh1yhrGIgAiJdjfibwaVO1Z6Qa0rfJzOZBcWz+4/L4RxshSYSLN8yIFy5x +eSwNMJf+IROsqCf43g0LobAQU4HIggXc+Qd2TEQZ+lNXO2Jpo0jyUr/DtNxSYrLk +PQOiZUeTRApEFMH/KR7E3CINnFWEsyBktr4UGNT+KbuS+h24QFgeMZS03ELCER/E +GtYgjYE8Njb+LLC82ZB0jVobo7sXqEu4xhunsysJMID7XF7cRbVH4jS4owBik+MM +0C4x3IWcUlPTRwCwz7ThdNmuaWysdcm6YKmDTjkg7QKBgQDgObGyYADOBVC986xD +rHQ6HsHjJoS7UA98OKtpwUfoHgmzCn787bL+28payYLjBQg8Lu0V4wz6IzryCjEY +DBDWT376lsIElYQBVO80JR+sQHveRKHj3nKxin8RFv+P1iR5l06MLFuI7lpgnfjf +zAJAMtob6hXTfhKd6ttw3LMJtQKBgQDZsEsv3iWEtJOlEk32b+cBtC9m3+3BRrsl +vhMqJRRiz3q8ESw4UeYnjHTMJPKixsNCTwJ+swLz1HzPjAPOn8J5g+ehx9szx04g ++SOjO0hYxiuMyEY1RcgQDz8W2eDIY79vQHK6eIdsDX9aDM8TKl0SVkX8yeZVH/l4 +ZrS0GcIlnQKBgHwO78YlX8ydHEM8or7+l3/grJ2lHiMU9LxiDlUA32wJ0owA7qm6 +SiLAMIFypLw+eIPz4rENYoyzGVX5VZhcdKReb37uEoK+xr6F46MSNyfytZYQmxGc +bhDU8+DcvKE0dhHgKVFc6XgliFOUk7IfCc/DUvHG3jSZuGsvGiKjuVtRAoGAL5DF +3P+9UvHiEP+e6WoqH8UvVls3qDO8UNLnfR39Blovh0h5URUUJTAblT+9hLPJloON +GAIlcwHLA86kRlHCPKsFwePZkcbK24mUr+YUONGUUn6wWJOW02uBbYzOAPtKqRR7 +/SK49HM+kH6cAKQREiCnykSTf6Ys7JIEihXWxkECgYEAo/PjT9L+QuYn9muv4p50 +UMJLGQ6kE+63n2dvcEUhXRtEx/i4KzgTkdJ0gBOiaBhJYjzI4BZt/DgtSrzqNC1Y +UUk5i9sJJF5Ypw0n3dX1mYEFlsJ1xFKDU+lwMakdY+X6b+VECKGIQZsh5DOrFRrv +QzRzF4x09HztPUADv4gCL40= +-----END PRIVATE KEY----- diff --git a/cpp/tests/vereign/encoding/base64_test.cc b/cpp/tests/vereign/encoding/base64_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..0e6316b90d2de75e8f9cefcf9c4772259355982b --- /dev/null +++ b/cpp/tests/vereign/encoding/base64_test.cc @@ -0,0 +1,86 @@ +#include <vereign/encoding/base64.hh> + +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> + +#include <catch2/catch.hpp> + +#include <iostream> + +using namespace vereign; + +TEST_CASE("base64::Encode", "[vereign/encoding/base64][vereign/encoding]") { + SECTION("RFC 4648 test cases plus some extras") { + struct Test { + std::string Input; + std::string Expected; + }; + + using namespace std::string_literals; + + std::vector<Test> tests{ + // taken from RFC 4648 + {"f", "Zg=="}, + {"fo", "Zm8="}, + {"foo", "Zm9v"}, + {"foob", "Zm9vYg=="}, + {"fooba", "Zm9vYmE="}, + {"foobar", "Zm9vYmFy"}, + + // additional test cases + {"", ""}, + {"\x00\x42"s "foobar", "AEJmb29iYXI="} + }; + + for (auto& test : tests) { + bytes::Buffer encoded; + encoding::base64::Encode(bytes::View(test.Input), encoded); + CHECK(test.Expected == encoded.View().String()); + } + } + + SECTION("big input must not contain new lines") { + auto input = crypto::Rand(1024); + bytes::Buffer encoded; + encoding::base64::Encode(input.View(), encoded); + + CHECK(encoded.View().String().find('\n') == std::string::npos); + CHECK(encoded.View().String().find('\r') == std::string::npos); + + bytes::Buffer decoded; + encoding::base64::Decode(encoded.View(), decoded); + + CHECK(input.View() == decoded.View()); + } +} + +TEST_CASE("base64::Decode", "[vereign/encoding/base64][vereign/encoding]") { + SECTION("RFC 4648 test cases plus some extras") { + struct Test { + std::string Expected; + std::string Input; + }; + + using namespace std::string_literals; + + std::vector<Test> tests{ + // taken from RFC 4648 + {"f", "Zg=="}, + {"fo", "Zm8="}, + {"foo", "Zm9v"}, + {"foob", "Zm9vYg=="}, + {"fooba", "Zm9vYmE="}, + {"foobar", "Zm9vYmFy"}, + + // additional test cases + {"", ""}, + {"\x00\x42"s "foobar", "AEJmb29iYXI="} + }; + + for (auto& test : tests) { + bytes::Buffer encoded; + encoding::base64::Decode(bytes::View(test.Input), encoded); + CHECK(test.Expected == encoded.View().String()); + } + } +} diff --git a/cpp/tests/vereign/encoding/hex_test.cc b/cpp/tests/vereign/encoding/hex_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..9b4fefea84b7de42c502de478613888a736578d9 --- /dev/null +++ b/cpp/tests/vereign/encoding/hex_test.cc @@ -0,0 +1,96 @@ +#include <vereign/encoding/hex.hh> + +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("encoding::hex::Encode", "[vereign/encoding/hex][vereign/encoding]") { + struct Test { + std::string Input; + std::string Expected; + }; + + using namespace std::string_literals; + + std::vector<Test> tests{ + {"", ""}, + {"f", "66"}, + {"fo", "666f"}, + {"foo", "666f6f"}, + {"foob", "666f6f62"}, + {"fooba", "666f6f6261"}, + {"foobar", "666f6f626172"}, + {"\x00\x42"s "foobar", "0042666f6f626172"} + }; + + for (auto& test : tests) { + bytes::Buffer encoded; + encoding::hex::Encode(bytes::View(test.Input), encoded); + CHECK(test.Expected == encoded.View().String()); + } +} + +TEST_CASE("encoding::hex::Decode", "[vereign/encoding/hex][vereign/encoding]") { + struct Test { + std::string Expected; + std::string Input; + }; + + using namespace std::string_literals; + + std::vector<Test> tests{ + {"", ""}, + {"f", "66"}, + {"fo", "666f"}, + {"foo", "666f6f"}, + {"foob", "666f6f62"}, + {"fooba", "666f6f6261"}, + {"foobar", "666f6f626172"}, + {"\x00\x42"s "foobar", "0042666f6f626172"} + }; + + for (auto& test : tests) { + bytes::Buffer decoded; + encoding::hex::Decode(bytes::View(test.Input), decoded); + CHECK(test.Expected == decoded.View().String()); + } + + auto input = crypto::Rand(16); + + bytes::Buffer encoded; + encoding::hex::Encode(input.View(), encoded); + + bytes::Buffer decoded; + encoding::hex::Decode(encoded.View(), decoded); + + CHECK(input.View() == decoded.View()); +} + +TEST_CASE("encoding::hex::EncodeReverse", "[vereign/encoding/hex][vereign/encoding]") { + struct Test { + std::string Input; + std::string Expected; + }; + + using namespace std::string_literals; + + std::vector<Test> tests{ + {"", ""}, + {"f", "66"}, + {"fo", "6f66"}, + {"foo", "6f6f66"}, + {"foob", "626f6f66"}, + {"fooba", "61626f6f66"}, + {"foobar", "7261626f6f66"}, + {"\x00\x42"s "foobar", "7261626f6f664200"} + }; + + for (auto& test : tests) { + bytes::Buffer encoded; + encoding::hex::EncodeReverse(bytes::View(test.Input), encoded); + CHECK(test.Expected == encoded.View().String()); + } +} diff --git a/cpp/tests/vereign/filesystem/temp.hh b/cpp/tests/vereign/filesystem/temp.hh new file mode 100644 index 0000000000000000000000000000000000000000..62c03d0c08c812e86ae7f9268d6936989db3ac3b --- /dev/null +++ b/cpp/tests/vereign/filesystem/temp.hh @@ -0,0 +1,31 @@ +#ifndef __VEREIGN_FILESYSTEM_TEMP_HH +#define __VEREIGN_FILESYSTEM_TEMP_HH + +namespace vereign::filesystem { + +std::filesystem::path CreateTmpDir() { + // auto tmp_dir = std::filesystem::temp_directory_path(); + // unsigned long long i = 0; + // std::random_device dev; + // std::mt19937 prng(dev()); + // std::uniform_int_distribution<uint64_t> rand(0); + // std::filesystem::path path; + // while (true) { + // std::stringstream ss; + // ss << std::hex << rand(prng); + // path = tmp_dir / ss.str(); + // // true if the directory was created. + // if (std::filesystem::create_directory(path)) { + // break; + // } + // if (i == max_tries) { + // throw std::runtime_error("could not find non-existing directory"); + // } + // i++; + // } + return path; +} + +} // namespace vereign::filesystem + +#endif // __VEREIGN_FILESYSTEM_TEMP_HH diff --git a/cpp/tests/vereign/grpc/event_api_test.cc b/cpp/tests/vereign/grpc/event_api_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..a1220024fe11cb51c2af899fc2779420e178f928 --- /dev/null +++ b/cpp/tests/vereign/grpc/event_api_test.cc @@ -0,0 +1,96 @@ +#include <vereign/grpc/server.hh> + +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/grpc/error_code.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/client_library/passport_api.gen.grpc.pb.h> +#include <vereign/client_library/identity_api.gen.grpc.pb.h> +#include <vereign/client_library/event_api.gen.grpc.pb.h> +#include <vereign/service/identity_service.hh> +#include <vereign/fs/util.hh> +#include <vereign/fs/path.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> + +#ifdef _WIN32 +# include <vereign/ncrypt/rsa.hh> +#endif + +#include <testutil/env.hh> +#include <testutil/protobuf.hh> +#include <grpcpp/create_channel.h> + +#include <catch2/catch.hpp> + +TEST_CASE("grpc::EventAPI::GetNewEvents", "[vereign/grpc][.integration]") { + SECTION("get events while not logged in") { + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + const std::string pin = "foo"; + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + // start listening for events, and exit when the device is confirmed + std::thread t1([&channel]() { + auto event_client = vereign::client_library::EventAPI::NewStub(channel); + auto req = vereign::client_library::GetNewEventsForm{}; + auto ctx = ::grpc::ClientContext{}; + auto resp_stream = event_client->GetNewEvents(&ctx, req); + + auto resp = vereign::client_library::GetNewEventsFormResponse{}; + for (;;) { + resp.Clear(); + auto ok = resp_stream->Read(&resp); + if (!ok) { + break; + } + + REQUIRE(resp.data().size() > 0); + for (auto& event : resp.data()) { + if (event.type() == "DeviceConfirmed") { + return; + } + } + + // std::cout << vereign::testutil::ProtobufToJson(resp) << std::endl; + } + }); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormNewDevice{}; + auto login_resp = vereign::client_library::LoginFormNewDeviceResponse{}; + login_req.set_pin(pin); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithNewDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == ""); + CHECK(login_resp.status() == "OK"); + REQUIRE(login_resp.code() == "200"); + + // the old device is used for new device confirmation and authorization + auto old_storage_path = vereign::fs::TempFilePath("test_db_"); + auto rm_old_storage_path = vereign::fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = vereign::test::ServiceContext{host, port, old_storage_path}; + auto old_device = vereign::test::Device{old_device_ctx}; + old_device.Login(public_key); + + // confirm the new device using an old device + old_device.ConfirmNewDevice(login_resp.data().qrcode(), login_resp.data().actionid()); + + t1.join(); + } +} diff --git a/cpp/tests/vereign/grpc/identity_api_test.cc b/cpp/tests/vereign/grpc/identity_api_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..2593b8f0781a19ee0886945e5ec15d8f1e23aca5 --- /dev/null +++ b/cpp/tests/vereign/grpc/identity_api_test.cc @@ -0,0 +1,337 @@ +#include <vereign/grpc/server.hh> + +#include <vereign/grpc/error_code.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/client_library/passport_api.gen.grpc.pb.h> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/client_library/identity_api.gen.grpc.pb.h> +#include <vereign/service/identity_service.hh> +#include <vereign/fs/util.hh> +#include <vereign/fs/path.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> + +#ifdef _WIN32 +# include <vereign/ncrypt/rsa.hh> +#endif + +#include <testutil/env.hh> +#include <testutil/protobuf.hh> +#include <grpcpp/create_channel.h> + +#include <catch2/catch.hpp> + +TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.integration]") { +#ifdef _WIN32 + SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") { + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + + auto provider = vereign::ncrypt::rsa::OpenStorageProvider(); + auto key = vereign::ncrypt::rsa::LoadKey( + provider.Get(), + std::string(vereign::kvstore::VereignKeyName) + ); + if (key) { + vereign::ncrypt::rsa::DeleteKey(key.Get()); + } + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() != ""); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::DeviceNotRegistered + )); + } + + SECTION("when the device key has been changed, it must fail with InvalidIdentity error") { + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + vereign::test::PrepareNewDevice(host, port, public_key, "", storage_path); + + auto provider = vereign::ncrypt::rsa::OpenStorageProvider(); + auto key = vereign::ncrypt::rsa::LoadKey( + provider.Get(), + std::string(vereign::kvstore::VereignKeyName) + ); + if (key) { + vereign::ncrypt::rsa::DeleteKey(key.Get()); + } + + vereign::ncrypt::rsa::CreateKey( + provider.Get(), + 2048, + std::string(vereign::kvstore::VereignKeyName), + {} + ); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == "invalid identity"); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::InvalidIdentity + )); + } +#else + SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") { + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + login_req.set_pin("foo"); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() != ""); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::DeviceNotRegistered + )); + } + + SECTION("when the pin is wrong, it must fail with InvalidPinCode error") { + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + const std::string pin = "foo"; + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + login_req.set_pin("invalid_pin"); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == "invalid pin code"); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::InvalidPinCode + )); + } + + SECTION("when the pin is empty, it must fail with InvalidPinCode error") { + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + const std::string pin = "foo"; + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + login_req.set_pin(""); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == "invalid pin code"); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::InvalidPinCode + )); + } + +#endif + + SECTION("when the device is not authorized, it must fail with DeviceNotRegistered error") { + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + // the old device is used later for new device confirmation and authorization + auto old_storage_path = vereign::fs::TempFilePath("test_db_"); + auto rm_old_storage_path = vereign::fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = vereign::test::ServiceContext{host, port, old_storage_path}; + auto old_device = vereign::test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + const std::string pin = "foo"; + + std::string qr_code; + std::string action_id; + + { + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + + auto register_req = vereign::client_library::LoginFormNewDevice{}; + auto register_resp = vereign::client_library::LoginFormNewDeviceResponse{}; + register_req.set_pin(pin); + + ::grpc::ClientContext register_ctx; + identity_client->LoginWithNewDevice(®ister_ctx, register_req, ®ister_resp); + + CHECK(register_resp.error() == ""); + CHECK(register_resp.status() == "OK"); + REQUIRE(register_resp.code() == "200"); + + qr_code = register_resp.data().qrcode(); + action_id = register_resp.data().actionid(); + } + + { + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + login_req.set_pin(pin); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == "Error retrieving entity "); + CHECK(login_resp.status() == "Error while login"); + REQUIRE(login_resp.code() == "400"); + + old_device.ConfirmNewDevice(qr_code, action_id); + + ::grpc::ClientContext login_ctx_confirmed; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx_confirmed, login_req, &login_resp); + + CHECK(login_resp.error() == "Device unauthorized"); + CHECK(login_resp.status() == "Error while login"); + REQUIRE(login_resp.code() == "400"); + } + } +} + +TEST_CASE("grpc::IdentityAPI::LoginWithNewDevice", "[vereign/grpc][.integration]") { +#ifdef _WIN32 +#else + SECTION("when the pin is empty, it must fail with InvalidPinCode error") { + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; + auto on_exit = vereign::core::MakeScopeGuard([&server] { + server.Shutdown(); + }); + + auto channel = ::grpc::CreateChannel( + "localhost:" + std::to_string(server.SelectedPort()), + ::grpc::InsecureChannelCredentials() + ); + + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormNewDevice{}; + auto login_resp = vereign::client_library::LoginFormNewDeviceResponse{}; + login_req.set_pin(""); + + ::grpc::ClientContext login_ctx; + identity_client->LoginWithNewDevice(&login_ctx, login_req, &login_resp); + + CHECK(login_resp.error() == "invalid pin code"); + CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus); + REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString( + vereign::grpc::ErrorCode::InvalidPinCode + )); + } +#endif // _WIN32 +} diff --git a/cpp/tests/vereign/grpc/json/encoder_test.cc b/cpp/tests/vereign/grpc/json/encoder_test.cc index c11b41e95013417b294c28f454a9722c50c510d5..7cd3095d85dff462f18cf8b8765a4fdbe1d485bb 100644 --- a/cpp/tests/vereign/grpc/json/encoder_test.cc +++ b/cpp/tests/vereign/grpc/json/encoder_test.cc @@ -2,8 +2,6 @@ #include <vereign/grpc/json/pb/messages.pb.h> #include <boost/math/constants/constants.hpp> -#include <vereign/core/hex.hh> - #include <catch2/catch.hpp> using namespace vereign; diff --git a/cpp/tests/vereign/grpc/server_test.cc b/cpp/tests/vereign/grpc/server_test.cc index 61e2ea3874131d4aaff740fea19b6364c26b7273..de3c47db053a0494fbaeb059d1f3535fc0bdcaee 100644 --- a/cpp/tests/vereign/grpc/server_test.cc +++ b/cpp/tests/vereign/grpc/server_test.cc @@ -1,20 +1,32 @@ #include <vereign/grpc/server.hh> + #include <vereign/core/scope_guard.hh> #include <vereign/client_library/passport_api.gen.grpc.pb.h> -#include <util/env.hh> -#include <util/protobuf.hh> - -#include <catch2/catch.hpp> -#include "vereign/client_library/types.gen.pb.h" +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/client_library/identity_api.gen.grpc.pb.h> +#include <vereign/service/identity_service.hh> +#include <vereign/fs/util.hh> +#include <vereign/fs/path.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> +#include <testutil/env.hh> +#include <testutil/protobuf.hh> #include <grpcpp/create_channel.h> +#include <catch2/catch.hpp> + TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); - auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); - auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); - vereign::grpc::Server server{"localhost:", host, port, publicKey}; + const std::string pin = "foo"; + auto storage_path = vereign::fs::TempDir("test_db_"); + auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path); + vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path); + + vereign::grpc::Server server{"localhost:", host, port, storage_path}; auto on_exit = vereign::core::MakeScopeGuard([&server] { server.Shutdown(); }); @@ -24,35 +36,33 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { ::grpc::InsecureChannelCredentials() ); - auto client = vereign::client_library::PassportAPI::NewStub(channel); + // register new device + auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel); + auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{}; + auto login_resp = vereign::client_library::EmptyResponse{}; + login_req.set_pin(pin); - vereign::client_library::ListPassportsForm req; - vereign::client_library::ListPassportsFormResponse resp; - ::grpc::ClientContext ctx; - auto status = client->ListPassports(&ctx, req, &resp); + ::grpc::ClientContext login_ctx; + identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp); - // std::cout << vereign::test::ProtobufToJson(resp) << std::endl; + CHECK(login_resp.error() == ""); + CHECK(login_resp.status() == "OK"); + REQUIRE(login_resp.code() == "200"); - REQUIRE(status.error_message() == ""); - REQUIRE(resp.error() == ""); - CHECK(resp.status() == "OK"); - CHECK(resp.code() == "200"); - CHECK(resp.data().size() > 0); - for (auto& passport : resp.data()) { - CHECK(passport.uuid().size() == 36); - } + auto passport_client = vereign::client_library::PassportAPI::NewStub(channel); - req.Clear(); - resp.Clear(); - ::grpc::ClientContext manually_ctx; - status = client->ListPassportsManually(&manually_ctx, req, &resp); + vereign::client_library::ListPassportsForm req; + vereign::client_library::ListPassportsFormResponse resp; + ::grpc::ClientContext ctx; + auto status = passport_client->ListPassports(&ctx, req, &resp); // std::cout << vereign::test::ProtobufToJson(resp) << std::endl; REQUIRE(status.error_message() == ""); - REQUIRE(resp.error() == ""); + REQUIRE(status.ok() == true); + CHECK(resp.error() == ""); CHECK(resp.status() == "OK"); - CHECK(resp.code() == "200"); + REQUIRE(resp.code() == "200"); CHECK(resp.data().size() > 0); for (auto& passport : resp.data()) { CHECK(passport.uuid().size() == 36); @@ -62,7 +72,7 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { getInterReq.set_uuid(resp.data().at(0).uuid()); vereign::client_library::GetInteractionsFormResponse getInterResp; ::grpc::ClientContext getInterCtx; - status = client->GetInteractions(&getInterCtx, getInterReq, &getInterResp); + status = passport_client->GetInteractions(&getInterCtx, getInterReq, &getInterResp); CHECK(status.error_message() == ""); CHECK(getInterResp.error() == ""); CHECK(getInterResp.status() == "OK"); @@ -74,6 +84,6 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl; - grpc_shutdown(); - google::protobuf::ShutdownProtobufLibrary(); + // grpc_shutdown(); + // google::protobuf::ShutdownProtobufLibrary(); } diff --git a/cpp/tests/vereign/identity/provider_test.cc b/cpp/tests/vereign/identity/provider_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..c4de48c2568c74315d2894410f4ff301b939d207 --- /dev/null +++ b/cpp/tests/vereign/identity/provider_test.cc @@ -0,0 +1,215 @@ +#include <vereign/identity/provider.hh> + +#include <vereign/fs/util.hh> +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/kvstore/crypto_storage.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> +#include <vereign/service/gen/passport_service.hh> +#include <vereign/crypto/cert.hh> +#include <vereign/crypto/rsa.hh> +#include <testutil/env.hh> +#include <testutil/protobuf.hh> + +#include <catch2/catch.hpp> +#include <iostream> + +using namespace vereign; + +TEST_CASE("identity::Provider::RecreateIdentity", "[vereign/identity]") { + auto storage_path = fs::TempFilePath("test_db_"); + auto rm_storage_path = fs::RemoveFileGuard{storage_path}; + + auto device_ctx = test::ServiceContext{"", "", storage_path}; + + std::string expected; + std::string actual; + + { + kvstore::SqliteStorage kvstorage{storage_path}; + kvstore::CryptoStorage storage{kvstorage, true}; + + identity::Provider provider{device_ctx.ClientSession(), storage}; + expected = provider.RecreateIdentity("foo"); + } + + { + kvstore::SqliteStorage kvstorage{storage_path}; + kvstore::CryptoStorage storage{kvstorage, true}; + + identity::Provider provider{device_ctx.ClientSession(), storage}; + actual = provider.LoadIdentity("foo"); + } + + CHECK(expected.size() > 0); + CHECK(actual.size() > 0); + CHECK(expected == actual); +} + +TEST_CASE("identity::Provider::GetProfileCertificate", "[vereign/identity][.integration]") { + auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + // prepare new device + auto old_storage_path = fs::TempFilePath("test_db_"); + auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = test::ServiceContext{host, port, old_storage_path}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto storage_path = fs::TempFilePath("test_db_"); + auto rm_storage_path = fs::RemoveFileGuard{storage_path}; + auto service_context = test::ServiceContext{host, port, storage_path}; + old_device.CreateNewDevice(service_context, "pin"); + + // retrieve a profile uuid + auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>(); + auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>(); + + auto passport_service = service::gen::PassportService{service_context.ClientSession()}; + auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get()); + + auto result = list_result.get(); + auto& list = result.Response; + REQUIRE(list->code() == "200"); + REQUIRE(list->data().size() > 0); + + auto profileUUID = list->data().at(0).uuid(); + + auto& identity_provider = service_context.IdentityProvider(); + + auto cert = identity_provider.GetProfileCertificate(profileUUID); + + REQUIRE(cert.get() != nullptr); + CHECK(cert->PrivateKeyPEM.Size() > 0); + CHECK(cert->CertificatePEM.Size() > 0); + CHECK(cert->CertificateUUID.size() > 0); + CHECK(cert->Chain.size() == 2); + + SECTION("the certificate private key must be importable") { + bssl::UniquePtr<EVP_PKEY> cert_pkey; + CHECK_NOTHROW( + cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View()) + ); + CHECK(cert_pkey.get() != nullptr); + } + + SECTION("the certificate and the certificates from the chain must be importable") { + bssl::UniquePtr<X509> x509_cert; + CHECK_NOTHROW( + x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View()) + ); + CHECK(x509_cert.get() != nullptr); + + for (auto& cert_pem : cert->Chain) { + x509_cert.reset(); + + CHECK_NOTHROW( + x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View()) + ); + CHECK(x509_cert.get() != nullptr); + } + } + + + SECTION("it must reuse already created profile certificates") { + auto new_cert = identity_provider.GetProfileCertificate(profileUUID); + + REQUIRE(new_cert.get() != nullptr); + CHECK(new_cert->PrivateKeyPEM.View() == cert->PrivateKeyPEM.View()); + CHECK(new_cert->CertificatePEM.View() == cert->CertificatePEM.View()); + CHECK(new_cert->CertificateUUID == cert->CertificateUUID); + REQUIRE(new_cert->Chain.size() == cert->Chain.size()); + for (std::size_t i = 0; i < new_cert->Chain.size(); i++) { + CHECK(new_cert->Chain[i].View() == cert->Chain[i].View()); + } + } +} + +TEST_CASE("identity::Provider::GetProfileOneTimeCertificate", "[vereign/identity][.integration]") { + auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + // prepare new device + auto old_storage_path = fs::TempFilePath("test_db_"); + auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = test::ServiceContext{host, port, old_storage_path}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto storage_path = fs::TempFilePath("test_db_"); + auto rm_storage_path = fs::RemoveFileGuard{storage_path}; + auto service_context = test::ServiceContext{host, port, storage_path}; + old_device.CreateNewDevice(service_context, "pin"); + + // retrieve a profile uuid + auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>(); + auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>(); + + auto passport_service = service::gen::PassportService{service_context.ClientSession()}; + auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get()); + + auto result = list_result.get(); + auto& list = result.Response; + REQUIRE(list->code() == "200"); + REQUIRE(list->data().size() > 0); + + auto profileUUID = list->data().at(0).uuid(); + + auto& identity_provider = service_context.IdentityProvider(); + + auto cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "foo@example.com"); + + REQUIRE(cert.get() != nullptr); + CHECK(cert->PrivateKeyPEM.Size() > 0); + CHECK(cert->CertificatePEM.Size() > 0); + CHECK(cert->CertificateUUID.size() == 0); + CHECK(cert->Chain.size() == 3); + + SECTION("the certificate private key must be importable") { + bssl::UniquePtr<EVP_PKEY> cert_pkey; + CHECK_NOTHROW( + cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View()) + ); + CHECK(cert_pkey.get() != nullptr); + } + + // fs::WriteFile("onetime", cert->CertificatePEM.View()); + // fs::WriteFile("userdevice", cert->Chain[0].View()); + // fs::WriteFile("serverside", cert->Chain[1].View()); + // fs::WriteFile("vereign", cert->Chain[2].View()); + + SECTION("the certificate and the certificates from the chain must be importable") { + bssl::UniquePtr<X509> x509_cert; + CHECK_NOTHROW( + x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View()) + ); + CHECK(x509_cert.get() != nullptr); + + for (auto& cert_pem : cert->Chain) { + x509_cert.reset(); + + CHECK_NOTHROW( + x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View()) + ); + CHECK(x509_cert.get() != nullptr); + } + } + + SECTION("it must create new certificate all the time, but the issuers chain must be the same") { + auto new_cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "bar@example.com"); + + REQUIRE(new_cert.get() != nullptr); + CHECK(new_cert->PrivateKeyPEM.View() != cert->PrivateKeyPEM.View()); + CHECK(new_cert->CertificatePEM.View() != cert->CertificatePEM.View()); + CHECK(new_cert->CertificateUUID.size() == 0); + REQUIRE(new_cert->Chain.size() == cert->Chain.size()); + for (std::size_t i = 0; i < new_cert->Chain.size(); i++) { + CHECK(new_cert->Chain[i].View() == cert->Chain[i].View()); + } + } +} diff --git a/cpp/tests/vereign/kvstore/crypto_storage_test.cc b/cpp/tests/vereign/kvstore/crypto_storage_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..01524db2f8dc171002b49e71addfa33289b20737 --- /dev/null +++ b/cpp/tests/vereign/kvstore/crypto_storage_test.cc @@ -0,0 +1,99 @@ +#include <vereign/kvstore/crypto_storage.hh> + +#include <vereign/encoding/errors.hh> +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/fs/util.hh> +#include <vereign/core/scope_guard.hh> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <iostream> + +using namespace vereign; + +TEST_CASE("kvstore::CryptoStorage::Recreate", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + // put value + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstore::CryptoStorage storage{kvstorage, true}; + + storage.Recreate("foo"); + std::string v{"test value"}; + storage.PutBytes("test", bytes::View(v)); + } + + // with another storage instance get the value + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstore::CryptoStorage storage{kvstorage, true}; + + bytes::Buffer v; + storage.Open("foo"); + storage.GetBytes("test", v); + + CHECK(v.View().String() == "test value"); + } +} + +TEST_CASE("kvstore::CryptoStorage::PutBytes", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto big_value = crypto::Rand(100000); + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstore::CryptoStorage storage{kvstorage, true}; + + storage.Recreate("foo"); + storage.PutBytes("test", big_value.View()); + + bytes::Buffer v; + storage.GetBytes("test", v); + + REQUIRE(v.Size() == big_value.Size()); + + auto cmp = std::memcmp(v.View().Data(), big_value.View().Data(), v.Size()); + CHECK(cmp == 0); +} + +TEST_CASE("kvstore::CryptoStorage::GetBytes", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstore::CryptoStorage storage{kvstorage, true}; + + storage.Recreate("foo"); + + SECTION("when the value does not exists, it must return false") { + bytes::Buffer v; + + CHECK(storage.GetBytes("does_not_exists", v) == false); + + CHECK(v.Size() == 0); + } + + SECTION("when the value cannot be decoded, it must throw encoding::Error") { + kvstorage.PutInt64("foo", 1); + + bytes::Buffer v; + + CHECK_THROWS_AS(storage.GetBytes("foo", v), encoding::Error); + + CHECK(v.Size() == 0); + } + + SECTION("when the value exists, it must retrieve the value") { + storage.PutBytes("foo", bytes::View("bar")); + + bytes::Buffer v; + storage.GetBytes("foo", v); + + CHECK(v.View() == bytes::View("bar")); + } +} diff --git a/cpp/tests/vereign/kvstore/lock_test.cc b/cpp/tests/vereign/kvstore/lock_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..7dd70ed7943d1ec7400c2e3db7534875d979a6a0 --- /dev/null +++ b/cpp/tests/vereign/kvstore/lock_test.cc @@ -0,0 +1,61 @@ +#include <vereign/kvstore/sqlite_storage.hh> + +#include <vereign/kvstore/lock.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/fs/util.hh> +#include <vereign/core/lock_guard.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/sqlite/errors.hh> +#include <testutil/error.hh> +#include <sqlite3.h> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <thread> +#include <chrono> +#include <limits> +#include <optional> + +using namespace vereign; + +TEST_CASE("kvstore::Lock", "[vereign/kvstore]") { + + SECTION("when the lock is released within the allowed retrials, the lock succeeds") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path); + auto bar_storage = kvstore::SqliteStorage(storage_path); + + foo_storage.Lock(); + + bool err = false; + auto th = std::thread{[&bar_storage, &err]() { + try { + kvstore::Lock l{bar_storage, std::numeric_limits<int>::max(), std::chrono::milliseconds{10}}; + } catch (...) { + err = true; + } + }}; + + foo_storage.Unlock(); + th.join(); + } + + SECTION("when the lock is not released within the allowed retrials, the lock fails") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path); + auto bar_storage = kvstore::SqliteStorage(storage_path); + + foo_storage.Lock(); + + CHECK_THROWS_AS( + kvstore::Lock(bar_storage, 2, std::chrono::milliseconds{1}), + kvstore::LockError + ); + } +} diff --git a/cpp/tests/vereign/kvstore/sqlite_storage_test.cc b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..dfe2d3d117038493c6338a3e202b8a1a2e8eec99 --- /dev/null +++ b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc @@ -0,0 +1,157 @@ +#include <vereign/kvstore/sqlite_storage.hh> + +#include <vereign/kvstore/lock.hh> +#include <vereign/kvstore/errors.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/fs/util.hh> +#include <vereign/core/scope_guard.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/sqlite/errors.hh> +#include <testutil/error.hh> +#include <sqlite3.h> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <thread> +#include <optional> + +using namespace vereign; + +TEST_CASE("kvstore::SqliteStorage::GetBytes", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto kvstorage = kvstore::SqliteStorage(storage_path); + + SECTION("when the value does not exists, it must return false") { + bytes::Buffer v; + + auto found = kvstorage.GetBytes("does_not_exists", v); + + CHECK(found == false); + CHECK(v.Size() == 0); + } + + SECTION("when the value exists, it must retrieve the value") { + kvstorage.PutBytes("foo", bytes::View("bar")); + + bytes::Buffer v; + auto found = kvstorage.GetBytes("foo", v); + + CHECK(found == true); + CHECK(v.View() == bytes::View("bar")); + } +} + +TEST_CASE("kvstore::SqliteStorage::GetInt64", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto kvstorage = kvstore::SqliteStorage(storage_path); + + SECTION("when the value does not exists, it must return false") { + int64_t value = 0; + auto found = kvstorage.GetInt64("does_not_exists", value); + + CHECK(found == false); + CHECK(value == 0); + } + + SECTION("when the value exists, it must retrieve the value") { + kvstorage.PutInt64("foo", 42); + + int64_t v = 0; + auto result = kvstorage.GetInt64("foo", v); + + CHECK(result == true); + CHECK(v == 42); + } +} + +TEST_CASE("kvstore::SqliteStorage::DeleteAll", "[vereign/kvstore]") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto kvstorage = kvstore::SqliteStorage(storage_path); + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + + int64_t v = 0; + kvstorage.GetInt64("foo", v); + CHECK(v == 42); + kvstorage.GetInt64("bar", v); + CHECK(v == 422); + + kvstorage.DeleteAll(); + + CHECK(kvstorage.GetInt64("foo", v) == false); + CHECK(kvstorage.GetInt64("bar", v) == false); +} + +TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") { + + SECTION("when locked using lock guard, it must unlock on scope exit") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + + kvstore::Lock l{kvstorage}; + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + + kvstore::Lock l{kvstorage}; + + int64_t v = 0; + kvstorage.GetInt64("foo", v); + CHECK(v == 42); + + kvstorage.GetInt64("bar", v); + CHECK(v == 422); + } + } + + SECTION("when locked, it must unlock on scope exit") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstorage.Lock(); + + kvstorage.PutInt64("foo", 42); + kvstorage.PutInt64("bar", 422); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path); + kvstorage.Lock(); + + int64_t v = 0; + kvstorage.GetInt64("foo", v); + CHECK(v == 42); + + kvstorage.GetInt64("bar", v); + CHECK(v == 422); + } + } + + SECTION("when the storage is already locked, it must fail with LockError") { + auto storage_path = fs::TempFilePath("test_db_"); + fs::RemoveFileGuard rm{storage_path}; + + auto foo_storage = kvstore::SqliteStorage(storage_path); + auto bar_storage = kvstore::SqliteStorage(storage_path); + + foo_storage.Lock(); + + CHECK_THROWS_AS(bar_storage.Lock(), kvstore::LockError); + } +} diff --git a/cpp/tests/vereign/ncrypt/rsa_test.cc b/cpp/tests/vereign/ncrypt/rsa_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..116ed579ca8a3dd518c5e04c40585d34096d706e --- /dev/null +++ b/cpp/tests/vereign/ncrypt/rsa_test.cc @@ -0,0 +1,209 @@ +#include <vereign/crypto/rand.hh> +#include <vereign/ncrypt/rsa.hh> + +#include <vereign/core/scope_guard.hh> +#include <vereign/ncrypt/unique_ptr.hh> +#include <vereign/bytes/view_dump.hh> + +#include <catch2/catch.hpp> +#include <iostream> + +using namespace vereign; + +TEST_CASE("ncrypt::rsa::OpenStorageProvider", "[vereign/ncrypt/rsa][vereign/ncrypt]") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + CHECK(provider.Get() != 0); +} + +TEST_CASE("ncrypt::CreateKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") { + const auto test_key = std::string{"vereign_test_key"}; + + SECTION("when the key does not exists, it must create a new key") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + CHECK(new_key.Get() != 0); + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() != 0); + + // cleanup + ncrypt::rsa::DeleteKey(key.Get()); + } + + SECTION("when the key already exists, it must fail with NTE_EXISTS") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + CHECK(new_key.Get() != 0); + + CHECK_THROWS_WITH( + ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}), + "creating rsa key failed: NTE_EXISTS" + ); + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() != 0); + + // cleanup + ncrypt::rsa::DeleteKey(key.Get()); + } +} + +TEST_CASE("ncrypt::rsa::LoadKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") { + const auto test_key = std::string{"vereign_test_key"}; + + SECTION("when the key exists, it must load the key") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + CHECK(new_key.Get() != 0); + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() != 0); + + // cleanup + ncrypt::rsa::DeleteKey(key.Get()); + } + + SECTION("when the key does not exists, it must fail") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() == 0); + } +} + +TEST_CASE("ncrypt::rsa::DeleteKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") { + const auto test_key = std::string{"vereign_test_key"}; + + SECTION("when the key exists, it must delete the key") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + CHECK(new_key.Get() != 0); + + ncrypt::rsa::DeleteKey(new_key.Get()); + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() == 0); + } + + SECTION("when the key does not exists, it must fail") { + auto provider = ncrypt::rsa::OpenStorageProvider(); + + // cleanup + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + CHECK(new_key.Get() != 0); + + key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + CHECK(key.Get() != 0); + + ncrypt::rsa::DeleteKey(new_key.Get()); + + CHECK_THROWS_WITH( + ncrypt::rsa::DeleteKey(key.Get()), + "deleting key failed: NTE_BAD_KEYSET" + ); + } +} + +TEST_CASE("ncrypt::rsa PublicKeyEncrypt/PrivateKeyDecrypt", "[vereign/ncrypt/rsa][vereign/ncrypt]") { + const auto test_key = std::string{"vereign_test_key"}; + auto provider = ncrypt::rsa::OpenStorageProvider(); + auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key); + if (key) { + ncrypt::rsa::DeleteKey(key.Get()); + } + + key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}); + REQUIRE(key.Get() != 0); + auto delete_key = core::ScopeGuard([&key] { ncrypt::rsa::DeleteKey(key.Get()); }); + + SECTION("small input") { + const std::string input{"foo bar"}; + bytes::Buffer encrypted; + + ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted); + + bytes::Buffer decrypted; + ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == bytes::View(input)); + } + + SECTION("zero input") { + const std::string input; + bytes::Buffer encrypted; + + CHECK_THROWS_WITH( + ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted), + "encryption failed: NTE_NULL_REFERENCE_POINTER" + ); + + bytes::Buffer decrypted; + CHECK_THROWS_WITH( + ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted), + "decryption failed: NTE_NULL_REFERENCE_POINTER" + ); + } + + SECTION("max size input") { + auto input = crypto::Rand(214); + bytes::Buffer encrypted; + + ncrypt::rsa::PublicKeyEncrypt(key.Get(), input.View(), encrypted); + + bytes::Buffer decrypted; + ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted); + + CHECK(decrypted.View() == input.View()); + } + + SECTION("invalid big input") { + auto input = crypto::Rand(215); + bytes::Buffer encrypted; + + CHECK_THROWS_WITH( + ncrypt::rsa::PublicKeyEncrypt(key.Get(), input.View(), encrypted), + "encryption failed: NTE_INVALID_PARAMETER" + ); + + CHECK(encrypted.Size() == 0); + } +} diff --git a/cpp/tests/vereign/restapi/client_session_test.cc b/cpp/tests/vereign/restapi/client_session_test.cc index 36a824999a5c725e0d5443d91c91369fac619c51..1730d834665459f72009e2b08e1ce702fa24f7cc 100644 --- a/cpp/tests/vereign/restapi/client_session_test.cc +++ b/cpp/tests/vereign/restapi/client_session_test.cc @@ -2,8 +2,8 @@ #include <vereign/restapi/client_session.hh> #include <vereign/core/scope_guard.hh> #include <vereign/client_library/types.gen.pb.h> -#include <util/env.hh> -#include <util/protobuf.hh> +#include <testutil/env.hh> +#include <testutil/protobuf.hh> #include <catch2/catch.hpp> @@ -11,22 +11,20 @@ #include <boost/asio/ssl/context.hpp> #include <fmt/core.h> -TEST_CASE("ClientSession::Post", "[vereign/restapi/client_session][.integration]") { +TEST_CASE("restapi::ClientSession::Post", "[vereign/restapi/client_session][.integration]") { namespace asio = boost::asio; namespace beast = boost::beast; asio::io_context ioc; auto work_guard = boost::asio::make_work_guard(ioc); asio::ssl::context ctx(asio::ssl::context::tlsv12_client); - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); - auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); - auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); - - // Verify the remote server's certificate - // ctx.set_verify_mode(ssl::verify_peer); + auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); vereign::restapi::Client client{ioc, ctx, host, port}; - vereign::restapi::ClientSession client_session{client, publicKey}; + vereign::restapi::ClientSession client_session{client}; + client_session.SetPubKey(publicKey, ""); std::thread ioc_thread([&ioc]{ ioc.run(); diff --git a/cpp/tests/vereign/restapi/client_test.cc b/cpp/tests/vereign/restapi/client_test.cc index 175501b247e0b321e04bd7aa39649094212f2783..27b71a716c6d4a7c22e3e8fbef88f8ae4dd83147 100644 --- a/cpp/tests/vereign/restapi/client_test.cc +++ b/cpp/tests/vereign/restapi/client_test.cc @@ -4,7 +4,7 @@ #include <vereign/client_library/identity_types.pb.h> #include <vereign/client_library/passport_api.gen.pb.h> #include <vereign/core/scope_guard.hh> -#include <util/env.hh> +#include <testutil/env.hh> #include "boost/asio/executor_work_guard.hpp" #include "boost/beast/core/error.hpp" @@ -25,9 +25,9 @@ TEST_CASE("Client::Post", "[vereign/restapi/client][.integration]") { auto work_guard = boost::asio::make_work_guard(ioc); asio::ssl::context ctx(asio::ssl::context::tlsv12_client); - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); - auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); - auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); // Verify the remote server's certificate // ctx.set_verify_mode(ssl::verify_peer); @@ -102,9 +102,9 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") { auto work_guard = boost::asio::make_work_guard(ioc); asio::ssl::context ctx(asio::ssl::context::tlsv12_client); - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); - auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); - auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); host = "localhost"; port = "9292"; @@ -129,7 +129,7 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") { threads.emplace_back( [&client, th]() mutable { - auto req = std::make_unique<client_library::LoginForm>(); + auto req = std::make_unique<client_library::EmptyRequest>(); auto resp = std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>(); for (int i = 0; i < 1000; i++) { @@ -137,7 +137,6 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") { req->Clear(); resp->Clear(); - req->set_mode(id); auto result = client.Post( "/", diff --git a/cpp/tests/vereign/service/gen/passport_service_test.cc b/cpp/tests/vereign/service/gen/passport_service_test.cc index ec0375b559d44d09d6452604b38af0660932da7e..2953fe0e466c60bba2797104b7e556b970d21bf4 100644 --- a/cpp/tests/vereign/service/gen/passport_service_test.cc +++ b/cpp/tests/vereign/service/gen/passport_service_test.cc @@ -1,11 +1,17 @@ #include <vereign/service/gen/passport_service.hh> + +#include <vereign/fs/util.hh> #include <vereign/core/scope_guard.hh> #include <vereign/client_library/types.gen.pb.h> -#include <util/env.hh> -#include <util/protobuf.hh> +#include <vereign/client_library/identity_types.pb.h> +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/service/identity_service.hh> +#include <vereign/identity/provider.hh> -#include <catch2/catch.hpp> +#include <testutil/env.hh> +#include <testutil/protobuf.hh> +#include <catch2/catch.hpp> #include <boost/asio/io_context.hpp> #include <boost/asio/ssl/context.hpp> #include <fmt/core.h> @@ -17,17 +23,25 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration] auto work_guard = boost::asio::make_work_guard(ioc); asio::ssl::context ctx(asio::ssl::context::tlsv12_client); - auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY"); - auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); - auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); + auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); // Verify the remote server's certificate // ctx.set_verify_mode(ssl::verify_peer); vereign::restapi::Client client{ioc, ctx, host, port}; - vereign::restapi::ClientSession client_session{client, publicKey}; + vereign::restapi::ClientSession client_session{client}; vereign::service::gen::PassportService service{client_session}; + auto storage_path = vereign::fs::TempFilePath("test_db_"); + vereign::fs::RemoveFileGuard rm{storage_path}; + + auto kvstorage = vereign::kvstore::SqliteStorage(storage_path); + vereign::kvstore::CryptoStorage storage{kvstorage}; + vereign::identity::Provider provider{client_session, storage}; + vereign::service::IdentityService idenity_service{client_session, provider}; + std::thread ioc_thread([&ioc]{ ioc.run(); }); @@ -39,6 +53,15 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration] } ); + // login + auto req = std::make_unique<vereign::client_library::LoginWithExistingPubKeyForm>(); + auto resp = std::make_unique<vereign::client_library::EmptyResponse>(); + req->set_pubkey(publicKey); + idenity_service.LoginWithExistingPubKey(req.get(), resp.get()); + CHECK(resp->error() == ""); + CHECK(resp->status() == "OK"); + REQUIRE(resp->code() == "200"); + for (int i = 0; i < 2; i++) { auto req = std::make_unique<vereign::client_library::ListPassportsForm>(); auto resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>(); @@ -50,9 +73,9 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration] // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl; auto& list = result.Response; - REQUIRE(list->error() == ""); + CHECK(list->error() == ""); CHECK(list->status() == "OK"); - CHECK(list->code() == "200"); + REQUIRE(list->code() == "200"); CHECK(list->data().size() > 0); for (auto& passport : list->data()) { CHECK(passport.uuid().size() == 36); diff --git a/cpp/tests/vereign/service/identity_service_test.cc b/cpp/tests/vereign/service/identity_service_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..4f48b025e33140f7a77bd8897ed393f7476c5ed3 --- /dev/null +++ b/cpp/tests/vereign/service/identity_service_test.cc @@ -0,0 +1,129 @@ +#include <vereign/service/identity_service.hh> + +#include <vereign/client_library/common_types.pb.h> +#include <vereign/client_library/identity_types.pb.h> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/service/gen/passport_service.hh> +#include <vereign/restapi/client_session.hh> +#include <vereign/identity/provider.hh> +#include <vereign/test/device.hh> +#include <vereign/test/service_context.hh> +#include <vereign/fs/util.hh> + +#include <testutil/env.hh> +#include <testutil/protobuf.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("service::IdentityService::LoginWithNewDevice", "[vereign/service][.integration]") { + auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + // the old device is used later for new device confirmation and authorization + auto old_storage_path = fs::TempFilePath("test_db_"); + auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = test::ServiceContext{host, port, old_storage_path}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto storage_path = fs::TempFilePath("test_db_"); + auto rm_storage_path = fs::RemoveFileGuard{storage_path}; + auto service_context = test::ServiceContext{host, port, storage_path}; + auto service = service::IdentityService{ + service_context.ClientSession(), + service_context.IdentityProvider() + }; + + // register new device + auto req = std::make_unique<vereign::client_library::LoginFormNewDevice>(); + auto resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>(); + req->set_pin("foo"); + + service.LoginWithNewDevice(req.get(), resp.get()); + + CHECK(resp->error() == ""); + CHECK(resp->status() == "OK"); + REQUIRE(resp->code() == "200"); + REQUIRE(resp->has_data() == true); + + // confirm and authorize the new device using an old device + old_device.ConfirmNewDevice(resp->data().qrcode(), resp->data().actionid()); + old_device.AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash()); + + // list passports with the new device + auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>(); + auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>(); + + auto passport_service = service::gen::PassportService{service_context.ClientSession()}; + auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get()); + + auto result = list_result.get(); + + // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl; + + auto& list = result.Response; + CHECK(list->error() == ""); + CHECK(list->status() == "OK"); + REQUIRE(list->code() == "200"); + CHECK(list->data().size() > 0); + for (const auto& passport : list->data()) { + CHECK(passport.uuid().size() == 36); + } +} + +TEST_CASE("service::IdentityService::LoginWithPreviouslyAddedDevice", "[vereign/service][.integration]") { + auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY"); + auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST"); + auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https"); + + auto storage_path = fs::TempFilePath("test_db_"); + auto rm_storage_path = fs::RemoveFileGuard{storage_path}; + + // prepare new device + auto old_storage_path = fs::TempFilePath("test_db_"); + auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = test::ServiceContext{host, port, old_storage_path}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + auto old_service_context = test::ServiceContext{host, port, storage_path}; + old_device.CreateNewDevice(old_service_context, "pin"); + + auto service_context = test::ServiceContext{host, port, storage_path}; + auto service = service::IdentityService{ + service_context.ClientSession(), + service_context.IdentityProvider() + }; + + auto req = std::make_unique<vereign::client_library::LoginFormPreviousAddedDevice>(); + req->set_pin("pin"); + auto resp = std::make_unique<vereign::client_library::EmptyResponse>(); + + service.LoginWithPreviouslyAddedDevice(req.get(), resp.get()); + + CHECK(resp->error() == ""); + CHECK(resp->status() == "OK"); + REQUIRE(resp->code() == "200"); + + // list passports with the logged device + auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>(); + auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>(); + + auto passport_service = service::gen::PassportService{service_context.ClientSession()}; + auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get()); + + auto result = list_result.get(); + + // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl; + + auto& list = result.Response; + CHECK(list->error() == ""); + CHECK(list->status() == "OK"); + REQUIRE(list->code() == "200"); + CHECK(list->data().size() > 0); + for (const auto& passport : list->data()) { + CHECK(passport.uuid().size() == 36); + } +} diff --git a/cpp/tests/vereign/test/device.cc b/cpp/tests/vereign/test/device.cc new file mode 100644 index 0000000000000000000000000000000000000000..bc47ef4c530a24b7f77e06fd1e9b305fe9b8b2a7 --- /dev/null +++ b/cpp/tests/vereign/test/device.cc @@ -0,0 +1,143 @@ +#include <vereign/test/device.hh> + +#include <vereign/client_library/common_types.pb.h> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/fs/util.hh> +#include <vereign/fs/path.hh> +#include <vereign/test/service_context.hh> +#include <vereign/restapi/client_session.hh> +#include <vereign/service/identity_service.hh> +#include <vereign/identity/provider.hh> +#include <vereign/client_library/identity_types.pb.h> + +#include <boost/filesystem/operations.hpp> + +namespace vereign::test { + +Device::Device(ServiceContext& service_context) + : service_context_{service_context}, + identity_service_{std::make_unique<service::IdentityService>( + service_context_.ClientSession(), + service_context_.IdentityProvider() + )} +{ +} + +Device::~Device() = default; + +void Device::Login(const std::string& public_key) { + auto req = std::make_unique<vereign::client_library::LoginWithExistingPubKeyForm>(); + auto resp = std::make_unique<vereign::client_library::EmptyResponse>(); + req->set_pubkey(public_key); + + identity_service_->LoginWithExistingPubKey(req.get(), resp.get()); + + if (resp->code() != "200") { + throw std::runtime_error("login to test old device failed with: " + resp->error()); + } +} + +void Device::ConfirmNewDevice(const std::string& qr_code, const std::string& action_id) { + auto req = std::make_unique<vereign::client_library::ConfirmNewDeviceForm>(); + req->set_code(qr_code); + req->set_actionid(action_id); + auto resp = std::make_unique<vereign::client_library::EmptyResponse>(); + + auto result = identity_service_->ConfirmNewDevice(req.get(), resp.get()); + result.wait(); + + if (resp->code() != "200") { + throw std::runtime_error("confirm new device failed with: " + resp->error()); + } +} + +void Device::AuthorizeDevice(const std::string& device_hash) { + auto req = std::make_unique<client_library::EmptyRequest>(); + auto resp = std::make_unique<client_library::ListDevicesHandlerFormResponse>(); + + auto result = identity_service_->ListDevices(req.get(), resp.get()); + result.wait(); + if (resp->code() != "200") { + throw std::runtime_error("authorize device failed with: " + resp->error()); + } + + std::string device_id; + for (auto& device : resp->data()) { + if (device.fingerprint() == device_hash) { + device_id = device.deviceid(); + break; + } + } + + if (device_id.empty()) { + throw std::runtime_error("authorize device failed with: device not found"); + } + + auto auth_req = std::make_unique<client_library::AuthorizeDeviceForm>(); + auth_req->set_deviceid(device_id); + auto auth_resp = std::make_unique<client_library::EmptyResponse>(); + + auto auth_result = identity_service_->AuthorizeDevice(auth_req.get(), auth_resp.get()); + auth_result.wait(); + if (auth_resp->code() != "200") { + throw std::runtime_error("authorize device failed with: " + auth_resp->error()); + } +} + +void Device::CreateNewDevice(ServiceContext& service_context, const std::string& pin) { + auto service = service::IdentityService{ + service_context.ClientSession(), + service_context.IdentityProvider() + }; + + // register new device + auto req = std::make_unique<vereign::client_library::LoginFormNewDevice>(); + auto resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>(); + req->set_pin(pin); + + service.LoginWithNewDevice(req.get(), resp.get()); + if (resp->code() != "200") { + throw std::runtime_error("creating new device failed with: " + resp->error()); + } + + // confirm and authorize the new device using an old device + ConfirmNewDevice(resp->data().qrcode(), resp->data().actionid()); + AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash()); +} + +void PrepareNewDevice( + const std::string& host, + const std::string& port, + const std::string& public_key, + const std::string& pin, + const std::string& storage_path +) { + // the old device is used later for new device confirmation and authorization + auto old_storage_path = fs::TempFilePath("test_db_"); + auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path}; + auto old_device_ctx = test::ServiceContext{host, port, old_storage_path}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto service_context = test::ServiceContext{host, port, fs::path::Join(storage_path, "db")}; + auto identity_service = service::IdentityService{ + service_context.ClientSession(), + service_context.IdentityProvider() + }; + + // register new device + auto register_req = std::make_unique<vereign::client_library::LoginFormNewDevice>(); + auto register_resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>(); + register_req->set_pin(pin); + + identity_service.LoginWithNewDevice(register_req.get(), register_resp.get()); + if (register_resp->code() != "200") { + throw std::runtime_error("register new device failed with: " + register_resp->error()); + } + + // confirm and authorize the new device using an old device + old_device.ConfirmNewDevice(register_resp->data().qrcode(), register_resp->data().actionid()); + old_device.AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash()); +} + +} // namespace vereign::test diff --git a/cpp/tests/vereign/test/device.hh b/cpp/tests/vereign/test/device.hh new file mode 100644 index 0000000000000000000000000000000000000000..9ad85bd8a42142f35f7c9a589244c2a8b25fd294 --- /dev/null +++ b/cpp/tests/vereign/test/device.hh @@ -0,0 +1,43 @@ +#ifndef __TESTS_VEREIGN_TEST_DEVICE_HH +#define __TESTS_VEREIGN_TEST_DEVICE_HH + +#include <memory> +#include <string> + +namespace vereign::service { +class IdentityService; +} + +namespace vereign::test { + +class ServiceContext; + +class Device { +public: + Device(ServiceContext& service_context); + ~Device(); + + Device(const Device&) = delete; + auto operator=(const Device&) -> Device& = delete; + + void Login(const std::string& public_key); + void ConfirmNewDevice(const std::string& qr_code, const std::string& action_id); + void AuthorizeDevice(const std::string& device_hash); + void CreateNewDevice(ServiceContext& service_context, const std::string& pin); + +private: + ServiceContext& service_context_; + std::unique_ptr<service::IdentityService> identity_service_; +}; + +void PrepareNewDevice( + const std::string& host, + const std::string& port, + const std::string& public_key, + const std::string& pin, + const std::string& storage_path +); + +} // namespace vereign::test + +#endif // __TESTS_VEREIGN_TEST_DEVICE_HH diff --git a/cpp/tests/vereign/test/service_context.cc b/cpp/tests/vereign/test/service_context.cc new file mode 100644 index 0000000000000000000000000000000000000000..934ff9084eb440a3c536d4e0f58baaf9c3518b0a --- /dev/null +++ b/cpp/tests/vereign/test/service_context.cc @@ -0,0 +1,54 @@ +#include <vereign/test/service_context.hh> + +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/kvstore/crypto_storage.hh> +#include <vereign/restapi/client.hh> +#include <vereign/restapi/client_session.hh> +#include <vereign/identity/provider.hh> + +#include <boost/filesystem/operations.hpp> + +namespace vereign::test { + +ServiceContext::ServiceContext( + const std::string& vereign_host, + const std::string& vereign_port, + std::string storage_path +) : work_guard_{boost::asio::make_work_guard(ioc_)}, + ssl_context_{boost::asio::ssl::context::tlsv12_client}, + client_{std::make_unique<restapi::Client>( + ioc_, ssl_context_, vereign_host, vereign_port + )}, + client_session_{std::make_unique<restapi::ClientSession>(*client_)}, + storage_path_{std::move(storage_path)}, + sqlite_storage_{std::make_unique<kvstore::SqliteStorage>(storage_path_)}, + storage_{std::make_unique<kvstore::CryptoStorage>(*sqlite_storage_, true)}, + identity_provider_{std::make_unique<identity::Provider>(*client_session_, *storage_)} +{ + service_thread_ = std::thread([this]() { + ioc_.run(); + }); +} + +auto ServiceContext::IdentityProvider() -> identity::Provider& { + return *identity_provider_; +} + +auto ServiceContext::ClientSession() -> restapi::ClientSession& { + return *client_session_; +} + +void ServiceContext::Shutdown() { + client_session_->Close(); + + work_guard_.reset(); + if (service_thread_.joinable()) { + service_thread_.join(); + } +} + +ServiceContext::~ServiceContext() { + Shutdown(); +} + +} diff --git a/cpp/tests/vereign/test/service_context.hh b/cpp/tests/vereign/test/service_context.hh new file mode 100644 index 0000000000000000000000000000000000000000..7666e29eba7aa135f3a048ef13d1162fd346ce4d --- /dev/null +++ b/cpp/tests/vereign/test/service_context.hh @@ -0,0 +1,63 @@ +#ifndef __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH +#define __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH + +#include <boost/asio/io_context.hpp> +#include <boost/asio/ssl/context.hpp> +#include <boost/asio/executor_work_guard.hpp> +#include <boost/filesystem/path.hpp> + +#include <thread> + +namespace vereign { + +namespace restapi { +class Client; +class ClientSession; +} + +namespace kvstore { +class SqliteStorage; +class CryptoStorage; +} + +namespace identity { +class Provider; +} + +} + +namespace vereign::test { + +class ServiceContext { +public: + ServiceContext( + const std::string& vereign_host, + const std::string& vereign_port, + std::string storage_path + ); + ~ServiceContext(); + + ServiceContext(const ServiceContext&) = delete; + auto operator=(const ServiceContext&) -> ServiceContext& = delete; + + auto IdentityProvider() -> identity::Provider&; + auto ClientSession() -> restapi::ClientSession&; + + void Shutdown(); + +private: + boost::asio::io_context ioc_; + boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_guard_; + boost::asio::ssl::context ssl_context_; + std::unique_ptr<restapi::Client> client_; + std::unique_ptr<restapi::ClientSession> client_session_; + std::string storage_path_; + std::unique_ptr<kvstore::SqliteStorage> sqlite_storage_; + std::unique_ptr<kvstore::CryptoStorage> storage_; + std::unique_ptr<identity::Provider> identity_provider_; + std::thread service_thread_; +}; + +} // namespace vereign::test + +#endif // __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH diff --git a/cpp/vendor/CMakeLists.txt b/cpp/vendor/CMakeLists.txt index 7d4cb218c8cc4ae5f18c2a2d6a792071df35ab0d..c0f4ec13688b907308716fb24ca8e50b1f074f5a 100644 --- a/cpp/vendor/CMakeLists.txt +++ b/cpp/vendor/CMakeLists.txt @@ -53,6 +53,7 @@ include(boring_ssl.cmake) include(boost.cmake) include(grpc.cmake) include(nlohmann.cmake) +include(sqlite3.cmake) string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type) message(STATUS "Summary: diff --git a/cpp/vendor/boost.cmake b/cpp/vendor/boost.cmake index 430902d781c72e6b107b4709b13a84f7160a1220..4d49081cc71229db423067033cdcd9551195644a 100644 --- a/cpp/vendor/boost.cmake +++ b/cpp/vendor/boost.cmake @@ -1,6 +1,6 @@ include(ExternalProject) -set(_boost_libs regex system thread date_time) +set(_boost_libs regex system thread date_time filesystem) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") list(TRANSFORM _boost_libs PREPEND --with-) diff --git a/cpp/vendor/cmake/sqlite3/CMakeLists.txt b/cpp/vendor/cmake/sqlite3/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c203d8643bb166579d9785a215cedc7ddfa5b104 --- /dev/null +++ b/cpp/vendor/cmake/sqlite3/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required (VERSION 3.16.5) + +project (sqlite3) + +add_library(sqlite3 STATIC + sqlite3.c +) + +install(TARGETS sqlite3 DESTINATION lib) +install(FILES sqlite3.h sqlite3ext.h DESTINATION include) diff --git a/cpp/vendor/sqlite3.cmake b/cpp/vendor/sqlite3.cmake new file mode 100644 index 0000000000000000000000000000000000000000..6c39ac93514e8c9795628a98d9007df8181fddf0 --- /dev/null +++ b/cpp/vendor/sqlite3.cmake @@ -0,0 +1,29 @@ + +ExternalProject_Add(sqlite3lib + PREFIX sqlite3 + URL https://www.sqlite.org/2020/sqlite-amalgamation-3320300.zip + URL_HASH SHA1=0c805bea134712a903290a26b2a61c3a8a3bd8cc + INSTALL_DIR ${VENDOR_INSTALL_DIR}/sqlite3 + + USES_TERMINAL_DOWNLOAD ON + USES_TERMINAL_UPDATE ON + USES_TERMINAL_CONFIGURE ON + USES_TERMINAL_BUILD ON + USES_TERMINAL_INSTALL ON + + UPDATE_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/cmake/sqlite3/CMakeLists.txt <SOURCE_DIR>/CMakeLists.txt + + CMAKE_CACHE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DCMAKE_MSVC_RUNTIME_LIBRARY:STRING=${CMAKE_MSVC_RUNTIME_LIBRARY} + -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} + -DCMAKE_C_FLAGS:STRING=${CMAKE_C_FLAGS} + -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} + -DCMAKE_C_FLAGS_DEBUG:STRING=${CMAKE_C_FLAGS_DEBUG} + -DCMAKE_CXX_FLAGS_DEBUG:STRING=${CMAKE_CXX_FLAGS_DEBUG} + -DCMAKE_C_FLAGS_RELEASE:STRING=${CMAKE_C_FLAGS_RELEASE} + -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELEASE} + -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON + -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> +) diff --git a/javascript/src/constants/secrets.js b/javascript/src/constants/secrets.js new file mode 100644 index 0000000000000000000000000000000000000000..41b9ef5ee76b93cffcc5141a9534e013ff065dbb --- /dev/null +++ b/javascript/src/constants/secrets.js @@ -0,0 +1,2 @@ +export const RECOMMENDED_TRUSTEES = 3; +export const THRESHOLD = 2; diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js index ce44402be8618dc5cdfa925a0d991ab0b2f9ee37..ecfa3dc105d501b7d5a6ba572142a667e4bb0544 100644 --- a/javascript/src/iframe/viamapi-iframe.js +++ b/javascript/src/iframe/viamapi-iframe.js @@ -41,6 +41,12 @@ import { STATUS_USER_BLOCKED } from "../constants/statuses"; import generateQrCode from "../utilities/generateQrCode"; +import { + generateRecoveryKey, + getRecoveryKeyShares, + checkRecoveryKeyCombine, + encryptShare +} from "../utilities/secrets"; const penpalMethods = require("../../temp/penpal-methods").default; const WopiAPI = require("./wopiapi-iframe"); @@ -271,14 +277,14 @@ window.lastTimeGetProfile = 0; let iframeParent = null; const handleIdentityLogin = (identity, uuid, token) => { - const { loadedIdentities, viamApi } = window; + const { viamApi, loadedIdentities } = window; const { publicKey } = identity.authentication; - viamApi.setSessionData(uuid, token); localStorage.setItem("uuid", uuid); localStorage.setItem("token", token); localStorage.setItem("authenticatedIdentity", publicKey); - window.currentlyAuthenticatedIdentity = loadedIdentities[publicKey]; + window.currentlyAuthenticatedIdentity = + loadedIdentities[publicKey] || identity; window.lastTimeGetProfile = 0; setKeyForUUID(uuid, publicKey); }; @@ -348,6 +354,7 @@ async function executeRestfulFunction(type, that, fn, config, ...args) { null, "previousaddeddevice" ); + if (loginResponse.data.code !== "200") { return loginResponse.data; } @@ -434,7 +441,7 @@ function getCertificateForPassport(passportUUID, internal) { const passportIdentity = window.currentlyAuthenticatedIdentity; const passport = passportIdentity.getPassport(passportUUID); if (passport === undefined || passport === null) { - createPassportCertificate(passportUUID).then(function(keys) { + createPassportCertificate(passportUUID).then(function (keys) { const cryptoData = new CryptoData(); cryptoData.setPublicKey(keys["publicKeyPEM"]); cryptoData.setPrivateKey(keys["privateKeyPEM"]); @@ -552,7 +559,7 @@ const connection = Penpal.connectToParent({ ...penpalMethods, createIdentity(pinCode) { return new Penpal.Promise(result => { - createPassportCertificate(makeid()).then(function(keys) { + createPassportCertificate(makeid()).then(function (keys) { const newIdentity = new Identity(); const cryptoData = new CryptoData(); cryptoData.setPublicKey(keys["publicKeyPEM"]); @@ -562,6 +569,10 @@ const connection = Penpal.connectToParent({ newIdentity.setPinCode(pinCode); window.currentlyLoadedIdentity = newIdentity; + localStorage.setItem( + "currentlyLoadedIdentity", + JSON.stringify(newIdentity) + ); const { publicKey, x509Certificate } = newIdentity.authentication; window.loadedIdentities[publicKey] = newIdentity; @@ -733,10 +744,7 @@ const connection = Penpal.connectToParent({ }); }); }, - finalizeEmployeeRegistration: async ( - identity, - identifier - ) => { + finalizeEmployeeRegistration: async (identity, identifier) => { viamApi.setIdentity(identity.authentication.publicKey); return executeRestfulFunction( "public", @@ -821,16 +829,9 @@ const connection = Penpal.connectToParent({ const responseToClient = Object.assign({}, identityLoginResponse); if (code === "200") { - if ( - mode === LOGIN_MODES.SMS || - mode === LOGIN_MODES.PREVIOUSLY_ADDED_DEVICE - ) { + if (mode === LOGIN_MODES.PREVIOUSLY_ADDED_DEVICE) { handleIdentityLogin(loginIdentity, data.Uuid, data.Session); await getProfileData(loginIdentity); - - if (mode === LOGIN_MODES.SMS) { - await setIdentityInLocalStorage(loginIdentity); - } } else if (mode === LOGIN_MODES.NEW_DEVICE) { const dataUrl = await generateQrCode( `${data.ActionID},${data.QrCode}` @@ -970,7 +971,7 @@ const connection = Penpal.connectToParent({ }; } }, - identityRestoreAccess(restoreAccessIdentity, identificator) { + identityRestoreAccess(restoreAccessIdentity, identificator, restoreType) { return new Penpal.Promise(result => { viamApi.setSessionData("", ""); viamApi.setIdentity(restoreAccessIdentity.authentication.publicKey); @@ -980,12 +981,164 @@ const connection = Penpal.connectToParent({ viamApi, viamApi.identityRestoreAccess, null, - identificator + identificator, + restoreType ).then(executeResult => { result(executeResult); }); }); }, + identityInitiateSocialRecovery: async accessToken => { + const response = await executeRestfulFunction( + "public", + viamApi, + viamApi.identityInitiateSocialRecovery, + null, + accessToken + ); + + return response; + }, + contactsCheckAccountRecoveryStatus: async () => { + const currentlyLoadedIdentity = localStorage.getItem( + "currentlyLoadedIdentity" + ); + const identity = new Identity(currentlyLoadedIdentity); + window.currentlyLoadedIdentity = identity; + const { publicKey } = identity.authentication; + window.loadedIdentities[publicKey] = identity; + window.viamAnonymousApi.setIdentity(publicKey); + window.viamApi.setSessionData("", ""); + window.viamApi.setIdentity(publicKey); + + const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)); + + async function checkAccountRecoveryStatus() { + const response = await executeRestfulFunction( + "public", + viamApi, + viamApi.contactsCheckAccountRecoveryStatus, + null + ); + + if (response.data === 0) { + await timeout(1000); + await checkAccountRecoveryStatus(); + return; + } + + const deviceHash = await createDeviceHash(publicKey); + window.viamApi.setDeviceHash(deviceHash); + + const identityLoginResponse = await executeRestfulFunction( + "public", + window.viamApi, + window.viamApi.identityLogin, + null, + "previousaddeddevice" + ); + + const { code, data } = identityLoginResponse; + + if (code === "200") { + await setIdentityInLocalStorage(identity); + handleIdentityLogin(identity, data.Uuid, data.Session); + await getProfileData(identity); + localStorage.removeItem("currentlyLoadedIdentity"); + } + } + + await checkAccountRecoveryStatus(); + }, + contactsGetTrusteeContactsPublicKeys: async () => { + try { + const response = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.contactsGetTrusteeContactsPublicKeys, + null + ); + + if (response.code !== "200") { + return response; + } + + const responseData = response.data; + const trusteesDevices = Object.values(responseData); + + /** Check if there are new trustees without added secret part */ + const hasNewTrustees = trusteesDevices.some(device => { + const deviceData = Object.values(device); + return deviceData.some(data => data.hasShamir === "0"); + }); + + if (!hasNewTrustees) { + return response; + } + + // Generate and split recovery key + const trusteesUuids = Object.keys(responseData); + const trusteesToDevices = Object.entries(responseData); + const sharesNumber = trusteesUuids.length; + const recoveryKey = generateRecoveryKey(); + let recoveryKeyShares = [recoveryKey]; + // Split the secret when sharesNumber is more than 1 because VereignPublicKey is always returned + if (sharesNumber > 1) { + recoveryKeyShares = getRecoveryKeyShares(recoveryKey, sharesNumber); + const sanityCheckResponse = checkRecoveryKeyCombine( + recoveryKey, + recoveryKeyShares + ); + + if (sanityCheckResponse.code !== "200") { + return sanityCheckResponse; + } + } + + // Encrypt each share with every publicKey of each contact device + const shamirPartsList = await Promise.all( + trusteesToDevices.map(async ([contactUuid, device], index) => { + const deviceIdsToPublicKeys = Object.entries(device); + // Encrypt secret shares in parallel + const deviceIdsToEncryptedPartsList = await Promise.all( + deviceIdsToPublicKeys.map(async ([deviceId, { content }]) => { + const encryptedShare = await encryptShare( + recoveryKeyShares[index], + content + ); + + return [deviceId, encryptedShare]; + }) + ); + // Turn deviceIdsToEncryptedPartsList array to object + const deviceIdsToEncryptedParts = Object.fromEntries( + deviceIdsToEncryptedPartsList + ); + + return [contactUuid, deviceIdsToEncryptedParts]; + }) + ); + // Turn shamirPartsList array to object + const shamirParts = Object.fromEntries(shamirPartsList); + + // Save Shamir parts to database + const saveShamirPartsResponse = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.contactsSaveShamirParts, + null, + shamirParts + ); + + if (saveShamirPartsResponse.code !== "200") { + return saveShamirPartsResponse; + } + + return response; + } catch (error) { + return encodeResponse("400", "", error.message); + } + }, parseSMIME, getCurrentlyLoggedInUUID() { return new Penpal.Promise(result => { @@ -1083,7 +1236,7 @@ const connection = Penpal.connectToParent({ emailArg, passportPrivateKey, passportCertificate - ).then(function(keys) { + ).then(function (keys) { const publicKeyOneTime = keys["publicKeyPEM"]; const privateKeyOneTime = keys["privateKeyPEM"]; const certificateOneTime = keys["certificatePEM"]; @@ -1426,10 +1579,7 @@ const connection = Penpal.connectToParent({ vCardImageClaimValue = vCardClaimResponse.data; } - if ( - vCardImageClaimValue && - "state" in vCardImageClaimValue - ) { + if (vCardImageClaimValue && "state" in vCardImageClaimValue) { return encodeResponse("200", vCardImageClaimValue.state, "OK"); } @@ -1489,14 +1639,13 @@ message SignatureData { return encodeResponse("400", "", "Identity not authenticated"); } - // Get vCard and QR Code Coordinates let vCardImageData; let vCardImageClaimValue; let qrCodeImageData; - let qrCodeCoordinates = {fromL: -1, fromR: -1, toL: -1, toR: -1}; + let qrCodeCoordinates = { fromL: -1, fromR: -1, toL: -1, toR: -1 }; if (signatureData) { const vCardImageClaimName = "vCardImage"; @@ -1682,7 +1831,7 @@ message SignatureData { let vCardImageClaimValue; let qrCodeImageData; - let qrCodeCoordinates = {fromL: -1, fromR: -1, toL: -1, toR: -1}; + let qrCodeCoordinates = { fromL: -1, fromR: -1, toL: -1, toR: -1 }; const vCardImageClaimName = "vCardImage"; const defaultTagName = "notag"; @@ -1818,7 +1967,6 @@ message SignatureData { passportChain.reverse(); - const signVCardResponse = await executeRestfulFunction( "private", window.viamApi, @@ -1936,7 +2084,7 @@ message SignatureData { return encodeResponse("200", response.data, "Document created"); }, - getVcardWithQrCode: async (passportUUID, QRCodeContent = null) =>{ + getVcardWithQrCode: async (passportUUID, QRCodeContent = null) => { //TODO: IMPLEMENT QR CODE backend method needed const authenticationPublicKey = localStorage.getItem( "authenticatedIdentity" @@ -2004,7 +2152,7 @@ message SignatureData { ); } } - return encodeResponse("200",vCardImageData, 'vCard got'); + return encodeResponse("200", vCardImageData, "vCard got"); }, documentPutDocument: async ( passportUUID, @@ -2422,7 +2570,7 @@ connection.promise.then(parent => { let previousLocalStorageToken; let previousLocalStorageIdentity; - setInterval(async function() { + setInterval(async function () { if (window.currentlyAuthenticatedIdentity) { const { authentication } = window.currentlyAuthenticatedIdentity; const pinCode = getPincode(authentication.publicKey); @@ -2467,6 +2615,8 @@ connection.promise.then(parent => { identityAuthenticatedEvent = false; window.currentlyLoadedIdentity = null; } + + localStorage.removeItem("currentlyLoadedIdentity"); } if (window.currentlyLoadedIdentity) { diff --git a/javascript/src/lib/secrets.js b/javascript/src/lib/secrets.js new file mode 100644 index 0000000000000000000000000000000000000000..9da60b9782fa0494d2827ae9c0b2193994a7d70d --- /dev/null +++ b/javascript/src/lib/secrets.js @@ -0,0 +1,1043 @@ +// @preserve author Alexander Stetsyuk +// @preserve author Glenn Rempe <glenn@rempe.us> +// @license MIT + +/*jslint passfail: false, bitwise: true, nomen: true, plusplus: true, todo: false, maxerr: 1000 */ +/*global define, require, module, exports, window, Uint32Array */ + +// eslint : http://eslint.org/docs/configuring/ +/*eslint-env node, browser, jasmine */ +/*eslint no-underscore-dangle:0 */ + +// UMD (Universal Module Definition) +// Uses Node, AMD or browser globals to create a module. This module creates +// a global even when AMD is used. This is useful if you have some scripts +// that are loaded by an AMD loader, but they still want access to globals. +// See : https://github.com/umdjs/umd +// See : https://github.com/umdjs/umd/blob/master/returnExportsGlobal.js +// +(function(root, factory) { + "use strict"; + + if (typeof define === "function" && define.amd) { + // AMD. Register as an anonymous module. + define([], function() { + /*eslint-disable no-return-assign */ + return (root.secrets = factory()); + /*eslint-enable no-return-assign */ + }); + } else if (typeof exports === "object") { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("crypto")); + } else { + // Browser globals (root is window) + root.secrets = factory(root.crypto); + } +})(this, function() { + "use strict"; + let crypto; + try { + crypto = require("crypto"); + } catch (err) { + console.warn("crypto support is disabled!"); + } + + if (!crypto) { + crypto = window.crypto; + } + + var defaults, config, preGenPadding, runCSPRNGTest, CSPRNGTypes; + + function reset() { + defaults = { + bits: 8, // default number of bits + radix: 16, // work with HEX by default + minBits: 3, + maxBits: 20, // this permits 1,048,575 shares, though going this high is NOT recommended in JS! + bytesPerChar: 2, + maxBytesPerChar: 6, // Math.pow(256,7) > Math.pow(2,53) + + // Primitive polynomials (in decimal form) for Galois Fields GF(2^n), for 2 <= n <= 30 + // The index of each term in the array corresponds to the n for that polynomial + // i.e. to get the polynomial for n=16, use primitivePolynomials[16] + primitivePolynomials: [ + null, + null, + 1, + 3, + 3, + 5, + 3, + 3, + 29, + 17, + 9, + 5, + 83, + 27, + 43, + 3, + 45, + 9, + 39, + 39, + 9, + 5, + 3, + 33, + 27, + 9, + 71, + 39, + 9, + 5, + 83 + ] + }; + config = {}; + preGenPadding = new Array(1024).join("0"); // Pre-generate a string of 1024 0's for use by padLeft(). + runCSPRNGTest = true; + + // WARNING : Never use 'testRandom' except for testing. + CSPRNGTypes = [ + "nodeCryptoRandomBytes", + "browserCryptoGetRandomValues", + "testRandom" + ]; + } + + function isSetRNG() { + if (config && config.rng && typeof config.rng === "function") { + return true; + } + + return false; + } + + // Pads a string `str` with zeros on the left so that its length is a multiple of `bits` + function padLeft(str, multipleOfBits) { + var missing; + + if (multipleOfBits === 0 || multipleOfBits === 1) { + return str; + } + + if (multipleOfBits && multipleOfBits > 1024) { + throw new Error("Padding must be multiples of no larger than 1024 bits."); + } + + multipleOfBits = multipleOfBits || config.bits; + + if (str) { + missing = str.length % multipleOfBits; + } + + if (missing) { + return (preGenPadding + str).slice( + -(multipleOfBits - missing + str.length) + ); + } + + return str; + } + + function hex2bin(str) { + var bin = "", + num, + i; + + for (i = str.length - 1; i >= 0; i--) { + num = parseInt(str[i], 16); + + if (isNaN(num)) { + throw new Error("Invalid hex character."); + } + + bin = padLeft(num.toString(2), 4) + bin; + } + return bin; + } + + function bin2hex(str) { + var hex = "", + num, + i; + + str = padLeft(str, 4); + + for (i = str.length; i >= 4; i -= 4) { + num = parseInt(str.slice(i - 4, i), 2); + if (isNaN(num)) { + throw new Error("Invalid binary character."); + } + hex = num.toString(16) + hex; + } + + return hex; + } + + // Browser supports crypto.getRandomValues() + function hasCryptoGetRandomValues() { + if ( + crypto && + typeof crypto === "object" && + (typeof crypto.getRandomValues === "function" || + typeof crypto.getRandomValues === "object") && + (typeof Uint32Array === "function" || typeof Uint32Array === "object") + ) { + return true; + } + + return false; + } + + // Node.js support for crypto.randomBytes() + function hasCryptoRandomBytes() { + if ( + typeof crypto === "object" && + typeof crypto.randomBytes === "function" + ) { + return true; + } + + return false; + } + + // Returns a pseudo-random number generator of the form function(bits){} + // which should output a random string of 1's and 0's of length `bits`. + // `type` (Optional) : A string representing the CSPRNG that you want to + // force to be loaded, overriding feature detection. Can be one of: + // "nodeCryptoRandomBytes" + // "browserCryptoGetRandomValues" + // + function getRNG(type) { + function construct(bits, arr, radix, size) { + var i = 0, + len, + str = "", + parsedInt; + + if (arr) { + len = arr.length - 1; + } + + while (i < len || str.length < bits) { + // convert any negative nums to positive with Math.abs() + parsedInt = Math.abs(parseInt(arr[i], radix)); + str = str + padLeft(parsedInt.toString(2), size); + i++; + } + + str = str.substr(-bits); + + // return null so this result can be re-processed if the result is all 0's. + if ((str.match(/0/g) || []).length === str.length) { + return null; + } + + return str; + } + + // Node.js : crypto.randomBytes() + // Note : Node.js and crypto.randomBytes() uses the OpenSSL RAND_bytes() function for its CSPRNG. + // Node.js will need to have been compiled with OpenSSL for this to work. + // See : https://github.com/joyent/node/blob/d8baf8a2a4481940bfed0196308ae6189ca18eee/src/node_crypto.cc#L4696 + // See : https://www.openssl.org/docs/crypto/rand.html + function nodeCryptoRandomBytes(bits) { + var buf, + bytes, + radix, + size, + str = null; + + radix = 16; + size = 4; + bytes = Math.ceil(bits / 8); + + while (str === null) { + buf = crypto.randomBytes(bytes); + str = construct(bits, buf.toString("hex"), radix, size); + } + + return str; + } + + // Browser : crypto.getRandomValues() + // See : https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-Crypto + // See : https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues + // Supported Browsers : http://caniuse.com/#search=crypto.getRandomValues + function browserCryptoGetRandomValues(bits) { + var elems, + radix, + size, + str = null; + + radix = 10; + size = 32; + elems = Math.ceil(bits / 32); + while (str === null) { + str = construct( + bits, + crypto.getRandomValues(new Uint32Array(elems)), + radix, + size + ); + } + + return str; + } + + // ///////////////////////////////////////////////////////////// + // WARNING : DO NOT USE. For testing purposes only. + // ///////////////////////////////////////////////////////////// + // This function will return repeatable non-random test bits. Can be used + // for testing only. Node.js does not return proper random bytes + // when run within a PhantomJS container. + function testRandom(bits) { + var arr, + elems, + int, + radix, + size, + str = null; + + radix = 10; + size = 32; + elems = Math.ceil(bits / 32); + int = 123456789; + arr = new Uint32Array(elems); + + // Fill every element of the Uint32Array with the same int. + for (var i = 0; i < arr.length; i++) { + arr[i] = int; + } + + while (str === null) { + str = construct(bits, arr, radix, size); + } + + return str; + } + + // Return a random generator function for browsers that support + // crypto.getRandomValues() or Node.js compiled with OpenSSL support. + // WARNING : NEVER use testRandom outside of a testing context. Totally non-random! + if (type && type === "testRandom") { + config.typeCSPRNG = type; + return testRandom; + } else if (type && type === "nodeCryptoRandomBytes") { + config.typeCSPRNG = type; + return nodeCryptoRandomBytes; + } else if (type && type === "browserCryptoGetRandomValues") { + config.typeCSPRNG = type; + return browserCryptoGetRandomValues; + } else if (hasCryptoRandomBytes()) { + config.typeCSPRNG = "nodeCryptoRandomBytes"; + return nodeCryptoRandomBytes; + } else if (hasCryptoGetRandomValues()) { + config.typeCSPRNG = "browserCryptoGetRandomValues"; + return browserCryptoGetRandomValues; + } + } + + // Splits a number string `bits`-length segments, after first + // optionally zero-padding it to a length that is a multiple of `padLength. + // Returns array of integers (each less than 2^bits-1), with each element + // representing a `bits`-length segment of the input string from right to left, + // i.e. parts[0] represents the right-most `bits`-length segment of the input string. + function splitNumStringToIntArray(str, padLength) { + var parts = [], + i; + + if (padLength) { + str = padLeft(str, padLength); + } + + for (i = str.length; i > config.bits; i -= config.bits) { + parts.push(parseInt(str.slice(i - config.bits, i), 2)); + } + + parts.push(parseInt(str.slice(0, i), 2)); + + return parts; + } + + // Polynomial evaluation at `x` using Horner's Method + // NOTE: fx=fx * x + coeff[i] -> exp(log(fx) + log(x)) + coeff[i], + // so if fx===0, just set fx to coeff[i] because + // using the exp/log form will result in incorrect value + function horner(x, coeffs) { + var logx = config.logs[x], + fx = 0, + i; + + for (i = coeffs.length - 1; i >= 0; i--) { + if (fx !== 0) { + fx = + config.exps[(logx + config.logs[fx]) % config.maxShares] ^ coeffs[i]; + } else { + fx = coeffs[i]; + } + } + + return fx; + } + + // Evaluate the Lagrange interpolation polynomial at x = `at` + // using x and y Arrays that are of the same length, with + // corresponding elements constituting points on the polynomial. + function lagrange(at, x, y) { + var sum = 0, + len, + product, + i, + j; + + for (i = 0, len = x.length; i < len; i++) { + if (y[i]) { + product = config.logs[y[i]]; + + for (j = 0; j < len; j++) { + if (i !== j) { + if (at === x[j]) { + // happens when computing a share that is in the list of shares used to compute it + product = -1; // fix for a zero product term, after which the sum should be sum^0 = sum, not sum^1 + break; + } + product = + (product + + config.logs[at ^ x[j]] - + config.logs[x[i] ^ x[j]] + + config.maxShares) % + config.maxShares; // to make sure it's not negative + } + } + + // though exps[-1] === undefined and undefined ^ anything = anything in + // chrome, this behavior may not hold everywhere, so do the check + sum = product === -1 ? sum : sum ^ config.exps[product]; + } + } + + return sum; + } + + // This is the basic polynomial generation and evaluation function + // for a `config.bits`-length secret (NOT an arbitrary length) + // Note: no error-checking at this stage! If `secret` is NOT + // a NUMBER less than 2^bits-1, the output will be incorrect! + function getShares(secret, numShares, threshold) { + var shares = [], + coeffs = [secret], + i, + len; + + for (i = 1; i < threshold; i++) { + coeffs[i] = parseInt(config.rng(config.bits), 2); + } + + for (i = 1, len = numShares + 1; i < len; i++) { + shares[i - 1] = { + x: i, + y: horner(i, coeffs) + }; + } + + return shares; + } + + function constructPublicShareString(bits, id, data) { + var bitsBase36, idHex, idMax, idPaddingLen, newShareString; + + id = parseInt(id, config.radix); + bits = parseInt(bits, 10) || config.bits; + bitsBase36 = bits.toString(36).toUpperCase(); + idMax = Math.pow(2, bits) - 1; + idPaddingLen = idMax.toString(config.radix).length; + idHex = padLeft(id.toString(config.radix), idPaddingLen); + + if (typeof id !== "number" || id % 1 !== 0 || id < 1 || id > idMax) { + throw new Error( + "Share id must be an integer between 1 and " + idMax + ", inclusive." + ); + } + + newShareString = bitsBase36 + idHex + data; + + return newShareString; + } + + // EXPORTED FUNCTIONS + // ////////////////// + + var secrets = { + init: function(bits, rngType) { + var logs = [], + exps = [], + x = 1, + primitive, + i; + + // reset all config back to initial state + reset(); + + if ( + bits && + (typeof bits !== "number" || + bits % 1 !== 0 || + bits < defaults.minBits || + bits > defaults.maxBits) + ) { + throw new Error( + "Number of bits must be an integer between " + + defaults.minBits + + " and " + + defaults.maxBits + + ", inclusive." + ); + } + + if (rngType && CSPRNGTypes.indexOf(rngType) === -1) { + throw new Error("Invalid RNG type argument : '" + rngType + "'"); + } + + config.radix = defaults.radix; + config.bits = bits || defaults.bits; + config.size = Math.pow(2, config.bits); + config.maxShares = config.size - 1; + + // Construct the exp and log tables for multiplication. + primitive = defaults.primitivePolynomials[config.bits]; + + for (i = 0; i < config.size; i++) { + exps[i] = x; + logs[x] = i; + x = x << 1; // Left shift assignment + if (x >= config.size) { + x = x ^ primitive; // Bitwise XOR assignment + x = x & config.maxShares; // Bitwise AND assignment + } + } + + config.logs = logs; + config.exps = exps; + + if (rngType) { + this.setRNG(rngType); + } + + if (!isSetRNG()) { + this.setRNG(); + } + + if ( + !isSetRNG() || + !config.bits || + !config.size || + !config.maxShares || + !config.logs || + !config.exps || + config.logs.length !== config.size || + config.exps.length !== config.size + ) { + throw new Error("Initialization failed."); + } + }, + + // Evaluates the Lagrange interpolation polynomial at x=`at` for + // individual config.bits-length segments of each share in the `shares` + // Array. Each share is expressed in base `inputRadix`. The output + // is expressed in base `outputRadix'. + combine: function(shares, at) { + var i, + j, + len, + len2, + result = "", + setBits, + share, + splitShare, + x = [], + y = []; + + at = at || 0; + + for (i = 0, len = shares.length; i < len; i++) { + share = this.extractShareComponents(shares[i]); + + // All shares must have the same bits settings. + if (setBits === undefined) { + setBits = share.bits; + } else if (share.bits !== setBits) { + throw new Error("Mismatched shares: Different bit settings."); + } + + // Reset everything to the bit settings of the shares. + if (config.bits !== setBits) { + this.init(setBits); + } + + // Proceed if this share.id is not already in the Array 'x' and + // then split each share's hex data into an Array of Integers, + // then 'rotate' those arrays where the first element of each row is converted to + // its own array, the second element of each to its own Array, and so on for all of the rest. + // Essentially zipping all of the shares together. + // + // e.g. + // [ 193, 186, 29, 150, 5, 120, 44, 46, 49, 59, 6, 1, 102, 98, 177, 196 ] + // [ 53, 105, 139, 49, 187, 240, 91, 92, 98, 118, 12, 2, 204, 196, 127, 149 ] + // [ 146, 211, 249, 167, 209, 136, 118, 114, 83, 77, 10, 3, 170, 166, 206, 81 ] + // + // becomes: + // + // [ [ 193, 53, 146 ], + // [ 186, 105, 211 ], + // [ 29, 139, 249 ], + // [ 150, 49, 167 ], + // [ 5, 187, 209 ], + // [ 120, 240, 136 ], + // [ 44, 91, 118 ], + // [ 46, 92, 114 ], + // [ 49, 98, 83 ], + // [ 59, 118, 77 ], + // [ 6, 12, 10 ], + // [ 1, 2, 3 ], + // [ 102, 204, 170 ], + // [ 98, 196, 166 ], + // [ 177, 127, 206 ], + // [ 196, 149, 81 ] ] + // + if (x.indexOf(share.id) === -1) { + x.push(share.id); + splitShare = splitNumStringToIntArray(hex2bin(share.data)); + for (j = 0, len2 = splitShare.length; j < len2; j++) { + y[j] = y[j] || []; + y[j][x.length - 1] = splitShare[j]; + } + } + } + + // Extract the secret from the 'rotated' share data and return a + // string of Binary digits which represent the secret directly. or in the + // case of a newShare() return the binary string representing just that + // new share. + for (i = 0, len = y.length; i < len; i++) { + result = padLeft(lagrange(at, x, y[i]).toString(2)) + result; + } + + // If 'at' is non-zero combine() was called from newShare(). In this + // case return the result (the new share data) directly. + // + // Otherwise find the first '1' which was added in the share() function as a padding marker + // and return only the data after the padding and the marker. Convert this Binary string + // to hex, which represents the final secret result (which can be converted from hex back + // to the original string in user space using `hex2str()`). + return bin2hex(at >= 1 ? result : result.slice(result.indexOf("1") + 1)); + }, + + getConfig: function() { + var obj = {}; + obj.radix = config.radix; + obj.bits = config.bits; + obj.maxShares = config.maxShares; + obj.hasCSPRNG = isSetRNG(); + obj.typeCSPRNG = config.typeCSPRNG; + return obj; + }, + + // Given a public share, extract the bits (Integer), share ID (Integer), and share data (Hex) + // and return an Object containing those components. + extractShareComponents: function(share) { + var bits, + id, + idLen, + max, + obj = {}, + regexStr, + shareComponents; + + // Extract the first char which represents the bits in Base 36 + bits = parseInt(share.substr(0, 1), 36); + + if ( + bits && + (typeof bits !== "number" || + bits % 1 !== 0 || + bits < defaults.minBits || + bits > defaults.maxBits) + ) { + throw new Error( + "Invalid share : Number of bits must be an integer between " + + defaults.minBits + + " and " + + defaults.maxBits + + ", inclusive." + ); + } + + // calc the max shares allowed for given bits + max = Math.pow(2, bits) - 1; + + // Determine the ID length which is variable and based on the bit count. + idLen = (Math.pow(2, bits) - 1).toString(config.radix).length; + + // Extract all the parts now that the segment sizes are known. + regexStr = "^([a-kA-K3-9]{1})([a-fA-F0-9]{" + idLen + "})([a-fA-F0-9]+)$"; + shareComponents = new RegExp(regexStr).exec(share); + + // The ID is a Hex number and needs to be converted to an Integer + if (shareComponents) { + id = parseInt(shareComponents[2], config.radix); + } + + if (typeof id !== "number" || id % 1 !== 0 || id < 1 || id > max) { + throw new Error( + "Invalid share : Share id must be an integer between 1 and " + + config.maxShares + + ", inclusive." + ); + } + + if (shareComponents && shareComponents[3]) { + obj.bits = bits; + obj.id = id; + obj.data = shareComponents[3]; + return obj; + } + + throw new Error("The share data provided is invalid : " + share); + }, + + // Set the PRNG to use. If no RNG function is supplied, pick a default using getRNG() + setRNG: function(rng) { + var errPrefix = "Random number generator is invalid ", + errSuffix = + " Supply an CSPRNG of the form function(bits){} that returns a string containing 'bits' number of random 1's and 0's."; + + if (rng && typeof rng === "string" && CSPRNGTypes.indexOf(rng) === -1) { + throw new Error("Invalid RNG type argument : '" + rng + "'"); + } + + // If RNG was not specified at all, + // try to pick one appropriate for this env. + if (!rng) { + rng = getRNG(); + } + + // If `rng` is a string, try to forcibly + // set the RNG to the type specified. + if (rng && typeof rng === "string") { + rng = getRNG(rng); + } + + if (runCSPRNGTest) { + if (rng && typeof rng !== "function") { + throw new Error(errPrefix + "(Not a function)." + errSuffix); + } + + if (rng && typeof rng(config.bits) !== "string") { + throw new Error(errPrefix + "(Output is not a string)." + errSuffix); + } + + if (rng && !parseInt(rng(config.bits), 2)) { + throw new Error( + errPrefix + + "(Binary string output not parseable to an Integer)." + + errSuffix + ); + } + + if (rng && rng(config.bits).length > config.bits) { + throw new Error( + errPrefix + + "(Output length is greater than config.bits)." + + errSuffix + ); + } + + if (rng && rng(config.bits).length < config.bits) { + throw new Error( + errPrefix + "(Output length is less than config.bits)." + errSuffix + ); + } + } + + config.rng = rng; + + return true; + }, + + // Converts a given UTF16 character string to the HEX representation. + // Each character of the input string is represented by + // `bytesPerChar` bytes in the output string which defaults to 2. + str2hex: function(str, bytesPerChar) { + var hexChars, + max, + out = "", + neededBytes, + num, + i, + len; + + if (typeof str !== "string") { + throw new Error("Input must be a character string."); + } + + if (!bytesPerChar) { + bytesPerChar = defaults.bytesPerChar; + } + + if ( + typeof bytesPerChar !== "number" || + bytesPerChar < 1 || + bytesPerChar > defaults.maxBytesPerChar || + bytesPerChar % 1 !== 0 + ) { + throw new Error( + "Bytes per character must be an integer between 1 and " + + defaults.maxBytesPerChar + + ", inclusive." + ); + } + + hexChars = 2 * bytesPerChar; + max = Math.pow(16, hexChars) - 1; + + for (i = 0, len = str.length; i < len; i++) { + num = str[i].charCodeAt(); + + if (isNaN(num)) { + throw new Error("Invalid character: " + str[i]); + } + + if (num > max) { + neededBytes = Math.ceil(Math.log(num + 1) / Math.log(256)); + throw new Error( + "Invalid character code (" + + num + + "). Maximum allowable is 256^bytes-1 (" + + max + + "). To convert this character, use at least " + + neededBytes + + " bytes." + ); + } + + out = padLeft(num.toString(16), hexChars) + out; + } + return out; + }, + + // Converts a given HEX number string to a UTF16 character string. + hex2str: function(str, bytesPerChar) { + var hexChars, + out = "", + i, + len; + + if (typeof str !== "string") { + throw new Error("Input must be a hexadecimal string."); + } + bytesPerChar = bytesPerChar || defaults.bytesPerChar; + + if ( + typeof bytesPerChar !== "number" || + bytesPerChar % 1 !== 0 || + bytesPerChar < 1 || + bytesPerChar > defaults.maxBytesPerChar + ) { + throw new Error( + "Bytes per character must be an integer between 1 and " + + defaults.maxBytesPerChar + + ", inclusive." + ); + } + + hexChars = 2 * bytesPerChar; + + str = padLeft(str, hexChars); + + for (i = 0, len = str.length; i < len; i += hexChars) { + out = + String.fromCharCode(parseInt(str.slice(i, i + hexChars), 16)) + out; + } + + return out; + }, + + // Generates a random bits-length number string using the PRNG + random: function(bits) { + if ( + typeof bits !== "number" || + bits % 1 !== 0 || + bits < 2 || + bits > 65536 + ) { + throw new Error( + "Number of bits must be an Integer between 1 and 65536." + ); + } + + return bin2hex(config.rng(bits)); + }, + + // Divides a `secret` number String str expressed in radix `inputRadix` (optional, default 16) + // into `numShares` shares, each expressed in radix `outputRadix` (optional, default to `inputRadix`), + // requiring `threshold` number of shares to reconstruct the secret. + // Optionally, zero-pads the secret to a length that is a multiple of padLength before sharing. + share: function(secret, numShares, threshold, padLength) { + var neededBits, + subShares, + x = new Array(numShares), + y = new Array(numShares), + i, + j, + len; + + // Security: + // For additional security, pad in multiples of 128 bits by default. + // A small trade-off in larger share size to help prevent leakage of information + // about small-ish secrets and increase the difficulty of attacking them. + padLength = padLength || 128; + + if (typeof secret !== "string") { + throw new Error("Secret must be a string."); + } + + if ( + typeof numShares !== "number" || + numShares % 1 !== 0 || + numShares < 2 + ) { + throw new Error( + "Number of shares must be an integer between 2 and 2^bits-1 (" + + config.maxShares + + "), inclusive." + ); + } + + if (numShares > config.maxShares) { + neededBits = Math.ceil(Math.log(numShares + 1) / Math.LN2); + throw new Error( + "Number of shares must be an integer between 2 and 2^bits-1 (" + + config.maxShares + + "), inclusive. To create " + + numShares + + " shares, use at least " + + neededBits + + " bits." + ); + } + + if ( + typeof threshold !== "number" || + threshold % 1 !== 0 || + threshold < 2 + ) { + throw new Error( + "Threshold number of shares must be an integer between 2 and 2^bits-1 (" + + config.maxShares + + "), inclusive." + ); + } + + if (threshold > config.maxShares) { + neededBits = Math.ceil(Math.log(threshold + 1) / Math.LN2); + throw new Error( + "Threshold number of shares must be an integer between 2 and 2^bits-1 (" + + config.maxShares + + "), inclusive. To use a threshold of " + + threshold + + ", use at least " + + neededBits + + " bits." + ); + } + + if (threshold > numShares) { + throw new Error( + "Threshold number of shares was " + + threshold + + " but must be less than or equal to the " + + numShares + + " shares specified as the total to generate." + ); + } + + if ( + typeof padLength !== "number" || + padLength % 1 !== 0 || + padLength < 0 || + padLength > 1024 + ) { + throw new Error( + "Zero-pad length must be an integer between 0 and 1024 inclusive." + ); + } + + secret = "1" + hex2bin(secret); // prepend a 1 as a marker so that we can preserve the correct number of leading zeros in our secret + secret = splitNumStringToIntArray(secret, padLength); + + for (i = 0, len = secret.length; i < len; i++) { + subShares = getShares(secret[i], numShares, threshold); + for (j = 0; j < numShares; j++) { + x[j] = x[j] || subShares[j].x.toString(config.radix); + y[j] = padLeft(subShares[j].y.toString(2)) + (y[j] || ""); + } + } + + for (i = 0; i < numShares; i++) { + x[i] = constructPublicShareString(config.bits, x[i], bin2hex(y[i])); + } + + return x; + }, + + // Generate a new share with id `id` (a number between 1 and 2^bits-1) + // `id` can be a Number or a String in the default radix (16) + newShare: function(id, shares) { + var share, radid; + + if (id && typeof id === "string") { + id = parseInt(id, config.radix); + } + + radid = id.toString(config.radix); + + if (id && radid && shares && shares[0]) { + share = this.extractShareComponents(shares[0]); + return constructPublicShareString( + share.bits, + radid, + this.combine(shares, id) + ); + } + + throw new Error("Invalid 'id' or 'shares' Array argument to newShare()."); + }, + + /* test-code */ + // export private functions so they can be unit tested directly. + _reset: reset, + _padLeft: padLeft, + _hex2bin: hex2bin, + _bin2hex: bin2hex, + _hasCryptoGetRandomValues: hasCryptoGetRandomValues, + _hasCryptoRandomBytes: hasCryptoRandomBytes, + _getRNG: getRNG, + _isSetRNG: isSetRNG, + _splitNumStringToIntArray: splitNumStringToIntArray, + _horner: horner, + _lagrange: lagrange, + _getShares: getShares, + _constructPublicShareString: constructPublicShareString + /* end-test-code */ + }; + + // Always initialize secrets with default settings. + secrets.init(); + + return secrets; +}); diff --git a/javascript/src/utilities/numberUtilities.js b/javascript/src/utilities/numberUtilities.js new file mode 100644 index 0000000000000000000000000000000000000000..dbee13839522df1f54fe79ff2fa57befd736ae68 --- /dev/null +++ b/javascript/src/utilities/numberUtilities.js @@ -0,0 +1,14 @@ +export function getRandomInt(max) { + return Math.floor(Math.random() * Math.floor(max)); +} + +export function getSliceRange(max) { + const beginIndex = getRandomInt(max); + const endIndex = getRandomInt(max); + + if (beginIndex === endIndex) { + return getSliceRange(max); + } + + return { beginIndex, endIndex }; +} diff --git a/javascript/src/utilities/secrets.js b/javascript/src/utilities/secrets.js new file mode 100644 index 0000000000000000000000000000000000000000..2223ae6533e57d2573b3a473e94f0c7e8287adfd --- /dev/null +++ b/javascript/src/utilities/secrets.js @@ -0,0 +1,98 @@ +import secrets from "../lib/secrets"; +import { encryptMessage } from "./signingUtilities"; +import { encodeResponse } from "./appUtility"; +import { getSliceRange } from "./numberUtilities"; +import { THRESHOLD } from "../constants/secrets"; + +/** Initialize + */ +export const initSecrets = (bits, rngType) => secrets.init(bits, rngType); + +export const setRNG = rngType => secrets.setRNG(rngType); +export const getSecretsConfig = () => secrets.getConfig(); + +/** + * Function generates a random bits length string, and output it in hexadecimal format + * + * @param {number} bits + * @returns {string} hex + */ +export const generateSecret = bits => secrets.random(bits); + +/** + * Divide a secret expressed in hexadecimal form into numShares number of shares, requiring that threshold number of shares be present for reconstructing the secret + * + * @param {string} secret + * @param {number} numShares + * @param {number} threshold + * @param {number} [padLength=128] + * @returns {array} + */ +export const divideSecretToShares = ( + secret, + numShares, + threshold, + padLength = 128 +) => secrets.share(secret, numShares, threshold, padLength); + +/** + * Reconstructs a secret from shares + * + * @param {array} shares + * @returns {string} + */ +export const combineSecret = shares => secrets.combine(shares); + +export const encryptShare = async (share, publicKey) => + await encryptMessage(share, publicKey, "secretPart"); + +/** Account Recovery key management */ + +export const generateRecoveryKey = () => { + const recoveryKey = generateSecret(512); + return recoveryKey; +}; + +export const getRecoveryKeyShares = (recoveryKey, sharesNumber) => { + return divideSecretToShares(recoveryKey, sharesNumber, THRESHOLD); +}; + +function getSecretSliceRange(max) { + const { beginIndex, endIndex } = getSliceRange(max); + if (endIndex - beginIndex < THRESHOLD) { + return getSecretSliceRange(max); + } + + return { beginIndex, endIndex }; +} + +export const checkRecoveryKeyCombine = (recoveryKey, recoveryKeyShares) => { + let checkKey; + + const { beginIndex, endIndex } = getSecretSliceRange( + recoveryKeyShares.length + 1 + ); + + checkKey = combineSecret(recoveryKeyShares.slice(beginIndex, endIndex)); + if (checkKey !== recoveryKey) { + return encodeResponse( + "400", + "", + "Sanity check with required number of shares failed" + ); + } + checkKey = combineSecret(recoveryKeyShares.slice(0, 1)); + if (checkKey === recoveryKey) { + return encodeResponse( + "400", + "", + "Sanity check with less than required shares failed" + ); + } + checkKey = combineSecret(recoveryKeyShares); + if (checkKey !== recoveryKey) { + return encodeResponse("400", "", "Sanity check with all shares failed"); + } + + return encodeResponse("200", "", "Check passed"); +};