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/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..3a833a5df6edb8f99e0f56fe8df2f83280f2ce73 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) @@ -138,6 +143,9 @@ endif() find_package(nlohmann_json 3.7.3 REQUIRED) +set(SQLite3_DIR ${VENDOR_INSTALL_DIR}) +find_package(SQLite3 REQUIRED) + add_subdirectory("src") add_subdirectory("tests") @@ -176,6 +184,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/proto b/cpp/proto index e861100984116aacf6d84cb8a09dc0ef81041509..9ef33cf108f8665204929ea56d9df812cb378821 160000 --- a/cpp/proto +++ b/cpp/proto @@ -1 +1 @@ -Subproject commit e861100984116aacf6d84cb8a09dc0ef81041509 +Subproject commit 9ef33cf108f8665204929ea56d9df812cb378821 diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 555515506f7c1d62b01e54cf2eeaeeb5401e783b..62c0db650441125d6066b1826169ddeabe99525f 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -6,6 +6,8 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") add_definitions(-DNOGDI) endif() +add_definitions(-DBOOST_FILESYSTEM_NO_DEPRECATED) + include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src @@ -45,17 +47,56 @@ if (VEREIGN_USE_PRECOMPILED_HEADERS) endif() set(VEREIGNLIB_SRC + vereign/core/rand.cc + vereign/core/temp.cc + vereign/core/fs.cc + vereign/restapi/detail/http_reader.cc vereign/restapi/client.cc - vereign/service/passport_service.cc - vereign/grpc/gen/gen.cc + + # vereign/grpc/gen/gen.cc vereign/grpc/json/encoder.cc vereign/grpc/service_registry.cc vereign/grpc/server.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/aes.cc + vereign/crypto/rsa.cc + vereign/crypto/bio.cc + vereign/crypto/digest.cc + + + vereign/kvstore/sqlite_storage.cc + vereign/kvstore/crypto_storage.cc + + vereign/identity/provider.cc + + vereign/service/gen/passport_service.cc + vereign/service/passport_service.cc + vereign/service/identity_service.cc + + # FIXME: remove this + vereign/service/gen/identity_service.cc + vereign/service/gen/passport_service.cc ) -file(GLOB GENERATED_SERVICES_SRC vereign/service/gen/*.cc) -list(APPEND VEREIGNLIB_SRC ${GENERATED_SERVICES_SRC}) + +if (LINUX) + list(APPEND VEREIGNLIB_SRC + vereign/kvstore/detail/linux_crypto_storage.cc + ) +endif() + +# file(GLOB GENERATED_SERVICES_SRC vereign/service/gen/*.cc) +# list(APPEND VEREIGNLIB_SRC ${GENERATED_SERVICES_SRC}) add_library(vereignlib STATIC ${VEREIGNLIB_SRC}) set_property(TARGET vereignlib PROPERTY POSITION_INDEPENDENT_CODE ON) @@ -67,7 +108,9 @@ target_link_libraries(vereignlib PUBLIC fmt::fmt gRPC::grpc++_reflection gRPC::grpc++ + Boost::filesystem $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time> + SQLite::SQLite3 ) add_library(vereign SHARED @@ -96,6 +139,8 @@ add_executable(csandbox ${csandbox_sources}) target_link_libraries(csandbox PRIVATE vereignlib $<$<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..0f3226d364e1b95441b80b2706f5f0222b347e8a 100644 --- a/cpp/src/csandbox.cc +++ b/cpp/src/csandbox.cc @@ -1,4 +1,20 @@ +#include <boost/core/ignore_unused.hpp> +#include <iostream> +#include <boost/filesystem.hpp> -auto main(int argc, char* argv[]) -> int { - return 0; +namespace fs = boost::filesystem; + +auto main(int argc, char** argv) -> int { + boost::ignore_unused(argc); + boost::ignore_unused(argv); + + // fs::path ; + + std::cout << "Temp directory is " << fs::temp_directory_path() << '\n'; + // std::cout << "Home directory is " << fs::user << '\n'; + + // p.wstring(); + // std::cout << p.string() << std::endl;; + + return 1; } diff --git a/cpp/src/vereign/bytes/buffer.cc b/cpp/src/vereign/bytes/buffer.cc new file mode 100644 index 0000000000000000000000000000000000000000..30e202dcd66dc63869a99cdfcde147d11a0e663e --- /dev/null +++ b/cpp/src/vereign/bytes/buffer.cc @@ -0,0 +1,179 @@ +#include <memory> +#include <new> +#include <stdexcept> +#include <vereign/bytes/buffer.hh> + +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_; + while (size > cap - size_) { + cap = cap * 2; + } + + 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 size) { + if (size_ + size > cap_) { + throw std::runtime_error("cannot increment size pass the capacity"); + } + + size_ += size; +} + +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 std::runtime_error("index out of bounds"); + } + + return data_[index]; +} + +auto Buffer::operator[](std::size_t index) const -> const uint8_t& { + if (index >= cap_) { + throw std::runtime_error("index out of bounds"); + } + + 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..c2fe622b87d3d7bb15c9b094b1d458c1bc22f9eb --- /dev/null +++ b/cpp/src/vereign/bytes/buffer.hh @@ -0,0 +1,54 @@ +#ifndef __VEREIGN_BYTES_BUFFER_HH +#define __VEREIGN_BYTES_BUFFER_HH + +#include <vereign/bytes/view.hh> +#include <cstring> + +namespace vereign::bytes { + +class Buffer { +public: + Buffer() noexcept; + Buffer(std::size_t cap); + Buffer(View src); + + Buffer(Buffer&& other) noexcept; + auto operator=(Buffer&& other) noexcept -> Buffer&; + + // disable copying + Buffer(const Buffer&) = delete; + auto operator=(const Buffer&) -> Buffer& = delete; + + ~Buffer(); + + auto begin() noexcept -> uint8_t*; + auto begin() const noexcept -> const uint8_t*; + + auto end() noexcept -> uint8_t*; + auto end() const noexcept -> const uint8_t*; + + auto operator[](std::size_t index) -> uint8_t&; + auto operator[](std::size_t index) const -> const uint8_t&; + + auto Size() const noexcept -> std::size_t; + auto Cap() const noexcept -> std::size_t; + void Reserve(std::size_t size); + void Reset(); + + void IncSize(std::size_t size); + auto FreeCap() const noexcept -> std::size_t; + auto WriteWithinCap(bytes::View src) noexcept -> std::size_t; + auto Write(bytes::View src) -> std::size_t; + + auto View() const noexcept -> bytes::View; + 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/view.hh b/cpp/src/vereign/bytes/view.hh new file mode 100644 index 0000000000000000000000000000000000000000..fb3f5b95ed72eed0da837cb984d4c557a69156d3 --- /dev/null +++ b/cpp/src/vereign/bytes/view.hh @@ -0,0 +1,91 @@ +#ifndef __VEREIGN_BYTES_VIEW_HH +#define __VEREIGN_BYTES_VIEW_HH + +#include <cstring> +#include <string> +#include <string_view> +#include <stdexcept> + +namespace vereign::bytes { + +class View { +public: + View() noexcept : size_{0}, data_{nullptr} {} + + View(const uint8_t* data, std::size_t size) noexcept + : size_{size}, + data_{data} + { + } + + View(std::string_view str) noexcept + : size_{str.length()}, + data_{str.length() > 0 ? reinterpret_cast<const uint8_t*>(str.data()): nullptr} + { + } + + View(const void* ptr, std::size_t size) noexcept + : size_{size}, + data_{static_cast<const uint8_t*>(ptr)} + { + } + + View(const View&) = default; + auto operator=(const View&) -> View& = default; + + auto Slice(std::size_t start) const -> View { + if (start >= size_) { + return View(data_, 0); + } + + return View(data_ + start, size_ - start); + } + + 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); + } + + auto Data() const noexcept -> const uint8_t* { + return data_; + } + + auto CharData() const noexcept -> const char* { + return reinterpret_cast<const char*>(data_); + } + + auto Size() const noexcept -> std::size_t { + return size_; + } + + auto String() noexcept -> std::string_view { + return std::string_view{CharData(), size_}; + } + + auto operator==(View other) const noexcept -> bool { + if (size_ != other.size_) { + return false; + } + + return std::memcmp(data_, other.data_, size_) == 0; + } + + auto operator[](std::size_t index) const -> const uint8_t& { + if (index >= size_ ) { + throw std::runtime_error("index out of bounds"); + } + + return data_[index]; + } + +private: + std::size_t size_; + const uint8_t* data_; +}; + +} // 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..5f2708842ff04778acf323aa2d777d33fadb5d69 --- /dev/null +++ b/cpp/src/vereign/bytes/view_dump.hh @@ -0,0 +1,38 @@ +#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 { +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 + + +auto dump(View buf) -> detail::ViewDump; +auto dump(const Buffer& buf) -> detail::ViewDump; + +} // namespace vereign::bytes + +#endif // __VEREIGN_BYTES_VIEW_DUMP_HH diff --git a/cpp/src/vereign/core/fs.cc b/cpp/src/vereign/core/fs.cc new file mode 100644 index 0000000000000000000000000000000000000000..bf79c66acc39b0d7784c995822d8a0a1994dc898 --- /dev/null +++ b/cpp/src/vereign/core/fs.cc @@ -0,0 +1,16 @@ +#include <vereign/core/fs.hh> + +#include <boost/filesystem/operations.hpp> + +namespace vereign::core { + +RemoveFileGuard::RemoveFileGuard(boost::filesystem::path path) + : path_{std::move(path)} +{ +} + +RemoveFileGuard::~RemoveFileGuard() { + boost::filesystem::remove(path_); +} + +} // namespace vereign::core diff --git a/cpp/src/vereign/core/fs.hh b/cpp/src/vereign/core/fs.hh new file mode 100644 index 0000000000000000000000000000000000000000..6eb1baded98a2c7a0bf70729f3f5784eab88d9c3 --- /dev/null +++ b/cpp/src/vereign/core/fs.hh @@ -0,0 +1,24 @@ +#ifndef __VEREIGN_CORE_FS_HH +#define __VEREIGN_CORE_FS_HH + +#include <boost/filesystem/path.hpp> + +namespace vereign::core { + +/** + * A RAII guard that deletes a file upon destruction. + */ +class RemoveFileGuard { +public: + RemoveFileGuard(boost::filesystem::path path); + ~RemoveFileGuard(); + + RemoveFileGuard(const RemoveFileGuard&) = delete; + auto operator=(const RemoveFileGuard&) -> RemoveFileGuard& = delete; +private: + boost::filesystem::path path_; +}; + +} // namespace vereign::core + +#endif // __VEREIGN_CORE_FS_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/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/temp.cc b/cpp/src/vereign/core/temp.cc new file mode 100644 index 0000000000000000000000000000000000000000..bce1880273af9a2ead7a7c8d9027fdf2dc199d61 --- /dev/null +++ b/cpp/src/vereign/core/temp.cc @@ -0,0 +1,16 @@ +#include <vereign/core/temp.hh> + +#include <vereign/core/rand.hh> +#include <boost/filesystem/operations.hpp> + +namespace vereign::core { + +auto TempFilePath(boost::filesystem::path dir, std::string_view prefix) -> boost::filesystem::path { + return dir.append(prefix).concat(RandLowerAlpha(10)); +} + +auto TempFilePath(std::string_view prefix) -> boost::filesystem::path { + return TempFilePath(boost::filesystem::temp_directory_path(), prefix); +} + +} // namespace vereign::core diff --git a/cpp/src/vereign/core/temp.hh b/cpp/src/vereign/core/temp.hh new file mode 100644 index 0000000000000000000000000000000000000000..bc8dec74849a05263c84368006bf66eaeb25e448 --- /dev/null +++ b/cpp/src/vereign/core/temp.hh @@ -0,0 +1,14 @@ +#ifndef __VEREIGN_CORE_TEMP_HH +#define __VEREIGN_CORE_TEMP_HH + +#include <boost/filesystem/path.hpp> +#include <string_view> + +namespace vereign::core { + +auto TempFilePath(boost::filesystem::path dir, std::string_view prefix) -> boost::filesystem::path; +auto TempFilePath(std::string_view prefix) -> boost::filesystem::path; + +} // namespace vereign::core + +#endif // __VEREIGN_CORE_TEMP_HH diff --git a/cpp/src/vereign/crypto/aes.cc b/cpp/src/vereign/crypto/aes.cc new file mode 100644 index 0000000000000000000000000000000000000000..59875fb3dd19c268bf8d86f591f008c3109de9ae --- /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& encrypted, + bytes::Buffer& tag +) { + iv.Reserve(gcmIVSizeBytes); + crypto::Rand(iv); + + 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, tag.FreeCap(), 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..861c749b8757cb7188824de79aa430ed246d6d41 --- /dev/null +++ b/cpp/src/vereign/crypto/aes.hh @@ -0,0 +1,27 @@ +#ifndef __VEREIGN_CRYPTO_AES_HH +#define __VEREIGN_CRYPTO_AES_HH + +#include "vereign/bytes/view.hh" +#include <vereign/bytes/buffer.hh> + +namespace vereign::crypto::aes { + +void GCM256Encrypt( + bytes::View src, + bytes::View key, + bytes::Buffer& iv, + bytes::Buffer& encrypted, + bytes::Buffer& tag +); + +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..8c1721430878d422d48de31e13d6f0931fa978b4 --- /dev/null +++ b/cpp/src/vereign/crypto/bio.hh @@ -0,0 +1,13 @@ +#ifndef __VEREIGN_CRYPTO_BIO_HH +#define __VEREIGN_CRYPTO_BIO_HH + +#include <vereign/bytes/view.hh> +#include <openssl/bio.h> + +namespace vereign::crypto::bio { + +auto View(const BIO* bio) -> bytes::View; + +} // vereign::crypto::bio + +#endif // __VEREIGN_CRYPTO_BIO_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..53f15d5b73791c6c3453665b541effe1e4f01968 --- /dev/null +++ b/cpp/src/vereign/crypto/digest.hh @@ -0,0 +1,12 @@ +#ifndef __VEREIGN_CRYPTO_DIGEST_HH +#define __VEREIGN_CRYPTO_DIGEST_HH + +#include <vereign/bytes/buffer.hh> + +namespace vereign::crypto::digest { + +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..c5282ade4ae3a4ab91ecc0b60fbb479e8b88a47c --- /dev/null +++ b/cpp/src/vereign/crypto/errors.hh @@ -0,0 +1,27 @@ +#ifndef __VEREIGN_CRYPTO_ERRORS_HH +#define __VEREIGN_CRYPTO_ERRORS_HH + +#include <stdexcept> +#include <openssl/err.h> + +namespace vereign::crypto { + +class Error : public std::runtime_error { +public: + Error(const std::string& what) + : std::runtime_error(what) + { + } +}; + +class OpenSSLError : public Error { +public: + OpenSSLError(const std::string& what) + : Error(what + ": " + ERR_reason_error_string(ERR_get_error())) + { + } +}; + +} // vereign::crypto + +#endif // __VEREIGN_CRYPTO_ERRORS_HH diff --git a/cpp/src/vereign/crypto/rand.hh b/cpp/src/vereign/crypto/rand.hh new file mode 100644 index 0000000000000000000000000000000000000000..4067b039adf2b80688b3f7af93340730b05081eb --- /dev/null +++ b/cpp/src/vereign/crypto/rand.hh @@ -0,0 +1,23 @@ +#ifndef __VEREIGN_CRYPTO_RAND_HH +#define __VEREIGN_CRYPTO_RAND_HH + +#include <vereign/bytes/buffer.hh> +#include <vereign/crypto/errors.hh> + +#include <openssl/rand.h> + +namespace vereign::crypto { + +inline void Rand(bytes::Buffer& buf) { + int result = RAND_bytes(buf.end(), buf.FreeCap()); + if (result == 0) { + ERR_clear_error(); + throw Error("crypto rand failed"); + } + + buf.IncSize(buf.FreeCap()); +} + +} // 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..e9a519f6b5887486f37819759e65d33108264602 --- /dev/null +++ b/cpp/src/vereign/crypto/rsa.cc @@ -0,0 +1,167 @@ +#include <vereign/crypto/rsa.hh> + +#include <vereign/bytes/view.hh> +#include <vereign/crypto/errors.hh> +#include <vereign/bytes/buffer.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 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 public 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..ab41d09e702ca3b6a9106a77ff5cbb2d1bd93fc8 --- /dev/null +++ b/cpp/src/vereign/crypto/rsa.hh @@ -0,0 +1,25 @@ +#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 { + +auto GenerateKey(int bits) -> bssl::UniquePtr<EVP_PKEY>; + +void PublicKeyEncrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& encrypted); +void PrivateKeyDecrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& decrypted); + +auto ExportPublicKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>; +auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>; + +auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>; +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..47ef85a53f69d204c474b6ad862a1cc0942ebbfa --- /dev/null +++ b/cpp/src/vereign/encoding/base64.cc @@ -0,0 +1,27 @@ +#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); +} + +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..2bfaab5f159151b2a10330bf176ef7e0394ae167 --- /dev/null +++ b/cpp/src/vereign/encoding/base64.hh @@ -0,0 +1,14 @@ +#ifndef __VEREIGN_ENCODING_BASE64_HH +#define __VEREIGN_ENCODING_BASE64_HH + +#include <vereign/bytes/buffer.hh> +#include <string> + +namespace vereign::encoding::base64 { + +void Encode(bytes::View src, bytes::Buffer& encoded); +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..0358fc236762f3d09cbf6ddd8a66f970e5f9a570 --- /dev/null +++ b/cpp/src/vereign/encoding/binary.cc @@ -0,0 +1,72 @@ +#include <vereign/encoding/binary.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 std::runtime_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) { + // FIXME: add encoding::Error exception + throw std::runtime_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 std::runtime_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..8d59c655446028fedec33532674166111b752930 --- /dev/null +++ b/cpp/src/vereign/encoding/binary.hh @@ -0,0 +1,24 @@ +#ifndef __VEREIGN_ENCODING_BINARY_HH +#define __VEREIGN_ENCODING_BINARY_HH + +#include <stdexcept> +#include <vereign/bytes/buffer.hh> + +// FIXME: add docs +namespace vereign::encoding::binary { + +void EncodeUint8(bytes::Buffer& out, uint8_t v); +auto DecodeUint8(bytes::View& b) -> uint8_t; + +void EncodeUint64(uint8_t* out, uint64_t v); +void EncodeUint64(bytes::Buffer& out, uint64_t v); + +auto DecodeUint64(const uint8_t* b) -> uint64_t; +auto DecodeUint64(bytes::View b) -> uint64_t; + +void EncodeBytes(bytes::Buffer& out, bytes::View bytes); +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/hex.cc b/cpp/src/vereign/encoding/hex.cc new file mode 100644 index 0000000000000000000000000000000000000000..6c8dc18cf76bc8d93ae78e82c739ea70744e77a1 --- /dev/null +++ b/cpp/src/vereign/encoding/hex.cc @@ -0,0 +1,54 @@ +#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(std::string_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); +} + +} // 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..495f09027acb88f9c7f4066fc33d29a05f476b49 --- /dev/null +++ b/cpp/src/vereign/encoding/hex.hh @@ -0,0 +1,14 @@ +#ifndef __VEREIGN_ENCODING_HEX_HH +#define __VEREIGN_ENCODING_HEX_HH + +#include <vereign/bytes/buffer.hh> +#include <string> + +namespace vereign::encoding::hex { + +void Encode(bytes::View src, bytes::Buffer& encoded); +void Decode(bytes::View src, bytes::Buffer& decoded); + +} // vereign::encoding::hex + +#endif // __VEREIGN_ENCODING_HEX_HH diff --git a/cpp/src/vereign/grpc/server.cc b/cpp/src/vereign/grpc/server.cc index fd3c911aae22ff35bb513772a27b7d8d3555b985..b5a85975a5927e5ee90355f235baeec5d7bf4ecd 100644 --- a/cpp/src/vereign/grpc/server.cc +++ b/cpp/src/vereign/grpc/server.cc @@ -2,8 +2,8 @@ #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/service/gen/gen.hh> +// #include <vereign/grpc/gen/gen.hh> #include <vereign/grpc/service_registry.hh> #include <vereign/service/passport_service.hh> @@ -19,8 +19,7 @@ #include <boost/asio/executor_work_guard.hpp> -namespace vereign { -namespace grpc { +namespace vereign::grpc { namespace asio = boost::asio; @@ -29,18 +28,14 @@ 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& vereignPort ) : 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 - )}, + client_session_{std::make_unique<restapi::ClientSession>(*client_)}, server_{nullptr} { @@ -57,7 +52,7 @@ public: services_registry_.RegisterIfNotExist<PassportAPI<service::PassportService>>(*client_session_); // register all generated services - grpc::gen::RegisterAll(*client_session_, services_registry_); + // grpc::gen::RegisterAll(*client_session_, services_registry_); services_registry_.RegisterIntoBuilder(builder); @@ -109,11 +104,9 @@ private: 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& vereignPort ) - : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort, publicKey)} + : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort)} { } @@ -129,5 +122,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..5ab7301ba1e614ad8f340948ab67d5508b613231 100644 --- a/cpp/src/vereign/grpc/server.hh +++ b/cpp/src/vereign/grpc/server.hh @@ -4,8 +4,7 @@ #include <string> #include <memory> -namespace vereign { -namespace grpc { +namespace vereign::grpc { /** * BindError is thrown when the Server::Server could not start listening. @@ -39,9 +38,7 @@ public: 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& vereignPort ); /** @@ -77,7 +74,6 @@ private: std::unique_ptr<Impl> impl_; }; -} // namespace grpc } // namespace vereign #endif diff --git a/cpp/src/vereign/identity/errors.hh b/cpp/src/vereign/identity/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..7140afec338eeef55150bc988beb260de2b901ac --- /dev/null +++ b/cpp/src/vereign/identity/errors.hh @@ -0,0 +1,18 @@ +#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) + { + } +}; + +} // 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..dd19a5f9b2c37f4c42ab6d181e7ee5e4c500b732 --- /dev/null +++ b/cpp/src/vereign/identity/provider.cc @@ -0,0 +1,80 @@ +#include "vereign/crypto/digest.hh" +#include <vereign/identity/provider.hh> + +#include <vereign/crypto/bio.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/encoding/base64.hh> + +namespace { + constexpr int rsaKeySizeBits = 2048; +} + +namespace vereign::identity { + +Provider::Provider(kvstore::CryptoStorage& storage) + : storage_{storage} +{} + +Provider::~Provider() = default; + +auto Provider::ResetIdentity(const std::string& pin) -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + storage_.Reset(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); + + 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); + + 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()); +} + +} // 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..80e17200920a334a7efd5f99ab84a6088971cc94 --- /dev/null +++ b/cpp/src/vereign/identity/provider.hh @@ -0,0 +1,33 @@ +#ifndef __VEREIGN_IDENTITY_PROVIDER_HH +#define __VEREIGN_IDENTITY_PROVIDER_HH + +#include <vereign/kvstore/crypto_storage.hh> + +#include <mutex> + +namespace vereign::identity { + +class Provider { +public: + Provider(kvstore::CryptoStorage& storage); + + // disable copying + Provider(const kvstore::Storage&) = delete; + auto operator=(const kvstore::Storage&) -> Provider& = delete; + + auto ResetIdentity(const std::string& pin) -> std::string; + auto LoadIdentity(const std::string& pin) -> std::string; + auto GetIdentityPublicKeyBase64() -> std::string; + auto GetDeviceHash() -> std::string; + + ~Provider(); + +private: + std::mutex mu_; + + kvstore::CryptoStorage& storage_; +}; + +} // namespace vereign::identity + +#endif // __VEREIGN_IDENTITY_PROVIDER_HH diff --git a/cpp/src/vereign/kvstore/crypto_storage.cc b/cpp/src/vereign/kvstore/crypto_storage.cc new file mode 100644 index 0000000000000000000000000000000000000000..4a24ea5d01b9b907b24929a33d991cafc1c81a52 --- /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(kvstore::Storage& storage) + : impl_{std::make_unique<detail::CryptoStorageImpl>(storage)} +{} + +CryptoStorage::~CryptoStorage() = default; + +void CryptoStorage::Reset(const std::string& pin) { + impl_->Reset(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); +} + +void CryptoStorage::GetBytes(const std::string& key, bytes::Buffer& value) { + 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..38d2b8439e50aabb66a73ae42bcf4b468cf339aa --- /dev/null +++ b/cpp/src/vereign/kvstore/crypto_storage.hh @@ -0,0 +1,32 @@ +#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; + +} + +class CryptoStorage { +public: + CryptoStorage(Storage& storage); + ~CryptoStorage(); + + void Reset(const std::string& pin); + void Open(const std::string& pin); + + void PutBytes(const std::string& key, bytes::View value); + void GetBytes(const std::string& key, bytes::Buffer& value); + +private: + std::unique_ptr<detail::CryptoStorageImpl> impl_; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_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..66ecd53e8e80c36fca05871db698fcbda057bc4e --- /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/crypto/rand.hh> +#include <vereign/crypto/rsa.hh> +#include <vereign/crypto/aes.hh> +#include <vereign/crypto/bio.hh> + +#include <openssl/base.h> +#include <openssl/bn.h> +#include <openssl/evp.h> +#include <openssl/sha.h> +#include <openssl/aes.h> +#include <openssl/rsa.h> +#include <openssl/pem.h> + +namespace { + constexpr int iterations = 1 << 18; + constexpr int saltSizeBytes = 64; + constexpr int aesKeySizeBytes = 32; +} + +namespace vereign::kvstore::detail { + +CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage) + : storage_{storage} +{} + +void CryptoStorageImpl::Open(const std::string& pin) { + bytes::Buffer salt; + auto iterations = storage_.GetInt64("master_key_iterations"); + if (iterations == 0) { + throw StorageNotInitializedError{}; + } + storage_.GetBytes("master_key_salt", salt); + + bytes::Buffer key{aesKeySizeBytes}; + + int result = PKCS5_PBKDF2_HMAC_SHA1( + pin.data(), + pin.length(), + salt.View().Data(), + salt.View().Size(), + iterations, + key.FreeCap(), + key.end() + ); + if (result == 0) { + throw Error("key derivation failed"); + } + + key.IncSize(aesKeySizeBytes); + + key_ = std::move(key); +} + +void CryptoStorageImpl::Reset(const std::string& pin) { + bytes::Buffer salt{saltSizeBytes}; + crypto::Rand(salt); + + bytes::Buffer key{aesKeySizeBytes}; + + int result = PKCS5_PBKDF2_HMAC_SHA1( + pin.data(), + pin.length(), + salt.View().Data(), + salt.View().Size(), + iterations, + key.FreeCap(), + key.end() + ); + if (result == 0) { + throw Error("key derivation failed"); + } + + key.IncSize(aesKeySizeBytes); + + storage_.PutInt64("master_key_iterations", iterations); + storage_.PutBytes("master_key_salt", salt.View()); + + key_ = std::move(key); +} + +void CryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) { + if (key_.Size() == 0) { + throw Error("crypto storage is not initialized"); + } + + bytes::Buffer iv; + bytes::Buffer tag; + bytes::Buffer encrypted; + + crypto::aes::GCM256Encrypt(value, key_.View(), iv, encrypted, tag); + + bytes::Buffer encoded_value; + EncodeEncryptedValue(encoded_value, iv.View(), tag.View(), encrypted.View()); + + storage_.PutBytes(key, encoded_value.View()); +} + +void CryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) { + if (key_.Size() == 0) { + throw Error("crypto storage is not initialized"); + } + + bytes::Buffer encoded; + storage_.GetBytes(key, encoded); + + bytes::View iv; + bytes::View tag; + bytes::View encrypted; + DecodeEncryptedValue(encoded.View(), iv, tag, encrypted); + + crypto::aes::GCM256Decrypt(encrypted, key_.View(), iv, tag, value); +} + + +} // 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..1a0cdc926400d2a3f235afa3180a4ecacce91064 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh @@ -0,0 +1,31 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH +#define __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH + +#include <vereign/kvstore/storage.hh> + +namespace vereign::kvstore::detail { + +class CryptoStorageImpl { +public: + CryptoStorageImpl(kvstore::Storage& storage); + + // disable copying + CryptoStorageImpl(const CryptoStorageImpl&) = delete; + auto operator=(const CryptoStorageImpl&) -> CryptoStorageImpl& = delete; + + void Reset(const std::string& pin); + void Open(const std::string& pin); + + void PutBytes(const std::string& key, bytes::View value); + void GetBytes(const std::string& key, bytes::Buffer& value); + +private: + kvstore::Storage& storage_; + + bytes::Buffer key_; +}; + +} // namespace vereign::kvstore::detail + + +#endif // __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH 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..101b77fa08f04c73ecf940239625c6c2de8c5c71 --- /dev/null +++ b/cpp/src/vereign/kvstore/detail/value_encoder.hh @@ -0,0 +1,49 @@ +#ifndef __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH +#define __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/buffer.hh> +#include <vereign/encoding/binary.hh> + +#include <vereign/kvstore/errors.hh> + +namespace vereign::kvstore::detail { + +// FIXME: move to implementation file +inline 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); +} + +inline 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 + +#endif // __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH diff --git a/cpp/src/vereign/kvstore/errors.hh b/cpp/src/vereign/kvstore/errors.hh new file mode 100644 index 0000000000000000000000000000000000000000..694b3c5d38c0a4b91c134ed56ae9b397d0c17a49 --- /dev/null +++ b/cpp/src/vereign/kvstore/errors.hh @@ -0,0 +1,26 @@ +#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() + : Error{"storage is not initialized"} + { + } +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_ERRORS_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..390c2cb4fd462068457288aefe93b9d200393bf0 --- /dev/null +++ b/cpp/src/vereign/kvstore/sqlite_storage.cc @@ -0,0 +1,81 @@ +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/encoding/binary.hh> + +#include <array> + +namespace vereign::kvstore { + +SqliteStorage::SqliteStorage(const std::string& db_path) + : db_{db_path} +{ + db_.Execute(R"( +CREATE TABLE IF NOT EXISTS storage ( + key TEXT PRIMARY KEY NOT NULL, + value BLOB +); + )"); +} + +void SqliteStorage::PutBytes(const std::string& key, bytes::View value) { + auto tr = db_.BeginExplicitTransaction(); + + auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);"); + stmt.BindText(1, key); + stmt.BindBlob(2, value); + stmt.Step(); + + tr.Commit(); +} + +void SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) { + auto tr = db_.BeginExplicitTransaction(); + + auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); + stmt.BindText(1, key); + auto end = stmt.Step(); + + if (!end) { + value.Write(stmt.GetColumnBlob(0)); + } + + tr.Commit(); +} + +void SqliteStorage::PutInt64(const std::string& key, int64_t value) { + auto tr = db_.BeginExplicitTransaction(); + 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(); + + tr.Commit(); +} + +auto SqliteStorage::GetInt64(const std::string& key) -> int64_t { + auto tr = db_.BeginExplicitTransaction(); + + auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?"); + stmt.BindText(1, key); + auto end = stmt.Step(); + + if (end) { + return 0; + } + + auto buf = stmt.GetColumnBlob(0); + if (buf.Size() != 8) { + // FIXME: probably throw an exception here + return 0; + } + + int64_t result = encoding::binary::DecodeUint64(buf); + + tr.Commit(); + + return result; +} + +} // 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..ef91c50e0a40dec5b07c16355a13de1bf01b7d4a --- /dev/null +++ b/cpp/src/vereign/kvstore/sqlite_storage.hh @@ -0,0 +1,25 @@ +#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 { + +class SqliteStorage : public Storage { +public: + SqliteStorage(const std::string& db_path); + + void PutBytes(const std::string& key, bytes::View value) override; + void GetBytes(const std::string& key, bytes::Buffer& value) override; + + void PutInt64(const std::string& key, int64_t value) override; + auto GetInt64(const std::string& key) -> int64_t override; + +private: + sqlite::Connection db_; +}; + +} // 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..a8adf3d756d070b949f1ac9c443ec284fb357b07 --- /dev/null +++ b/cpp/src/vereign/kvstore/storage.hh @@ -0,0 +1,21 @@ +#ifndef __VEREIGN_KVSTORE_STORAGE_HH +#define __VEREIGN_KVSTORE_STORAGE_HH + +#include <vereign/bytes/buffer.hh> + +namespace vereign::kvstore { + +class Storage { +public: + virtual void PutBytes(const std::string& key, bytes::View value) = 0; + virtual void GetBytes(const std::string& key, bytes::Buffer& value) = 0; + + virtual void PutInt64(const std::string& key, int64_t value) = 0; + virtual auto GetInt64(const std::string& key) -> int64_t = 0; + + virtual ~Storage() = default; +}; + +} // namespace vereign::kvstore + +#endif // __VEREIGN_KVSTORE_STORAGE_HH diff --git a/cpp/src/vereign/restapi/client.cc b/cpp/src/vereign/restapi/client.cc index 1dd1b8cc9e874ce972ff6725af6ce3768759a4c5..fe22698c3330d009e9a727024e60853a39896029 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,6 +8,12 @@ #include <boost/beast/version.hpp> #include <boost/asio/dispatch.hpp> +#include <chrono> + +namespace { + constexpr std::string_view httpUserAgent = "Vereign Client Library"; +} + namespace vereign { namespace restapi { @@ -21,7 +25,8 @@ Client::Client( const std::string& host, const std::string& port ) - : executor_{asio::make_strand(ioc)}, + : user_agent_{httpUserAgent}, + executor_{asio::make_strand(ioc)}, ssl_ctx_{ssl_ctx}, resolver_{ioc}, reader_{nullptr}, @@ -42,7 +47,8 @@ Client::Client( const 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}, @@ -60,6 +66,10 @@ Client::~Client() noexcept { Close(); } +auto Client::UserAgent() const -> const std::string& { + return user_agent_; +} + void Client::Close() { asio::post( executor_, @@ -213,7 +223,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_); diff --git a/cpp/src/vereign/restapi/client.hh b/cpp/src/vereign/restapi/client.hh index 1bb1821f37dacbffdac972e6a5d3e75fe3415be0..cc8fe0d267bf2da188beb499e6fbcb68298b9a7c 100644 --- a/cpp/src/vereign/restapi/client.hh +++ b/cpp/src/vereign/restapi/client.hh @@ -102,6 +102,15 @@ public: Client(const Client&) = delete; Client& operator=(const 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. * @@ -310,6 +319,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_; diff --git a/cpp/src/vereign/restapi/client_session.hh b/cpp/src/vereign/restapi/client_session.hh index 28e848a7e270c52f09e083132a862c51e08caded..a94fdce3cb020f7ea9c1a11eec61044c4ba7c83d 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,24 @@ inline AuthError MakeAuthError( }; } -inline AuthError NoAuthError() { +inline auto NoAuthError() -> AuthError { return boost::none; } +struct Session { + 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 +66,12 @@ 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"}, + pub_key_{""} {} - // FIXME: the key should come from a storage /** * Constructs ClientSession instance. * @@ -77,18 +79,14 @@ 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)}, + pub_key_{""} {} // Disable copying. ClientSession(const ClientSession&) = delete; - ClientSession& operator=(const ClientSession&) = delete; + auto operator=(const ClientSession&) -> ClientSession& = delete; /** * Closes the connection. @@ -97,6 +95,37 @@ 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::lock_guard<std::mutex> l{mu_}; + + pub_key_ = std::move(key); + session_token_ = ""; + uuid_ = ""; + } + + /** + * Retrieve the current public key used for authentication. + */ + auto GetPubKey() -> std::string { + std::lock_guard<std::mutex> l{mu_}; + + return pub_key_; + } + + /** + * Retrieve the underlying http client. + */ + auto GetClient() -> Client& { + return client_; + } + /** * Close closes the connection. * @@ -196,7 +225,72 @@ public: ); } + /** + * 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, client_library::EmptyResponse* resp) { + std::promise<void> promise; + auto future = promise.get_future(); + + { + std::lock_guard<std::mutex> l{mu_}; + pub_key_ = pub_key; + 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(); + } + + /** + * 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: + auto hasSession() -> bool { + std::lock_guard<std::mutex> l{mu_}; + + return session_token_.size() != 0; + } + + 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{ + session_token_, + uuid_ + }; + } template <class RequestPtr, class ResponsePtr, class CompletionFunc> void authPostAsync( @@ -216,15 +310,16 @@ 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", GetPubKey()}, + {"token", session.Token}, + {"uuid", session.UUID} }, std::bind( &ClientSession::handlePostResult<RequestPtr, ResponsePtr, CompletionFunc>, @@ -254,15 +349,16 @@ 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", GetPubKey()}, + {"token", session.Token}, + {"uuid", session.UUID} }, std::move(cf) ); @@ -300,7 +396,7 @@ private: asio::post( client_.GetExecutor(), [this, cf = std::move(cf)]() mutable { - if (session_token_.size() != 0) { + if (hasSession()) { cf(detail::NoAuthError()); return; } @@ -314,7 +410,7 @@ private: std::make_unique<client_library::EmptyRequest>(), std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>(), std::vector<vereign::restapi::HttpHeader>{ - {"publicKey", pub_key_} + {"publicKey", GetPubKey()} }, [this, cf = std::move(cf)] (ResultType&& result) mutable { if (result.Response->code() != "200") { @@ -335,8 +431,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 +441,26 @@ 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_; }; -} // namespace restapi } // namespace vereign #endif // __VEREIGN_RESTAPI_CLIENT_SESSION_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..3b6eae9866c4a068c7ef8ea6eae45d7b90722228 --- /dev/null +++ b/cpp/src/vereign/service/identity_service.cc @@ -0,0 +1,66 @@ +#include "vereign/service/gen/identity_service.hh" +#include "vereign/client_library/common_types.pb.h" +#include <vereign/service/identity_service.hh> + +#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( + identity::Provider& identity_provider, + restapi::ClientSession& client_session +) + : gen::IdentityService{client_session}, + identity_provider_{identity_provider}, + client_session_{client_session} +{} + +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_.ResetIdentity(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); + } +} + + +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, 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..02acf73d485a3d301657a7b975fb5edf70ed9a77 --- /dev/null +++ b/cpp/src/vereign/service/identity_service.hh @@ -0,0 +1,58 @@ +#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: + IdentityService( + identity::Provider& identity_provider, + restapi::ClientSession& client_session + ); + + IdentityService(const IdentityService&) = delete; + auto operator=(const IdentityService&) -> IdentityService& = delete; + + void LoginWithExistingPubKey( + const client_library::LoginWithExistingPubKeyForm* req, + client_library::EmptyResponse* resp + ); + + void LoginWithNewDevice( + const client_library::LoginFormNewDevice* req, + client_library::LoginFormNewDeviceResponse* resp + ); + + void LoginWithPreviouslyAddedDevice( + const client_library::LoginFormPreviousAddedDevice* req, + client_library::EmptyResponse* resp + ); + +private: + identity::Provider& identity_provider_; + restapi::ClientSession& client_session_; +}; + +} // namespace service +} // namespace vereign + + +#endif // __VEREIGN_SERVICE_IDENITY_SERVICE_HH diff --git a/cpp/src/vereign/sqlite/connection.cc b/cpp/src/vereign/sqlite/connection.cc new file mode 100644 index 0000000000000000000000000000000000000000..d5af4f388aff59a068d51de0879176379dead23f --- /dev/null +++ b/cpp/src/vereign/sqlite/connection.cc @@ -0,0 +1,169 @@ +#include <vereign/sqlite/connection.hh> +#include <vereign/sqlite/errors.hh> + +#include <vereign/core/scope_guard.hh> + +#include <fmt/format.h> +#include <sqlite3.h> +#include <memory> + +namespace vereign::sqlite { + +Transaction::Transaction(sqlite3* db) + : finished_{false}, + db_{db} +{} + +Transaction::Transaction(Transaction&& other) + : finished_{other.finished_}, + db_{other.db_} +{ + other.finished_ = true; +} + +void Transaction::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)); + } + + finished_ = true; +} + +void Transaction::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)); + } + + finished_ = true; +} + +Transaction::~Transaction() { + if (!finished_) { + Rollback(); + } +} + +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_))}; + } +} + +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_); +} + +auto Connection::BeginExplicitTransaction() -> Transaction { + 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)); + } + + return Transaction{db_}; +} + + +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..ffa4ef85b885675e8bd662c9bd436ec492a1a675 --- /dev/null +++ b/cpp/src/vereign/sqlite/connection.hh @@ -0,0 +1,78 @@ +#ifndef __VEREIGN_SQLITE_CONNECTION_HH +#define __VEREIGN_SQLITE_CONNECTION_HH + +#include <vereign/bytes/view.hh> +#include <vereign/bytes/bytes.hh> + +#include <string> + +struct sqlite3; +struct sqlite3_stmt; + +namespace vereign::sqlite { + +class Transaction { +private: + friend class Connection; + + explicit Transaction(sqlite3* db); + Transaction(Transaction&&); + +public: + void Commit(); + void Rollback(); + + ~Transaction(); + + Transaction(const Transaction&) = delete; + auto operator=(const Transaction&) -> Transaction& = delete; + +private: + bool finished_; + sqlite3* db_; +}; + +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_; +}; + +class Connection { +public: + Connection(const std::string& path); + ~Connection() noexcept; + + Connection(const Connection&) = delete; + auto operator=(const Connection&) -> Connection& = delete; + + auto BeginExplicitTransaction() -> Transaction; + 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..14fa1d81431e2cb4dfe555d292b1952f10bd80c7 --- /dev/null +++ b/cpp/src/vereign/sqlite/errors.hh @@ -0,0 +1,30 @@ +#ifndef __VEREIGN_SQLITE_ERROR_HH +#define __VEREIGN_SQLITE_ERROR_HH + +#include <stdexcept> + +namespace vereign::sqlite { + +class Error : virtual std::exception { +public: + Error(int code, std::string&& msg) + : code_{code}, + msg_{std::move(msg)} + {} + + auto what() const noexcept -> const char* override { + return msg_.data(); + } + + auto code() const noexcept -> int { + return code_; + } + +private: + int code_; + std::string msg_; +}; + +} // namespace vereign::sqlite + +#endif // __VEREIGN_SQLITE_CONNECTION_HH diff --git a/cpp/tests/vereign/CMakeLists.txt b/cpp/tests/vereign/CMakeLists.txt index 2c2b6fce316e7721ed05a80e53330200081a4486..31ba5f294ee8062cf252096588ebfa17d9f289f9 100644 --- a/cpp/tests/vereign/CMakeLists.txt +++ b/cpp/tests/vereign/CMakeLists.txt @@ -14,11 +14,27 @@ list(APPEND tests_src init_tests.cc ../util/protobuf.cc ../experiment/array.cc + test/device.cc + test/service_context.cc sync/channel_test.cc + + encoding/base64_test.cc + + crypto/aes_test.cc + crypto/rsa_test.cc + crypto/digest_test.cc + restapi/client_test.cc restapi/client_session_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 ) diff --git a/cpp/tests/vereign/crypto/aes_test.cc b/cpp/tests/vereign/crypto/aes_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..4989c0675fcca437d527000e0924f3662b311e74 --- /dev/null +++ b/cpp/tests/vereign/crypto/aes_test.cc @@ -0,0 +1,26 @@ +#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("aes::GCM256Encrypt", "[vereign/crypto/aes][vereign/crypto]") { + const std::string data{"foo bar"}; + bytes::Buffer key{32}; + crypto::Rand(key); + + bytes::Buffer iv; + bytes::Buffer tag; + bytes::Buffer encrypted; + + crypto::aes::GCM256Encrypt(bytes::View(data), key.View(), iv, encrypted, tag); + + 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/digest_test.cc b/cpp/tests/vereign/crypto/digest_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..0bf6018f3bcc7c1120dc699e8d80024f2c4f3cbb --- /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("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/rsa_test.cc b/cpp/tests/vereign/crypto/rsa_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..c9356e1f456547b6137ca8423709e7c75ba9de55 --- /dev/null +++ b/cpp/tests/vereign/crypto/rsa_test.cc @@ -0,0 +1,102 @@ +#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(4096); + + 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(4096); + + 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(4096); + + bytes::Buffer input{470}; + crypto::Rand(input); + 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(4096); + + bytes::Buffer input{471}; + crypto::Rand(input); + 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/encoding/base64_test.cc b/cpp/tests/vereign/encoding/base64_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..2038a634f9a27e24b85b77aec670b7d432e2be52 --- /dev/null +++ b/cpp/tests/vereign/encoding/base64_test.cc @@ -0,0 +1,87 @@ +#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") { + bytes::Buffer input{1024}; + crypto::Rand(input); + 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/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/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..e00956cea6e85bed97ea8598a73c3397f0ec6405 100644 --- a/cpp/tests/vereign/grpc/server_test.cc +++ b/cpp/tests/vereign/grpc/server_test.cc @@ -14,7 +14,7 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST"); auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https"); - vereign::grpc::Server server{"localhost:", host, port, publicKey}; + vereign::grpc::Server server{"localhost:", host, port}; auto on_exit = vereign::core::MakeScopeGuard([&server] { server.Shutdown(); }); @@ -34,9 +34,9 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") { // std::cout << vereign::test::ProtobufToJson(resp) << std::endl; REQUIRE(status.error_message() == ""); - REQUIRE(resp.error() == ""); + 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); diff --git a/cpp/tests/vereign/identity/provider_test.cc b/cpp/tests/vereign/identity/provider_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..d72215ea12119f3c592228a27ac24db60cc05285 --- /dev/null +++ b/cpp/tests/vereign/identity/provider_test.cc @@ -0,0 +1,41 @@ +#include <vereign/identity/provider.hh> + +#include <vereign/core/fs.hh> +#include <vereign/core/temp.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 <catch2/catch.hpp> +#include <iostream> + +using namespace vereign; + +TEST_CASE("Provider::ResetIdentity", "[vereign/identity]") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + std::string expected; + std::string actual; + + { + kvstore::SqliteStorage kvstorage{storage_path.string()}; + kvstore::CryptoStorage storage{kvstorage}; + + identity::Provider provider{storage}; + expected = provider.ResetIdentity("foo"); + } + + { + kvstore::SqliteStorage kvstorage{storage_path.string()}; + kvstore::CryptoStorage storage{kvstorage}; + + identity::Provider provider{storage}; + actual = provider.LoadIdentity("foo"); + } + + CHECK(expected.size() > 0); + CHECK(actual.size() > 0); + CHECK(expected == actual); +} 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..b381e8e373d18c05e4525f4458eab50f88dd5e98 --- /dev/null +++ b/cpp/tests/vereign/kvstore/crypto_storage_test.cc @@ -0,0 +1,63 @@ +#include <vereign/kvstore/crypto_storage.hh> + +#include <vereign/kvstore/sqlite_storage.hh> +#include <vereign/crypto/rand.hh> +#include <vereign/bytes/view_dump.hh> +#include <vereign/core/temp.hh> +#include <vereign/core/fs.hh> +#include <vereign/core/scope_guard.hh> + +#include <catch2/catch.hpp> +#include <boost/filesystem.hpp> +#include <iostream> + +using namespace vereign; + +TEST_CASE("CryptoStorage::Reset", "[vereign/kvstore]") { + auto storage_path = core::TempFilePath("test_db_"); + core::RemoveFileGuard rm{storage_path}; + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstore::CryptoStorage storage{kvstorage}; + + storage.Reset("foo"); + std::string v{"test value"}; + storage.PutBytes("test", bytes::View(v)); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstore::CryptoStorage storage{kvstorage}; + + bytes::Buffer v; + storage.Open("foo"); + storage.GetBytes("test", v); + + CHECK(v.View().String() == "test value"); + } + + bytes::Buffer big_value{100000}; + crypto::Rand(big_value); + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstore::CryptoStorage storage{kvstorage}; + + storage.Reset("foo"); + storage.PutBytes("test", big_value.View()); + } + + { + auto kvstorage = kvstore::SqliteStorage(storage_path.string()); + kvstore::CryptoStorage storage{kvstorage}; + + bytes::Buffer v; + storage.Open("foo"); + 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); + } +} diff --git a/cpp/tests/vereign/restapi/client_test.cc b/cpp/tests/vereign/restapi/client_test.cc index 175501b247e0b321e04bd7aa39649094212f2783..c0c1a17198abca5ee1e50f85fe57675308d4d597 100644 --- a/cpp/tests/vereign/restapi/client_test.cc +++ b/cpp/tests/vereign/restapi/client_test.cc @@ -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..74a9516db0ed7a86f5b0a8f32fdf6918f8627586 100644 --- a/cpp/tests/vereign/service/gen/passport_service_test.cc +++ b/cpp/tests/vereign/service/gen/passport_service_test.cc @@ -1,11 +1,18 @@ #include <vereign/service/gen/passport_service.hh> + +#include <vereign/core/temp.hh> +#include <vereign/core/fs.hh> #include <vereign/core/scope_guard.hh> #include <vereign/client_library/types.gen.pb.h> +#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 <util/env.hh> #include <util/protobuf.hh> #include <catch2/catch.hpp> - #include <boost/asio/io_context.hpp> #include <boost/asio/ssl/context.hpp> #include <fmt/core.h> @@ -25,9 +32,17 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration] // 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::core::TempFilePath("test_db_"); + vereign::core::RemoveFileGuard rm{storage_path}; + + auto kvstorage = vereign::kvstore::SqliteStorage(storage_path.string()); + vereign::kvstore::CryptoStorage storage{kvstorage}; + vereign::identity::Provider provider{storage}; + vereign::service::IdentityService idenity_service{provider, client_session}; + std::thread ioc_thread([&ioc]{ ioc.run(); }); @@ -39,6 +54,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 +74,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..f793944772decf11e213a7f5502190d8873bae61 --- /dev/null +++ b/cpp/tests/vereign/service/identity_service_test.cc @@ -0,0 +1,121 @@ +#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/core/temp.hh> + +#include <util/env.hh> +#include <util/protobuf.hh> + +#include <catch2/catch.hpp> + +using namespace vereign; + +TEST_CASE("service::IdentityService::LoginWithNewDevice", "[vereign/service][.integration]") { + 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"); + + // the old device is used later for new device confirmation and authorization + auto old_device_ctx = test::ServiceContext{host, port, core::TempFilePath("test_db_")}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto service_context = test::ServiceContext{host, port, core::TempFilePath("test_db_")}; + auto service = service::IdentityService{ + service_context.IdentityProvider(), + service_context.ClientSession() + }; + + // 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 (auto& passport : list->data()) { + CHECK(passport.uuid().size() == 36); + } +} + +TEST_CASE("service::IdentityService::LoginWithPreviouslyAddedDevice", "[vereign/service][.integration]") { + 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"); + + // prepare new device + auto storage_path = core::TempFilePath("test_db_"); + auto old_device_ctx = test::ServiceContext{host, port, core::TempFilePath("test_db_")}; + 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.IdentityProvider(), + service_context.ClientSession() + }; + + 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 (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..6713ffbe147db3d8ea22fe91e4c7b6865e1ec044 --- /dev/null +++ b/cpp/tests/vereign/test/device.cc @@ -0,0 +1,122 @@ +#include <vereign/test/device.hh> + +#include <vereign/client_library/common_types.pb.h> +#include <vereign/client_library/types.gen.pb.h> +#include <vereign/core/temp.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_.IdentityProvider(), + service_context_.ClientSession() + )} +{ +} + +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.IdentityProvider(), + service_context.ClientSession() + }; + + // 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 boost::filesystem::path& storage_path +) { + auto old_device_ctx = test::ServiceContext{host, port, core::TempFilePath("test_db_")}; + auto old_device = test::Device{old_device_ctx}; + old_device.Login(public_key); + + auto service_context = test::ServiceContext{host, port, storage_path}; + old_device.CreateNewDevice(service_context, pin); +} + +} // 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..225289f7d1325072b4520ce48d1f403e1d206bb5 --- /dev/null +++ b/cpp/tests/vereign/test/device.hh @@ -0,0 +1,48 @@ +#ifndef __TESTS_VEREIGN_TEST_DEVICE_HH +#define __TESTS_VEREIGN_TEST_DEVICE_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 <memory> +#include <thread> + +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 boost::filesystem::path& 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..7f326c9cf81259d2c4fa76f1da9f0c05ff88ae2b --- /dev/null +++ b/cpp/tests/vereign/test/service_context.cc @@ -0,0 +1,61 @@ +#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 <vereign/core/temp.hh> + +#include <boost/filesystem/operations.hpp> + +namespace vereign::test { + +ServiceContext::ServiceContext( + const std::string& vereign_host, + const std::string& vereign_port, + boost::filesystem::path 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_.string())}, + storage_{std::make_unique<kvstore::CryptoStorage>(*sqlite_storage_)}, + identity_provider_{std::make_unique<identity::Provider>(*storage_)} +{ + service_thread_ = std::thread([this]() { + ioc_.run(); + }); +} + +auto ServiceContext::IdentityProvider() -> identity::Provider& { + return *identity_provider_; +} + +auto ServiceContext::ClientSession() -> restapi::ClientSession& { + return *client_session_; +} + +auto ServiceContext::StoragePath() const -> const boost::filesystem::path& { + return storage_path_; +} + +void ServiceContext::Shutdown() { + client_session_->Close(); + + work_guard_.reset(); + if (service_thread_.joinable()) { + service_thread_.join(); + } + + boost::filesystem::remove(storage_path_); +} + +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..a80e43657a49ce4f052e963c26fca8fafc8ea998 --- /dev/null +++ b/cpp/tests/vereign/test/service_context.hh @@ -0,0 +1,64 @@ +#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, + boost::filesystem::path storage_path + ); + ~ServiceContext(); + + ServiceContext(const ServiceContext&) = delete; + auto operator=(const ServiceContext&) -> ServiceContext& = delete; + + auto IdentityProvider() -> identity::Provider&; + auto ClientSession() -> restapi::ClientSession&; + auto StoragePath() const -> const boost::filesystem::path&; + + 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_; + boost::filesystem::path 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> +)