diff --git a/.gitignore b/.gitignore
index 02bce2e92c459988286bf0fede31144a407d64a2..683bcea58a3a309d3b90ee42ed144f6b3af3fd54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,11 +6,3 @@ Gopkg.lock
 temp/
 yarn-error.log
 /.project
-
-/cpp/cmake-build*
-/cpp/cmake-install*
-/cpp/compile_commands.json
-/cpp/.clangd
-/cpp/tags
-
-/cpp/docs/doxy
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 78e537aecaf90b57fd0de6eca8bf310b11e92cb6..c6336e75c17af7e4a7bd88867a7218453c82379b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,12 +1,10 @@
-image: registry.vereign.com/docker/docker:master
-services:
- - registry.vereign.com/docker/docker:dind-master
-
 variables:
   DOCKER_CLI_EXPERIMENTAL: 'enabled'
   JOB_IMAGE: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/$CI_JOB_NAME:$CI_COMMIT_REF_NAME
   MANIFEST_IMAGE:  $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:$CI_COMMIT_REF_NAME
-  CI_DEBUG_TRACE: "true"
+  IMAGE_amd64: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/amd64:$CI_COMMIT_REF_NAME
+  IMAGE_ppc64le: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/ppc64le:$CI_COMMIT_REF_NAME
+  CI_DEBUG_TRACE: "false"
 
 stages:
 - build
@@ -18,7 +16,7 @@ before_script:
 
 .build:
   script:
-    - docker build --build-arg CI_PROJECT_NAME=$CI_PROJECT_NAME --build-arg GITLAB_LOGIN=gitlab-ci-token --build-arg GITLAB_PASSWORD=$CI_JOB_TOKEN --pull -t $JOB_IMAGE -f Dockerfile .
+    - docker build --build-arg GITLAB_LOGIN=gitlab-ci-token --build-arg GITLAB_PASSWORD=$CI_JOB_TOKEN --pull -t $JOB_IMAGE -f Dockerfile .
     - docker push $JOB_IMAGE
 
 ppc64le:
@@ -37,9 +35,6 @@ amd64:
 
 manifest:
   stage: manifest
-  variables:
-    IMAGE_amd64: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/amd64:$CI_COMMIT_REF_NAME
-    IMAGE_ppc64le: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/ppc64le:$CI_COMMIT_REF_NAME
   tags:
     - ppc64le-docker
   script:
diff --git a/Dockerfile b/Dockerfile
index a072a900e07bee18ab81b1e4794745777199af8c..d088fec85bfda919e93a4b1811f27ac3c38fb0f8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM registry.vereign.com/docker/vcl-build-base:buster as builder
+FROM registry.vereign.com/docker/vcl-build-base:golang1.14.1 as builder
 
 ARG GITLAB_LOGIN
 ARG GITLAB_PASSWORD
@@ -11,5 +11,7 @@ RUN git config --global url."https://$GITLAB_LOGIN:$GITLAB_PASSWORD@code.vereign
 
 FROM registry.vereign.com/docker/go-runtime:master
 COPY --from=builder /go/src/code.vereign.com/code/vcl/javascript/dist /srv/dist
+COPY --from=builder /go/src/code.vereign.com/code/vcl/Gopkg.lock /srv/dist/
+
 ENTRYPOINT ["/bin/cp","-a","/srv/dist/.","/srv/target"]
 
diff --git a/cpp/.clang-format b/cpp/.clang-format
new file mode 100644
index 0000000000000000000000000000000000000000..4314079c156529a96bc58ba0c63a1f2b890a1ddd
--- /dev/null
+++ b/cpp/.clang-format
@@ -0,0 +1,33 @@
+---
+Language: Cpp
+AlignAfterOpenBracket: AlwaysBreak
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignConsecutiveMacros: true
+AlignOperands: false
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: false
+AllowAllConstructorInitializersOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Empty
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: false
+BinPackParameters: false
+ColumnLimit: 80
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ContinuationIndentWidth: 2
+Cpp11BracedListStyle: true
+FixNamespaceComments: true
+IndentWidth: 2
+ObjCBlockIndentWidth: 2
+PointerAlignment: Left
+SpacesInSquareBrackets: false
+Standard: Cpp11
+TabWidth: 2
+UseTab: Never
\ No newline at end of file
diff --git a/cpp/.gitignore b/cpp/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..48d08e65e0c7b04f833e2cb69913b72a86acc93c
--- /dev/null
+++ b/cpp/.gitignore
@@ -0,0 +1,6 @@
+/cmake-*
+/compile_commands.json
+/.clangd
+/tags
+
+/docs/doxy
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index abf9b756454dc84725418d3d5426339b5354583f..35a77a1a47dd339cb229a2fc1ab63a90f3605997 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -9,6 +9,10 @@ option(VEREIGN_USE_PRECOMPILED_HEADERS "Use precompiled headers" OFF)
 option(VEREIGN_USE_TIME_TRACE "Use compilation profiler" OFF)
 option(VEREIGN_ENABLE_BENCHMARKING "Enable tests benchmarks" OFF)
 
+if (UNIX AND NOT APPLE)
+  set(LINUX TRUE)
+endif()
+
 if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
   if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "19.0.24215.1")
     message(FATAL_ERROR "Microsoft Visual C++ version MSVC 19.0.24215.1 required")
@@ -91,6 +95,7 @@ set(_cmake_prefix_paths
   ${VENDOR_INSTALL_DIR}
   ${VENDOR_INSTALL_DIR}/grpc
   ${VENDOR_INSTALL_DIR}/nlohmann
+  ${VENDOR_INSTALL_DIR}/sqlite3
 )
 set(CMAKE_PREFIX_PATH ${_cmake_prefix_paths} CACHE STRING "")
 
@@ -115,7 +120,7 @@ find_package(
   1.72.0
   EXACT
   REQUIRED
-  COMPONENTS regex thread system date_time
+  COMPONENTS regex thread system date_time filesystem
 )
 
 find_package(Protobuf CONFIG REQUIRED)
@@ -137,6 +142,7 @@ else()
 endif()
 
 find_package(nlohmann_json 3.7.3 REQUIRED)
+find_package(SQLite3 REQUIRED)
 
 add_subdirectory("src")
 add_subdirectory("tests")
@@ -176,6 +182,7 @@ message(STATUS "summary of build options:
     Boost libs      ${Boost_LIBRARIES}
     gRPC            ${gRPC_FOUND} [${gRPC_VERSION}] (LIBS='${_grpc_libs}')
     nlohmann        ${nlohmann_json_FOUND} [${nlohmann_json_VERSION}] (HEADERS='${nlohmann_json_DIR}')
+    sqlite3         ${SQLite3_FOUND} [${SQLite3_VERSION}] (LIBS='${SQLite3_LIBRARIES}')
   Options:
     VEREIGN_USE_LLD ${VEREIGN_USE_LLD}
     VEREIGN_USE_PRECOMPILED_HEADERS ${VEREIGN_USE_PRECOMPILED_HEADERS}
diff --git a/cpp/include/vereign/vereign.h b/cpp/include/vereign/vereign.h
index c450bd819efba614e91f8c0cac23dfbe4c5503a3..f77824c16926b03dacd6b6cb9366d53a538474c6 100644
--- a/cpp/include/vereign/vereign.h
+++ b/cpp/include/vereign/vereign.h
@@ -1,5 +1,5 @@
-#ifndef VEREIGN_VEREIGN_H_
-#define VEREIGN_VEREIGN_H_
+#ifndef __VEREIGN_VEREIGN_H
+#define __VEREIGN_VEREIGN_H
 
 #ifdef _WIN32
   #ifdef WIN_EXPORT
@@ -84,8 +84,12 @@ typedef struct vereign_service vereign_service;
  * **NOTE: On failure the `err` object must be freed with vereign_error_free method.**
  *
  * @param listen_address gRPC listen address, for example "localhost:".
- * @param vereignHost Vereign restapi host.
- * @param vereignPort Vereign restapi port - https, 443...
+ * @param vereign_host Vereign restapi host.
+ * @param vereign_port Vereign restapi port - https, 443...
+ * @param storage_path Full path to directory where the storage files will stay.
+ *    If the `storage_path` is `nullptr`, a default will be used. Under linux this default is
+ *    `$HOME/vereign`, and under windows it is `C:\Users\<user>\AppData\Local\vereign`.
+ *
  * @param err On failure err is initialized with the reason of the failure,
  *    otherwise err is set to nullptr.
  * @returns vereign_service object if the gRPC is up and running, otherwise returns nullptr.
@@ -94,8 +98,7 @@ PUBLIC_API vereign_service* vereign_service_start(
   const char* listen_address,
   const char* vereign_host,
   const char* vereign_port,
-  // FIXME: public_key must come from a storage internally.
-  const char* public_key,
+  const char* storage_path,
   vereign_error** err
 );
 
@@ -122,4 +125,4 @@ PUBLIC_API void vereign_service_shutdown(vereign_service* service);
 };
 #endif
 
-#endif // VEREIGN_VEREIGN_H_
+#endif // __VEREIGN_VEREIGN_H
diff --git a/cpp/proto b/cpp/proto
index e861100984116aacf6d84cb8a09dc0ef81041509..cfea384107dec09a91ae566481432bfd1b6702dc 160000
--- a/cpp/proto
+++ b/cpp/proto
@@ -1 +1 @@
-Subproject commit e861100984116aacf6d84cb8a09dc0ef81041509
+Subproject commit cfea384107dec09a91ae566481432bfd1b6702dc
diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt
index 555515506f7c1d62b01e54cf2eeaeeb5401e783b..205e69aee397da56add3ed4d5514932d4461c041 100644
--- a/cpp/src/CMakeLists.txt
+++ b/cpp/src/CMakeLists.txt
@@ -3,9 +3,11 @@ if (fmt_FOUND)
 endif()
 
 if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
-  add_definitions(-DNOGDI)
+  add_definitions(-DNOGDI -DNOMINMAX)
 endif()
 
+add_definitions(-DBOOST_FILESYSTEM_NO_DEPRECATED)
+
 include_directories(
   ${CMAKE_CURRENT_BINARY_DIR}
   ${CMAKE_SOURCE_DIR}/src
@@ -16,6 +18,8 @@ include_directories(
 )
 
 file(GLOB PROTO_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/*.cc)
+file(GLOB PROTO_INTERNAL_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/internal/*.cc)
+list(APPEND PROTO_SRC ${PROTO_INTERNAL_SRC})
 list(APPEND PROTO_SRC
   ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/annotations.pb.cc
   ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/http.pb.cc
@@ -45,15 +49,63 @@ if (VEREIGN_USE_PRECOMPILED_HEADERS)
 endif()
 
 set(VEREIGNLIB_SRC
+  vereign/core/rand.cc
+  vereign/core/string.cc
+  vereign/core/time.cc
+  vereign/fs/util.cc
+  vereign/fs/operations.cc
+  vereign/fs/path.cc
+
   vereign/restapi/detail/http_reader.cc
   vereign/restapi/client.cc
-  vereign/service/passport_service.cc
+
   vereign/grpc/gen/gen.cc
   vereign/grpc/json/encoder.cc
   vereign/grpc/service_registry.cc
   vereign/grpc/server.cc
+
+  vereign/sqlite/statement.cc
+  vereign/sqlite/connection.cc
+
+  vereign/bytes/view_dump.cc
+  vereign/bytes/buffer.cc
+
+  vereign/encoding/binary.cc
+  vereign/encoding/base64.cc
+  vereign/encoding/hex.cc
+
+  vereign/crypto/rand.cc
+  vereign/crypto/aes.cc
+  vereign/crypto/rsa.cc
+  vereign/crypto/bio.cc
+  vereign/crypto/digest.cc
+  vereign/crypto/cert.cc
+
+  vereign/kvstore/lock.cc
+  vereign/kvstore/detail/base_crypto_storage.cc
+  vereign/kvstore/sqlite_storage.cc
+  vereign/kvstore/crypto_storage.cc
+  vereign/kvstore/detail/value_encoder.cc
+
+  vereign/event/broker.cc
+  vereign/identity/provider.cc
+
+  vereign/service/identity_service.cc
 )
 
+if (LINUX)
+  list(APPEND VEREIGNLIB_SRC
+    vereign/kvstore/detail/linux_crypto_storage.cc
+  )
+elseif (WIN32)
+  list(APPEND VEREIGNLIB_SRC
+    vereign/ncrypt/errors.cc
+    vereign/ncrypt/unique_ptr.cc
+    vereign/ncrypt/rsa.cc
+    vereign/kvstore/detail/win_crypto_storage.cc
+  )
+endif()
+
 file(GLOB GENERATED_SERVICES_SRC vereign/service/gen/*.cc)
 list(APPEND VEREIGNLIB_SRC ${GENERATED_SERVICES_SRC})
 
@@ -67,7 +119,11 @@ target_link_libraries(vereignlib PUBLIC
   fmt::fmt
   gRPC::grpc++_reflection
   gRPC::grpc++
-  $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
+  Boost::filesystem
+  Boost::date_time
+  SQLite::SQLite3
+  $<$<CXX_COMPILER_ID:MSVC>:ncrypt.lib>
+  $<$<CXX_COMPILER_ID:MSVC>:cryptui.lib>
 )
 
 add_library(vereign SHARED
@@ -93,9 +149,14 @@ set(csandbox_sources
 
 add_executable(csandbox ${csandbox_sources})
 
-target_link_libraries(csandbox
-  PRIVATE vereignlib
-  $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
+target_link_libraries(csandbox PRIVATE
+  vereignlib
+  # OpenSSL::Crypto
+  # OpenSSL::SSL
+  profiler
+  # $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
+  # Boost::filesystem
+  # Boost::file
   # Boost::thread
   # vereign
   # fmt::fmt
diff --git a/cpp/src/csandbox.cc b/cpp/src/csandbox.cc
index 561cf2123f21bd6b5ad1a9b6858d2fb5b2e83a7d..5cbbd22b6df93c5e2eab5ddd8e5d89a49e78efc4 100644
--- a/cpp/src/csandbox.cc
+++ b/cpp/src/csandbox.cc
@@ -1,4 +1,57 @@
+#include "vereign/client_library/event_types.pb.h"
+#include "vereign/client_library/internal/event_types.pb.h"
+#include "vereign/sync/channel.hh"
+#include "vereign/container/bounded_queue.hh"
+#include <thread>
+// #include "vereign/event/message_pool.hh"
+// #include <future>
+// #include <thread>
+// #include <vereign/crypto/cert.hh>
 
-auto main(int argc, char* argv[]) -> int {
-  return 0;
+// #include <vereign/crypto/rsa.hh>
+// #include <vereign/crypto/rand.hh>
+// #include <vereign/crypto/bio.hh>
+// #include <vereign/bytes/view_dump.hh>
+// #include <vereign/fs/util.hh>
+// #include <vereign/core/time.hh>
+// #include <boost/date_time.hpp>
+// #include <boost/pool/object_pool.hpp>
+
+// #include <openssl/x509v3.h>
+
+// #include <chrono>
+// #include <iostream>
+// #include <fstream>
+
+auto main(int argc, char** argv) -> int {
+  argc = 0;
+  argv = nullptr;
+
+  int64_t iterations = int64_t(1000)*1000*1000*10;
+
+  using clock = std::chrono::high_resolution_clock;
+  auto q = vereign::container::BoundedQueue<int>(10);
+
+  auto start = clock::now();
+  int64_t s = 0;
+
+  for (int64_t i = 0; i < iterations; i++) {
+    if (q.IsFull()) {
+      q.PopFront();
+    }
+
+    q.PushBack(i);
+  }
+
+  auto end = clock::now() - start;
+
+  std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end).count() << std::endl;
+  std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(end).count()/iterations << std::endl;
+
+  while (!q.IsEmpty()) {
+    s += q.Front();
+    q.PopFront();
+  }
+
+  std::cout << s << std::endl;
 }
diff --git a/cpp/src/vereign/bytes/buffer.cc b/cpp/src/vereign/bytes/buffer.cc
new file mode 100644
index 0000000000000000000000000000000000000000..546a4228f372ea30a4dc914b5c66f054ce1c1527
--- /dev/null
+++ b/cpp/src/vereign/bytes/buffer.cc
@@ -0,0 +1,180 @@
+#include <vereign/bytes/buffer.hh>
+
+#include <vereign/bytes/errors.hh>
+
+#include <memory>
+
+namespace vereign::bytes {
+
+Buffer::Buffer() noexcept
+  : cap_{0},
+    size_{0},
+    data_{nullptr}
+{
+}
+
+Buffer::Buffer(std::size_t cap)
+  : cap_{cap},
+    size_{0},
+    data_{nullptr}
+{
+  if (cap == 0) {
+    return;
+  }
+
+  data_ = reinterpret_cast<uint8_t*>(std::malloc(cap));
+  if (data_ == nullptr) {
+    throw std::bad_alloc{};
+  }
+}
+
+Buffer::Buffer(bytes::View src)
+  : cap_{src.Size()},
+    size_{src.Size()},
+    data_{nullptr}
+{
+  if (size_ == 0) {
+    return;
+  }
+
+  data_ = reinterpret_cast<uint8_t*>(std::malloc(cap_));
+  if (data_ == nullptr) {
+    throw std::bad_alloc{};
+  }
+
+  std::memcpy(data_, src.Data(), src.Size());
+}
+
+Buffer::Buffer(Buffer&& other) noexcept
+  : cap_{other.cap_},
+    size_{other.size_},
+    data_{other.data_}
+{
+  other.cap_ = 0;
+  other.size_ = 0;
+  other.data_ = nullptr;
+}
+
+auto Buffer::operator=(Buffer&& other) noexcept -> Buffer& {
+  std::swap(cap_, other.cap_);
+  std::swap(size_, other.size_);
+  std::swap(data_, other.data_);
+
+  return *this;
+}
+
+
+Buffer::~Buffer() {
+  std::free(data_);
+}
+
+
+auto Buffer::Size() const noexcept -> std::size_t {
+  return size_;
+}
+
+auto Buffer::FreeCap() const noexcept -> std::size_t {
+  return cap_ - size_;
+}
+
+auto Buffer::Cap() const noexcept -> std::size_t {
+  return cap_;
+}
+
+auto Buffer::begin() noexcept -> uint8_t* {
+  return data_;
+}
+
+auto Buffer::begin() const noexcept -> const uint8_t* {
+  return data_;
+}
+
+auto Buffer::end() noexcept -> uint8_t* {
+  return data_ + size_;
+}
+
+auto Buffer::end() const noexcept -> const uint8_t* {
+  return data_ + size_;
+}
+
+void Buffer::Reserve(std::size_t size) {
+  if (cap_ == 0) {
+    cap_ = size;
+    data_ = reinterpret_cast<uint8_t*>(std::malloc(cap_));
+    if (data_ == nullptr) {
+      throw std::bad_alloc{};
+    }
+
+    return;
+  }
+
+  if (size <= cap_ - size_) {
+    return;
+  }
+
+  auto cap = cap_ * 2;
+  if (size > cap - size_) {
+    cap = size_ + size;
+  }
+
+  auto newData = reinterpret_cast<uint8_t*>(std::realloc(data_, cap));
+  if (newData == nullptr) {
+    throw std::bad_alloc{};
+  }
+
+  data_ = newData;
+  cap_ = cap;
+}
+
+void Buffer::Reset() {
+  size_ = 0;
+}
+
+void Buffer::IncSize(std::size_t val) {
+  if (size_ + val > cap_) {
+    throw IncrementOutOfBounds{};
+  }
+
+  size_ += val;
+}
+
+auto Buffer::WriteWithinCap(bytes::View src) noexcept -> std::size_t {
+  auto size = std::min(cap_ - size_, src.Size());
+
+  std::memcpy(data_ + size_, src.Data(), size);
+  size_ += size;
+
+  return size;
+}
+
+auto Buffer::Write(bytes::View src) -> std::size_t {
+  Reserve(src.Size());
+
+  return WriteWithinCap(src);
+}
+
+auto Buffer::View() const noexcept -> bytes::View {
+  return bytes::View{data_, size_};
+}
+
+auto Buffer::View(std::size_t start) const noexcept -> bytes::View {
+  return bytes::View{data_, size_}.Slice(start);
+}
+
+auto Buffer::operator[](std::size_t index) -> uint8_t& {
+  if (index >= cap_) {
+    throw IndexOutOfBounds{};
+  }
+
+  return data_[index];
+}
+
+auto Buffer::operator[](std::size_t index) const -> const uint8_t& {
+  if (index >= cap_) {
+    throw IndexOutOfBounds{};
+  }
+
+  return data_[index];
+}
+
+} // namespace vereign::bytes
diff --git a/cpp/src/vereign/bytes/buffer.hh b/cpp/src/vereign/bytes/buffer.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6d0e5c7a8bddee458283afc810b3b79765e76968
--- /dev/null
+++ b/cpp/src/vereign/bytes/buffer.hh
@@ -0,0 +1,275 @@
+#ifndef __VEREIGN_BYTES_BUFFER_HH
+#define __VEREIGN_BYTES_BUFFER_HH
+
+#include <vereign/bytes/view.hh>
+#include <cstring>
+
+namespace vereign::bytes {
+
+/**
+ * Dynamically expandable memory buffer.
+ *
+ * The buffer is a 3-tuple - pointer, size and capacity.
+ * Typically used in functions for output parameters and return values.
+ * Provides API that is easy to use with C APIs.
+ *
+ * The buffer is move only.
+ */
+class Buffer {
+public:
+  /**
+   * Creates empty buffer.
+   */
+  Buffer() noexcept;
+
+  /**
+   * Creates a buffer with reserved memory capacity.
+   *
+   * The size of the buffer is zero.
+   *
+   * @param cap The capacity of the buffer.
+   *
+   * @throws std::bad_alloc when memory reservation fails.
+   */
+  Buffer(std::size_t cap);
+
+  /**
+   * Creates a buffer by copying from source bytes view.
+   *
+   * @param src The source that will be copied from.
+   *
+   * @throws std::bad_alloc when memory reservation fails.
+   */
+  Buffer(View src);
+
+  /**
+   * The buffer is movable.
+   */
+  Buffer(Buffer&& other) noexcept;
+  auto operator=(Buffer&& other) noexcept -> Buffer&;
+
+  // disable copying
+  Buffer(const Buffer&) = delete;
+  auto operator=(const Buffer&) -> Buffer& = delete;
+
+  /**
+   * Frees the buffer memory.
+   */
+  ~Buffer();
+
+  /**
+   * Returns a pointer to the first byte of the buffer.
+   *
+   * Buffer::begin(), Buffer::end() pair is useful for range loops.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("foo")};
+   * for (const auto& byte : buf) {
+   *   byte = 'x';
+   * }
+   *
+   * assert(buf.View().String() == "xxx");
+   * @endcode
+   */
+  auto begin() noexcept -> uint8_t*;
+
+  /**
+   * Returns a pointer to the first byte of the buffer.
+   *
+   * Buffer::begin(), Buffer::end() pair is useful for range loops.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("foo bar")};
+   * std::string s;
+   *
+   * for (const auto& byte : buf) {
+   *   s += byte;
+   * }
+   *
+   * assert(s == "foo bar");
+   * @endcode
+   */
+  auto begin() const noexcept -> const uint8_t*;
+
+  /**
+   * Returns a pointer to the byte following the last byte in the buffer.
+   *
+   * Note that this is the last byte in the range [0, size).
+   * It is often used when calling C APIs.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("foo bar")};
+   *
+   * buf.Reserve(4);
+   * std::strncpy((char*)buf.end(), " baz", 4);
+   * buf.IncSize(4);
+   *
+   * assert(buf.View().String() == "foo bar baz");
+   * @endcode
+   */
+  auto end() noexcept -> uint8_t*;
+
+  /**
+   * Returns a read only pointer to the byte following the last byte in the buffer.
+   *
+   * Note that this is the last byte in the range [0, size).
+   */
+  auto end() const noexcept -> const uint8_t*;
+
+  /**
+   * Access a byte in the range of [0, cap).
+   *
+   * @param index The index of the byte to access.
+   *
+   * @throws std::runtime_error when the passed index is out of bounds.
+   */
+  auto operator[](std::size_t index) -> uint8_t&;
+
+  /**
+   * Read only access a byte in the range of [0, cap).
+   *
+   * @param index The index of the byte to access.
+   *
+   * @throws std::runtime_error when the passed index is out of bounds.
+   */
+  auto operator[](std::size_t index) const -> const uint8_t&;
+
+  /**
+   * Retrieve buffer size.
+   *
+   * @returns the buffer size.
+   */
+  auto Size() const noexcept -> std::size_t;
+
+  /**
+   * Retrieve buffer capacity.
+   *
+   * @returns the buffer capacity.
+   */
+  auto Cap() const noexcept -> std::size_t;
+
+  /**
+   * Reserve memory, so that there is at least `size` free capacity.
+   *
+   * If there is already enough free capacity, no memory allocation is done.
+   * The allocated memory may be bigger than what is needed.
+   *
+   * If the call is successful then it is guaranteed that `this->FreeCap() >= size`.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("foo bar")};
+   *
+   * buf.Reserve(4); // ensure there will be a free capacity for 4 bytes
+   * std::strncpy((char*)buf.end(), " baz", 4); // copy the bytes
+   * buf.IncSize(4); // update the buffer size with the newly written bytes
+   *
+   * assert(buf.View().String() == "foo bar baz");
+   * @endcode
+   *
+   * @param size The desired free capacity.
+   *
+   * @throws std::bad_alloc when memory allocation fails.
+   */
+  void Reserve(std::size_t size);
+
+  /**
+   * Sets the buffer size to zero.
+   *
+   * This does not free any memory, so the capacity stays the intact, and the buffer can be reused.
+   */
+  void Reset();
+
+  /**
+   * Increments the size of the buffer.
+   *
+   * It is typically used after some function has written bytes to the end of the buffer.
+   *
+   * @param val The value that will be added to the current size.
+   */
+  void IncSize(std::size_t val);
+
+  /**
+   * Retrieve the buffer free capacity.
+   *
+   * This is equal to `this->Cap() - this->Size()`.
+   */
+  auto FreeCap() const noexcept -> std::size_t;
+
+  /**
+   * Adds bytes up to the currently available buffer capacity.
+   *
+   * After the operation succeeds, the buffer size will be incremented with the number of bytes
+   * that have been copied.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{3};
+   * buf.WriteWithinCap(bytes::View("foo bar"));
+   *
+   * // only 3 bytes are written
+   * assert(buf.View.String() == "foo");
+   * @endcode
+   *
+   * @param src The source that will be appended to the buffer.
+   * @returns The amount of bytes that were actually copied into the buffer.
+   */
+  auto WriteWithinCap(bytes::View src) noexcept -> std::size_t;
+
+  /**
+   * Adds a source view of bytes to the buffer.
+   *
+   * If the buffer does not have enough capacity, it will be expanded.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{3};
+   * buf.WriteWithinCap(bytes::View("foo bar"));
+   *
+   * // all bytes are written
+   * assert(buf.View.String() == "foo bar");
+   * @endcode
+   *
+   * @param The source that will be appended to the buffer.
+   * @returns The amount of bytes that were copied into the buffer. That is equal to src.Size().
+   */
+  auto Write(bytes::View src) -> std::size_t;
+
+  /**
+   * Retrieve a read only view of the buffer.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("123")};
+   * assert(buf.View().String() == "123");
+   * @endcode
+   *
+   * @returns a read only view range [0, this->Size()).
+   */
+  auto View() const noexcept -> bytes::View;
+
+  /**
+   * Retrieve a read only view of the buffer staring from a given offset.
+   *
+   * Example:
+   * @code
+   * auto buf = bytes::Buffer{bytes::View("123")};
+   * assert(buf.View(1).String() == "23");
+   * @endcode
+   *
+   * @returns a read only view range [start, this->Size()).
+   */
+  auto View(std::size_t start) const noexcept -> bytes::View;
+
+private:
+  std::size_t cap_;
+  std::size_t size_;
+  uint8_t* data_;
+};
+
+} // namespace vereign::bytes
+
+#endif // __VEREIGN_BYTES_BUFFER_HH
diff --git a/cpp/src/vereign/bytes/bytes.hh b/cpp/src/vereign/bytes/bytes.hh
new file mode 100644
index 0000000000000000000000000000000000000000..437a692f7d9b5cd753836a6bfc9d1c591fa5844a
--- /dev/null
+++ b/cpp/src/vereign/bytes/bytes.hh
@@ -0,0 +1,45 @@
+#ifndef __VEREIGN_BYTES_BYTES_HH
+#define __VEREIGN_BYTES_BYTES_HH
+
+#include <vereign/bytes/view.hh>
+#include <cstring>
+
+namespace vereign::bytes {
+
+class Bytes {
+public:
+  Bytes(uint8_t* data, std::size_t size)
+    : size_{size},
+      data_{data}
+  {}
+
+  Bytes(const Bytes&) = default;
+  auto operator=(const Bytes&) -> Bytes& = default;
+
+  auto Data() const -> uint8_t* {
+    return data_;
+  }
+
+  auto CharData() const -> unsigned char* {
+    return data_;
+  }
+
+  auto Size() const -> std::size_t {
+    return size_;
+  }
+
+private:
+  std::size_t size_;
+  uint8_t* data_;
+};
+
+inline auto Copy(Bytes dst, View src) noexcept -> std::size_t {
+  auto size = std::min(src.Size(), dst.Size());
+  std::memcpy(dst.Data(), src.Data(), size);
+
+  return size;
+}
+
+} // namespace vereign::bytes
+
+#endif // __VEREIGN_BYTES_BYTES_HH
diff --git a/cpp/src/vereign/bytes/errors.hh b/cpp/src/vereign/bytes/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ac7e9e66a800a30f340e6b3f0de9c9cbf959cabd
--- /dev/null
+++ b/cpp/src/vereign/bytes/errors.hh
@@ -0,0 +1,35 @@
+#ifndef __VEREIGN_BYTES_ERRORS_HH
+#define __VEREIGN_BYTES_ERRORS_HH
+
+#include <stdexcept>
+
+namespace vereign::bytes {
+
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error{what}
+  {
+
+  }
+};
+
+class IndexOutOfBounds : public Error {
+public:
+  IndexOutOfBounds()
+    : Error{"index out of bounds"}
+  {
+  }
+};
+
+class IncrementOutOfBounds : public Error {
+public:
+  IncrementOutOfBounds()
+    : Error{"cannot increment size pass the capacity"}
+  {
+  }
+};
+
+} // namespace vereign::bytes
+
+#endif // __VEREIGN_BYTES_ERRORS_HH
diff --git a/cpp/src/vereign/bytes/view.hh b/cpp/src/vereign/bytes/view.hh
new file mode 100644
index 0000000000000000000000000000000000000000..4520934c927170ed79682ab95d9f9ed5e430dee8
--- /dev/null
+++ b/cpp/src/vereign/bytes/view.hh
@@ -0,0 +1,211 @@
+#ifndef __VEREIGN_BYTES_VIEW_HH
+#define __VEREIGN_BYTES_VIEW_HH
+
+#include <vereign/bytes/errors.hh>
+
+#include <cstring>
+#include <string>
+#include <string_view>
+#include <stdexcept>
+#include <algorithm>
+
+namespace vereign::bytes {
+
+/**
+ * Bytes view represents a read only access to a range of bytes.
+ *
+ * The View is a 2-tuple with pointer and size.
+ * Typically used in functions for input parameters.
+ *
+ * **NOTE: The View does not own the memory that it references.**
+ */
+class View {
+public:
+  /**
+   * Creates empty view.
+   */
+  View() = default;
+
+  /**
+   * Creates a view from raw pointer and a size.
+   *
+   * @param data Pointer to the memory.
+   * @param size The size of the memory.
+   */
+  View(const uint8_t* data, std::size_t size) noexcept
+    : size_{size},
+      data_{data}
+  {
+  }
+
+  /**
+   * Create a view from a string view.
+   *
+   * @param str The input string view.
+   */
+  View(std::string_view str) noexcept
+    : size_{str.length()},
+      data_{str.length() > 0 ? reinterpret_cast<const uint8_t*>(str.data()): nullptr}
+  {
+  }
+
+  /**
+   * Creates a view from wide string view.
+   *
+   * @param str The input string.
+   */
+  View(std::wstring_view str) noexcept
+    : size_{str.length() * sizeof(wchar_t)},
+      data_{size_ > 0 ? reinterpret_cast<const uint8_t*>(str.data()): nullptr}
+  {
+  }
+
+  /**
+   * Creates a view from void pointer and a size.
+   *
+   * @param data Pointer to the memory.
+   * @param size The size of the memory.
+   */
+  View(const void* ptr, std::size_t size) noexcept
+    : size_{size},
+      data_{static_cast<const uint8_t*>(ptr)}
+  {
+  }
+
+  // default copyable.
+  View(const View&) = default;
+  auto operator=(const View&) -> View& = default;
+
+  /**
+   * Slice returns a new view in the interval [start, size).
+   *
+   * If the start is bigger than the size of the slice it returns empty view.
+   *
+   * @param start The beginning of the new View.
+   * @returns a new view in the interval [start, size).
+   */
+  auto Slice(std::size_t start) const -> View {
+    if (start >= size_) {
+      return View(data_, 0);
+    }
+
+    return View(data_ + start, size_ - start);
+  }
+
+  /**
+   * Slice returns a new view in the interval [start, end).
+   *
+   * If the start is bigger than the size of the slice it returns empty view.
+   * If the end is bigger than the size of the slice it returns [start, size).
+   *
+   * @param start The beginning of the new View.
+   * @returns a new view in the interval [start, size).
+   */
+  auto Slice(std::size_t start, std::size_t end) const -> View {
+    if (start >= size_) {
+      return View(data_, 0);
+    }
+
+    return View(data_ + start, std::min(size_, end) - start);
+  }
+
+  /**
+   * Retrieve a pointer to the data.
+   *
+   * @returns a pointer to the data.
+   */
+  auto Data() const noexcept -> const uint8_t* {
+    return data_;
+  }
+
+  /**
+   * Retrieve a char pointer to the data.
+   *
+   * @returns a char pointer to the data.
+   */
+  auto CharData() const noexcept -> const char* {
+    return reinterpret_cast<const char*>(data_);
+  }
+
+  /**
+   * Retrieve a wide char pointer to the data.
+   *
+   * @returns a wide char pointer to the data.
+   */
+  auto WideCharData() const noexcept -> const wchar_t* {
+    return reinterpret_cast<const wchar_t*>(data_);
+  }
+
+  /**
+   * Retrieve view size.
+   *
+   * @returns view size.
+   */
+  auto Size() const noexcept -> std::size_t {
+    return size_;
+  }
+
+  /**
+   * Retrieve a string view of the data.
+   *
+   * @returns a string view of the data.
+   */
+  auto String() const noexcept -> std::string_view {
+    return std::string_view{CharData(), size_};
+  }
+
+  /**
+   * Retrieve a wide string view of the data.
+   *
+   * @returns a wide string view of the data.
+   */
+  auto WideString() const noexcept -> std::wstring_view {
+    return std::wstring_view{WideCharData(), size_/sizeof(wchar_t)};
+  }
+
+  /**
+   * Binary compare the contents of two views.
+   *
+   * @returns true if the views are of the same size and if all the bytes in the two views are equal.
+   */
+  auto operator==(View other) const noexcept -> bool {
+    if (size_ != other.size_) {
+      return false;
+    }
+
+    return std::memcmp(data_, other.data_, size_) == 0;
+  }
+
+  /**
+   * Binary compare the contents of two views.
+   *
+   * @returns true if the views are of different size or if the bytes in the two views are not equal.
+   */
+  auto operator!=(View other) const noexcept -> bool {
+    return !(*this == other);
+  }
+
+  /**
+   * Access a single byte in the view.
+   *
+   * @param index The index of the byte that will be returned.
+   * @returns the byte at the specified index.
+   *
+   * @throws bytes::IndexOutOfBounds when the index is out of bounds.
+   */
+  auto operator[](std::size_t index) const -> const uint8_t& {
+    if (index >= size_ ) {
+      throw IndexOutOfBounds{};
+    }
+
+    return data_[index];
+  }
+
+private:
+  std::size_t size_ = 0;
+  const uint8_t* data_ = nullptr;
+};
+
+} // namespace vereign::bytes
+
+#endif // __VEREIGN_BYTES_VIEW_HH
diff --git a/cpp/src/vereign/bytes/view_dump.cc b/cpp/src/vereign/bytes/view_dump.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7d892bb9e350710109b6b34d3472e7334fd3690e
--- /dev/null
+++ b/cpp/src/vereign/bytes/view_dump.cc
@@ -0,0 +1,64 @@
+#include <vereign/bytes/view_dump.hh>
+
+#include <iomanip>
+
+namespace vereign::bytes {
+
+namespace detail {
+
+auto operator<<(std::ostream& os, const ViewDump& vd) -> std::ostream& {
+  if (vd.buf_.Size() == 0) {
+    os << "empty buffer\n";
+    return os;
+  }
+
+  core::IosFlagsLock os_flags(os);
+
+  os << std::hex;
+
+  for (std::size_t i = 0; i < vd.buf_.Size(); ++i) {
+    if (i % 16 == 0) {
+      os << "0x" << std::setfill('0') << std::setw(4) << i << ": ";
+    }
+
+    if (i % 2 == 0) {
+      os << " ";
+    }
+
+    os << std::setfill('0') << std::setw(2) << (unsigned int) vd.buf_.Data()[i];
+
+    if ((i != 0 && (i + 1) % 16 == 0) || i == vd.buf_.Size() - 1) {
+      int bytes = (i + 1) % 16 == 0 ? 0 : (16 - (i + 1) % 16);
+      os << "  " << std::setfill(' ') << std::setw(2 * bytes + bytes / 2) << "";
+      vd.printChars(os, i - i % 16);
+      os << "\n";
+    }
+  }
+
+  return os;
+}
+
+void ViewDump::printChars(std::ostream& os, std::size_t offset) const {
+  auto seq = buf_.Slice(offset, offset + 16);
+  for (int i = 0; i < (int) seq.Size(); ++i) {
+    auto ch = seq.Data()[i];
+    if (std::isprint(ch) != 0) {
+      os << ch;
+    } else {
+      os << ".";
+    }
+  }
+}
+
+} // namespace detail
+
+
+auto dump(View buf) -> detail::ViewDump {
+  return detail::ViewDump{buf};
+}
+
+auto dump(const Buffer& buf) -> detail::ViewDump {
+  return detail::ViewDump{buf.View()};
+}
+
+} // namespace vereign::bytes
diff --git a/cpp/src/vereign/bytes/view_dump.hh b/cpp/src/vereign/bytes/view_dump.hh
new file mode 100644
index 0000000000000000000000000000000000000000..47625f49016bee9fd8181dbc66746d54b7b3131f
--- /dev/null
+++ b/cpp/src/vereign/bytes/view_dump.hh
@@ -0,0 +1,92 @@
+#ifndef __VEREIGN_BYTES_VIEW_DUMP_HH
+#define __VEREIGN_BYTES_VIEW_DUMP_HH
+
+#include <vereign/bytes/view.hh>
+#include <vereign/bytes/buffer.hh>
+#include <vereign/core/io_flags_lock.hh>
+#include <iostream>
+
+namespace vereign::bytes {
+
+namespace detail {
+class ViewDump;
+}
+
+/**
+ * Returns a view dump object that can write the view dump into std::ostream.
+ *
+ * The dump is the good old hexadecimal format.
+ *
+ * Example:
+ * @code
+ * std::string input{"foo bar"};
+ * std::cout << bytes::dump(bytes::View(input)) << std::endl;
+ *
+ * // Output:
+ * // 0x0000:  666f 6f20 6261 72                        foo bar
+ *
+ * auto buf = crypto::Rand(32);
+ * std::cout << bytes::dump(buf.View()) << std::endl;
+ *
+ * // Output:
+ * // 0x0000:  1666 855a 650a 7549 ed0f ec01 4d87 09bf  .f.Ze.uI....M...
+ * // 0x0010:  e644 dbc8 7943 37b4 185c dbab 4977 ff3f  .D..yC7..\..Iw.?
+ *
+ * @endcode
+ */
+auto dump(View buf) -> detail::ViewDump;
+
+/**
+ * Returns a dump object that can write the buffer dump into std::ostream.
+ *
+ * The dump is the good old hexadecimal format.
+ *
+ * Example:
+ * @code
+  auto buf = bytes::Buffer{bytes::View("foo bar")};
+  std::cout << bytes::dump(buf) << std::endl;
+ *
+ * // Output:
+ * // 0x0000:  666f 6f20 6261 72                        foo bar
+ *
+ * auto buf = crypto::Rand(32);
+ * std::cout << bytes::dump(buf) << std::endl;
+ *
+ * // Output:
+ * // 0x0000:  1666 855a 650a 7549 ed0f ec01 4d87 09bf  .f.Ze.uI....M...
+ * // 0x0010:  e644 dbc8 7943 37b4 185c dbab 4977 ff3f  .D..yC7..\..Iw.?
+ *
+ * @endcode
+ */
+auto dump(const Buffer& buf) -> detail::ViewDump;
+
+namespace detail {
+
+/**
+ * Helper for dumping a view to a std::ostream.
+ *
+ * Users typically will use the bytes::dump functions.
+ */
+class ViewDump {
+public:
+  auto operator=(const ViewDump&) -> ViewDump& = delete;
+
+  explicit ViewDump(View buf)
+    : buf_(buf)
+  {
+  }
+
+  friend auto operator<<(std::ostream& os, const ViewDump& vd) -> std::ostream&;
+
+private:
+  void printChars(std::ostream& os, std::size_t offset) const;
+
+private:
+  View buf_;
+};
+
+} // namespace detail
+
+} // namespace vereign::bytes
+
+#endif // __VEREIGN_BYTES_VIEW_DUMP_HH
diff --git a/cpp/src/vereign/container/bounded_queue.hh b/cpp/src/vereign/container/bounded_queue.hh
new file mode 100644
index 0000000000000000000000000000000000000000..fbb2ab64b0110e3dd9d5ca0219c9902ab9ee06df
--- /dev/null
+++ b/cpp/src/vereign/container/bounded_queue.hh
@@ -0,0 +1,139 @@
+#ifndef __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
+#define __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
+
+#include <vector>
+
+namespace vereign::container {
+
+/**
+ * A queue (FIFO) data structure with fixed size.
+ *
+ * Being a fixed in its size, the BoundedQueue has the opportunity to internally allocate only once
+ * upon construction a single continues block of memory for holding the queue elements.
+ */
+template <typename T>
+class BoundedQueue {
+public:
+  /**
+   * The type of the values in the queue.
+   */
+  using ValueType = T;
+
+  // Helper type that is properly aligned so that it can act as a storage for the ValueType.
+  using StorageType = typename std::aligned_storage_t<sizeof(T), alignof(T)>;
+
+  /**
+   * Creates a queue with given capacity.
+   *
+   * The capacity cannot be changed after construction.
+   */
+  BoundedQueue(std::size_t capacity)
+    : begin_{0},
+      size_{0},
+      cap_{capacity},
+      queue_{new StorageType[capacity]}
+  {
+  }
+
+  /**
+   * Destroy the elements if any, and free all the memory.
+   */
+  ~BoundedQueue() {
+    while (!IsEmpty()) {
+      PopFront();
+    }
+
+    delete [] queue_;
+  }
+
+  // disable copying
+  BoundedQueue(const BoundedQueue&) = delete;
+  auto operator=(const BoundedQueue&) -> BoundedQueue& = delete;
+
+  /**
+   * Retrieve the number of elements in the queue.
+   *
+   * @returns the number of elements in the queue.
+   */
+  auto Size() -> std::size_t {
+    return size_;
+  }
+
+  /**
+   * Check if the queue is empty.
+   *
+   * @returns true when the queue is empty, false otherwise.
+   */
+  auto IsEmpty() -> bool {
+    return size_ == 0;
+  }
+
+  /**
+   * Check if the queue is full up to its capacity.
+   *
+   * @returns true when the queue is full, false otherwise.
+   */
+  auto IsFull() -> bool {
+    return size_ == cap_;
+  }
+
+  /**
+   * Moves a value to the end of the queue.
+   *
+   * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour.
+   */
+  void PushBack(ValueType&& value) {
+    auto pos = (begin_ + size_) % cap_;
+
+    new(&queue_[pos]) T(std::move(value));
+
+    size_++;
+  }
+
+  /**
+   * Copies a value to the end of the queue.
+   *
+   * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour.
+   */
+  void PushBack(const ValueType& value) {
+    auto pos = (begin_ + size_) % cap_;
+
+    new(&queue_[pos]) T(value);
+
+    size_++;
+  }
+
+  /**
+   * Access the first value in the queue.
+   *
+   * @note Calling BoundedQueue::Front on empty queue is undefined behaviour.
+   */
+  auto Front() -> ValueType& {
+    return reinterpret_cast<ValueType&>(queue_[begin_]);
+  }
+
+  /**
+   * Remove the fist item from the queue.
+   *
+   * @note Calling BoundedQueue::PopFront on empty queue is undefined behaviour.
+   */
+  void PopFront() {
+    reinterpret_cast<ValueType*>(&queue_[begin_])->~ValueType();
+
+    size_--;
+    begin_ = (begin_ == cap_ - 1) ? 0 : begin_ + 1;
+  }
+
+private:
+  std::size_t begin_;
+
+  std::size_t size_;
+
+  std::size_t cap_;
+
+  StorageType* queue_;
+};
+
+} // namespace vereign::container
+
+#endif // __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
diff --git a/cpp/src/vereign/core/hex.hh b/cpp/src/vereign/core/hex.hh
deleted file mode 100644
index a370a6e98f39594c98578e1c455a8e390881b69e..0000000000000000000000000000000000000000
--- a/cpp/src/vereign/core/hex.hh
+++ /dev/null
@@ -1,55 +0,0 @@
-#ifndef __VEREIGN_CORE_HEX_HH
-#define __VEREIGN_CORE_HEX_HH
-
-#include <string>
-
-namespace vereign {
-namespace core {
-namespace detail {
-
-inline auto charToInt(char ch) -> int {
-  if (ch >= '0' && ch <= '9') {
-    return ch - '0';
-  }
-
-  if (ch >= 'A' && ch <= 'F') {
-    return ch - 'A' + 10;
-  }
-
-  if (ch >= 'a' && ch <= 'f') {
-    return ch - 'a' + 10;
-  }
-
-  return 0;
-}
-
-} // namespace detail
-
-inline auto BinToHex(const unsigned char* src, std::size_t size) -> std::string {
-  static const char* nibbles = { "0123456789abcdef" };
-
-  std::string result;
-  result.reserve(size * 2);
-
-  for (std::size_t i = 0; i < size; ++i) {
-    result.push_back(nibbles[src[i] >> 4]);
-    result.push_back(nibbles[src[i] & 0x0F]);
-  }
-
-  return result;
-}
-
-inline auto BinToHex(const std::string& src) -> std::string {
-  return BinToHex((unsigned char*) src.c_str(), src.size());
-}
-
-inline void hex_to_bin(const std::string& src, unsigned char* dst) {
-  for (int i = 0, len = (int) src.size() - 1; i < len; i += 2) {
-    dst[i/2] = detail::charToInt(src[i]) * 16 + detail::charToInt(src[i + 1]);
-  }
-}
-
-} // namespace core
-} // namespace vereign
-
-#endif // __VEREIGN_CORE_HEX_HH
diff --git a/cpp/src/vereign/core/io_flags_lock.hh b/cpp/src/vereign/core/io_flags_lock.hh
new file mode 100644
index 0000000000000000000000000000000000000000..8a6feda70736c794162f434650c8cbbb34815691
--- /dev/null
+++ b/cpp/src/vereign/core/io_flags_lock.hh
@@ -0,0 +1,37 @@
+#ifndef __VEREIGN_CORE_IOS_FLAGS_LOCK_HH
+#define __VEREIGN_CORE_IOS_FLAGS_LOCK_HH
+
+#include <ios>
+
+namespace vereign::core {
+
+/**
+ * Restore the ios flags of a stream when is it self destroyed.
+ */
+class IosFlagsLock {
+public:
+  IosFlagsLock(const IosFlagsLock&) = delete;
+  auto operator=(const IosFlagsLock&) -> IosFlagsLock& = delete;
+
+  IosFlagsLock(std::ios_base& str)
+    : str_(str),
+    flags_(str.flags())
+  {
+  }
+
+  ~IosFlagsLock() {
+    restore();
+  }
+
+  void restore() {
+    str_.flags(flags_);
+  }
+
+private:
+  std::ios_base& str_;
+  std::ios_base::fmtflags flags_;
+};
+
+} // namespace vereign::core
+
+#endif // __TURBO_CORE_IOS_FLAGS_LOCK_HH
diff --git a/cpp/src/vereign/core/lock_guard.hh b/cpp/src/vereign/core/lock_guard.hh
new file mode 100644
index 0000000000000000000000000000000000000000..9e310b21ca53ce5c53fea581a483ecaa568004f4
--- /dev/null
+++ b/cpp/src/vereign/core/lock_guard.hh
@@ -0,0 +1,28 @@
+#ifndef __VEREIGN_LOCK_GUARD_HH
+#define __VEREIGN_LOCK_GUARD_HH
+
+namespace vereign::core {
+
+template <class Lockable>
+class LockGuard {
+public:
+  explicit LockGuard(Lockable& lock)
+    : lock_{lock}
+  {
+    lock.Lock();
+  }
+
+  ~LockGuard() noexcept {
+    lock_.Unlock();
+  }
+
+  LockGuard(const LockGuard&) = delete;
+  auto operator=(const LockGuard&) -> LockGuard&  = delete;
+
+private:
+  Lockable& lock_;
+};
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_LOCK_GUARD_HH
diff --git a/cpp/src/vereign/core/rand.cc b/cpp/src/vereign/core/rand.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7e65a25ae0c2e45a79e34328edd05d54e53c9df1
--- /dev/null
+++ b/cpp/src/vereign/core/rand.cc
@@ -0,0 +1,30 @@
+#include <vereign/core/rand.hh>
+
+#include <random>
+
+namespace vereign::core {
+
+auto RandNumber(int n) -> int {
+  static std::random_device dev;
+  static std::mt19937 rng(dev());
+  using Dist = std::uniform_int_distribution<std::mt19937::result_type>;
+  static Dist dist{};
+
+  return dist(dev, Dist::param_type{0, static_cast<unsigned long>(n) - 1} );
+}
+
+auto RandLowerAlpha(int len) -> std::string {
+  using namespace std::string_view_literals;
+  static constexpr std::string_view chars = "0123456789abcdefghijklmnopqrstuvwxyz"sv;
+
+  std::string result;
+  result.resize(len);
+
+  for (int i = 0; i < len; i++) {
+    result[i] = chars[RandNumber(chars.size())];
+  }
+
+  return result;
+}
+
+} // namespace vereign::core
diff --git a/cpp/src/vereign/core/rand.hh b/cpp/src/vereign/core/rand.hh
new file mode 100644
index 0000000000000000000000000000000000000000..a39eae5e551a259259717e33bc6ceba2395e8a8d
--- /dev/null
+++ b/cpp/src/vereign/core/rand.hh
@@ -0,0 +1,26 @@
+#ifndef __VEREIGN_CORE_RAND_HH
+#define __VEREIGN_CORE_RAND_HH
+
+#include <random>
+
+namespace vereign::core {
+
+/**
+ * Generated random number in the half closed interval [0, n).
+ *
+ * @param n Upper bound of the generated random number.
+ * @returns a random number in the half closed interval [0, n).
+ */
+auto RandNumber(int n) -> int;
+
+/**
+ * Generates random string with alpha lower case characters.
+ *
+ * @param len The length of the desired random string.
+ * @returns a random string of size len consisting of numbers 0-9 and lower case latin chars a-z.
+ */
+auto RandLowerAlpha(int len) -> std::string;
+
+} // namespace vereign::core
+
+#endif // __VEREIGN_CORE_RAND_HH
diff --git a/cpp/src/vereign/core/string.cc b/cpp/src/vereign/core/string.cc
new file mode 100644
index 0000000000000000000000000000000000000000..bf2340fcb9ebafd0b6c8ec5ccdec511ee24a5ecd
--- /dev/null
+++ b/cpp/src/vereign/core/string.cc
@@ -0,0 +1,94 @@
+#include <vereign/core/string.hh>
+
+#include <iostream>
+
+#ifdef _WIN32
+# include <windows.h>
+# include <stringapiset.h>
+#endif
+
+namespace vereign::string {
+
+#ifdef _WIN32
+
+auto widen(std::string_view utf8_str) -> std::wstring {
+  if (utf8_str.empty()) {
+    return L"";
+  }
+
+  int num_chars = MultiByteToWideChar(CP_UTF8 , 0, utf8_str.data(), utf8_str.length(), nullptr, 0);
+  if (num_chars == 0) {
+    return std::wstring{};
+  }
+
+  std::wstring result;
+  result.resize(num_chars);
+  num_chars = MultiByteToWideChar(
+    CP_UTF8,
+    0,
+    utf8_str.data(),
+    utf8_str.length(),
+    result.data(),
+    num_chars
+  );
+
+  if (num_chars == 0) {
+    return L"";
+  }
+
+  return result;
+}
+
+auto narrow(std::wstring_view utf16_str) -> std::string {
+  if (utf16_str.empty()) {
+    return "";
+  }
+
+  int num_chars = WideCharToMultiByte(
+    CP_UTF8,
+    0,
+    utf16_str.data(),
+    utf16_str.length(),
+    nullptr,
+    0,
+    nullptr,
+    nullptr
+  );
+
+  if (num_chars == 0) {
+    return "";
+  }
+
+  std::string result;
+  result.resize(num_chars);
+  int new_size = WideCharToMultiByte(
+    CP_UTF8,
+    0,
+    utf16_str.data(),
+    utf16_str.length(),
+    result.data(),
+    num_chars,
+    nullptr,
+    nullptr
+  );
+
+  if (new_size == 0) {
+    return "";
+  }
+
+  result.resize(new_size);
+
+  return result;
+}
+
+#endif
+
+auto tail(const std::string& src, std::size_t length) -> std::string {
+  if (length >= src.size()) {
+    return src;
+  }
+
+  return src.substr(src.size() - length);
+}
+
+} // vereign::string
diff --git a/cpp/src/vereign/core/string.hh b/cpp/src/vereign/core/string.hh
new file mode 100644
index 0000000000000000000000000000000000000000..62429b28a0d77feaf77575edd8b630372b571f87
--- /dev/null
+++ b/cpp/src/vereign/core/string.hh
@@ -0,0 +1,32 @@
+#ifndef __VEREIGN_CORE_STRING_HH
+#define __VEREIGN_CORE_STRING_HH
+
+#include <string>
+#include <codecvt>
+
+namespace vereign::string {
+
+#ifdef _WIN32
+
+auto widen(std::string_view utf8_str) -> std::wstring;
+auto narrow(std::wstring_view utf16_str) -> std::string;
+
+#endif
+
+/**
+ * Retrieve string tail.
+ *
+ * @code
+ * std::string str = "foobar";
+ * std::string tail = vereign::string::tail(str, 3);
+ * assert(tail == "bar");
+ * @endcode
+ *
+ * @param src Source string.
+ * @param length How many characters from the string tail to return.
+ */
+auto tail(const std::string& src, std::size_t length) -> std::string;
+
+} // vereign::string
+
+#endif // __VEREIGN_CORE_STRING_HH
diff --git a/cpp/src/vereign/core/time.cc b/cpp/src/vereign/core/time.cc
new file mode 100644
index 0000000000000000000000000000000000000000..33030082a682d2bc3091602a24d5e39c22da4b96
--- /dev/null
+++ b/cpp/src/vereign/core/time.cc
@@ -0,0 +1,73 @@
+#include <vereign/core/time.hh>
+
+namespace vereign::time {
+
+auto Epoch() -> boost::posix_time::ptime {
+  static const auto epoch = boost::posix_time::ptime{boost::gregorian::date{1970, 1, 1}};
+
+  return epoch;
+}
+
+auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t {
+  return time_t((t - Epoch()).total_seconds());
+}
+
+auto MakePosixTime(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int minutes,
+  int seconds
+) -> boost::posix_time::ptime {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return ptime(
+    date{
+      static_cast<date::year_type>(year),
+      static_cast<date::month_type>(month),
+      static_cast<date::day_type>(day)
+    },
+    time_duration(hours, minutes, seconds)
+  );
+}
+
+auto MakeTimeUTC(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int seconds,
+  int milliseconds
+) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(
+    ptime(
+      date{
+        static_cast<date::year_type>(year),
+        static_cast<date::month_type>(month),
+        static_cast<date::day_type>(day)
+      },
+      time_duration(hours, seconds, milliseconds)
+    )
+  );
+}
+
+auto MakeTimeUTCFromString(const std::string& str) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(boost::posix_time::time_from_string(str));
+}
+
+auto MakeTimeUTCFromISO(const std::string& str) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(from_iso_string(str));
+}
+
+} // vereign::time
diff --git a/cpp/src/vereign/core/time.hh b/cpp/src/vereign/core/time.hh
new file mode 100644
index 0000000000000000000000000000000000000000..5b7d775606d25a7c2ac264d543bbbdeac833088c
--- /dev/null
+++ b/cpp/src/vereign/core/time.hh
@@ -0,0 +1,72 @@
+#ifndef __VEREIGN_CORE_TIME_HH
+#define __VEREIGN_CORE_TIME_HH
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace vereign::time {
+
+/**
+ * Returns the gregorian epoch boost posix time - 1970:01:01.
+ */
+auto Epoch() -> boost::posix_time::ptime;
+
+/**
+ * Converts boost posix time to time_t timestamp.
+ *
+ * @param t The source boost posix time.
+ * @returns timestamp.
+ */
+auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t;
+
+/**
+ * Creates boost posix timetamp.
+ *
+ * @param year Year component.
+ * @param month Month component 1-12.
+ * @param day Day component 1-31.
+ * @param hours The hours component 0-23.
+ * @param minutes The minutes component 0-59.
+ * @param seconds The minutes component 0-59.
+ * @returns posix time.
+ */
+auto MakePosixTime(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int minutes,
+  int seconds
+) -> boost::posix_time::ptime;
+
+/**
+ * Creates UTC timestamp.
+ *
+ * @param year Year component.
+ * @param month Month component 1-12.
+ * @param day Day component 1-31.
+ * @param hours The hours component 0-23.
+ * @param minutes The minutes component 0-59.
+ * @param seconds The minutes component 0-59.
+ * @returns timestamp.
+ */
+auto MakeTimeUTC(int year, int month, int day, int hours, int seconds, int milliseconds) -> time_t;
+
+/**
+ * Creates UTC timestamp from a string.
+ *
+ * @param str Time string representation, example: "2002-01-20 23:59:59.000".
+ * @returns timestamp.
+ */
+auto MakeTimeUTCFromString(const std::string& str) -> time_t;
+
+/**
+ * Creates UTC timestamp from ISO formatted string.
+ *
+ * @param str Time string representation, example: "20020120T235959".
+ * @returns timestamp.
+ */
+auto MakeTimeUTCFromISO(const std::string& str) -> time_t;
+
+} // vereign::time
+
+#endif // __VEREIGN_CORE_TIME_HH
diff --git a/cpp/src/vereign/crypto/aes.cc b/cpp/src/vereign/crypto/aes.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f9bcefab4365b17f31e960ffb802d662692695d2
--- /dev/null
+++ b/cpp/src/vereign/crypto/aes.cc
@@ -0,0 +1,128 @@
+#include <openssl/err.h>
+#include <vereign/crypto/aes.hh>
+
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/errors.hh>
+
+#include <openssl/base.h>
+#include <openssl/evp.h>
+
+namespace {
+  constexpr int gcmIVSizeBytes = 12;
+  constexpr int gcmTagSizeBytes = 16;
+  constexpr int aes256BlockSizeBytes = 32;
+}
+
+namespace vereign::crypto::aes {
+
+void GCM256Encrypt(
+  bytes::View src,
+  bytes::View key,
+  bytes::Buffer& iv,
+  bytes::Buffer& tag,
+  bytes::Buffer& encrypted
+) {
+  iv.Reserve(gcmIVSizeBytes);
+  crypto::Rand(iv, gcmIVSizeBytes);
+
+  encrypted.Reserve(src.Size() + aes256BlockSizeBytes);
+
+  bssl::UniquePtr<EVP_CIPHER_CTX> ctx{EVP_CIPHER_CTX_new()};
+  if (!ctx) {
+    throw OpenSSLError("evp cipher context cannot be created");
+  }
+
+  auto r = EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
+  if (r != 1) {
+    throw OpenSSLError("AES GCM encrypt init failed");
+  }
+
+  r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.Size(), nullptr);
+  if (r != 1) {
+    throw OpenSSLError("iv size init failed");
+  }
+
+  r = EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key.Data(), iv.View().Data());
+  if (r != 1) {
+    throw OpenSSLError("key and iv init failed");
+  }
+
+  int bytes_written;
+  r = EVP_EncryptUpdate(ctx.get(), encrypted.end(), &bytes_written, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("encrypt failed");
+  }
+  encrypted.IncSize(bytes_written);
+
+  r = EVP_EncryptFinal_ex(ctx.get(), encrypted.end(), &bytes_written);
+  if (r != 1) {
+    throw OpenSSLError("finalize encryption failed");
+  }
+  encrypted.IncSize(bytes_written);
+
+  tag.Reserve(gcmTagSizeBytes);
+  r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, gcmTagSizeBytes, tag.end());
+  if (r != 1) {
+    throw OpenSSLError("getting GCM tag failed");
+  }
+  tag.IncSize(gcmTagSizeBytes);
+
+  r = EVP_CIPHER_CTX_cleanup(ctx.get());
+  if (r != 1) {
+    ERR_clear_error();
+  }
+}
+
+void GCM256Decrypt(
+  bytes::View src,
+  bytes::View key,
+  bytes::View iv,
+  bytes::View tag,
+  bytes::Buffer& decrypted
+) {
+  bssl::UniquePtr<EVP_CIPHER_CTX> ctx{EVP_CIPHER_CTX_new()};
+  if (!ctx) {
+    throw OpenSSLError("evp cipher context cannot be created");
+  }
+
+  auto r = EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
+  if (r != 1) {
+    throw OpenSSLError("AES GCM decrypt init failed");
+  }
+
+  r = EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.Size(), nullptr);
+  if (r != 1) {
+    throw OpenSSLError("iv size init failed");
+  }
+
+  r = EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.Data(), iv.Data());
+  if (r != 1) {
+    throw OpenSSLError("key and iv init failed");
+  }
+
+  decrypted.Reserve(src.Size());
+
+  int bytes_written;
+  r = EVP_DecryptUpdate(ctx.get(), decrypted.end(), &bytes_written, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("decrypt failed");
+  }
+  decrypted.IncSize(bytes_written);
+
+  r = EVP_CIPHER_CTX_ctrl(
+    ctx.get(),
+    EVP_CTRL_AEAD_SET_TAG,
+    tag.Size(),
+    const_cast<uint8_t*>(tag.Data())
+  );
+  if (r != 1) {
+    throw OpenSSLError("setting GCM tag failed");
+  }
+
+  r = EVP_DecryptFinal_ex(ctx.get(), decrypted.end(), &bytes_written);
+  if (r != 1) {
+    throw OpenSSLError("verification failed");
+  }
+}
+
+} // vereign::crypto::aes
diff --git a/cpp/src/vereign/crypto/aes.hh b/cpp/src/vereign/crypto/aes.hh
new file mode 100644
index 0000000000000000000000000000000000000000..09be668b964bddd8f31aecc93e874ae35834eb3b
--- /dev/null
+++ b/cpp/src/vereign/crypto/aes.hh
@@ -0,0 +1,80 @@
+#ifndef __VEREIGN_CRYPTO_AES_HH
+#define __VEREIGN_CRYPTO_AES_HH
+
+#include <vereign/bytes/buffer.hh>
+
+/**
+ * Provides utilities for AES encryption/decryption.
+ */
+namespace vereign::crypto::aes {
+
+/**
+ * Encrypt given bytes with AES256-GCM.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto key = crypto::Rand(32); // 256 bits
+ *
+ * bytes::Buffer iv;
+ * bytes::Buffer tag;
+ * bytes::Buffer encrypted;
+ *
+ * crypto::aes::GCM256Encrypt(bytes::View(input), key.View(), iv, tag, encrypted);
+ * @endcode
+ *
+ * @param src The bytes that will be encrypted.
+ * @param key The AES 256 bit key.
+ * @param iv The initialization vector that was used during the encryption.
+ * @param tag The authentication tag that was produced during the encryption.
+ * @param encrypted The result of the encryption.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+void GCM256Encrypt(
+  bytes::View src,
+  bytes::View key,
+  bytes::Buffer& iv,
+  bytes::Buffer& tag,
+  bytes::Buffer& encrypted
+);
+
+/**
+ * Decrypts given bytes with AES256-GCM.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto key = crypto::Rand(32); // 256 bits
+ *
+ * bytes::Buffer iv;
+ * bytes::Buffer tag;
+ * bytes::Buffer encrypted;
+ *
+ * crypto::aes::GCM256Encrypt(bytes::View(input), key.View(), iv, tag, encrypted);
+ *
+ * bytes::Buffer decrypted;
+ * crypto::aes::GCM256Decrypt(encrypted.View(), key.View(), iv.View(), tag.View(), decrypted);
+ *
+ * assert(input == decrypted.View().String());
+ * @endcode
+ *
+ * @param src The bytes that will be decrypted.
+ * @param key The AES 256 bit key.
+ * @param iv The initialization vector that was used during the encryption.
+ * @param tag The authentication tag that was produced during the encryption.
+ * @param encrypted The result of the decryption.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+void GCM256Decrypt(
+  bytes::View src,
+  bytes::View key,
+  bytes::View iv,
+  bytes::View tag,
+  bytes::Buffer& decrypted
+);
+
+} // vereign::crypto::aes
+
+#endif // __VEREIGN_CRYPTO_AES_HH
diff --git a/cpp/src/vereign/crypto/bio.cc b/cpp/src/vereign/crypto/bio.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5a630f6eedcd17a51dddb79c826a992827485e2e
--- /dev/null
+++ b/cpp/src/vereign/crypto/bio.cc
@@ -0,0 +1,18 @@
+#include <vereign/crypto/bio.hh>
+
+#include <vereign/crypto/errors.hh>
+
+namespace vereign::crypto::bio {
+
+auto View(const BIO* bio) -> bytes::View {
+  const uint8_t* data;
+  std::size_t size;
+  auto r = BIO_mem_contents(bio, &data, &size);
+  if (r != 1) {
+    throw OpenSSLError("fetching bio mem contents failed");
+  }
+
+  return bytes::View(data, size);
+}
+
+} // vereign::crypto::bio
diff --git a/cpp/src/vereign/crypto/bio.hh b/cpp/src/vereign/crypto/bio.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2bd5cedacee0b6c94c81a0f2af50e448982b6d3f
--- /dev/null
+++ b/cpp/src/vereign/crypto/bio.hh
@@ -0,0 +1,19 @@
+#ifndef __VEREIGN_CRYPTO_BIO_HH
+#define __VEREIGN_CRYPTO_BIO_HH
+
+#include <vereign/bytes/view.hh>
+#include <openssl/bio.h>
+
+namespace vereign::crypto::bio {
+
+/**
+ * Creates a bytes::View for given OpenSSL BIO.
+ *
+ * @param bio The input BIO.
+ * @returns a bytes view of OpenSSL BIO.
+ */
+auto View(const BIO* bio) -> bytes::View;
+
+} // vereign::crypto::bio
+
+#endif // __VEREIGN_CRYPTO_BIO_HH
diff --git a/cpp/src/vereign/crypto/cert.cc b/cpp/src/vereign/crypto/cert.cc
new file mode 100644
index 0000000000000000000000000000000000000000..bc6f84d2d6f74904056f6b5bfe7c4c9d5402d2a4
--- /dev/null
+++ b/cpp/src/vereign/crypto/cert.cc
@@ -0,0 +1,428 @@
+#include <vereign/crypto/cert.hh>
+
+#include <vereign/crypto/bio.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/encoding/base64.hh>
+
+#include <openssl/x509v3.h>
+#include <openssl/digest.h>
+#include <openssl/pem.h>
+#include <sstream>
+
+namespace {
+  constexpr const char* certDefaultHashAlg = "SHA256";
+  constexpr const int certVersion = 2;
+}
+
+namespace vereign::crypto::cert {
+
+static auto addCertExtension(
+  const X509* issuer_cert,
+  X509* cert,
+  int nid,
+  const std::string& value
+) -> int {
+  X509V3_CTX ctx;
+
+  // This sets the 'context' of the extensions.
+  // No configuration database
+  X509V3_set_ctx_nodb(&ctx);
+
+  // Issuer and subject certs: both the target since it is self signed,
+  // no request and no CRL
+  X509V3_set_ctx(&ctx, const_cast<X509*>(issuer_cert), cert, nullptr, nullptr, 0);
+  bssl::UniquePtr<X509_EXTENSION> ex{X509V3_EXT_nconf_nid(
+    nullptr,
+    &ctx,
+    nid,
+    const_cast<std::string&>(value).data()
+  )};
+  if (!ex) {
+    return 0;
+  }
+
+  return X509_add_ext(cert, ex.get(), -1);
+}
+
+static void addSubjAltNameExt(X509* cert, const std::string& email, const std::string& url) {
+  auto gens = bssl::UniquePtr<GENERAL_NAMES>(sk_GENERAL_NAME_new_null());
+  if (!gens) {
+    throw OpenSSLError("creating GENERAL_NAMES stack failed");
+  }
+
+  if (!email.empty()) {
+    auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new());
+    if (!gen) {
+      throw OpenSSLError("creating GENERAL_NAME failed");
+    }
+
+    auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new());
+    if (!ia5) {
+      throw OpenSSLError("creating ASN1_IA5STRING failed");
+    }
+
+    auto r = ASN1_STRING_set(ia5.get(), email.data(), -1);
+    if (r != 1) {
+      throw OpenSSLError("set certificate alternative name email part failed");
+    }
+
+    GENERAL_NAME_set0_value(gen.get(), GEN_EMAIL, ia5.release());
+    r = sk_GENERAL_NAME_push(gens.get(), gen.release());
+    if (r == 0) {
+      throw OpenSSLError("pushing email to certificate subject alternative name failed");
+    }
+  }
+
+  if (!url.empty()) {
+    auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new());
+    if (!gen) {
+      throw OpenSSLError("creating GENERAL_NAME failed");
+    }
+
+    auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new());
+    if (!ia5) {
+      throw OpenSSLError("creating ASN1_IA5STRING failed");
+    }
+
+    auto r = ASN1_STRING_set(ia5.get(), url.data(), -1);
+    if (r != 1) {
+      throw OpenSSLError("set certificate alternative name URL part failed");
+    }
+
+    GENERAL_NAME_set0_value(gen.get(), GEN_DNS, ia5.release());
+    r = sk_GENERAL_NAME_push(gens.get(), gen.release());
+    if (r == 0) {
+      throw OpenSSLError("pushing URL to certificate subject alternative name failed");
+    }
+  }
+
+  auto r = X509_add1_ext_i2d(cert, NID_subject_alt_name, gens.get(), 0, 0);
+  if (r != 1) {
+    throw OpenSSLError("set certificate alternative name failed");
+  }
+}
+
+static void setCertSubject(const CertData& cert_data, X509* cert) {
+  const CertSubject& subject = cert_data.Subject;
+  auto subject_name = X509_get_subject_name(cert);
+  int r = 0;
+
+  if (!subject.CommonName.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_commonName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.CommonName.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Common Name failed");
+    }
+  }
+
+  if (!subject.Country.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_countryName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Country.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Country Name failed");
+    }
+  }
+
+  if (!subject.Locality.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_localityName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Locality.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Locality Name failed");
+    }
+  }
+
+  if (!subject.State.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_stateOrProvinceName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.State.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject State Name failed");
+    }
+  }
+
+  if (!subject.Organization.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_organizationName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Organization.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Organization Name failed");
+    }
+  }
+
+  if (!subject.OrganizationUnit.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_organizationalUnitName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.OrganizationUnit.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Organizational Unit Name failed");
+    }
+  }
+
+
+  if (!cert_data.Email.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_pkcs9_emailAddress,
+      MBSTRING_ASC,
+      (unsigned char*) cert_data.Email.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Email Address failed");
+    }
+  }
+}
+
+static void addCertExtensions(const CertData& cert_data, const X509* issuer_cert, X509* cert) {
+  // basic constraints
+  const std::string basic_constraints = cert_data.IsCA ? "critical,CA:TRUE" : "critical,CA:FALSE";
+  auto r = addCertExtension(issuer_cert, cert, NID_basic_constraints, basic_constraints);
+  if (r != 1) {
+    throw OpenSSLError("set certificate basic constraints failed");
+  }
+
+  // key usage
+  const std::string key_usage = cert_data.IsCA
+    ? "critical,digitalSignature,keyCertSign,cRLSign"
+    : "critical,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment";
+  r = addCertExtension(issuer_cert, cert, NID_key_usage, key_usage);
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+
+  // extended key usage
+  if (!cert_data.IsCA && !cert_data.Email.empty()) {
+    r = addCertExtension(issuer_cert, cert, NID_ext_key_usage, "emailProtection");
+    if (r != 1) {
+      throw OpenSSLError("set certificate extended key usage failed");
+    }
+  }
+
+  // subject alternative name
+  if (!cert_data.Email.empty() || !cert_data.Url.empty()) {
+    addSubjAltNameExt(cert, cert_data.Email, cert_data.Url);
+  }
+
+  // subject key identifier
+  r = addCertExtension(issuer_cert, cert, NID_subject_key_identifier, "hash");
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+
+  // authority key identifier
+  r = addCertExtension(issuer_cert, cert, NID_authority_key_identifier, "keyid,issuer:always");
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+}
+
+static void setCertValidity(const CertData& cert_data, X509* cert) {
+  using clock = std::chrono::system_clock;
+
+  long not_before_adj = 0;
+  time_t not_before = cert_data.Validity.NotBefore;
+
+  if (not_before == 0) {
+    auto now = clock::now();
+    not_before = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
+    not_before_adj = -(not_before % (24 * 60 * 60));
+  }
+
+  auto timeret = X509_time_adj(X509_get_notBefore(cert), not_before_adj, &not_before);
+  if (timeret == nullptr) {
+    throw OpenSSLError("adjusting certificate not before validity failed");
+  }
+
+  long not_after_adj = 0;
+  time_t not_after = cert_data.Validity.NotAfter;
+
+  if (not_after == 0) {
+    not_after = not_before;
+
+    int valid_years = cert_data.Validity.ValidYears;
+    if (valid_years == 0) {
+      valid_years = 1;
+    }
+
+    not_after_adj = not_before_adj + valid_years * 365 * 24 * 60 * 60;
+  }
+
+  timeret = X509_time_adj(X509_get_notAfter(cert), not_after_adj, &not_after);
+  if (timeret == nullptr) {
+    throw OpenSSLError("adjusting certificate not after validity failed");
+  }
+}
+
+static auto createCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+  const EVP_MD* hash_alg = nullptr;
+  if (cert_data.Algorithms.HashAlg.empty()) {
+    hash_alg = EVP_get_digestbyname(certDefaultHashAlg);
+  } else {
+    hash_alg = EVP_get_digestbyname(cert_data.Algorithms.HashAlg.data());
+  }
+
+  if (hash_alg == nullptr) {
+    throw crypto::OpenSSLError("cannot find hash algorithm");
+  }
+
+  auto cert = bssl::UniquePtr<X509>(X509_new());
+
+  if (issuer_cert == nullptr || issuer_pkey == nullptr) {
+    issuer_cert = cert.get();
+    issuer_pkey = pkey;
+  }
+
+  // set public key
+  auto r = X509_set_pubkey(cert.get(), const_cast<EVP_PKEY*>(pkey));
+  if (r != 1) {
+    throw OpenSSLError("set public key to certificate failed");
+  }
+
+  // set certificate version
+  r = X509_set_version(cert.get(), certVersion);
+  if (r != 1) {
+    throw OpenSSLError("set version to certificate failed");
+  }
+
+  // set serial number
+  auto serial_number = cert_data.SerialNumber;
+  if (serial_number == 0) {
+    serial_number = crypto::RandUint64();
+  }
+  r = ASN1_INTEGER_set_uint64(X509_get_serialNumber(cert.get()), serial_number);
+  if (r != 1) {
+    throw OpenSSLError("set certificate serial number");
+  }
+
+  // set subject
+  setCertSubject(cert_data, cert.get());
+
+  // set issuer name
+  auto issuer_name = X509_get_subject_name(const_cast<X509*>(issuer_cert));
+  r = X509_set_issuer_name(cert.get(), issuer_name);
+  if (r != 1) {
+    throw OpenSSLError("set certificate issuer name failed");
+  }
+
+  // set validity
+  setCertValidity(cert_data, cert.get());
+
+  // add extensions
+  addCertExtensions(cert_data, issuer_cert, cert.get());
+
+  r = X509_sign(cert.get(), const_cast<EVP_PKEY*>(issuer_pkey), hash_alg);
+  if (r == 0) {
+    throw OpenSSLError("certificate sign failed");
+  }
+
+  return cert;
+}
+
+auto CreateCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+
+  return createCert(cert_data, issuer_cert, issuer_pkey, pkey);
+}
+
+auto CreateSelfSignedCert(
+  const CertData& cert_data,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+
+  return createCert(cert_data, nullptr, nullptr, pkey);
+}
+
+auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO> {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = PEM_write_bio_X509(mem.get(), const_cast<X509*>(cert));
+  if (r != 1) {
+    throw OpenSSLError("exporting certificate to PEM failed");
+  }
+
+  return mem;
+}
+
+auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509> {
+  bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size()));
+  if (mem == nullptr) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto cert = bssl::UniquePtr<X509>(PEM_read_bio_X509(mem.get(), nullptr, nullptr, nullptr));
+  if (cert == nullptr) {
+    throw OpenSSLError("importing certificate from PEM failed");
+  }
+
+  return cert;
+}
+
+void PrintCert(std::ostream& os, const X509* cert) {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = X509_print(mem.get(), const_cast<X509*>(cert));
+  if (r != 1) {
+    throw OpenSSLError("printing certificate failed");
+  }
+
+  os << bio::View(mem.get()).String();
+}
+
+} // vereign::crypto::cert
diff --git a/cpp/src/vereign/crypto/cert.hh b/cpp/src/vereign/crypto/cert.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6a5ed4e010a68dce886223936afbb6f537ded563
--- /dev/null
+++ b/cpp/src/vereign/crypto/cert.hh
@@ -0,0 +1,170 @@
+#ifndef __VEREIGN_CRYPTO_CERT_HH
+#define __VEREIGN_CRYPTO_CERT_HH
+
+#include <vereign/bytes/buffer.hh>
+
+#include <openssl/base.h>
+#include <openssl/x509.h>
+#include <chrono>
+
+namespace vereign::crypto::cert {
+
+/**
+ * Certificate algorithms configuration used for creating certificates.
+ *
+ * Used in CertData struct.
+ */
+struct CertAlgorithms {
+  /**
+   * The hash algorithm, if it is empty the default SHA256 will be used.
+   */
+  std::string HashAlg;
+
+  // FIXME: SignAlg is unused
+  std::string SignAlg;
+  // FIXME: KeyLength is unused
+  int KeyLength;
+};
+
+/**
+ * Certificate subject configuration used for creating certificates.
+ *
+ * All fields are optional for leaf, recommended for CA.
+ *
+ * Used in CertData struct.
+ */
+struct CertSubject {
+  std::string CommonName;
+  std::string Country;
+  std::string Locality;
+  std::string State;
+  std::string Organization;
+  std::string OrganizationUnit;
+};
+
+/**
+ * Certificate validity configuration used for creating certificates.
+ *
+ * Used in CertData struct.
+ */
+struct CertValidity {
+  /**
+   * If 0, the default is today at 00:00:00.
+   */
+  time_t NotBefore;
+
+  /**
+   * If 0, the default is NotBefore + ValidYears at 23:59:59.
+   */
+  time_t NotAfter;
+
+  /**
+   * If 0, the default is 1.
+   */
+  int ValidYears;
+};
+
+/**
+ * Certificate configuration used for creating certificates.
+ *
+ * @see CreateCert
+ * @see CreateSelfSignedCert
+ */
+struct CertData {
+  CertAlgorithms Algorithms;
+  CertSubject Subject;
+  CertValidity Validity;
+
+  /**
+   * If 0, the default is the current milliseconds timestamp - the number of milliseconds since epoch.
+   */
+  uint64_t SerialNumber;
+
+  /**
+   * Added to DN and Subject Alternative Name extension.
+   * Optional for CA. Mandatory for leaf certificate, used for email protection.
+   */
+  std::string Email;
+
+  /**
+   * Optional, recommended for CA, added to Subject Alternative Name extension.
+   */
+  std::string Url;
+
+  /**
+   * Denotes if the created certificate is CA or leaf certificate. The default is false.
+   */
+  bool IsCA;
+};
+
+/**
+ * Creates and signs a certificate.
+ *
+ * @param cert_data Certificate configuration.
+ * @param issuer_cert The issuer certificate.
+ * @param issuer_pkey The issuer private key.
+ * @param pkey The private key for the newly created certificate.
+ * @returns new certificate signed by the provided issuer.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto CreateCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509>;
+
+/**
+ * Creates self signed certificate.
+ *
+ * @param cert_data Certificate configuration.
+ * @param pkey The private key for the newly created certificate.
+ * @returns the new certificate signed by its own private key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto CreateSelfSignedCert(
+  const CertData& cert_data,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509>;
+
+/**
+ * Exports certificate to PEM format.
+ *
+ * @param cert The certificate to export.
+ * @returns a memory BIO with the exported certificate.
+ */
+auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO>;
+
+/**
+ * Exports certificate to PEM format into string.
+ *
+ * @param cert The certificate to export.
+ * @returns a string with the exported certificate.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportCertToPEMString(const X509* cert) -> std::string;
+
+/**
+ * Imports certificate from PEM format.
+ *
+ * @param pem PEM encoded certificate.
+ * @returns imported X509 certificate.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509>;
+
+/**
+ * Print textual representation of the certificate into output stream.
+ *
+ * @param os The stream where to write the textual representation of the certificate.
+ * @param cert The certificate.
+ */
+void PrintCert(std::ostream& os, const X509* cert);
+
+} // vereign::crypto::cert
+
+#endif // __VEREIGN_CRYPTO_CERT_HH
diff --git a/cpp/src/vereign/crypto/digest.cc b/cpp/src/vereign/crypto/digest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..ab9ed2567189b0a1cb68ab092c5afd2725408901
--- /dev/null
+++ b/cpp/src/vereign/crypto/digest.cc
@@ -0,0 +1,33 @@
+#include <vereign/crypto/digest.hh>
+
+#include <vereign/crypto/errors.hh>
+
+#include <openssl/base.h>
+#include <openssl/evp.h>
+
+namespace {
+  constexpr int sha1SizeBytes = 20;
+}
+
+namespace vereign::crypto::digest {
+
+void sha1(bytes::View src, bytes::Buffer& result) {
+  bssl::UniquePtr<EVP_MD_CTX> ctx(EVP_MD_CTX_new());
+  if (!ctx) {
+    throw OpenSSLError("evp digest context cannot be created");
+  }
+
+  auto r = EVP_DigestInit_ex(ctx.get(), EVP_sha1(), nullptr);
+  if (r != 1) {
+    throw OpenSSLError("sha1 init failed");
+  }
+
+  result.Reserve(sha1SizeBytes);
+
+  EVP_DigestUpdate(ctx.get(), src.Data(), src.Size());
+  EVP_DigestFinal_ex(ctx.get(), result.end(), nullptr);
+
+  result.IncSize(sha1SizeBytes);
+}
+
+} // vereign::crypto::digest
diff --git a/cpp/src/vereign/crypto/digest.hh b/cpp/src/vereign/crypto/digest.hh
new file mode 100644
index 0000000000000000000000000000000000000000..365d45e9af3fa5fd3be617d539aaa823ac210af0
--- /dev/null
+++ b/cpp/src/vereign/crypto/digest.hh
@@ -0,0 +1,28 @@
+#ifndef __VEREIGN_CRYPTO_DIGEST_HH
+#define __VEREIGN_CRYPTO_DIGEST_HH
+
+#include <vereign/bytes/buffer.hh>
+
+namespace vereign::crypto::digest {
+
+/**
+ * Creates a SHA1 hash of given bytes.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ *
+ * bytes::Buffer hash;
+ * crypto::digest::sha1(bytes::View(data), hash);
+ * @endcode
+ *
+ * @param src The input bytes.
+ * @param result The SHA1 hash of the input.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+void sha1(bytes::View src, bytes::Buffer& result);
+
+} // vereign::crypto::digest
+
+#endif // __VEREIGN_CRYPTO_DIGEST_HH
diff --git a/cpp/src/vereign/crypto/errors.hh b/cpp/src/vereign/crypto/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..825af7e98baf03a468ace2ced84cea76960b5c4f
--- /dev/null
+++ b/cpp/src/vereign/crypto/errors.hh
@@ -0,0 +1,43 @@
+#ifndef __VEREIGN_CRYPTO_ERRORS_HH
+#define __VEREIGN_CRYPTO_ERRORS_HH
+
+#include <stdexcept>
+#include <openssl/err.h>
+
+namespace vereign::crypto {
+
+/**
+ * The base error type for the namespace vereign::crypto.
+ */
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+/**
+ * An error thrown by the crypto functions.
+ */
+class OpenSSLError : public Error {
+public:
+  OpenSSLError(const std::string& what)
+    : Error(getOpenSSLErrorString(what))
+  {
+  }
+
+private:
+  static auto getOpenSSLErrorString(const std::string& what) -> std::string {
+    auto error = ERR_get_error();
+    if (error == 0) {
+      return what + ": no openssl error in the error queue";
+    }
+
+    return what + ": " + ERR_reason_error_string(error);
+  }
+};
+
+} // vereign::crypto
+
+#endif // __VEREIGN_CRYPTO_ERRORS_HH
diff --git a/cpp/src/vereign/crypto/rand.cc b/cpp/src/vereign/crypto/rand.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f61274887d5a826a22e0db178c86564ae2adcad8
--- /dev/null
+++ b/cpp/src/vereign/crypto/rand.cc
@@ -0,0 +1,37 @@
+#include <vereign/crypto/rand.hh>
+
+#include <openssl/rand.h>
+#include <vereign/crypto/errors.hh>
+
+namespace vereign::crypto {
+
+void Rand(bytes::Buffer& buf, std::size_t size) {
+  buf.Reserve(size);
+  int result = RAND_bytes(buf.end(), size);
+  if (result == 0) {
+    ERR_clear_error();
+    throw Error("crypto rand failed");
+  }
+
+  buf.IncSize(size);
+}
+
+auto Rand(std::size_t size) -> bytes::Buffer {
+  bytes::Buffer buf{size};
+  Rand(buf, size);
+
+  return buf;
+}
+
+auto RandUint64() -> uint64_t {
+  uint64_t x = 0;
+  int result = RAND_bytes((uint8_t*) &x, sizeof(x));
+  if (result == 0) {
+    ERR_clear_error();
+    throw Error("crypto rand failed");
+  }
+
+  return x;
+}
+
+} // vereign::crypto
diff --git a/cpp/src/vereign/crypto/rand.hh b/cpp/src/vereign/crypto/rand.hh
new file mode 100644
index 0000000000000000000000000000000000000000..e377ac88ce42544b57f337ef82f8ba7c9b24f71c
--- /dev/null
+++ b/cpp/src/vereign/crypto/rand.hh
@@ -0,0 +1,57 @@
+#ifndef __VEREIGN_CRYPTO_RAND_HH
+#define __VEREIGN_CRYPTO_RAND_HH
+
+#include <vereign/bytes/buffer.hh>
+
+namespace vereign::crypto {
+
+/**
+ * Appends a random bytes to a buffer.
+ *
+ * After the operation is finished, the passed `buf` will be with incremented size of the newly
+ * added random bytes.
+ *
+ * Example:
+ * @code
+ * bytes::Bytes buf;
+ * crypto::Rand(buf, 16);
+ *
+ * assert(buf.Size() == 16);
+ * @endcode
+ *
+ * @param buf The buffer that will be filled with random bytes.
+ * @param size The number of the random bytes.
+ *
+ * @throws crypto::Error on failure.
+ */
+void Rand(bytes::Buffer& buf, std::size_t size);
+
+/**
+ * Generate random bytes.
+ *
+ * Example:
+ * @code
+ * auto buf = crypto::Rand(16);
+ *
+ * assert(buf.Size() == 16);
+ * @endcode
+ *
+ * @param size The number of the random bytes.
+ * @returns buffer with the generated random bytes.
+ *
+ * @throws crypto::Error on failure.
+ */
+auto Rand(std::size_t size) -> bytes::Buffer;
+
+/**
+ * Generates random uint64_t.
+ *
+ * @returns random unsigned 64 bit integer.
+ *
+ * @throws crypto::Error on failure.
+ */
+auto RandUint64() -> uint64_t;
+
+} // vereign::crypto
+
+#endif // __VEREIGN_CRYPTO_RAND_HH
diff --git a/cpp/src/vereign/crypto/rsa.cc b/cpp/src/vereign/crypto/rsa.cc
new file mode 100644
index 0000000000000000000000000000000000000000..2c89dfcb8078be5b4f232a33eb9c23781690b8cc
--- /dev/null
+++ b/cpp/src/vereign/crypto/rsa.cc
@@ -0,0 +1,174 @@
+#include <vereign/crypto/rsa.hh>
+
+#include <vereign/bytes/view.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/bytes/buffer.hh>
+#include <vereign/crypto/bio.hh>
+
+#include <openssl/base.h>
+#include <openssl/bn.h>
+#include <openssl/rsa.h>
+#include <openssl/mem.h>
+#include <openssl/evp.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+
+namespace vereign::crypto::rsa {
+
+auto GenerateKey(int bits) -> bssl::UniquePtr<EVP_PKEY> {
+  bssl::UniquePtr<BIGNUM> bn{BN_new()};
+  if (!bn) {
+    throw OpenSSLError("rsa key generation failed");
+  }
+
+  auto r = BN_set_word(bn.get(), RSA_F4);
+  if (r != 1) {
+    throw OpenSSLError("rsa key generation failed");
+  }
+
+  bssl::UniquePtr<RSA> rsa{RSA_new()};
+  r = RSA_generate_key_ex(rsa.get(), bits, bn.get(), nullptr);
+  if (r != 1) {
+    throw OpenSSLError("rsa key generation failed");
+  }
+
+  bssl::UniquePtr<EVP_PKEY> key(EVP_PKEY_new());
+  if (key == nullptr) {
+    throw OpenSSLError("creating key failed");
+  }
+
+  r = EVP_PKEY_assign_RSA(key.get(), rsa.release());
+  if (r != 1) {
+    throw OpenSSLError("rsa key assign to evp failed");
+  }
+
+  return key;
+}
+
+void PublicKeyEncrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& encrypted) {
+  bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(key, nullptr));
+  if (!ctx) {
+    throw OpenSSLError("creating evp ctx failed");
+  }
+
+  auto r = EVP_PKEY_encrypt_init(ctx.get());
+  if (r != 1) {
+    throw OpenSSLError("encrypt init failed");
+  }
+
+  r = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING);
+  if (r != 1) {
+    throw OpenSSLError("pkey padding init failed");
+  }
+
+  std::size_t outlen = 0;
+  r = EVP_PKEY_encrypt(ctx.get(), nullptr, &outlen, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("determining ciphertext size failed");
+  }
+
+  encrypted.Reserve(outlen);
+
+  r = EVP_PKEY_encrypt(ctx.get(), encrypted.end(), &outlen, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("encrypting failed");
+  }
+
+  encrypted.IncSize(outlen);
+}
+
+void PrivateKeyDecrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& decrypted) {
+  bssl::UniquePtr<EVP_PKEY_CTX> ctx(EVP_PKEY_CTX_new(key, nullptr));
+  if (!ctx) {
+    throw OpenSSLError("creating evp ctx failed");
+  }
+
+  auto r = EVP_PKEY_decrypt_init(ctx.get());
+  if (r != 1) {
+    throw OpenSSLError("decrypt init failed");
+  }
+
+  r = EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING);
+  if (r != 1) {
+    throw OpenSSLError("init pkey padding failed");
+  }
+
+  std::size_t outlen = 0;
+  r = EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("determining decrypted buffer size failed");
+  }
+
+  decrypted.Reserve(outlen);
+
+  r = EVP_PKEY_decrypt(ctx.get(), decrypted.end(), &outlen, src.Data(), src.Size());
+  if (r != 1) {
+    throw OpenSSLError("decrypting failed");
+  }
+
+  decrypted.IncSize(outlen);
+}
+
+auto ExportPublicKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO> {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = PEM_write_bio_PUBKEY(mem.get(), key);
+  if (r != 1) {
+    throw OpenSSLError("exporting public key to PEM failed");
+  }
+
+  return mem;
+}
+
+auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> {
+  bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size()));
+  if (mem == nullptr) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  bssl::UniquePtr<EVP_PKEY> key(PEM_read_bio_PUBKEY(mem.get(), nullptr, nullptr, nullptr));
+  if (key == nullptr) {
+    throw OpenSSLError("importing public key from PEM failed");
+  }
+
+  return key;
+}
+
+auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO> {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = PEM_write_bio_PrivateKey(mem.get(), key, nullptr, nullptr, 0, nullptr, nullptr);
+  if (r != 1) {
+    throw OpenSSLError("exporting private key to PEM failed");
+  }
+
+  return mem;
+}
+
+auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string {
+  auto key_bio = ExportPublicKeyToPEM(key);
+
+  return std::string{crypto::bio::View(key_bio.get()).String()};
+}
+
+auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> {
+  bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size()));
+  if (mem == nullptr) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto key_ptr = PEM_read_bio_PrivateKey(mem.get(), nullptr, nullptr, nullptr);
+  if (key_ptr == nullptr) {
+    throw OpenSSLError("importing private key from PEM failed");
+  }
+
+  return bssl::UniquePtr<EVP_PKEY>(key_ptr);
+}
+
+} // vereign::crypto::rsa
diff --git a/cpp/src/vereign/crypto/rsa.hh b/cpp/src/vereign/crypto/rsa.hh
new file mode 100644
index 0000000000000000000000000000000000000000..03a6c136503f3b0bfdda1b1df80d3a8a640b80f1
--- /dev/null
+++ b/cpp/src/vereign/crypto/rsa.hh
@@ -0,0 +1,164 @@
+#ifndef __VEREIGN_CRYPTO_RSA_HH
+#define __VEREIGN_CRYPTO_RSA_HH
+
+#include <vereign/bytes/view.hh>
+#include <vereign/bytes/buffer.hh>
+
+#include <openssl/base.h>
+#include <openssl/evp.h>
+
+namespace vereign::crypto::rsa {
+
+/**
+ * Generates new RSA key.
+ *
+ * Example:
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ * @endcode
+ *
+ * @param bits The length of the key.
+ * @returns the newly generated key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto GenerateKey(int bits) -> bssl::UniquePtr<EVP_PKEY>;
+
+/**
+ * Encrypts given bytes with RSA public key.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto key = crypto::rsa::GenerateKey(2048);
+ * bytes::Buffer encrypted;
+ *
+ * crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted);
+ *
+ * bytes::Buffer decrypted;
+ * crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+ *
+ * assert(decrypted.View() == bytes.View(input));
+ * @endcode
+ *
+ * @param key The RSA key.
+ * @param src The bytes that will be encrypted.
+ * @param encrypted The result of the encryption.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+void PublicKeyEncrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& encrypted);
+
+/**
+ * Decrypts given bytes with RSA private key.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto key = crypto::rsa::GenerateKey(2048);
+ * bytes::Buffer encrypted;
+ *
+ * crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted);
+ *
+ * bytes::Buffer decrypted;
+ * crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+ *
+ * assert(decrypted.View() == bytes.View(input));
+ * @endcode
+ *
+ * @param key The RSA key.
+ * @param src The bytes that will be decrypted.
+ * @param decrypted The result of the decryption.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+void PrivateKeyDecrypt(EVP_PKEY* key, bytes::View src, bytes::Buffer& decrypted);
+
+/**
+ * Exports a public key part to PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get());
+ * std::cout << crypto::bio::View(bio.get()).String() << std::endl;
+ * @endcode
+ *
+ * @param key The key to export.
+ * @returns a memory BIO with the exported key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportPublicKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>;
+
+/**
+ * Import public key from PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get());
+ * auto imported_key = crypto::rsa::ImportPublicKeyFromPEM(crypto::bio::View(bio.get()));
+ * @endcode
+ *
+ * @param pem PEM encoded key.
+ * @returns imported key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>;
+
+/**
+ * Export private key from PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+ * std::cout << crypto::bio::View(bio.get()).String() << std::endl;
+ * @endcode
+ *
+ * @param key The key to export.
+ * @returns a memory BIO with the exported key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>;
+
+/**
+ * Export private key from PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto pem  = crypto::rsa::ExportPrivateKeyToPEMString(key.get());
+ * std::cout << pem << std::endl;
+ * @endcode
+ *
+ * @param key The key to export.
+ * @returns the exported key in PEM format.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string;
+
+/**
+ * Import private key from PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+ * auto imported_key = crypto::rsa::ImportPrivateKeyFromPEM(crypto::bio::View(bio.get()));
+ * @endcode
+ *
+ * @param pem PEM encoded key.
+ * @returns imported key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>;
+
+} // vereign::crypto::rsa
+
+#endif // __VEREIGN_CRYPTO_RSA_HH
diff --git a/cpp/src/vereign/encoding/base64.cc b/cpp/src/vereign/encoding/base64.cc
new file mode 100644
index 0000000000000000000000000000000000000000..88e1e539f83ac5c8cb960a500a7b605fde093fcc
--- /dev/null
+++ b/cpp/src/vereign/encoding/base64.cc
@@ -0,0 +1,40 @@
+#include <vereign/encoding/base64.hh>
+
+#include <boost/beast/core/detail/base64.hpp>
+
+namespace vereign::encoding::base64 {
+
+void Encode(bytes::View src, bytes::Buffer& encoded) {
+  if (src.Size() == 0) {
+    return;
+  }
+
+  encoded.Reserve(boost::beast::detail::base64::encoded_size(src.Size()));
+  auto written = boost::beast::detail::base64::encode(encoded.end(), src.Data(), src.Size());
+  encoded.IncSize(written);
+}
+
+auto EncodeToString(bytes::View src) -> std::string {
+  if (src.Size() == 0) {
+    return "";
+  }
+
+  std::string encoded;
+  encoded.resize(boost::beast::detail::base64::encoded_size(src.Size()));
+  auto written = boost::beast::detail::base64::encode(encoded.data(), src.Data(), src.Size());
+  encoded.resize(written);
+
+  return encoded;
+}
+
+void Decode(bytes::View src, bytes::Buffer& decoded) {
+  if (src.Size() == 0) {
+    return ;
+  }
+
+  decoded.Reserve(boost::beast::detail::base64::decoded_size(src.Size()));
+  auto written = boost::beast::detail::base64::decode(decoded.end(), src.CharData(), src.Size());
+  decoded.IncSize(written.first);
+}
+
+} // vereign::encoding::base64
diff --git a/cpp/src/vereign/encoding/base64.hh b/cpp/src/vereign/encoding/base64.hh
new file mode 100644
index 0000000000000000000000000000000000000000..d3ee3ce4b13a22628dca0c1fe76653e762c3aacd
--- /dev/null
+++ b/cpp/src/vereign/encoding/base64.hh
@@ -0,0 +1,65 @@
+#ifndef __VEREIGN_ENCODING_BASE64_HH
+#define __VEREIGN_ENCODING_BASE64_HH
+
+#include <vereign/bytes/buffer.hh>
+#include <string>
+
+namespace vereign::encoding::base64 {
+
+/**
+ * Encodes source bytes into base64 encoding.
+ *
+ * No new lines are inserted.
+ *
+ * Example:
+ * @code
+ * std::string s{"foob"};
+ * bytes::Buffer encoded;
+ * encoding::base64::Encode(bytes::View(s), encoded);
+ *
+ * assert(encoded.View().String() == "Zm9vYg==")
+ * @endcode
+ *
+ * @param src The source bytes that will be encoded.
+ * @param encoded The encoded bytes.
+ */
+void Encode(bytes::View src, bytes::Buffer& encoded);
+
+/**
+ * Encodes source bytes into base64 encoding.
+ *
+ * No new lines are inserted.
+ *
+ * Example:
+ * @code
+ * std::string s{"foob"};
+ * auto encoded = encoding::base64::Encode(bytes::View(s));
+ *
+ * assert(encoded == "Zm9vYg==")
+ * @endcode
+ *
+ * @param src The source bytes that will be encoded.
+ * @returns the encoded string.
+ */
+auto EncodeToString(bytes::View src) -> std::string;
+
+/**
+ * Decodes base64 encoded bytes.
+ *
+ * Example:
+ * @code
+ * std::string s{"Zm9vYg=="};
+ * bytes::Buffer decoded;
+ * encoding::base64::Decode(bytes::View(s), decoded);
+ *
+ * assert(decoded.View().String() == "foob");
+ * @endcode
+ *
+ * @param src The base64 encoded bytes that will be decoded.
+ * @param decoded The decoded bytes.
+ */
+void Decode(bytes::View src, bytes::Buffer& decoded);
+
+} // vereign::encoding::base64
+
+#endif // __VEREIGN_ENCODING_BASE64_HH
diff --git a/cpp/src/vereign/encoding/binary.cc b/cpp/src/vereign/encoding/binary.cc
new file mode 100644
index 0000000000000000000000000000000000000000..3c25446ccf77e123d6c19ec71578a38ab895c34e
--- /dev/null
+++ b/cpp/src/vereign/encoding/binary.cc
@@ -0,0 +1,73 @@
+#include <vereign/encoding/binary.hh>
+
+#include <vereign/encoding/errors.hh>
+
+namespace vereign::encoding::binary {
+
+void EncodeUint8(bytes::Buffer& out, uint8_t v) {
+  out.Reserve(1);
+  out.end()[0] = v;
+  out.IncSize(1);
+}
+
+auto DecodeUint8(bytes::View& b) -> uint8_t {
+  if (b.Size() < 1) {
+    throw encoding::Error("decoding failed the input size is less than 1 bytes");
+  }
+
+  return b.Data()[0];
+}
+
+void EncodeUint64(uint8_t* out, uint64_t v) {
+  out[0] = uint8_t(v);
+  out[1] = uint8_t(v >> 8);
+  out[2] = uint8_t(v >> 16);
+  out[3] = uint8_t(v >> 24);
+  out[4] = uint8_t(v >> 32);
+  out[5] = uint8_t(v >> 40);
+  out[6] = uint8_t(v >> 48);
+  out[7] = uint8_t(v >> 56);
+}
+
+void EncodeUint64(bytes::Buffer& out, uint64_t v) {
+  out.Reserve(8);
+  EncodeUint64(out.end(), v);
+  out.IncSize(8);
+}
+
+auto DecodeUint64(const uint8_t* b) -> uint64_t {
+  return uint64_t(b[0])
+    | uint64_t(b[1]) << 8
+    | uint64_t(b[2]) << 16
+    | uint64_t(b[3]) << 24
+    | uint64_t(b[4]) << 32
+    | uint64_t(b[5]) << 40
+    | uint64_t(b[6]) << 48
+    | uint64_t(b[7]) << 56;
+}
+
+auto DecodeUint64(bytes::View b) -> uint64_t {
+  if (b.Size() < 8) {
+    throw encoding::Error("decoding failed the input size is less than 8 bytes");
+  }
+
+  return DecodeUint64(b.Data());
+}
+
+void EncodeBytes(bytes::Buffer& out, bytes::View bytes) {
+  EncodeUint64(out, uint64_t(bytes.Size()));
+  out.Write(bytes);
+}
+
+auto DecodeBytes(bytes::View b, bytes::View& out) -> std::size_t {
+  auto size = DecodeUint64(b);
+  if (size < 0 || size + 8 > b.Size()) {
+    throw encoding::Error("decode bytes failed: invalid size");
+  }
+
+  out = b.Slice(8, size + 8);
+
+  return 8 + size;
+}
+
+} // namespace vereign::encoding::binary
diff --git a/cpp/src/vereign/encoding/binary.hh b/cpp/src/vereign/encoding/binary.hh
new file mode 100644
index 0000000000000000000000000000000000000000..8be71c31f3a76f1c4c36c7e631f8731253edd64d
--- /dev/null
+++ b/cpp/src/vereign/encoding/binary.hh
@@ -0,0 +1,101 @@
+#ifndef __VEREIGN_ENCODING_BINARY_HH
+#define __VEREIGN_ENCODING_BINARY_HH
+
+#include <stdexcept>
+#include <vereign/bytes/buffer.hh>
+
+/**
+ * Binary encoding of integers, bytes.
+ *
+ * The integers are encoded in little endian.
+ */
+namespace vereign::encoding::binary {
+
+/**
+ * Encodes a byte.
+ *
+ * On success the out buffer size will be incremented with 1.
+ *
+ * @param out buffer where the byte will be written.
+ * @param v the byte that will be encoded.
+ */
+void EncodeUint8(bytes::Buffer& out, uint8_t v);
+
+/**
+ * Decodes a byte.
+ *
+ * @param b the source bytes from where the single byte will be decoded.
+ */
+auto DecodeUint8(bytes::View& b) -> uint8_t;
+
+/**
+ * Encodes a uint64.
+ *
+ * The number is encoded in little endian.
+ *
+ * @param out where the value will be written.
+ * @param v the number that will be encoded.
+ */
+void EncodeUint64(uint8_t* out, uint64_t v);
+
+/**
+ * Encodes a uint64.
+ *
+ * On success the out buffer size will be incremented with 8.
+ * The number is encoded in little endian.
+ *
+ * @param out where the value will be written.
+ * @param v the number that will be encoded.
+ */
+void EncodeUint64(bytes::Buffer& out, uint64_t v);
+
+/**
+ * Decodes a uint64.
+ *
+ * The number is expected to be encoded in little endian.
+ *
+ * @param b the source number.
+ * @returns the decoded number.
+ */
+auto DecodeUint64(const uint8_t* b) -> uint64_t;
+
+/**
+ * Decodes a uint64.
+ *
+ * The number is expected to be encoded in little endian.
+ *
+ * @param b the source bytes where the encoded number will be read from.
+ * @returns the decoded number.
+ *
+ * @throws encoding::Error when the source bytes size is less than 8.
+ */
+auto DecodeUint64(bytes::View b) -> uint64_t;
+
+/**
+ * Encodes bytes.
+ *
+ * The result is 8 bytes for the size, followed by the bytes them self.
+ * The out buffer size is incremented with out.Size() + 8.
+ *
+ * @param out the where the encoded value will be written.
+ * @param bytes the source bytes that will be encoded.
+ */
+void EncodeBytes(bytes::Buffer& out, bytes::View bytes);
+
+/**
+ * Decode bytes.
+ *
+ * Note that the decoded bytes reference to the source buffer.
+ * In other words it is only a view, there is no copying involved.
+ *
+ * @param b the source with the encoded bytes.
+ * @param out the decoded bytes.
+ * @returns the bytes read during the decoding (8 + out.Size()).
+ *
+ * @throws encoding::Error when the decoding fails.
+ */
+auto DecodeBytes(bytes::View b, bytes::View& out) -> std::size_t;
+
+} // namespace vereign::encoding::binary
+
+#endif // __VEREIGN_ENCODING_BINARY_HH
diff --git a/cpp/src/vereign/encoding/errors.hh b/cpp/src/vereign/encoding/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..d9d012210165fb645f600dc60a989dfb5ffcb392
--- /dev/null
+++ b/cpp/src/vereign/encoding/errors.hh
@@ -0,0 +1,21 @@
+#ifndef __VEREIGN_ENCODING_ERRORS_HH
+#define __VEREIGN_ENCODING_ERRORS_HH
+
+#include <stdexcept>
+
+namespace vereign::encoding {
+
+/**
+ * Base error for namespace vereign::encoding.
+ */
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+} // namespace vereign::encoding::binary
+
+#endif // __VEREIGN_ENCODING_ERRORS_HH
diff --git a/cpp/src/vereign/encoding/hex.cc b/cpp/src/vereign/encoding/hex.cc
new file mode 100644
index 0000000000000000000000000000000000000000..46ed21564dbd5e8cc363e43e04123a9c6a5bddaf
--- /dev/null
+++ b/cpp/src/vereign/encoding/hex.cc
@@ -0,0 +1,71 @@
+#include <vereign/encoding/hex.hh>
+
+namespace vereign::encoding::hex {
+
+namespace detail {
+
+auto charToInt(char ch) -> int {
+  if (ch >= '0' && ch <= '9') {
+    return ch - '0';
+  }
+
+  if (ch >= 'A' && ch <= 'F') {
+    return ch - 'A' + 10;
+  }
+
+  if (ch >= 'a' && ch <= 'f') {
+    return ch - 'a' + 10;
+  }
+
+  return 0;
+}
+
+} // namespace detail
+
+void Encode(bytes::View src, bytes::Buffer& encoded) {
+  if (src.Size() == 0) {
+    return;
+  }
+
+  static const char* nibbles = { "0123456789abcdef" };
+
+  encoded.Reserve(src.Size() * 2);
+
+  for (std::size_t i = 0; i < src.Size(); ++i) {
+    encoded[i*2] = nibbles[src[i] >> 4];
+    encoded[i*2 + 1] = nibbles[src[i] & 0x0F];
+  }
+
+  encoded.IncSize(src.Size() * 2);
+}
+
+void Decode(bytes::View src, bytes::Buffer& decoded) {
+  if (src.Size() == 0) {
+    return ;
+  }
+
+  decoded.Reserve(src.Size() / 2);
+  for (int i = 0, len = (int) src.Size() - 1; i < len; i += 2) {
+    decoded[i/2] = detail::charToInt(src[i]) * 16 + detail::charToInt(src[i + 1]);
+  }
+  decoded.IncSize(src.Size() / 2);
+}
+
+void EncodeReverse(bytes::View src, bytes::Buffer& encoded) {
+  if (src.Size() == 0) {
+    return;
+  }
+
+  static const char* nibbles = { "0123456789abcdef" };
+
+  encoded.Reserve(src.Size() * 2);
+
+  for (std::size_t i = 0; i < src.Size(); ++i) {
+    encoded[i*2] = nibbles[src[src.Size() - 1 - i] >> 4];
+    encoded[i*2 + 1] = nibbles[src[src.Size() - 1 - i] & 0x0F];
+  }
+
+  encoded.IncSize(src.Size() * 2);
+}
+
+} // vereign::encoding::base64
diff --git a/cpp/src/vereign/encoding/hex.hh b/cpp/src/vereign/encoding/hex.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6dec0f13270bae1d163d7f9114e56276f082515d
--- /dev/null
+++ b/cpp/src/vereign/encoding/hex.hh
@@ -0,0 +1,59 @@
+#ifndef __VEREIGN_ENCODING_HEX_HH
+#define __VEREIGN_ENCODING_HEX_HH
+
+#include <vereign/bytes/buffer.hh>
+#include <string>
+
+namespace vereign::encoding::hex {
+
+/**
+ * Encodes bytes in hexadecimal.
+ *
+ * Example:
+ * @code
+ * bytes::Buffer encoded;
+ * encoding::hex::Decode(bytes::View("foobar"), encoded);
+ *
+ * assert(encoded.View().String() == "666f6f626172");
+ * @endcode
+ *
+ * @param src The bytes that will be encoded.
+ * @param encoded The buffer where the encoded bytes will be written.
+ */
+void Encode(bytes::View src, bytes::Buffer& encoded);
+
+/**
+ * Decodes hexadecimal encoded bytes.
+ *
+ * Example:
+ * @code
+ * bytes::Buffer decoded;
+ * encoding::hex::Decode(bytes::View("666f6f626172"), decoded);
+ *
+ * assert(decoded.View().String() == "foobar");
+ * @endcode
+ *
+ * @param src The bytes that will be decoded.
+ * @param encoded The buffer where the decoded bytes will be written.
+ */
+void Decode(bytes::View src, bytes::Buffer& decoded);
+
+/**
+ * Encodes bytes in hexadecimal in reverse order.
+ *
+ * Example:
+ * @code
+ * bytes::Buffer encoded;
+ * encoding::hex::Decode(bytes::View("foobar"), encoded);
+ *
+ * assert(encoded.View().String() == "7261626f6f66");
+ * @endcode
+ *
+ * @param src The bytes that will be encoded.
+ * @param encoded The buffer where the encoded bytes will be written.
+ */
+void EncodeReverse(bytes::View src, bytes::Buffer& encoded);
+
+} // vereign::encoding::hex
+
+#endif // __VEREIGN_ENCODING_HEX_HH
diff --git a/cpp/src/vereign/event/broker.cc b/cpp/src/vereign/event/broker.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f86e3e3932ec44b7d2cd0e943e5d1f2bcc499bab
--- /dev/null
+++ b/cpp/src/vereign/event/broker.cc
@@ -0,0 +1,266 @@
+#include <vereign/event/broker.hh>
+
+#include <vereign/restapi/client_session.hh>
+#include <vereign/identity/provider.hh>
+
+namespace {
+
+const auto pollInterval = std::chrono::seconds(1);
+
+const auto eventsModeEntity = std::string{"entity"};
+const auto eventsModeDeviceKey = std::string{"devicekey"};
+
+const auto getNewEventsWithoutSessionPath = std::string{"/event/getNewEventsWithoutSession"};
+const auto getNewEventsPath = std::string{"/event/getNewEvents"};
+
+const auto updateLastViewedWithoutSessionPath = std::string{"/event/updateLastViewedWithoutSession"};
+const auto updateLastViewedPath = std::string{"/event/updateLastViewed"};
+
+auto eventBatchTime(vereign::client_library::GetNewEventsFormResponse& resp) -> int64_t {
+  int64_t max_time = 0;
+  for (auto& event : resp.data()) {
+    max_time = std::max(max_time, event.stamp());
+  }
+
+  return max_time;
+}
+
+}
+
+namespace vereign::event {
+
+Broker::Broker(restapi::ClientSession& client_session)
+  : client_session_{client_session}
+{
+  entity_req_.set_mode(eventsModeEntity);
+  device_req_.set_mode(eventsModeDeviceKey);
+
+  update_entity_req_.set_mode(eventsModeEntity);
+  update_device_req_.set_mode(eventsModeDeviceKey);
+
+  dispatch_thread_ = std::thread([this] {
+    pollAndDispatch();
+  });
+}
+
+auto Broker::Subscribe() -> Subscription {
+  auto Subscribe() -> Subscription;
+  auto channel = std::make_shared<sync::Channel<EventTypeSharedPtr>>(10);
+
+  uint64_t id = 0;
+
+  {
+    std::lock_guard<std::mutex> lock{mu_};
+
+    id = ++next_subscription_id_;
+    subscriptions_[id] = Subscription{};
+    subscriptions_[id].ID = id;
+    subscriptions_[id].EventsChannel = channel;
+  }
+
+  cv_.notify_one();
+
+  return subscriptions_[id];
+}
+
+void Broker::Unsubscribe(const Subscription& subscription) {
+  std::unique_lock<std::mutex> lock{mu_};
+
+  auto it = subscriptions_.find(subscription.ID);
+  if (it == subscriptions_.end()) {
+    return;
+  }
+
+  it->second.EventsChannel->Close();
+
+  subscriptions_.erase(it);
+}
+
+auto Broker::dispatchEvents(EventTypeSharedPtr events) -> int {
+  std::unique_lock<std::mutex> lock{mu_};
+  cv_.wait(lock, [this] {
+    return subscriptions_.size() > 0;
+  });
+
+  int dispatched_cnt = 0;
+
+  for (auto& s : subscriptions_) {
+    auto channel = s.second.EventsChannel;
+
+    auto r = channel->TryAdd(events);
+    if (r.IsOk()) {
+      dispatched_cnt++;
+    }
+  }
+
+  return dispatched_cnt;
+}
+
+// NOTE: it is assumed that the mutex is already locked.
+void Broker::closeAllSubscriptions() {
+  for (auto& s: subscriptions_) {
+    s.second.EventsChannel->Close();
+  }
+}
+
+void Broker::pollEventsWithoutSession() {
+  if (!client_session_.HasIdentity()) {
+    return;
+  }
+
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.PublicPost(
+    getNewEventsWithoutSessionPath,
+    &device_req_,
+    resp.get()
+  );
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_device_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.PublicPost(
+    updateLastViewedWithoutSessionPath,
+    &update_device_req_,
+    &update_device_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollEntityEvents() {
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.Post(getNewEventsPath, &entity_req_, resp.get());
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_entity_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.Post(
+    updateLastViewedPath,
+    &update_entity_req_,
+    &update_entity_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollDeviceEvents() {
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.Post(getNewEventsPath, &device_req_, resp.get());
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_device_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.Post(
+    updateLastViewedPath,
+    &update_device_req_,
+    &update_device_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollAndDispatch() {
+  for (;;) {
+    {
+      std::unique_lock<std::mutex> lock{mu_};
+      cv_.wait(lock, [this] {
+        return subscriptions_.size() > 0 || stopped_ == true;
+      });
+
+      if (stopped_) {
+        closeAllSubscriptions();
+        break;
+      }
+    }
+
+    if (!client_session_.HasSession()) {
+      pollEventsWithoutSession();
+    } else {
+      pollDeviceEvents();
+      pollEntityEvents();
+    }
+
+    std::this_thread::sleep_for(pollInterval);
+  }
+}
+
+void Broker::Shutdown() {
+  {
+    std::lock_guard<std::mutex> lock{mu_};
+
+    if (stopped_) {
+      return;
+    }
+
+    stopped_ = true;
+  }
+
+  cv_.notify_one();
+
+  if (dispatch_thread_.joinable()) {
+    dispatch_thread_.join();
+  }
+}
+
+Broker::~Broker() {
+  Shutdown();
+}
+
+} // namespace vereign::event
diff --git a/cpp/src/vereign/event/broker.hh b/cpp/src/vereign/event/broker.hh
new file mode 100644
index 0000000000000000000000000000000000000000..f8842dc209a989cb1d14f118dbc5ac63aa22a5e6
--- /dev/null
+++ b/cpp/src/vereign/event/broker.hh
@@ -0,0 +1,131 @@
+#ifndef __VEREIGN_EVENT_BROKER_HH
+#define __VEREIGN_EVENT_BROKER_HH
+
+#include <vereign/sync/channel.hh>
+#include <vereign/client_library/event_types.pb.h>
+#include <vereign/client_library/internal/event_types.pb.h>
+#include <vereign/client_library/common_types.pb.h>
+
+#include <thread>
+#include <unordered_map>
+
+// forward declarations
+namespace vereign::restapi {
+
+class ClientSession;
+
+} // namespace vereign
+
+namespace vereign::event {
+
+using EventType = client_library::GetNewEventsFormResponse;
+using EventTypeSharedPtr = std::shared_ptr<EventType>;
+using EventsChannelSharedPtr = std::shared_ptr<sync::Channel<EventTypeSharedPtr>>;
+
+/**
+ * Subscription that is returned, when calling Broker::Subscribe.
+ *
+ * Once the consumer is done consuming, it must call Broker::Unsubscribe, passing the
+ * Subscription object.
+ */
+struct Subscription {
+  /**
+   * Subscription id.
+   */
+  uint64_t ID;
+
+  /**
+   * Channel that the subscribers can use to receive dispatched events.
+   */
+  EventsChannelSharedPtr EventsChannel;
+};
+
+/**
+ * Broker polls restful API for events and dispatches them to a set of subscribers.
+ *
+ * All public methods are thread safe.
+ */
+class Broker {
+public:
+  /**
+   * Creates a broker.
+   *
+   * @param client_session The client session used for polling the restful API for new events.
+   */
+  Broker(restapi::ClientSession& client_session);
+
+  /**
+   * Shutdown the broker.
+   */
+  ~Broker();
+
+  // disable copying
+  Broker(const Broker&) = delete;
+  auto operator=(const Broker&) -> Broker = delete;
+
+  /**
+   * Subscribe for events.
+   *
+   * Once the consumer is done with the returned Subscription, it must call
+   * Broker::Unsubscribe, passing the Subscription object.
+   *
+   * @returns a subscription.
+   */
+  auto Subscribe() -> Subscription;
+
+  /**
+   * Remove a subscription.
+   *
+   * The broker stops to dispatch events to the `subscription` channel.
+   */
+  void Unsubscribe(const Subscription& subscription);
+
+  /**
+   * Shutdown the broker.
+   *
+   * All subscription channels will be closed.
+   *
+   * Shutdown is idempotent, meaning that it can be safely called more than once.
+   */
+  void Shutdown();
+
+private:
+  void pollEventsWithoutSession();
+  void pollDeviceEvents();
+  void pollEntityEvents();
+  void pollAndDispatch();
+  auto dispatchEvents(EventTypeSharedPtr events) -> int;
+  void closeAllSubscriptions();
+
+private:
+  restapi::ClientSession& client_session_;
+
+  std::thread dispatch_thread_;
+
+  client_library::internal::GetNewEventsForm entity_req_;
+
+  client_library::internal::GetNewEventsForm device_req_;
+
+  client_library::internal::UpdateLastViewedForm update_entity_req_;
+
+  client_library::internal::UpdateLastViewedForm update_device_req_;
+
+  client_library::EmptyResponse update_entity_resp_;
+
+  client_library::EmptyResponse update_device_resp_;
+
+  // protects the fields that follow
+  std::mutex mu_;
+
+  std::condition_variable cv_;
+
+  bool stopped_ = false;
+
+  int64_t next_subscription_id_ = 0;
+
+  std::unordered_map<int64_t, Subscription> subscriptions_;
+};
+
+} // namespace vereign::event
+
+#endif // __VEREIGN_EVENT_BROKER_HH
diff --git a/cpp/src/vereign/fs/errors.hh b/cpp/src/vereign/fs/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ef72c8c00655fd544e4cca926ca1493f94161d73
--- /dev/null
+++ b/cpp/src/vereign/fs/errors.hh
@@ -0,0 +1,35 @@
+#ifndef __VEREIGN_FS_ERRORS_HH
+#define __VEREIGN_FS_ERRORS_HH
+
+#include <string>
+#include <stdexcept>
+
+namespace vereign::fs {
+
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error{what}
+  {
+  }
+};
+
+class HomeNotFoundError : public Error {
+public:
+  HomeNotFoundError()
+    : Error{"cannot find user's home dir"}
+  {
+  }
+};
+
+class TempDirNotFoundError : public Error {
+public:
+  TempDirNotFoundError()
+    : Error{"cannot find user's temp dir"}
+  {
+  }
+};
+
+} // namespace vereign::fs
+
+#endif // __VEREIGN_FS_ERRORS_HH
diff --git a/cpp/src/vereign/fs/operations.cc b/cpp/src/vereign/fs/operations.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9f33ed2d31d3322c2748ee32ba91d4b9c477f53f
--- /dev/null
+++ b/cpp/src/vereign/fs/operations.cc
@@ -0,0 +1,29 @@
+#include <vereign/fs/operations.hh>
+
+#include <vereign/fs/util.hh>
+#include <vereign/core/string.hh>
+
+#include <boost/filesystem/directory.hpp>
+#include <boost/filesystem/operations.hpp>
+#include <fstream>
+
+namespace vereign::fs {
+
+auto Exists(const std::string& path) -> bool {
+#ifdef _WIN32
+  std::ifstream f{string::widen(path)};
+#else
+  std::ifstream f{path};
+#endif
+  return f.good();
+}
+
+auto IsDir(const std::string& path) -> bool {
+  return boost::filesystem::is_directory(fs::detail::StringToPath(path));
+}
+
+auto CreateDir(const std::string& path) -> bool {
+  return boost::filesystem::create_directory(fs::detail::StringToPath(path));
+}
+
+} // namespace vereign::fs
diff --git a/cpp/src/vereign/fs/operations.hh b/cpp/src/vereign/fs/operations.hh
new file mode 100644
index 0000000000000000000000000000000000000000..e5f60c773a1eb9de85d76b32100e0c11d033c5bf
--- /dev/null
+++ b/cpp/src/vereign/fs/operations.hh
@@ -0,0 +1,29 @@
+#ifndef __VEREIGN_FS_OPERATIONS_HH
+#define __VEREIGN_FS_OPERATIONS_HH
+
+#include <string>
+
+namespace vereign::fs {
+
+/**
+ * Check if file exists and is readable.
+ */
+auto Exists(const std::string& path) -> bool;
+
+/**
+ * Check if given path is a directory.
+ *
+ * @param path Path to check.
+ */
+auto IsDir(const std::string& path) -> bool;
+
+/**
+ * Creates the last directory of the provided path.
+ *
+ * @param path Path of the directory that will be created.
+ */
+auto CreateDir(const std::string& path) -> bool;
+
+} // namespace vereign::fs
+
+#endif // __VEREIGN_FS_OPERATIONS_HH
diff --git a/cpp/src/vereign/fs/path.cc b/cpp/src/vereign/fs/path.cc
new file mode 100644
index 0000000000000000000000000000000000000000..fc92ce61945f17a34df5f3575b4605303733a978
--- /dev/null
+++ b/cpp/src/vereign/fs/path.cc
@@ -0,0 +1,37 @@
+#include <vereign/fs/path.hh>
+
+namespace vereign::fs::path {
+
+/**
+ * Convert relative path from host OS to unix format.
+ *
+ * It replaces the host path separator with the unix path separator.
+ * For example "foo\\bar" on windows becomes "foo/bar".
+ *
+ * @param path Source path.
+ */
+auto to_unix(std::string path) -> std::string {
+  if (sep[0] != unix_sep[0]) {
+    std::replace(path.begin(), path.end(), sep[0], unix_sep[0]);
+  }
+
+  return path;
+}
+
+/**
+ * Convert relative path from unix format to native OS format.
+ *
+ * It replaces the unix path separator with the native path separator.
+ * For example "foo/bar" on windows becomes "foo\\bar".
+ *
+ * @param path Source path.
+ */
+auto to_native(std::string path) -> std::string {
+  if (sep[0] != unix_sep[0]) {
+    std::replace(path.begin(), path.end(), unix_sep[0], sep[0]);
+  }
+
+  return path;
+}
+
+} // namespace vereign::fs::path
diff --git a/cpp/src/vereign/fs/path.hh b/cpp/src/vereign/fs/path.hh
new file mode 100644
index 0000000000000000000000000000000000000000..87d58adc00845b30d16ed155010a1ea23ca4b244
--- /dev/null
+++ b/cpp/src/vereign/fs/path.hh
@@ -0,0 +1,60 @@
+#ifndef __VEREIGN_FS_PATH_HH
+#define __VEREIGN_FS_PATH_HH
+
+#include <boost/core/ignore_unused.hpp>
+#include <algorithm>
+#include <string>
+
+namespace vereign::fs::path {
+
+#if defined(_WIN32)
+  constexpr inline const char* unix_sep = "/";
+  constexpr inline const char* sep = "\\";
+  constexpr inline const char* prefix = "\\\\?\\";
+  constexpr inline const char* drive_sep = ":\\";
+  constexpr inline const char invalid_escape_char = '_';
+#else
+  constexpr inline const char* unix_sep = "/";
+  constexpr inline const char* sep = "/";
+  constexpr inline const char* prefix = "/";
+  constexpr inline const char* drive_sep = "";
+  constexpr inline const char invalid_escape_char = '_';
+#endif
+
+
+/**
+ * Join path fragments with the file system path separator.
+ */
+template <typename... Args>
+auto Join(const Args&... args) -> std::string {
+  std::string path;
+  int unpack[] { 0, (path = path + sep + args, 0)... };
+  boost::ignore_unused(unpack);
+  path.erase(0, 1);
+
+  return path;
+}
+
+/**
+ * Convert relative path from host OS to unix format.
+ *
+ * It replaces the host path separator with the unix path separator.
+ * For example "foo\\bar" on windows becomes "foo/bar".
+ *
+ * @param path Source path.
+ */
+auto to_unix(std::string path) -> std::string;
+
+/**
+ * Convert relative path from unix format to native OS format.
+ *
+ * It replaces the unix path separator with the native path separator.
+ * For example "foo/bar" on windows becomes "foo\\bar".
+ *
+ * @param path Source path.
+ */
+auto to_native(std::string path) -> std::string;
+
+} // namespace vereign::fs::path
+
+#endif // __VEREIGN_FS_PATH_HH
diff --git a/cpp/src/vereign/fs/util.cc b/cpp/src/vereign/fs/util.cc
new file mode 100644
index 0000000000000000000000000000000000000000..8d60e3e827c1228e2f5db2a00444ebab66674db2
--- /dev/null
+++ b/cpp/src/vereign/fs/util.cc
@@ -0,0 +1,153 @@
+#include <vereign/fs/util.hh>
+
+#include <vereign/fs/operations.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/fs/errors.hh>
+
+#include <vereign/core/rand.hh>
+#include <vereign/core/string.hh>
+#include <boost/filesystem/operations.hpp>
+#include <boost/filesystem/fstream.hpp>
+
+// FIXME: remove
+#include <iostream>
+
+#ifdef _WIN32
+# include <shlobj_core.h>
+#endif
+
+namespace vereign::fs {
+
+namespace detail {
+
+auto PathToString(const boost::filesystem::path& path) -> std::string {
+#ifdef _WIN32
+  return string::narrow(path.wstring());
+#else
+  return path.string();
+#endif
+}
+
+auto StringToPath(std::string_view path) -> boost::filesystem::path {
+#ifdef _WIN32
+  return boost::filesystem::path{string::widen(path)};
+#else
+  return boost::filesystem::path{std::string{path}};
+#endif
+}
+
+}
+
+RemoveFileGuard::RemoveFileGuard(std::string path)
+  : path_{std::move(path)}
+{
+}
+
+RemoveFileGuard::~RemoveFileGuard() {
+  boost::filesystem::remove(detail::StringToPath(path_));
+}
+
+RemoveAllGuard::RemoveAllGuard(std::string path)
+  : path_{std::move(path)}
+{
+}
+
+RemoveAllGuard::~RemoveAllGuard() {
+  boost::filesystem::remove_all(detail::StringToPath(path_));
+}
+
+auto TempFilePath(std::string_view dir, std::string_view prefix) -> std::string {
+  return path::Join(std::string(dir), std::string(prefix) + core::RandLowerAlpha(10));
+}
+
+auto TempFilePath(std::string_view prefix) -> std::string {
+  return TempFilePath(detail::PathToString(boost::filesystem::temp_directory_path()), prefix);
+}
+
+auto TempDir(std::string_view prefix) -> std::string {
+  auto dir = TempFilePath(prefix);
+
+  CreateDir(dir);
+
+  return dir;
+}
+
+auto HomePath() -> std::string {
+#ifdef _WIN32
+  PWSTR home;
+  SHGetKnownFolderPath(
+    FOLDERID_LocalAppData,
+    0,
+    nullptr,
+    &home
+  );
+
+  std::wstring wpath;
+  try {
+    wpath = home;
+    CoTaskMemFree(home);
+  } catch (...) {
+    CoTaskMemFree(home);
+    throw;
+  }
+
+  return "";
+
+  auto path = boost::filesystem::path(wpath);
+  if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) {
+    return detail::PathToString(path);
+  }
+
+  throw HomeNotFoundError{};
+#else
+  auto home = std::getenv("HOME");
+  if (home == nullptr) {
+    throw HomeNotFoundError{};
+  }
+
+  auto path = boost::filesystem::path{home};
+  if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) {
+    return detail::PathToString(path);
+  }
+
+  throw HomeNotFoundError{};
+#endif
+}
+
+auto ReadFile(std::string_view path) -> bytes::Buffer {
+  boost::filesystem::ifstream file{
+    detail::StringToPath(path),
+    boost::filesystem::fstream::binary | boost::filesystem::fstream::ate,
+  };
+
+  if (!file.is_open()) {
+    throw fs::Error("open file for reading failed");
+  }
+
+  auto size = file.tellg();
+  bytes::Buffer result{static_cast<size_t>(size)};
+  file.seekg(0);
+
+  file.read((char*) result.end(), size);
+  result.IncSize(file.gcount());
+
+  file.close();
+
+  return result;
+}
+
+void WriteFile(std::string_view path, bytes::View data) {
+  boost::filesystem::ofstream file{
+    detail::StringToPath(path),
+    boost::filesystem::fstream::binary | boost::filesystem::fstream::trunc,
+  };
+
+  if (!file.is_open()) {
+    throw fs::Error("open file for writing failed");
+  }
+
+  file.write(data.CharData(), data.Size());
+  file.close();
+}
+
+} // namespace vereign::fs
diff --git a/cpp/src/vereign/fs/util.hh b/cpp/src/vereign/fs/util.hh
new file mode 100644
index 0000000000000000000000000000000000000000..d665902476cd8f2760a7729fe3649027463c13cc
--- /dev/null
+++ b/cpp/src/vereign/fs/util.hh
@@ -0,0 +1,117 @@
+#ifndef __VEREIGN_FS_UTIL_HH
+#define __VEREIGN_FS_UTIL_HH
+
+#include <vereign/bytes/buffer.hh>
+#include <boost/filesystem/path.hpp>
+#include <string>
+#include <string_view>
+
+namespace vereign::fs {
+
+namespace detail {
+
+auto PathToString(const boost::filesystem::path& path) -> std::string;
+auto StringToPath(std::string_view path) -> boost::filesystem::path;
+
+} // namespace detail
+
+/**
+ * A RAII guard that deletes a file upon destruction.
+ */
+class RemoveFileGuard {
+public:
+  RemoveFileGuard(std::string path);
+  ~RemoveFileGuard();
+
+  RemoveFileGuard(const RemoveFileGuard&) = delete;
+  auto operator=(const RemoveFileGuard&) -> RemoveFileGuard& = delete;
+private:
+  std::string path_;
+};
+
+/**
+ * A RAII guard that deletes all files and directories recursively.
+ */
+class RemoveAllGuard {
+public:
+  RemoveAllGuard(std::string path);
+  ~RemoveAllGuard();
+
+  RemoveAllGuard(const RemoveAllGuard&) = delete;
+  auto operator=(const RemoveAllGuard&) -> RemoveAllGuard& = delete;
+private:
+  std::string path_;
+};
+
+/**
+ * Generates a file path usable as temporary file.
+ *
+ * For a temp file path under the system tmp dir use the other TempFilePath overload.
+ *
+ * @code
+ * auto path = fs::TempFilePath("/tmp/foo", "test_db_");
+ * std::cout << path << std::endl;
+ *
+ * // Output:
+ * // /tmp/foo/test_db_bh0vcr0jbz
+ * @endcode
+ *
+ * @param dir The directory of the temp file path.
+ * @param prefix A prefix to prepend to the temp file name.
+ * @returns a file path usable as temporary file.
+ */
+auto TempFilePath(std::string_view dir, std::string_view prefix) -> std::string;
+
+/**
+ * Generates a file path usable as temporary file under the system temporary directory.
+ *
+ * It tries to detect the correct temp dir under different operation systems.
+ * For Linux this is `/tmp` and for Windows this is `C:\Users\<username>\AppData\Local\Temp`.
+ *
+ * @code
+ * auto path = fs::TempFilePath("test_db_");
+ * std::cout << path << std::endl;
+ *
+ * // Output:
+ * // /tmp/test_db_bh0vcr0jbz
+ * @endcode
+ *
+ * @param prefix A prefix to prepend to the temp file name.
+ * @returns a file path usable as temporary file.
+ */
+auto TempFilePath(std::string_view prefix) -> std::string;
+
+/**
+ * Creates a temporary sub directory under the system temporary directory.
+ *
+ * It tries to detect the correct temp dir under different operation systems.
+ * For Linux this is `/tmp` and for Windows this is `C:\Users\<username>\AppData\Local\Temp`.
+ *
+ * @code
+ * auto path = fs::TempDir("test_db_");
+ * std::cout << path << std::endl;
+ *
+ * // Output:
+ * // /tmp/test_db_bh0vcr0jbz
+ * @endcode
+ *
+ * @param prefix A prefix to prepend to the temp dir name.
+ * @returns a the path of the created temporary directory.
+ */
+auto TempDir(std::string_view prefix) -> std::string;
+
+/**
+ * Returns current user home directory.
+ *
+ * On Windows this is `C:\Users\<username>\AppData\Local`.
+ *
+ * @returns the user's home directory full path.
+ */
+auto HomePath() -> std::string;
+
+auto ReadFile(std::string_view path) -> bytes::Buffer;
+void WriteFile(std::string_view path, bytes::View data);
+
+} // namespace vereign::fs
+
+#endif // __VEREIGN_FS_UTIL_HH
diff --git a/cpp/src/vereign/grpc/error_code.hh b/cpp/src/vereign/grpc/error_code.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2c5c26e9d6ac060ac6351405aab90f0b4b0b5010
--- /dev/null
+++ b/cpp/src/vereign/grpc/error_code.hh
@@ -0,0 +1,38 @@
+#ifndef __VEREIGN_GRPC_ERROR_CODES_HH
+#define __VEREIGN_GRPC_ERROR_CODES_HH
+
+#include <cstdint>
+#include <string>
+
+namespace vereign::grpc {
+
+static constexpr const char* ClientErrorStatus = "Vereign Client Library Error";
+
+/**
+ * Error codes returned into the gRPC API response `code` field on various failures.
+ *
+ * These are errors that happen inside the Vereign Client Library.
+ * The errors that happen inside the Vereign Restful API are in the standard HTTP status code range
+ * below 600.
+ */
+enum class ErrorCode : uint64_t {
+  ClientError                 = 1000,
+  UnexpectedError             = 1001,
+  DeviceNotRegistered         = 1002,
+  InvalidPinCode              = 1003,
+  InvalidIdentity             = 1004
+};
+
+/**
+ * Convert the error code to string.
+ *
+ * @param ec The error code.
+ * @returns the error code integer as string.
+ */
+inline auto ErrorCodeAsString(ErrorCode ec) -> std::string {
+  return std::to_string(uint64_t(ec));
+}
+
+}
+
+#endif // __VEREIGN_GRPC_ERROR_CODES_HH
diff --git a/cpp/src/vereign/grpc/event_api.hh b/cpp/src/vereign/grpc/event_api.hh
new file mode 100644
index 0000000000000000000000000000000000000000..42afa02396fec2ce80a280924e9d1d897f978be6
--- /dev/null
+++ b/cpp/src/vereign/grpc/event_api.hh
@@ -0,0 +1,86 @@
+#ifndef __VEREIGN_GRPC_EVENT_API_HH
+#define __VEREIGN_GRPC_EVENT_API_HH
+
+#include <vereign/grpc/gen/event_api.hh>
+
+#include <vereign/event/broker.hh>
+#include <vereign/core/scope_guard.hh>
+
+// #include <vereign/grpc/error_code.hh>
+// #include <vereign/kvstore/errors.hh>
+// #include <vereign/client_library/common_types.pb.h>
+// #include <vereign/client_library/identity_types.pb.h>
+#include <boost/core/ignore_unused.hpp>
+
+#include <thread>
+
+namespace vereign::grpc {
+
+/**
+ * Implementation of the gRPC `vereign::client_library::EventAPI::Service` service.
+ *
+ * Inherits all the API implementations from the generated gen::EventAPI and adds some
+ * additional implementations.
+ */
+template <class VereignService>
+class EventAPI final : public gen::EventAPI<VereignService> {
+public:
+  // API service name.
+  static constexpr const char* Name = gen::EventAPI<VereignService>::Name;
+
+  using VereignServiceType = VereignService;
+  using VereignServicePtr = std::unique_ptr<VereignService>;
+
+  /**
+   * Constructs EventAPI instance.
+   *
+   * @param service The client library Event service.
+   */
+  EventAPI(VereignServicePtr&& service, event::Broker& events_broker)
+    : gen::EventAPI<VereignService>{std::move(service)},
+      events_broker_{events_broker}
+  {}
+
+  // disable copying
+  EventAPI(const EventAPI&) = delete;
+  auto operator=(const EventAPI&) -> EventAPI& = delete;
+
+  /**
+   * Returns to the gRPC client a gRPC stream for receiving last available events.
+   */
+  auto GetNewEvents(
+    ::grpc::ServerContext* ctx,
+    const client_library::GetNewEventsForm* req,
+    ::grpc::ServerWriter<client_library::GetNewEventsFormResponse>* resp_stream
+  ) -> ::grpc::Status override {
+    boost::ignore_unused(ctx);
+    boost::ignore_unused(req);
+
+    auto subscription = events_broker_.Subscribe();
+    auto unsubscribe = core::MakeScopeGuard([this, &subscription] {
+      events_broker_.Unsubscribe(subscription);
+    });
+    auto channel = subscription.EventsChannel;
+
+    for (;;) {
+      auto msg = channel->Get();
+      if (!msg) {
+        break;
+      }
+
+      auto ok = resp_stream->Write(*msg.Value());
+      if (!ok) {
+        break;
+      }
+    }
+
+    return ::grpc::Status::OK;
+  }
+
+private:
+  event::Broker& events_broker_;
+};
+
+} // namespace vereign::grpc
+
+#endif // __VEREIGN_GRPC_IDENTITY_API_HH
diff --git a/cpp/src/vereign/grpc/identity_api.hh b/cpp/src/vereign/grpc/identity_api.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6083548eb488823dafedf3991c9c704af929b91f
--- /dev/null
+++ b/cpp/src/vereign/grpc/identity_api.hh
@@ -0,0 +1,150 @@
+#ifndef __VEREIGN_GRPC_IDENTITY_API_HH
+#define __VEREIGN_GRPC_IDENTITY_API_HH
+
+#include <vereign/grpc/gen/identity_api.hh>
+
+#include <vereign/grpc/error_code.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/client_library/common_types.pb.h>
+#include <vereign/client_library/identity_types.pb.h>
+#include <boost/core/ignore_unused.hpp>
+
+namespace vereign::grpc {
+
+/**
+ * Implementation of the gRPC `vereign::client_library::IdentityAPI::Service` service.
+ *
+ * Inherits all the API implementations from the generated gen::IdentityAPI and adds some
+ * additional implementations.
+ *
+ * IdentityAPI is a thin layer on top of the service::IdentityService.
+ */
+template <class VereignService>
+class IdentityAPI final : public gen::IdentityAPI<VereignService> {
+public:
+  // API service name.
+  static constexpr const char* Name = gen::IdentityAPI<VereignService>::Name;
+
+  using VereignServiceType = VereignService;
+  using VereignServicePtr = std::unique_ptr<VereignService>;
+
+  /**
+   * Constructs IdentityAPI instance.
+   *
+   * @param service The client library Identity service.
+   */
+  IdentityAPI(VereignServicePtr&& service)
+    : gen::IdentityAPI<VereignService>{std::move(service)}
+  {}
+
+  // disable copying
+  IdentityAPI(const IdentityAPI&) = delete;
+  auto operator=(const IdentityAPI&) -> IdentityAPI& = delete;
+
+  /**
+   * Registers a new device.
+   *
+   * req.pin is required only under Linux.
+   *
+   * Under windows the system cypto storage is used.
+   * When the device is registered a master key is created and the user will be asked for his
+   * consent by showing a dialog window.
+   *
+   * Unexpected error codes:
+   * - ErrorCode::ClientError Error that happen inside the Vereign Client Library
+   * - ErrorCode::UnexpectedError Should never happen.
+   *
+   * Error codes of interest:
+   * - ErrorCode::InvalidPinCode The pin code is invalid, currently during the registration an empty
+   *    pin code is considered invalid.
+   */
+  auto LoginWithNewDevice(
+    ::grpc::ServerContext* ctx,
+    const client_library::LoginFormNewDevice* req,
+    client_library::LoginFormNewDeviceResponse* resp
+  ) -> ::grpc::Status override {
+    boost::ignore_unused(ctx);
+
+    try {
+      this->service_->LoginWithNewDevice(req, resp);
+
+    } catch (const kvstore::InvalidPinCodeError& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::InvalidPinCode));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (const std::exception& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::ClientError));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (...) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::UnexpectedError));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(ClientErrorStatus);
+    }
+
+    return ::grpc::Status::OK;
+  }
+
+  /**
+   * Login with already registered device.
+   *
+   * req.pin is required only under Linux.
+   *
+   * Under windows the system cypto storage is used.
+   * When the device is registered a master key is created and the user will be asked for his
+   * consent by showing a dialog window.
+   *
+   * Unexpected error codes:
+   * - ErrorCode::ClientError Error that happen inside the Vereign Client Library
+   * - ErrorCode::UnexpectedError Should never happen.
+   *
+   * Error codes of interest:
+   * - ErrorCode::DeviceNotRegistered The device is not registered.
+   * - ErrorCode::InvalidPinCode The pin code is invalid and the crypto storage cannot be unlocked.
+   * - ErrorCode::InvalidIdentity Under windows if for some reason the RSA master key has been changed.
+   */
+  auto LoginWithPreviouslyAddedDevice(
+    ::grpc::ServerContext* ctx,
+    const client_library::LoginFormPreviousAddedDevice* req,
+    client_library::EmptyResponse* resp
+  ) -> ::grpc::Status override {
+    boost::ignore_unused(ctx);
+
+    try {
+      this->service_->LoginWithPreviouslyAddedDevice(req, resp);
+
+    } catch (const kvstore::StorageNotInitializedError& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::DeviceNotRegistered));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (const kvstore::InvalidPinCodeError& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::InvalidPinCode));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (const kvstore::InvalidIdentityError& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::InvalidIdentity));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (const std::exception& e) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::ClientError));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(e.what());
+
+    } catch (...) {
+      resp->set_code(ErrorCodeAsString(ErrorCode::UnexpectedError));
+      resp->set_status(ClientErrorStatus);
+      resp->set_error(ClientErrorStatus);
+    }
+
+    return ::grpc::Status::OK;
+  }
+};
+
+} // namespace vereign::grpc
+
+#endif // __VEREIGN_GRPC_IDENTITY_API_HH
diff --git a/cpp/src/vereign/grpc/passport_api.hh b/cpp/src/vereign/grpc/passport_api.hh
deleted file mode 100644
index 02eb2b36c07dfe12e4d7f46c9bf67d5a5ffac6bf..0000000000000000000000000000000000000000
--- a/cpp/src/vereign/grpc/passport_api.hh
+++ /dev/null
@@ -1,50 +0,0 @@
-#ifndef __VEREIGN_GRPC_PASSPORT_API_HH
-#define __VEREIGN_GRPC_PASSPORT_API_HH
-
-#include <vereign/grpc/gen/passport_api.hh>
-
-namespace vereign {
-namespace grpc {
-
-template <class VereignService>
-class PassportAPI final : public gen::PassportAPI<VereignService> {
-public:
-  static constexpr const char* Name = gen::PassportAPI<VereignService>::Name;
-
-  using VereignServiceType = VereignService;
-  using VereignServicePtr = std::unique_ptr<VereignService>;
-
-  PassportAPI(VereignServicePtr&& service)
-    : gen::PassportAPI<VereignService>{std::move(service)}
-  {}
-
-  PassportAPI(const PassportAPI&) = delete;
-  PassportAPI& operator=(const PassportAPI&) = delete;
-
-  ::grpc::Status ListPassportsManually(
-    ::grpc::ServerContext*,
-    const client_library::ListPassportsForm* request,
-    client_library::ListPassportsFormResponse* response
-  ) override {
-    auto result_future = this->service_->ListPassports(request, response);
-
-    try {
-      result_future.get();
-    } catch (const std::exception& e) {
-      response->set_code("500");
-      response->set_status("Internal Service Error");
-      response->set_error(e.what());
-    } catch (...) {
-      response->set_code("500");
-      response->set_status("Internal Service Error");
-      response->set_error("Internal Service Error");
-    }
-
-    return ::grpc::Status::OK;
-  }
-};
-
-} // namespace grpc
-} // namespace vereign
-
-#endif // __VEREIGN_GRPC_PASSPORT_API_HH
diff --git a/cpp/src/vereign/grpc/server.cc b/cpp/src/vereign/grpc/server.cc
index fd3c911aae22ff35bb513772a27b7d8d3555b985..5e1e899f139a340fae45b06ca97874cee9f90ab4 100644
--- a/cpp/src/vereign/grpc/server.cc
+++ b/cpp/src/vereign/grpc/server.cc
@@ -1,13 +1,23 @@
 #include <vereign/grpc/server.hh>
 
+#include <vereign/fs/path.hh>
+#include <vereign/fs/operations.hh>
+#include <vereign/fs/util.hh>
+
+#include <vereign/kvstore/crypto_storage.hh>
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/kvstore/storage.hh>
+
 #include <vereign/restapi/client.hh>
 #include <vereign/restapi/client_session.hh>
 #include <vereign/service/gen/gen.hh>
 #include <vereign/grpc/gen/gen.hh>
 #include <vereign/grpc/service_registry.hh>
 
-#include <vereign/service/passport_service.hh>
-#include <vereign/grpc/passport_api.hh>
+// manually written api
+#include <vereign/service/identity_service.hh>
+#include <vereign/grpc/identity_api.hh>
+#include <vereign/grpc/event_api.hh>
 
 #include <grpcpp/server.h>
 #include <boost/asio/io_context.hpp>
@@ -19,42 +29,67 @@
 
 #include <boost/asio/executor_work_guard.hpp>
 
-namespace vereign {
-namespace grpc {
+namespace {
+
+constexpr auto shutdownTimeout = std::chrono::seconds(2);
+
+}
+
+namespace vereign::grpc {
 
 namespace asio = boost::asio;
 
 class Server::Impl {
 public:
   Impl(
-    const std::string& listenAddress,
-    const std::string& vereignHost,
-    const std::string& vereignPort,
-    // FIXME: the public key must come from a storage
-    const std::string& publicKey
+    const std::string& listen_address,
+    const std::string& vereign_host,
+    const std::string& vereign_port,
+    std::string storage_path
   ) : selected_port_{0},
     work_guard_{asio::make_work_guard(ioc_)},
     ssl_context_{asio::ssl::context::tlsv12_client},
     client_{std::make_unique<restapi::Client>(
-      ioc_, ssl_context_, vereignHost, vereignPort
-    )},
-    client_session_{std::make_unique<restapi::ClientSession>(
-      *client_, publicKey
+      ioc_, ssl_context_, vereign_host, vereign_port
     )},
+    client_session_{std::make_unique<restapi::ClientSession>(*client_)},
+    event_broker_{std::make_unique<event::Broker>(*client_session_)},
+    kvstorage_{nullptr},
+    crypto_storage_{nullptr},
+    identity_provider_{nullptr},
     server_{nullptr}
   {
+    if (storage_path == "") {
+      storage_path = fs::path::Join(fs::HomePath(), "vereign");
+      fs::CreateDir(storage_path);
+    }
+
+    storage_path = fs::path::Join(storage_path, "db");
+
+    kvstorage_ = std::make_unique<kvstore::SqliteStorage>(storage_path);
+    crypto_storage_ = std::make_unique<kvstore::CryptoStorage>(*kvstorage_);
+    identity_provider_ = std::make_unique<identity::Provider>(*client_session_, *crypto_storage_);
 
     // FIXME: Verify the remote server's certificate
     // ssl_context.set_verify_mode(ssl::verify_peer);
     ::grpc::ServerBuilder builder;
     builder.AddListeningPort(
-      listenAddress,
+      listen_address,
       ::grpc::InsecureServerCredentials(),
       &selected_port_
     );
 
     // register manually written services
-    services_registry_.RegisterIfNotExist<PassportAPI<service::PassportService>>(*client_session_);
+    services_registry_.RegisterIfNotExist<IdentityAPI<service::IdentityService>>(
+      *client_session_,
+      *identity_provider_
+    );
+    services_registry_.RegisterIfNotExist(
+      std::make_unique<EventAPI<service::gen::EventService>>(
+        std::make_unique<service::gen::EventService>(*client_session_),
+        *event_broker_
+      )
+    );
 
     // register all generated services
     grpc::gen::RegisterAll(*client_session_, services_registry_);
@@ -76,9 +111,17 @@ public:
   }
 
   void Shutdown() {
+    if (stopped_.test_and_set()) {
+      return;
+    }
+
     client_session_->Close();
 
-    server_->Shutdown();
+    event_broker_->Shutdown();
+
+    auto deadline = std::chrono::system_clock::now() + shutdownTimeout;
+
+    server_->Shutdown(deadline);
     if (server_thread_.joinable()) {
       server_thread_.join();
     }
@@ -100,20 +143,24 @@ private:
   asio::ssl::context ssl_context_;
   std::unique_ptr<vereign::restapi::Client> client_;
   std::unique_ptr<vereign::restapi::ClientSession> client_session_;
+  std::unique_ptr<vereign::event::Broker> event_broker_;
   ServiceRegistry services_registry_;
+  std::unique_ptr<kvstore::Storage> kvstorage_;
+  std::unique_ptr<kvstore::CryptoStorage> crypto_storage_;
+  std::unique_ptr<identity::Provider> identity_provider_;
   std::unique_ptr<::grpc::Server> server_;
   std::thread server_thread_;
   std::thread service_thread_;
+  std::atomic_flag stopped_ = ATOMIC_FLAG_INIT;
 };
 
 Server::Server(
   const std::string& listenAddress,
   const std::string& vereignHost,
   const std::string& vereignPort,
-  // FIXME: the public key must come from a storage
-  const std::string& publicKey
+  const std::string& storage_path
 )
-  : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort, publicKey)}
+  : impl_{std::make_unique<Impl>(listenAddress, vereignHost, vereignPort, storage_path)}
 {
 }
 
@@ -129,5 +176,4 @@ auto Server::SelectedPort() const -> int {
   return impl_->SelectedPort();
 }
 
-} // namespace grpc
 } // namespace vereign
diff --git a/cpp/src/vereign/grpc/server.hh b/cpp/src/vereign/grpc/server.hh
index bcadb25458827207c43b6a0050c92c5dda9cd0fa..ef3e8903c76490ded00529f1cfc9af0e5bd09ac5 100644
--- a/cpp/src/vereign/grpc/server.hh
+++ b/cpp/src/vereign/grpc/server.hh
@@ -4,13 +4,12 @@
 #include <string>
 #include <memory>
 
-namespace vereign {
-namespace grpc {
+namespace vereign::grpc {
 
 /**
  * BindError is thrown when the Server::Server could not start listening.
  */
-class BindError: public virtual std::exception {
+class BindError: public std::exception {
 public:
   auto what() const noexcept -> const char* override {
     return "gRPC listen failed";
@@ -34,14 +33,17 @@ public:
    * @param listenAddress gRPC listen address, for example "localhost:".
    * @param vereignHost Vereign restapi host.
    * @param vereignPort Vereign restapi port - https, 443...
+   * @param storage_path Path to directory that will be used for the crypto storage.
+   *    If storage_path is empty string, a default is used. Under linux this default is
+   *    `$HOME/vereign`, and under windows it is `C:\Users\<user>\AppData\Local\vereign`.
+   *
    * @throws BindError when the gRPC server could not start listening.
    */
   explicit Server(
     const std::string& listenAddress,
     const std::string& vereignHost,
     const std::string& vereignPort,
-    // FIXME: the public key must come from a storage
-    const std::string& publicKey
+    const std::string& storage_path = ""
   );
 
   /**
@@ -77,7 +79,6 @@ private:
   std::unique_ptr<Impl> impl_;
 };
 
-} // namespace grpc
 } // namespace vereign
 
 #endif
diff --git a/cpp/src/vereign/grpc/service_registry.hh b/cpp/src/vereign/grpc/service_registry.hh
index cddc32a89d1a323761bcaa6fa54bb4262a613a89..cc60016be9b268e3ad3572242736a73ce72334e6 100644
--- a/cpp/src/vereign/grpc/service_registry.hh
+++ b/cpp/src/vereign/grpc/service_registry.hh
@@ -3,18 +3,30 @@
 
 #include <grpcpp/server_builder.h>
 #include <vereign/restapi/client_session.hh>
+#include <vereign/identity/provider.hh>
 
 #include <unordered_map>
 
-namespace vereign {
-namespace grpc {
+namespace vereign::grpc {
 
 class ServiceRegistry {
 public:
   void RegisterIntoBuilder(::grpc::ServerBuilder& builder);
 
   template <class API>
-  bool RegisterIfNotExist(restapi::ClientSession& client_session) {
+  auto RegisterIfNotExist(std::unique_ptr<API>&& api) -> bool {
+    auto it = services_.find(API::Name);
+    if (it != services_.end()) {
+      return false;
+    }
+
+    services_[API::Name] = std::move(api);
+
+    return true;
+  }
+
+  template <class API>
+  auto RegisterIfNotExist(restapi::ClientSession& client_session) -> bool {
     auto it = services_.find(API::Name);
     if (it != services_.end()) {
       return false;
@@ -26,11 +38,28 @@ public:
     return true;
   }
 
+  template <class API>
+  auto RegisterIfNotExist(
+    restapi::ClientSession& client_session,
+    identity::Provider& identity_provider
+  ) -> bool {
+    auto it = services_.find(API::Name);
+    if (it != services_.end()) {
+      return false;
+    }
+
+    using Service = typename API::VereignServiceType;
+    services_[API::Name] = std::make_unique<API>(
+      std::make_unique<Service>(client_session, identity_provider)
+    );
+
+    return true;
+  }
+
 private:
   std::unordered_map<std::string, std::unique_ptr<::grpc::Service>> services_;
 };
 
-} // namespace grpc
-} // namespace vereign
+} // namespace vereign::grpc
 
 #endif // __VEREIGN_GRPC_SERVICE_REGISTRY_HH
diff --git a/cpp/src/vereign/identity/errors.hh b/cpp/src/vereign/identity/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ffc59e9f9564d081c8c3467747cab9055e5a8598
--- /dev/null
+++ b/cpp/src/vereign/identity/errors.hh
@@ -0,0 +1,37 @@
+#ifndef __VEREIGN_IDENTITY_ERRORS_HH
+#define __VEREIGN_IDENTITY_ERRORS_HH
+
+#include <stdexcept>
+
+namespace vereign::identity {
+
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+class ServerSignCertificateError : public Error {
+public:
+  std::string Code;
+  std::string Status;
+  std::string Error;
+
+  ServerSignCertificateError(
+    std::string code,
+    std::string status,
+    std::string error
+  )
+    : identity::Error(error),
+      Code{std::move(code)},
+      Status{std::move(status)},
+      Error{std::move(error)}
+  {
+  }
+};
+
+} // namespace vereign::identity
+
+#endif // __VEREIGN_IDENTITY_ERRORS_HH
diff --git a/cpp/src/vereign/identity/provider.cc b/cpp/src/vereign/identity/provider.cc
new file mode 100644
index 0000000000000000000000000000000000000000..6e96552f319de93e66201bd4423752ce1868fb05
--- /dev/null
+++ b/cpp/src/vereign/identity/provider.cc
@@ -0,0 +1,245 @@
+#include <vereign/identity/provider.hh>
+
+#include <vereign/identity/errors.hh>
+
+#include <vereign/crypto/digest.hh>
+#include <vereign/crypto/bio.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/cert.hh>
+#include <vereign/encoding/base64.hh>
+
+#include <vereign/client_library/sign_types.pb.h>
+
+namespace {
+
+constexpr int rsaKeySizeBits = 2048;
+constexpr const auto profileCertCountry = std::string_view{"CH"};
+constexpr const auto profileCertState = std::string_view{"Zug"};
+constexpr const auto profileCertLocality = std::string_view{"Zug"};
+constexpr const auto profileCertOrganization = std::string_view{"Vereign AG"};
+constexpr const auto profileCertOrganizationUnit = std::string_view{"Business Dep"};
+constexpr const int profileCertValidityYears = 5;
+
+const auto signCertificatePath = std::string{"/sign/signCertificate"};
+
+}
+
+namespace vereign::identity {
+
+static auto makeProfileCertificate(
+  bytes::View key_pem,
+  const client_library::SignCertificateFormResponsePayload& msg
+) -> std::unique_ptr<ProfileCertificate> {
+
+  auto profile_cert = std::make_unique<ProfileCertificate>();
+
+  profile_cert->PrivateKeyPEM = bytes::Buffer{key_pem};
+  encoding::base64::Decode(bytes::View(msg.signedcertificate()), profile_cert->CertificatePEM);
+  profile_cert->CertificateUUID = msg.certificateuuid();
+
+  for (auto& cert : msg.chain()) {
+    bytes::Buffer buf;
+    encoding::base64::Decode(bytes::View(cert), buf);
+    profile_cert->Chain.push_back(std::move(buf));
+  }
+
+  return profile_cert;
+}
+
+Provider::Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage)
+  : client_session_{client_session},
+    storage_{storage}
+{}
+
+Provider::~Provider() = default;
+
+auto Provider::RecreateIdentity(const std::string& pin) -> std::string {
+  std::lock_guard<std::mutex> l{mu_};
+
+  storage_.Recreate(pin);
+
+  auto rsa = crypto::rsa::GenerateKey(rsaKeySizeBits);
+
+  auto private_key = crypto::rsa::ExportPrivateKeyToPEM(rsa.get());
+  storage_.PutBytes("identity_private_key", crypto::bio::View(private_key.get()));
+
+  auto public_key = crypto::rsa::ExportPublicKeyToPEM(rsa.get());
+  storage_.PutBytes("identity_public_key", crypto::bio::View(public_key.get()));
+
+  bytes::Buffer encoded;
+  encoding::base64::Encode(crypto::bio::View(public_key.get()), encoded);
+
+  has_identity_ = true;
+
+  return std::string{encoded.View().String()};
+}
+
+auto Provider::LoadIdentity(const std::string& pin) -> std::string {
+  std::lock_guard<std::mutex> l{mu_};
+
+  storage_.Open(pin);
+
+  bytes::Buffer public_key;
+  storage_.GetBytes("identity_public_key", public_key);
+
+  bytes::Buffer encoded;
+  encoding::base64::Encode(public_key.View(), encoded);
+
+  has_identity_ = true;
+
+  return std::string(encoded.View().String());
+}
+
+auto Provider::GetIdentityPublicKeyBase64() -> std::string {
+  std::lock_guard<std::mutex> l{mu_};
+
+  bytes::Buffer public_key;
+  storage_.GetBytes("identity_public_key", public_key);
+
+  bytes::Buffer encoded;
+  encoding::base64::Encode(public_key.View(), encoded);
+
+  return std::string(encoded.View().String());
+}
+
+auto Provider::GetDeviceHash() -> std::string {
+  std::lock_guard<std::mutex> l{mu_};
+
+  bytes::Buffer public_key;
+  storage_.GetBytes("identity_public_key", public_key);
+
+  bytes::Buffer hash;
+  crypto::digest::sha1(public_key.View(), hash);
+
+  bytes::Buffer encoded;
+  encoding::base64::Encode(hash.View(), encoded);
+
+  return std::string(encoded.View().String());
+}
+
+auto Provider::getProfileCertificateFromStorage(
+  const std::string& profile_uuid
+) -> std::unique_ptr<ProfileCertificate> {
+
+  std::lock_guard<std::mutex> l{mu_};
+
+  auto key_pem = bytes::Buffer{};
+  auto cert_binary = bytes::Buffer{};
+
+  storage_.GetBytes("profile-key-" + profile_uuid, key_pem);
+  storage_.GetBytes("profile-cert-" + profile_uuid, cert_binary);
+
+  if (key_pem.Size() == 0 || cert_binary.Size() == 0) {
+    return nullptr;
+  }
+
+  auto cert_msg = client_library::SignCertificateFormResponsePayload{};
+  auto result = cert_msg.ParseFromArray(cert_binary.View().Data(), cert_binary.View().Size());
+  if (!result) {
+    return nullptr;
+  }
+
+  return makeProfileCertificate(key_pem.View(), cert_msg);
+}
+
+auto Provider::GetProfileCertificate(
+  const std::string& profile_uuid
+) -> std::unique_ptr<ProfileCertificate> {
+  auto profile_cert = getProfileCertificateFromStorage(profile_uuid);
+  if (profile_cert) {
+    return profile_cert;
+  }
+
+  auto key = crypto::rsa::GenerateKey(rsaKeySizeBits);
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Subject.CommonName = profile_uuid + "-userdevice";
+  cert_data.Subject.Country = profileCertCountry;
+  cert_data.Subject.State = profileCertState;
+  cert_data.Subject.Locality = profileCertLocality;
+  cert_data.Subject.Organization = profileCertOrganization;
+  cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit;
+
+  // cert_data.Email = "ca@vereign.com"; // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
+  // cert_data.Url = "www.vereign.com"; // optional url, recommended for CA, added to Subject Alternative Name extension
+
+  cert_data.Validity.ValidYears = profileCertValidityYears;
+  cert_data.IsCA = true;
+
+  auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get());
+  auto cert_pem_bio = crypto::cert::ExportCertToPEM(cert.get());
+  auto cert_pem = encoding::base64::EncodeToString(crypto::bio::View(cert_pem_bio.get()));
+
+  // sign the new profile certificate
+  auto req = client_library::SignCertificateForm{};
+  req.set_passportuuid(profile_uuid);
+  req.set_certificate(cert_pem);
+  auto resp = client_library::SignCertificateFormResponse{};
+
+  auto result = client_session_.Post(signCertificatePath, &req, &resp);
+  result.wait();
+
+  if (resp.code() != "200") {
+    throw ServerSignCertificateError(resp.code(), resp.status(), resp.error());
+  }
+
+  // serialize cert data
+  auto payload_size = resp.data().ByteSizeLong();
+  auto cert_binary = bytes::Buffer{payload_size};
+  resp.data().SerializeToArray(cert_binary.end(), payload_size);
+  cert_binary.IncSize(payload_size);
+
+  auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+
+  {
+    std::lock_guard<std::mutex> l{mu_};
+
+    storage_.PutBytes("profile-key-" + profile_uuid, crypto::bio::View(key_bio.get()));
+    storage_.PutBytes("profile-cert-" + profile_uuid, cert_binary.View());
+  }
+
+  return makeProfileCertificate(crypto::bio::View(key_bio.get()), resp.data());
+}
+
+auto Provider::GetProfileOneTimeCertificate(
+  const std::string& profile_uuid,
+  const std::string& email
+) -> std::unique_ptr<ProfileCertificate> {
+  auto profile_cert = GetProfileCertificate(profile_uuid);
+
+  auto key = crypto::rsa::GenerateKey(rsaKeySizeBits);
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Subject.CommonName = profile_uuid + "-onetime";
+  cert_data.Subject.Country = profileCertCountry;
+  cert_data.Subject.State = profileCertState;
+  cert_data.Subject.Locality = profileCertLocality;
+  cert_data.Subject.Organization = profileCertOrganization;
+  cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit;
+  cert_data.Email = email;
+
+  cert_data.Validity.ValidYears = profileCertValidityYears;
+  cert_data.IsCA = false;
+
+  auto issuer_cert = crypto::cert::ImportCertFromPEM(profile_cert->CertificatePEM.View());
+  auto issuer_pkey = crypto::rsa::ImportPrivateKeyFromPEM(profile_cert->PrivateKeyPEM.View());
+
+  auto cert = crypto::cert::CreateCert(cert_data, issuer_cert.get(), issuer_pkey.get(),  key.get());
+  auto cert_bio = crypto::cert::ExportCertToPEM(cert.get());
+
+  auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+
+  auto result = std::make_unique<ProfileCertificate>();
+  result->PrivateKeyPEM = crypto::bio::View(key_bio.get());
+  result->CertificatePEM = crypto::bio::View(cert_bio.get());
+
+  result->Chain.emplace_back(profile_cert->CertificatePEM.View());
+
+  for (const auto& chain_cert : profile_cert->Chain) {
+    result->Chain.emplace_back(chain_cert.View());
+  }
+
+  return result;
+}
+
+} // namespace vereign::identity
diff --git a/cpp/src/vereign/identity/provider.hh b/cpp/src/vereign/identity/provider.hh
new file mode 100644
index 0000000000000000000000000000000000000000..c20763d835204009ea56b2c541147f5b8e91a4c3
--- /dev/null
+++ b/cpp/src/vereign/identity/provider.hh
@@ -0,0 +1,150 @@
+#ifndef __VEREIGN_IDENTITY_PROVIDER_HH
+#define __VEREIGN_IDENTITY_PROVIDER_HH
+
+#include <vereign/restapi/client_session.hh>
+#include <vereign/kvstore/crypto_storage.hh>
+
+#include <mutex>
+
+namespace vereign::identity {
+
+/**
+ * A DTO representing a profile/onetime certificate with its private key, UUID and certificate chain.
+ *
+ * @see Provider::GetProfileCertificate
+ * @see Provider::GetProfileOneTimeCertificate
+ */
+struct ProfileCertificate {
+  bytes::Buffer PrivateKeyPEM;
+  bytes::Buffer CertificatePEM;
+  std::string CertificateUUID;
+  std::vector<bytes::Buffer> Chain;
+};
+
+/**
+ * Identity provider that manages the locally stored user identity.
+ *
+ * All public methods are thread safe.
+ */
+class Provider {
+public:
+  /**
+   * Creates Provider instance.
+   *
+   * @param storage The crypto storage used for read/write identity properties.
+   */
+  Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage);
+
+  /**
+   * Default constructor.
+   *
+   * Does nothing.
+   */
+  ~Provider();
+
+  // disable copying
+  Provider(const kvstore::Storage&) = delete;
+  auto operator=(const kvstore::Storage&) -> Provider& = delete;
+
+  /**
+   * Recreates the current identity.
+   *
+   * @param pin Required only under Linux. The pin code used for derivation of the crypto storage
+   *    master key.
+   *
+   * @returns The base64 encoded PEM encoded identity public key.
+   */
+  auto RecreateIdentity(const std::string& pin) -> std::string;
+
+  /**
+   * Loads the local identity.
+   *
+   * @param pin Required only under Linux. The pin code used for derivation of the crypto storage
+   *    master key.
+   *
+   * @returns The base64 encoded PEM encoded identity public key.
+   */
+  auto LoadIdentity(const std::string& pin) -> std::string;
+
+  /**
+   * Retrieve identity public key.
+   *
+   * @returns The base64 encoded PEM encoded identity public key.
+   */
+  auto GetIdentityPublicKeyBase64() -> std::string;
+
+  /**
+   * @returns base64 encoded SHA1 hash of the identity public key.
+   */
+  auto GetDeviceHash() -> std::string;
+
+  /**
+   * Retrieve profile certificate.
+   *
+   * When the profile certificate does not exist in the crypto storage just yet, a new private key
+   * and certificate is created and signed by Vereign server. Then the private key, the certificate
+   * and the issuers certificate chain are stored into the crypto storage.
+   *
+   * This means that the profile certificate is reused for the lifetime of the device registration.
+   *
+   * The profile certificate is a CA certificate and is used for signing one-time certificates.
+   *
+   * The `Common Name` of the certificate is derived from the
+   * `profile_uuid` - "<profile_uuid>-userdevice".
+   *
+   * Currently the profile certificate is part of the following certificate chain:
+   * 1. profile-userdevice
+   * 2. profile-serverside
+   * 3. vereign
+   *
+   * @param profile_uuid The profile UUID.
+   * @returns the profile certificate, its private key and the issuers certificate chain.
+   */
+  auto GetProfileCertificate(const std::string& profile_uuid) -> std::unique_ptr<ProfileCertificate>;
+
+  /**
+   * Creates and returns a one-time certificate for given profile.
+   *
+   * The one-time certificate is signed by the user-device profile certificate.
+   *
+   * When the profile certificate does not exist in the crypto storage just yet, a new private key
+   * and certificate is created and signed by Vereign server. Then the private key, the certificate
+   * and the issuers certificate chain are stored into the crypto storage.
+   *
+   * This means that the profile certificate is reused for the lifetime of the device registration.
+   *
+   * The one-time certificate is a leaf certificate and must be used one only sign operation.
+   *
+   * The `Common Name` of the certificate is derived from the
+   * `profile_uuid` - "<profile_uuid>-onetime".
+   *
+   * Currently the one-time certificate is part of the following certificate chain:
+   * 1. profile-onetime
+   * 2. profile-userdevice
+   * 3. profile-serverside
+   * 4. vereign
+   *
+   */
+  auto GetProfileOneTimeCertificate(
+    const std::string& profile_uuid,
+    const std::string& email
+  ) -> std::unique_ptr<ProfileCertificate>;
+
+private:
+  auto getProfileCertificateFromStorage(
+    const std::string& profile_uuid
+  ) -> std::unique_ptr<ProfileCertificate>;
+
+private:
+  restapi::ClientSession& client_session_;
+
+  std::mutex mu_;
+
+  kvstore::CryptoStorage& storage_;
+
+  bool has_identity_ = false;
+};
+
+} // namespace vereign::identity
+
+#endif // __VEREIGN_IDENTITY_PROVIDER_HH
diff --git a/cpp/src/vereign/kvstore/README.md b/cpp/src/vereign/kvstore/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..62a67dc5f3d413eb11c04a66e68c26bb62cca1ce
--- /dev/null
+++ b/cpp/src/vereign/kvstore/README.md
@@ -0,0 +1,71 @@
+# Vereign C++ Client Library Storage Spec {#vcl_storage_spec}
+
+## Overview
+
+The Vereign C++ Library Storage is used for securely store locally user's identity and profile
+certificates.
+
+The storage is a key/value storage where the values are encrypted with `master key` using
+`AES256-GCM` cypher.
+
+The current backend of the storage is sqlite3, where the database file is normally located at 
+`${HOME}/vereign/db`.
+
+The `master key` key is provided in different way depending on the operating system.
+
+## windows master key
+
+Under windows [CNG](https://docs.microsoft.com/en-us/windows/win32/seccng/about-cng) is used for 
+generating a persistent RSA key pair 2048 bits long. The name of the key in the windows storage by 
+default is `vereign_key`. This key is used to encrypt a randomly generated AES256 key, and the 
+encrypted key is saved in the storage under a special key `__master_key`.
+
+## linux master key
+
+Under linux the user must provide a password (pin code).
+This password along with randomly generated salt is used to derive a master key using
+`PKCS5_PBKDF2_HMAC` with `SHA256` where the salt is 16 bytes and the iterations are 2^18.
+The salt and the iterations are stored in the storage under special keys `__master_key_salt` and
+`master_key_iterations`. Note that these are stored in plain form without encryption.
+
+## locking and storage tag
+
+Every time the storage is going to be modified the whole storage must be locked, which with the 
+sqlite3 backend is achieved with so called exclusive transaction.
+This is needed in order to have consistency when more then a single application use the same storage.
+
+But locking is not enough to achieve consistency. If an application A already had opened the storage,
+and application B reset the identity (changing the master key), and after some time A writes to the
+storage it will overwrite values written by B leaving the storage in a state where different values
+are encrypted with different keys.
+
+In order to prevent this, when the master key is created for the first time a 16 byte long random
+tag is generated and stored encrypted with the master key into a special key `__tag`.
+Then whenever a value must be written after the storage is locked additionally the `__tag` is read
+and decrypted. If the tag retrieval or decryption fails, and error is triggered and the write is
+cancelled.
+
+## encrypted values encoding
+
+As already mentioned the values are encrypted with `AES256-GCM`.
+
+The encrypted values are encoded according to the BNF grammars described below.
+
+The primitives used are:
+* `uint8` - a single byte.
+* `uint64` - unsigned 64 bit integer in Little Endian.
+* `bytes` - a sequence of bytes.
+
+### encoding version 1
+
+```
+Bytes = Size Data
+  Size = uint64
+  Data = bytes
+
+EncryptedValue = Version IV Tag EncryptedData
+  Version = uint8
+  IV = bytes
+  Tag = bytes
+  EncryptedData = bytes
+```
diff --git a/cpp/src/vereign/kvstore/crypto_storage.cc b/cpp/src/vereign/kvstore/crypto_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1990c7f86211005f96ed3618757b1eeacf1af8ba
--- /dev/null
+++ b/cpp/src/vereign/kvstore/crypto_storage.cc
@@ -0,0 +1,33 @@
+#include <vereign/kvstore/crypto_storage.hh>
+
+#if defined(_WIN32)
+# include <vereign/kvstore/detail/win_crypto_storage.hh>
+#else
+# include <vereign/kvstore/detail/linux_crypto_storage.hh>
+#endif
+
+namespace vereign::kvstore {
+
+CryptoStorage::CryptoStorage(Storage& storage, bool disable_key_protection)
+  : impl_{std::make_unique<detail::CryptoStorageImpl>(storage, disable_key_protection)}
+{}
+
+CryptoStorage::~CryptoStorage() = default;
+
+void CryptoStorage::Recreate(const std::string& pin) {
+  impl_->Recreate(pin);
+}
+
+void CryptoStorage::Open(const std::string& pin) {
+  impl_->Open(pin);
+}
+
+void CryptoStorage::PutBytes(const std::string& key, bytes::View value) {
+  impl_->PutBytes(key, value);
+}
+
+auto CryptoStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool {
+  return impl_->GetBytes(key, value);
+}
+
+} // namespace vereign::identity
diff --git a/cpp/src/vereign/kvstore/crypto_storage.hh b/cpp/src/vereign/kvstore/crypto_storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..23f0e2fa5af3858d3d0677d012e00e6a3eed02f8
--- /dev/null
+++ b/cpp/src/vereign/kvstore/crypto_storage.hh
@@ -0,0 +1,111 @@
+#ifndef __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH
+#define __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH
+
+#include <vereign/kvstore/storage.hh>
+#include <memory>
+
+namespace vereign::kvstore {
+
+namespace detail {
+
+class CryptoStorageImpl;
+
+}
+
+/**
+ * Crypto storage is used for securely store sensitive user's data.
+ *
+ * This includes user's device private/public key, profile certificates etc.
+ *
+ * The CryptoStorage has different implementations per operating system.
+ * Check detail/linux_crypto_storage.cc and detail/win_crypto_storage.cc.
+ *
+ * For more information about the crypto storage design check the [storage spec](@ref vcl_storage_spec)
+ * in the README.md.
+ *
+ */
+class CryptoStorage {
+public:
+  /**
+   * Creates CryptoStorage.
+   *
+   * @param storage The underlying key/value storage.
+   * @param disable_key_protection Used only under Windows. If false when the master RSA key
+   *    is created and loaded, the user will be asked for his consent by showing a dialog window
+   *    with text information specified in the UI policy.
+   */
+  CryptoStorage(Storage& storage, bool disable_key_protection = false);
+
+  /**
+   * Default destructor - does nothing.
+   */
+  ~CryptoStorage();
+
+  // disable copying
+  CryptoStorage(const CryptoStorage&) = delete;
+  auto operator=(const CryptoStorage&) -> CryptoStorage& = delete;
+
+  /**
+   * Reinitializes the storage, by effectively initializing a new empty storage.
+   *
+   * Although the resulting storage is empty it will include some internal data needed for the
+   * master key management.
+   *
+   * @param pin Used only under Linux. A pin code that will be used for derivation of the master key.
+   *    Under windows ncrypt is used for generating a RSA key which is used for encryption of the
+   *    master key, and thus the pin is not used.
+   *
+   * @throws Error on failure.
+   */
+  void Recreate(const std::string& pin);
+
+  /**
+   * Open the storage.
+   *
+   * @param pin Used only under Linux. A pin code that will be used for derivation of the master key.
+   *    Under windows ncrypt is used for generating a RSA key which is used for encryption of the
+   *    master key, and thus the pin is not used.
+   *
+   * @throws StorageNotInitializedError when storage is not initialized yet. One must use
+   *    CryptoStorage::Reset to initialize the storage prior calling CryptoStorage::Open.
+   *
+   * @throws InvalidPinCodeError (only under Linux) when the pin code is empty or does not match
+   *    the pin code used with CryptoStorage::Reset.
+   *
+   * @throws Error when the key derivation under Linux fails.
+   *
+   * @throws InvalidIdentityError when the master key could not be decrypted under Windows.
+   *    This possibly means that the RSA key was changed.
+   */
+  void Open(const std::string& pin);
+
+  /**
+   * Encrypt and store bytes value into the storage.
+   *
+   * @param key The key under which the value will be stored.
+   * @param value The bytes that will be encrypted and stored.
+   *
+   * @throws StorageNotInitializedError when the storage is not initialized.
+   * @throws InvalidIdentityError when another application had reset the storage.
+   */
+  void PutBytes(const std::string& key, bytes::View value);
+
+  /**
+   * Retrieve and decrypt bytes value from the storage.
+   *
+   * @param key The key of the value that will be retrieved.
+   * @param value Buffer where the value will be returned.
+   *
+   * @throws StorageNotInitializedError when the storage is not initialized.
+   * @throws encoding::Error when the encrypted value cannot be decoded.
+   * @throws crypto::OpenSSLError when the value cannot be decrypted.
+   */
+  auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool;
+
+private:
+  std::unique_ptr<detail::CryptoStorageImpl> impl_;
+};
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_KVSTORE_CRYPTO_STORAGE_HH
diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..44243c88497a01759a559f8e6ece6d430e5a237e
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc
@@ -0,0 +1,102 @@
+#include <vereign/kvstore/detail/base_crypto_storage.hh>
+
+#include <vereign/kvstore/detail/value_encoder.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/kvstore/lock.hh>
+
+#include <vereign/crypto/digest.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/aes.hh>
+#include <vereign/crypto/bio.hh>
+
+#include <boost/core/ignore_unused.hpp>
+#include <chrono>
+
+namespace {
+
+  // FIXME: should these be injected and provided by the integrator
+  constexpr int tagSizeBytes = 16;
+  constexpr int lockRetryCount = 10;
+  constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+}
+
+namespace vereign::kvstore::detail {
+
+BaseCryptoStorageImpl::BaseCryptoStorageImpl(kvstore::Storage& storage)
+  : storage_{storage}
+{
+}
+
+void BaseCryptoStorageImpl::encryptBytes(const std::string& key, bytes::View value) {
+  if (key_.Size() == 0) {
+    throw StorageNotInitializedError{"key not initialized"};
+  }
+
+  bytes::Buffer iv;
+  bytes::Buffer tag;
+  bytes::Buffer encrypted;
+
+  crypto::aes::GCM256Encrypt(value, key_.View(), iv, tag, encrypted);
+
+  bytes::Buffer encoded_value;
+  EncodeEncryptedValue(encoded_value, iv.View(), tag.View(), encrypted.View());
+
+  storage_.PutBytes(key, encoded_value.View());
+}
+
+void BaseCryptoStorageImpl::PutBytes(const std::string& key, bytes::View value) {
+  kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+  if (!isTagValid()) {
+    throw InvalidIdentityError{};
+  }
+
+  encryptBytes(key, value);
+}
+
+auto BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) const -> bool {
+  kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+  if (key_.Size() == 0) {
+    throw StorageNotInitializedError{"key not initialized"};
+  }
+
+  bytes::Buffer encoded;
+  auto found = storage_.GetBytes(key, encoded);
+  if (!found) {
+    return false;
+  }
+
+  bytes::View iv;
+  bytes::View tag;
+  bytes::View encrypted;
+  DecodeEncryptedValue(encoded.View(), iv, tag, encrypted);
+
+  crypto::aes::GCM256Decrypt(encrypted, key_.View(), iv, tag, value);
+
+  return true;
+}
+
+void BaseCryptoStorageImpl::initKey(bytes::Buffer&& key) {
+  key_ = std::move(key);
+}
+
+auto BaseCryptoStorageImpl::isTagValid() const -> bool {
+  bytes::Buffer tag;
+  try {
+    return GetBytes("__tag", tag);
+
+  } catch (const crypto::Error&) {
+    return false;
+  }
+}
+
+void BaseCryptoStorageImpl::updateTag() {
+  auto tag = crypto::Rand(tagSizeBytes);
+
+  encryptBytes("__tag", tag.View());
+}
+
+} // namespace vereign::kvstore::detail
diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..3039b8fe0fbb82f9207689dc9faa213795fab5c5
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh
@@ -0,0 +1,37 @@
+#ifndef __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH
+#define __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH
+
+#include <vereign/kvstore/storage.hh>
+
+namespace vereign::kvstore::detail {
+
+class BaseCryptoStorageImpl {
+public:
+  BaseCryptoStorageImpl(kvstore::Storage& storage);
+
+  // disable copying
+  BaseCryptoStorageImpl(const BaseCryptoStorageImpl&) = delete;
+  auto operator=(const BaseCryptoStorageImpl&) -> BaseCryptoStorageImpl& = delete;
+
+  void PutBytes(const std::string& key, bytes::View value);
+  auto GetBytes(const std::string& key, bytes::Buffer& value) const -> bool;
+
+protected:
+  void initKey(bytes::Buffer&& key);
+  auto isTagValid() const -> bool;
+  void updateTag();
+
+private:
+  void encryptBytes(const std::string& key, bytes::View value);
+
+protected:
+  kvstore::Storage& storage_;
+
+private:
+  bytes::Buffer key_;
+};
+
+} // namespace vereign::kvstore::detail
+
+
+#endif // __VEREIGN_KVSTORE_DETAIL_BASE_CRYPTO_STORAGE_HH
diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..eb49ca7171ff49c3009c6bd5fbbf7cb9a9303e76
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc
@@ -0,0 +1,120 @@
+#include <vereign/kvstore/detail/linux_crypto_storage.hh>
+
+#include <vereign/kvstore/detail/value_encoder.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/kvstore/lock.hh>
+
+#include <vereign/crypto/digest.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/aes.hh>
+#include <vereign/crypto/bio.hh>
+
+#include <boost/core/ignore_unused.hpp>
+#include <chrono>
+
+namespace {
+
+// FIXME: should these be injected and provided by the integrator
+constexpr int iterations = 1 << 18;
+constexpr int saltSizeBytes = 16;
+constexpr int aesKeySizeBytes = 32;
+
+constexpr int lockRetryCount = 10;
+constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+
+}
+
+namespace vereign::kvstore::detail {
+
+CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection)
+  : BaseCryptoStorageImpl{storage}
+{
+  boost::ignore_unused(disable_key_protection);
+}
+
+void CryptoStorageImpl::Open(const std::string& pin) {
+  if (pin.empty()) {
+    throw InvalidPinCodeError{};
+  }
+
+  kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+  bytes::Buffer salt;
+  int64_t iterations = 0;
+
+  try {
+    storage_.GetInt64("__master_key_iterations", iterations);
+    storage_.GetBytes("__master_key_salt", salt);
+  } catch (const std::exception& e) {
+    throw StorageNotInitializedError{e.what()};
+  }
+
+  if (iterations == 0 || salt.Size() == 0) {
+    throw StorageNotInitializedError{"iterations cannot be zero"};
+  }
+
+  bytes::Buffer key{aesKeySizeBytes};
+
+  int result = PKCS5_PBKDF2_HMAC(
+    pin.data(),
+    pin.length(),
+    salt.View().Data(),
+    salt.View().Size(),
+    iterations,
+    EVP_sha256(),
+    aesKeySizeBytes,
+    key.end()
+  );
+  if (result == 0) {
+    throw Error("key derivation failed");
+  }
+
+  key.IncSize(aesKeySizeBytes);
+
+  initKey(std::move(key));
+  if (!isTagValid()) {
+    throw InvalidPinCodeError{};
+  }
+}
+
+void CryptoStorageImpl::Recreate(const std::string& pin) {
+  if (pin.empty()) {
+    throw InvalidPinCodeError{};
+  }
+
+  auto salt = crypto::Rand(saltSizeBytes);
+
+  bytes::Buffer key{aesKeySizeBytes};
+
+  int result = PKCS5_PBKDF2_HMAC(
+    pin.data(),
+    pin.length(),
+    salt.View().Data(),
+    salt.View().Size(),
+    iterations,
+    EVP_sha256(),
+    aesKeySizeBytes,
+    key.end()
+  );
+  if (result == 0) {
+    throw Error("key derivation failed");
+  }
+
+  key.IncSize(aesKeySizeBytes);
+  initKey(std::move(key));
+
+  {
+    kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+    storage_.DeleteAll();
+
+    updateTag();
+
+    storage_.PutInt64("__master_key_iterations", iterations);
+    storage_.PutBytes("__master_key_salt", salt.View());
+  }
+}
+
+} // namespace vereign::kvstore::detail
diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..fcc3e1fc0b67664ede137622c8ac79a7c765a6f6
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.hh
@@ -0,0 +1,24 @@
+#ifndef __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH
+#define __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH
+
+#include <vereign/kvstore/storage.hh>
+#include <vereign/kvstore/detail/base_crypto_storage.hh>
+
+namespace vereign::kvstore::detail {
+
+class CryptoStorageImpl : public BaseCryptoStorageImpl {
+public:
+  CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection);
+
+  // disable copying
+  CryptoStorageImpl(const CryptoStorageImpl&) = delete;
+  auto operator=(const CryptoStorageImpl&) -> CryptoStorageImpl& = delete;
+
+  void Recreate(const std::string& pin);
+  void Open(const std::string& pin);
+};
+
+} // namespace vereign::kvstore::detail
+
+
+#endif // __VEREIGN_KVSTORE_DETAIL_LINUX_CRYPTO_STORAGE_HH
diff --git a/cpp/src/vereign/kvstore/detail/value_encoder.cc b/cpp/src/vereign/kvstore/detail/value_encoder.cc
new file mode 100644
index 0000000000000000000000000000000000000000..6144bebbdffdee83f9797cf063d9729b34d96c35
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/value_encoder.cc
@@ -0,0 +1,42 @@
+#include <vereign/kvstore/detail/value_encoder.hh>
+
+#include <vereign/kvstore/errors.hh>
+#include <vereign/encoding/binary.hh>
+
+namespace vereign::kvstore::detail {
+
+void EncodeEncryptedValue(
+  bytes::Buffer& out,
+  bytes::View iv,
+  bytes::View tag,
+  bytes::View encrypted
+) {
+  // 25 = buffers size numbers (3 int64) plus 1 byte for the version
+  out.Reserve(iv.Size() + tag.Size() + encrypted.Size() + 25);
+  // encode version
+  encoding::binary::EncodeUint8(out, 1);
+
+  // encode data
+  encoding::binary::EncodeBytes(out, iv);
+  encoding::binary::EncodeBytes(out, tag);
+  encoding::binary::EncodeBytes(out, encrypted);
+}
+
+void DecodeEncryptedValue(
+  bytes::View encoded,
+  bytes::View& iv,
+  bytes::View& tag,
+  bytes::View& encrypted
+) {
+  auto version = encoding::binary::DecodeUint8(encoded);
+  if (version != 1) {
+    throw Error("decoding encrypted value failed: invalid version");
+  }
+
+  auto offset = 1;
+  offset += encoding::binary::DecodeBytes(encoded.Slice(offset), iv);
+  offset += encoding::binary::DecodeBytes(encoded.Slice(offset), tag);
+  offset += encoding::binary::DecodeBytes(encoded.Slice(offset), encrypted);
+}
+
+} // namespace vereign::kvstore::detail
diff --git a/cpp/src/vereign/kvstore/detail/value_encoder.hh b/cpp/src/vereign/kvstore/detail/value_encoder.hh
new file mode 100644
index 0000000000000000000000000000000000000000..3c9e532853a853febaadf44b39c7941c1f0f28e1
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/value_encoder.hh
@@ -0,0 +1,46 @@
+#ifndef __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH
+#define __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH
+
+#include <vereign/bytes/view.hh>
+#include <vereign/bytes/buffer.hh>
+
+namespace vereign::kvstore::detail {
+
+/**
+ * Encodes value encrypted with AES in GCM mode.
+ *
+ * @param out The encoded result.
+ * @param iv The IV of the encrypted value.
+ * @param tag The tag of the encrypted value.
+ * @param encrypted The encrypted bytes.
+ */
+void EncodeEncryptedValue(
+  bytes::Buffer& out,
+  bytes::View iv,
+  bytes::View tag,
+  bytes::View encrypted
+);
+
+/**
+ * Decode value encrypted with AES in GCM mode.
+ *
+ * Note that iv, tag and encrypted are decoded in-place, meaning that they point to the same memory
+ * of the `encoded` input.
+ *
+ * @param out The encoded value.
+ * @param iv The decoded IV of the encrypted value.
+ * @param tag The decoded tag of the encrypted value.
+ * @param encrypted The decoded encrypted bytes.
+ *
+ * @throws kvstore::Error on failure.
+ */
+void DecodeEncryptedValue(
+  bytes::View encoded,
+  bytes::View& iv,
+  bytes::View& tag,
+  bytes::View& encrypted
+);
+
+} // namespace vereign::kvstore::detail
+
+#endif // __VEREIGN_KVSTORE_DETAIL_VALUE_ENCODER_HH
diff --git a/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1e78842bec9d48ad965f2486df9836da57d266d0
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc
@@ -0,0 +1,110 @@
+#include <vereign/kvstore/detail/win_crypto_storage.hh>
+
+#include <vereign/kvstore/detail/value_encoder.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/kvstore/lock.hh>
+
+#include <vereign/crypto/digest.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/aes.hh>
+#include <vereign/crypto/bio.hh>
+
+#include <boost/core/ignore_unused.hpp>
+#include <vereign/ncrypt/rsa.hh>
+#include <vereign/ncrypt/errors.hh>
+
+#include <chrono>
+
+namespace {
+
+// FIXME: should these be injected and provided by the integrator
+constexpr int keySizeBits = 2048;
+constexpr int aesKeySizeBytes = 32;
+
+constexpr int lockRetryCount = 10;
+constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+
+// FIXME: ask business for these values
+constexpr const auto vereignKeyCreationTitle = std::string_view{"Vereign Client"};
+constexpr const auto vereignKeyDescription = std::string_view{
+  "Vereign Client will use this key to authenticate with the Vereign Services"
+};
+constexpr const auto vereignKeyFriendlyName = std::string_view{"Vereign Client Identity Key"};
+
+}
+
+namespace vereign::kvstore::detail {
+
+CryptoStorageImpl::CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection)
+  : BaseCryptoStorageImpl{storage},
+    disable_key_protection_{disable_key_protection}
+{}
+
+void CryptoStorageImpl::Open(const std::string& pin) {
+  boost::ignore_unused(pin);
+  kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+  auto provider = ncrypt::rsa::OpenStorageProvider();
+  auto rsa_key = ncrypt::rsa::LoadKey(provider.Get(), std::string(VereignKeyName));
+  if (!rsa_key) {
+    throw StorageNotInitializedError{"key not initialized"};
+  }
+
+  bytes::Buffer key;
+  try {
+    bytes::Buffer encrypted_key;
+    storage_.GetBytes("__master_key", encrypted_key);
+
+    ncrypt::rsa::PrivateKeyDecrypt(rsa_key.Get(), encrypted_key.View(), key);
+  } catch(const std::exception&) {
+    throw InvalidIdentityError{};
+  }
+
+  initKey(std::move(key));
+  // FIXME: write tests for tampering and regular identity change
+  if (!isTagValid()) {
+    throw InvalidIdentityError{};
+  }
+}
+
+void CryptoStorageImpl::Recreate(const std::string& pin) {
+  boost::ignore_unused(pin);
+  kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
+
+  auto provider = ncrypt::rsa::OpenStorageProvider();
+  auto old_key = ncrypt::rsa::LoadKey(provider.Get(), std::string(VereignKeyName));
+  if (old_key) {
+    ncrypt::rsa::DeleteKey(old_key.Get());
+  }
+
+  std::optional<ncrypt::rsa::KeyUIPolicy> ui_policy;
+  if (!disable_key_protection_) {
+    ui_policy = ncrypt::rsa::KeyUIPolicy{
+      vereignKeyCreationTitle,
+      vereignKeyDescription,
+      vereignKeyFriendlyName
+    };
+  }
+
+  auto rsa_key = ncrypt::rsa::CreateKey(
+    provider.Get(),
+    keySizeBits,
+    std::string(VereignKeyName),
+    ui_policy
+  );
+
+  storage_.DeleteAll();
+
+  auto key = crypto::Rand(aesKeySizeBytes);
+
+  bytes::Buffer encrypted_key;
+  ncrypt::rsa::PublicKeyEncrypt(rsa_key.Get(), key.View(), encrypted_key);
+  storage_.PutBytes("__master_key", encrypted_key.View());
+
+  initKey(std::move(key));
+  updateTag();
+}
+
+}
diff --git a/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..48150ac4ff6068fbe79b3abffd257f508b89e907
--- /dev/null
+++ b/cpp/src/vereign/kvstore/detail/win_crypto_storage.hh
@@ -0,0 +1,27 @@
+#ifndef __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH
+#define __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH
+
+#include <vereign/kvstore/storage.hh>
+#include <vereign/kvstore/detail/base_crypto_storage.hh>
+
+namespace vereign::kvstore::detail {
+
+class CryptoStorageImpl : public BaseCryptoStorageImpl {
+public:
+  CryptoStorageImpl(kvstore::Storage& storage, bool disable_key_protection);
+
+  // disable copying
+  CryptoStorageImpl(const CryptoStorageImpl&) = delete;
+  auto operator=(const CryptoStorageImpl&) -> CryptoStorageImpl& = delete;
+
+  void Recreate(const std::string& pin);
+  void Open(const std::string& pin);
+
+private:
+  bool disable_key_protection_ = false;
+};
+
+} // namespace vereign::kvstore::detail
+
+
+#endif // __VEREIGN_KVSTORE_DETAIL_WIN_CRYPTO_STORAGE_HH
diff --git a/cpp/src/vereign/kvstore/errors.hh b/cpp/src/vereign/kvstore/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..50ec7271532c6b55cc6b8fc3adc7a1ddb1a569db
--- /dev/null
+++ b/cpp/src/vereign/kvstore/errors.hh
@@ -0,0 +1,50 @@
+#ifndef __VEREIGN_KVSTORE_ERRORS_HH
+#define __VEREIGN_KVSTORE_ERRORS_HH
+
+#include <stdexcept>
+
+namespace vereign::kvstore {
+
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+class StorageNotInitializedError : public Error {
+public:
+  StorageNotInitializedError(const std::string& reason)
+    : Error{"storage is not initialized: " + reason}
+  {
+  }
+};
+
+class LockError : public Error {
+public:
+  LockError()
+    : Error{"cannot acquire storage lock"}
+  {
+  }
+};
+
+class InvalidPinCodeError : public Error {
+public:
+  InvalidPinCodeError()
+    : Error{"invalid pin code"}
+  {
+  }
+};
+
+class InvalidIdentityError : public Error {
+public:
+  InvalidIdentityError()
+    : Error{"invalid identity"}
+  {
+  }
+};
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_KVSTORE_ERRORS_HH
diff --git a/cpp/src/vereign/kvstore/lock.cc b/cpp/src/vereign/kvstore/lock.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5a0863e967ea6de6243652a7bbb1752c8a286480
--- /dev/null
+++ b/cpp/src/vereign/kvstore/lock.cc
@@ -0,0 +1,39 @@
+#include <vereign/kvstore/lock.hh>
+
+#include <vereign/kvstore/errors.hh>
+
+#include <thread>
+#include <sqlite3.h>
+
+namespace vereign::kvstore {
+
+Lock::Lock(Storage& storage)
+  : storage_{storage}
+{
+  storage.Lock();
+}
+
+Lock::Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval)
+  : storage_{storage}
+{
+  for (int i = 0; i < retry_count; i++) {
+    try {
+      storage.Lock();
+
+      return;
+    } catch (const LockError&) {
+      std::this_thread::sleep_for(sleep_interval);
+
+      continue;
+    }
+  }
+
+  throw LockError{};
+}
+
+Lock::~Lock() noexcept {
+  storage_.Unlock();
+}
+
+
+} // namespace vereign::kvstore
diff --git a/cpp/src/vereign/kvstore/lock.hh b/cpp/src/vereign/kvstore/lock.hh
new file mode 100644
index 0000000000000000000000000000000000000000..1989b1dd0376353417224edfd29c249c1fae4108
--- /dev/null
+++ b/cpp/src/vereign/kvstore/lock.hh
@@ -0,0 +1,53 @@
+#ifndef __VEREIGN_KVSTORE_LOCK_HH
+#define __VEREIGN_KVSTORE_LOCK_HH
+
+#include <vereign/kvstore/storage.hh>
+#include <chrono>
+
+namespace vereign::kvstore {
+
+/**
+ * Lock guard used for lock/unlock Storage within a scope.
+ *
+ * When the Lock is constructed it locks the Storage, and when it is destroyed it unlock it.
+ */
+class Lock {
+public:
+  /**
+   * Creates a Lock and locks the Storage.
+   *
+   * @param storage The storage to lock.
+   * @throws LockError when the lock is held by another process.
+   */
+  explicit Lock(Storage& storage);
+
+  /**
+   * Creates a Lock and locks the Storage.
+   *
+   * If the lock is not possible it retries `retry_count` and sleeps between retries `sleep_interval`.
+   *
+   * @param storage The storage to lock.
+   * @param retry_count How many times to retry if the lock is held by another process.
+   * @param sleep_interval How many time to sleep between retries.
+   *
+   * @throws LockError If the lock could not be held after `retry_count` retries.
+   */
+  Lock(Storage& storage, int retry_count, std::chrono::milliseconds sleep_interval);
+
+  /**
+   * Unlocks the storage.
+   */
+  ~Lock() noexcept;
+
+  // copying is disabled.
+  Lock(const Lock&) = delete;
+  auto operator=(const Lock&) -> Lock&  = delete;
+
+private:
+  Storage& storage_;
+};
+
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_KVSTORE_LOCK_HH
diff --git a/cpp/src/vereign/kvstore/sqlite_storage.cc b/cpp/src/vereign/kvstore/sqlite_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..21371363d40fc84cb497c283b869b999baed8ab3
--- /dev/null
+++ b/cpp/src/vereign/kvstore/sqlite_storage.cc
@@ -0,0 +1,135 @@
+#include <vereign/kvstore/sqlite_storage.hh>
+
+#include <vereign/kvstore/errors.hh>
+#include <vereign/kvstore/lock.hh>
+
+#include <vereign/encoding/binary.hh>
+#include <boost/optional.hpp>
+#include <vereign/core/lock_guard.hh>
+#include <vereign/sqlite/errors.hh>
+#include <sqlite3.h>
+
+#include <array>
+
+namespace {
+constexpr int createTableRetryCount = 10;
+constexpr auto createTableRetrySleep = std::chrono::milliseconds{1000};
+}
+
+namespace vereign::kvstore {
+
+SqliteStorage::SqliteStorage(const std::string& db_path)
+  : db_{db_path}
+{
+  kvstore::Lock l{*this, createTableRetryCount, createTableRetrySleep};
+
+  db_.Execute(R"(
+CREATE TABLE IF NOT EXISTS storage (
+  key TEXT PRIMARY KEY NOT NULL,
+  value BLOB
+);
+  )");
+}
+
+SqliteStorage::~SqliteStorage() {
+  if (lock_count_ != 0) {
+    db_.Commit();
+  }
+}
+
+void SqliteStorage::Lock() {
+  if (lock_count_ != 0) {
+    lock_count_++;
+
+    return;
+  }
+
+  try {
+    db_.BeginExplicitTransaction();
+    lock_count_++;
+  } catch (const sqlite::Error& err) {
+    if (err.code() == SQLITE_BUSY) {
+      throw LockError{};
+    }
+
+    throw;
+  }
+}
+
+void SqliteStorage::Unlock() {
+  if (lock_count_ == 0) {
+    throw Error{"unexpected call Unlock with non existent lock"};
+  }
+
+  lock_count_--;
+  if (lock_count_ == 0) {
+    db_.Commit();
+  }
+}
+
+
+void SqliteStorage::DeleteAll() {
+  kvstore::Lock l{*this};
+
+  db_.Execute("DELETE FROM storage;");
+}
+
+void SqliteStorage::PutBytes(const std::string& key, bytes::View value) {
+  kvstore::Lock l{*this};
+
+  auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);");
+  stmt.BindText(1, key);
+  stmt.BindBlob(2, value);
+  stmt.Step();
+}
+
+auto SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool {
+  kvstore::Lock l{*this};
+
+  auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?");
+  stmt.BindText(1, key);
+  auto end = stmt.Step();
+
+  if (end) {
+    return false;
+  }
+
+  value.Write(stmt.GetColumnBlob(0));
+
+  return true;
+}
+
+void SqliteStorage::PutInt64(const std::string& key, int64_t value) {
+  kvstore::Lock l{*this};
+
+  std::array<uint8_t, 8> encoded;
+  encoding::binary::EncodeUint64(encoded.data(), value);
+
+  auto stmt = db_.Prepare("REPLACE INTO storage(key, value) VALUES(?, ?);");
+  stmt.BindText(1, key);
+  stmt.BindBlob(2, bytes::View(encoded.data(), 8));
+  stmt.Step();
+}
+
+auto SqliteStorage::GetInt64(const std::string& key, int64_t& value) -> bool {
+  kvstore::Lock l{*this};
+
+  auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?");
+  stmt.BindText(1, key);
+  auto end = stmt.Step();
+
+  if (end) {
+    return false;
+  }
+
+  auto buf = stmt.GetColumnBlob(0);
+  if (buf.Size() != 8) {
+    throw Error("cannot decode int64 value");
+  }
+
+  value = encoding::binary::DecodeUint64(buf);
+
+  return true;
+}
+
+} // namespace vereign::kvstore
diff --git a/cpp/src/vereign/kvstore/sqlite_storage.hh b/cpp/src/vereign/kvstore/sqlite_storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6fdfce9d8e996351f99c85f0f9dc1f7e6c2525c8
--- /dev/null
+++ b/cpp/src/vereign/kvstore/sqlite_storage.hh
@@ -0,0 +1,106 @@
+#ifndef __VEREIGN_KVSTORE_SQLITE_STORAGE_HH
+#define __VEREIGN_KVSTORE_SQLITE_STORAGE_HH
+
+#include <vereign/kvstore/storage.hh>
+#include <vereign/sqlite/connection.hh>
+
+namespace vereign::kvstore {
+
+/**
+ * Sqlite implementation of the kvstore::Storage interface.
+ */
+class SqliteStorage : public Storage {
+public:
+  /**
+   * Creates SqliteStorage instance.
+   *
+   * @param db_path Full path to the sqlite database file.
+   */
+  SqliteStorage(const std::string& db_path);
+
+  /**
+   * Closes the connection with the database.
+   */
+  ~SqliteStorage() override;
+
+  // disable copying
+  SqliteStorage(const SqliteStorage&) = delete;
+  auto operator=(const SqliteStorage&) -> SqliteStorage& = delete;
+
+  /**
+   * Locks the storage.
+   *
+   * The lock must be recursive, meaning that may be called multiple times.
+   * Storage::Unlock must be called the same number of times the Storage::Lock was called.
+   *
+   * Use kvstore::Lock for lock guard and lock with retrials.
+   *
+   * @throws LockError when the lock is held by another process.
+   */
+  void Lock() override;
+
+  /**
+   * Locks the storage.
+   *
+   * The lock must be recursive, meaning that may be called multiple times.
+   * Storage::Unlock must be called the same number of times the Storage::Lock was called.
+   *
+   * Use kvstore::Lock for lock guard and lock with retrials.
+   */
+  void Unlock() override;
+
+  /**
+   * Deletes all the values in the storage.
+   *
+   * @throws sqlite::Error on failure.
+   */
+  void DeleteAll() override;
+
+  /**
+   * Store bytes value into the storage.
+   *
+   * @param key The key under which the value will be stored.
+   * @param value The bytes that will be stored.
+   *
+   * @throws sqlite::Error on failure.
+   */
+  void PutBytes(const std::string& key, bytes::View value) override;
+
+  /**
+   * Retrieve bytes value from the storage.
+   *
+   * @param key The key of the value that will be retrieved.
+   * @param value Buffer where the value will be returned.
+   *
+   * @throws sqlite::Error on failure.
+   */
+  auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool override;
+
+  /**
+   * Store int64_t value into the storage.
+   *
+   * @param key The key under which the value will be stored.
+   * @param value The value that will be stored.
+   *
+   * @throws sqlite::Error on failure.
+   */
+  void PutInt64(const std::string& key, int64_t value) override;
+
+  /**
+   * Retrieve int64_t value from the storage.
+   *
+   * @param key The key of the value that will be retrieved.
+   * @param value Buffer where the value will be returned.
+   *
+   * @throws sqlite::Error on failure.
+   */
+  auto GetInt64(const std::string& key, int64_t& value) -> bool override;
+
+private:
+  sqlite::Connection db_;
+  int lock_count_ = 0;
+};
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_KVSTORE_SQLITE_STORAGE_HH
diff --git a/cpp/src/vereign/kvstore/storage.hh b/cpp/src/vereign/kvstore/storage.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ccb809468144a940243ee0e7d4769726056b8673
--- /dev/null
+++ b/cpp/src/vereign/kvstore/storage.hh
@@ -0,0 +1,83 @@
+#ifndef __VEREIGN_KVSTORE_STORAGE_HH
+#define __VEREIGN_KVSTORE_STORAGE_HH
+
+#include <vereign/bytes/buffer.hh>
+
+namespace vereign::kvstore {
+
+// The vereign RSA master key name.
+constexpr const auto VereignKeyName = std::string_view{"vereign_key"};
+
+/**
+ * Key/value storage interface.
+ */
+class Storage {
+public:
+  /**
+   * Locks the storage.
+   *
+   * The lock must be recursive, meaning that may be called multiple times.
+   * Storage::Unlock must be called the same number of times the Storage::Lock was called.
+   *
+   * Use kvstore::Lock for lock guard and lock with retrials.
+   *
+   * @throws LockError when the lock is held by another process.
+   */
+  virtual void Lock() = 0;
+
+  /**
+   * Locks the storage.
+   *
+   * The lock must be recursive, meaning that may be called multiple times.
+   * Storage::Unlock must be called the same number of times the Storage::Lock was called.
+   *
+   * Use kvstore::Lock for lock guard and lock with retrials.
+   */
+  virtual void Unlock() = 0;
+
+  /**
+   * Deletes all the values in the storage.
+   */
+  virtual void DeleteAll() = 0;
+
+  /**
+   * Store bytes value into the storage.
+   *
+   * @param key The key under which the value will be stored.
+   * @param value The bytes that will be stored.
+   */
+  virtual void PutBytes(const std::string& key, bytes::View value) = 0;
+
+  /**
+   * Retrieve bytes value from the storage.
+   *
+   * @param key The key of the value that will be retrieved.
+   * @param value Buffer where the value will be returned.
+   */
+  virtual auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool = 0;
+
+  /**
+   * Store int64_t value into the storage.
+   *
+   * @param key The key under which the value will be stored.
+   * @param value The value that will be stored.
+   */
+  virtual void PutInt64(const std::string& key, int64_t value) = 0;
+
+  /**
+   * Retrieve int64_t value from the storage.
+   *
+   * @param key The key of the value that will be retrieved.
+   * @param value Buffer where the value will be returned.
+   */
+  virtual auto GetInt64(const std::string& key, int64_t& value) -> bool = 0;
+
+  /**
+   * Destroy and cleanup.
+   */
+  virtual ~Storage() = default;
+};
+
+} // namespace vereign::kvstore
+
+#endif // __VEREIGN_KVSTORE_STORAGE_HH
diff --git a/cpp/src/vereign/ncrypt/errors.cc b/cpp/src/vereign/ncrypt/errors.cc
new file mode 100644
index 0000000000000000000000000000000000000000..fedb516591b3709bad2cbc244caabc0f4f9a3e79
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/errors.cc
@@ -0,0 +1,45 @@
+#include <vereign/ncrypt/errors.hh>
+#include <vereign/bytes/buffer.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/encoding/hex.hh>
+#include <vereign/encoding/binary.hh>
+
+#include <string>
+
+namespace vereign::ncrypt {
+
+auto SecurityStatusToString(SECURITY_STATUS status) -> std::string {
+  switch (status) {
+  case NTE_FAIL:
+    return "NTE_FAIL";
+  case NTE_INVALID_PARAMETER:
+    return "NTE_INVALID_PARAMETER";
+  case NTE_BUFFER_TOO_SMALL:
+    return "NTE_BUFFER_TOO_SMALL";
+  case NTE_EXISTS:
+    return "NTE_EXISTS";
+  case NTE_BAD_KEYSET:
+    return "NTE_BAD_KEYSET";
+  case NTE_INVALID_HANDLE:
+    return "NTE_INVALID_HANDLE";
+  case NTE_NOT_SUPPORTED:
+    return "NTE_NOT_SUPPORTED";
+  case NTE_NULL_REFERENCE_POINTER:
+    return "NTE_NULL_REFERENCE_POINTER";
+  default:
+    bytes::Buffer encoded;
+    encoding::hex::EncodeReverse(bytes::View(&status, sizeof(status)), encoded);
+    return std::string(encoded.View().String());
+  }
+}
+
+Error::Error(SECURITY_STATUS status, const std::string& msg)
+  : std::runtime_error{msg + ": " + SecurityStatusToString(status)},
+    status_{status}
+{}
+
+auto Error::SecurityStatus() const -> SECURITY_STATUS {
+  return status_;
+}
+
+} // vereign::ncrypt
diff --git a/cpp/src/vereign/ncrypt/errors.hh b/cpp/src/vereign/ncrypt/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..fe6920132b3e3cc1f1596c68af9671c3cfa2764f
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/errors.hh
@@ -0,0 +1,49 @@
+#ifndef __VEREIGN_NCRYPT_ERRORS_HH
+#define __VEREIGN_NCRYPT_ERRORS_HH
+
+#include <windows.h>
+#include <ncrypt.h>
+#include <stdexcept>
+
+namespace vereign::ncrypt {
+
+// windows returns this error when there is unexpected NULL input
+constexpr SECURITY_STATUS NTE_NULL_REFERENCE_POINTER = 0x800706f4;
+
+/**
+ * Returns a string representation of the SECURITY_STATUS errors.
+ *
+ * @param status The SECURITY_STATUS error.
+ * @returns a string representation of the SECURITY_STATUS error.
+ */
+auto SecurityStatusToString(SECURITY_STATUS status) -> std::string;
+
+/**
+ * The base error type for the namespace vereign::ncrypto.
+ */
+class Error : public std::runtime_error {
+public:
+  /**
+   * Creates Error with status and additional message.
+   *
+   * Example:
+   * @code
+   * auto err = Error{NTE_FAIL, "operation failed"};
+   * assert(err.what() == "operation failed: NTE_FAIL");
+   * assert(err.SecurityStatus() == NTE_FAIL);
+   * @endcode
+   */
+  Error(SECURITY_STATUS status, const std::string& msg);
+
+  /**
+   * Returns the SECURITY_STATUS associated with this error.
+   */
+  auto SecurityStatus() const -> SECURITY_STATUS;
+
+private:
+  SECURITY_STATUS status_;
+};
+
+} // vereign::ncrypt
+
+#endif // __VEREIGN_NCRYPT_ERRORS_HH
diff --git a/cpp/src/vereign/ncrypt/rsa.cc b/cpp/src/vereign/ncrypt/rsa.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a9e351d4a3e7f15f513167b05d154716a5d2d188
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/rsa.cc
@@ -0,0 +1,182 @@
+#include <vereign/ncrypt/rsa.hh>
+
+#include <vereign/ncrypt/errors.hh>
+#include <vereign/core/string.hh>
+
+namespace vereign::ncrypt::rsa {
+
+auto OpenStorageProvider() -> UniquePtr {
+  UniquePtr provider{};
+  auto status = NCryptOpenStorageProvider(provider.Ref(), MS_KEY_STORAGE_PROVIDER, 0);
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "open crypto store failed"};
+  }
+
+  return provider;
+}
+
+auto LoadKey(NCRYPT_PROV_HANDLE provider, const std::string& key_name) -> UniquePtr {
+  UniquePtr key{};
+  auto wkey_name = string::widen(key_name);
+  auto status = NCryptOpenKey(provider, key.Ref(), wkey_name.data(), 0, 0);
+  if (status != ERROR_SUCCESS && status != NTE_BAD_KEYSET) {
+    throw Error{status, "open key failed"};
+  }
+
+  return key;
+}
+
+auto CreateKey(
+  NCRYPT_PROV_HANDLE provider,
+  int bits,
+  const std::string& key_name,
+  std::optional<KeyUIPolicy> ui_policy
+) -> UniquePtr {
+  UniquePtr key{};
+  auto wkey_name = string::widen(key_name);
+  auto status = NCryptCreatePersistedKey(
+    provider,
+    key.Ref(),
+    BCRYPT_RSA_ALGORITHM,
+    wkey_name.data(),
+    0,
+    0
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "creating rsa key failed"};
+  }
+
+  auto key_len = DWORD(bits);
+  status = NCryptSetProperty(
+    key.Get(),
+    NCRYPT_LENGTH_PROPERTY,
+    (PBYTE)&key_len,
+    sizeof(key_len),
+    NCRYPT_PERSIST_FLAG
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "setup rsa key length failed"};
+  }
+
+  if (ui_policy) {
+    auto creation_title = vereign::string::widen(ui_policy->CreationTitle);
+    auto description = vereign::string::widen(ui_policy->Description);
+    auto friendly_name = vereign::string::widen(ui_policy->FriendlyName);
+
+    NCRYPT_UI_POLICY ui_policy_prop{};
+    ui_policy_prop.dwVersion = 1;
+    ui_policy_prop.dwFlags = NCRYPT_UI_PROTECT_KEY_FLAG;
+    ui_policy_prop.pszCreationTitle = creation_title.data();
+    ui_policy_prop.pszDescription = description.data();
+    ui_policy_prop.pszFriendlyName = friendly_name.data();
+
+    status = NCryptSetProperty(
+      key.Get(),
+      NCRYPT_UI_POLICY_PROPERTY,
+      (PBYTE)&ui_policy_prop,
+      sizeof(ui_policy_prop),
+      NCRYPT_PERSIST_FLAG
+    );
+    if (status != ERROR_SUCCESS) {
+      throw Error{status, "configure key ui policy failed"};
+    }
+  }
+
+  status = NCryptFinalizeKey(key.Get(), 0);
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "finalizing rsa key failed"};
+  }
+
+  return key;
+}
+
+void DeleteKey(NCRYPT_KEY_HANDLE key) {
+  auto status = NCryptDeleteKey(key, 0);
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "deleting key failed"};
+  }
+}
+
+void PublicKeyEncrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& encrypted) {
+  BCRYPT_OAEP_PADDING_INFO pad;
+  int flags = NCRYPT_PAD_OAEP_FLAG;
+  pad.pszAlgId = BCRYPT_SHA1_ALGORITHM;
+  pad.pbLabel = nullptr;
+  pad.cbLabel = 0;
+  DWORD size;
+
+  auto status = NCryptEncrypt(
+    key,
+    (PBYTE)src.Data(),
+    (DWORD)src.Size(),
+    &pad,
+    nullptr,
+    0,
+    &size,
+    flags
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "encryption failed"};
+  }
+
+  encrypted.Reserve(size);
+
+  status = NCryptEncrypt(
+    key,
+    (PBYTE)src.Data(),
+    (DWORD)src.Size(),
+    &pad,
+    (PBYTE)encrypted.end(),
+    size,
+    &size,
+    flags
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "encryption failed"};
+  }
+
+  encrypted.IncSize(size);
+}
+
+void PrivateKeyDecrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& decrypted) {
+  BCRYPT_OAEP_PADDING_INFO pad;
+  int flags = NCRYPT_PAD_OAEP_FLAG;
+  pad.pszAlgId = BCRYPT_SHA1_ALGORITHM;
+  pad.pbLabel = nullptr;
+  pad.cbLabel = 0;
+  DWORD size;
+
+  auto status = NCryptDecrypt(
+    key,
+    (PBYTE)src.Data(),
+    (DWORD)src.Size(),
+    &pad,
+    nullptr,
+    0,
+    &size,
+    flags
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "decryption failed"};
+  }
+
+  decrypted.Reserve(size);
+
+  status = NCryptDecrypt(
+    key,
+    (PBYTE)src.Data(),
+    (DWORD)src.Size(),
+    &pad,
+    (PBYTE)decrypted.end(),
+    size,
+    &size,
+    flags
+  );
+  if (status != ERROR_SUCCESS) {
+    throw Error{status, "decryption failed"};
+  }
+
+  decrypted.IncSize(size);
+}
+
+} // vereign::ncrypt::rsa
diff --git a/cpp/src/vereign/ncrypt/rsa.hh b/cpp/src/vereign/ncrypt/rsa.hh
new file mode 100644
index 0000000000000000000000000000000000000000..06d3f05d2cb1892b6a43956c6e6940f6d6b3d0a0
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/rsa.hh
@@ -0,0 +1,150 @@
+#ifndef __VEREIGN_NCRYPT_RSA_HH
+#define __VEREIGN_NCRYPT_RSA_HH
+
+#include <vereign/ncrypt/unique_ptr.hh>
+#include <vereign/bytes/buffer.hh>
+
+#include <windows.h>
+#include <ncrypt.h>
+#include <string>
+#include <optional>
+
+namespace vereign::ncrypt::rsa {
+
+/**
+ * KeyUIPolicy is used to specify some UI captions shown to the user in a dialog when a key is
+ * created or opened for the first time in a process.
+ */
+struct KeyUIPolicy {
+  std::string_view CreationTitle;
+  std::string_view Description;
+  std::string_view FriendlyName;
+};
+
+/**
+ * Opens the default ncrypt storage provider.
+ *
+ * @returns the storage provider handle.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+auto OpenStorageProvider() -> UniquePtr;
+
+/**
+ * Loads a key from a ncrypt storage provider.
+ *
+ * Example:
+ * @code
+ * auto provider = ncrypt::rsa::OpenStorageProvider();
+ * auto key = ncrypt::rsa::LoadKey(provider.Get(), "test_key");
+ * assert(key.Get() != 0);
+ * @endcode
+ *
+ * @param provider The ncrypt storage provider.
+ * @param key_name The name of the key that will be loaded.
+ * @returns the loaded key if the key exists, or null ncrypt::UniquePtr if the key does not exists.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+auto LoadKey(NCRYPT_PROV_HANDLE provider, const std::string& key_name) -> UniquePtr;
+
+/**
+ * Creates persistent RSA key into a given storage provider.
+ *
+ * Example:
+ * @code
+ * auto provider = ncrypt::rsa::OpenStorageProvider();
+ * // create key without UI policy
+ * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {});
+ * assert(key.Get() != 0);
+ * @endcode
+ *
+ * @param provider The ncrypt storage provider.
+ * @param bits The length of the key in bits.
+ * @param key_name The new key name.
+ * @param ui_policy The UI policy. If the UI policy is not provided, the key will not have any
+ *    protection. If the UI policy is provided when the key is created and loaded the user will be
+ *    asked for his consent by showing a dialog window with text information specified in the UI policy.
+ *
+ * @returns the created key on success.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+auto CreateKey(
+  NCRYPT_PROV_HANDLE provider,
+  int bits,
+  const std::string& key_name,
+  std::optional<KeyUIPolicy> ui_policy
+) -> UniquePtr;
+
+/**
+ * Deletes a key.
+ *
+ * Example:
+ * @code
+ * auto provider = ncrypt::rsa::OpenStorageProvider();
+ * auto key = ncrypt::rsa::LoadKey(provider.Get(), "test_key");
+ * if (key) {
+ *   ncryp::rsa::DeleteKey(key.Get());
+ * }
+ * @endcode
+ *
+ * @param key The key to delete.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+void DeleteKey(NCRYPT_KEY_HANDLE key);
+
+/**
+ * Encrypts given bytes with RSA public key.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto provider = ncrypt::rsa::OpenStorageProvider();
+ * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {});
+ *
+ * bytes::Buffer encrypted;
+ * ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted);
+ *
+ * bytes::Buffer decrypted;
+ * ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted);
+ * assert(decrypted.View() == encrypted.View());
+ * @endcode
+ *
+ * @param key The RSA key.
+ * @param src The bytes that will be encrypted.
+ * @param encrypted The result of the encryption.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+void PublicKeyEncrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& encrypted);
+
+/**
+ * Decrypts given bytes with RSA private key.
+ *
+ * Example:
+ * @code
+ * const std::string input{"foo bar"};
+ * auto provider = ncrypt::rsa::OpenStorageProvider();
+ * auto key = ncrypt::rsa::CreateKey(provider.Get(), 2048, "test_key", {});
+ *
+ * bytes::Buffer encrypted;
+ * ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted);
+ *
+ * bytes::Buffer decrypted;
+ * ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted);
+ * assert(decrypted.View() == encrypted.View());
+ * @endcode
+ *
+ * @param key The RSA key.
+ * @param src The bytes that will be decrypted.
+ * @param decrypted The result of the decryption.
+ *
+ * @throws ncrypt::Error on failure.
+ */
+void PrivateKeyDecrypt(NCRYPT_KEY_HANDLE key, bytes::View src, bytes::Buffer& decrypted);
+
+} // vereign::ncrypt::rsa
+
+#endif // __VEREIGN_NCRYPT_RSA_HH
diff --git a/cpp/src/vereign/ncrypt/unique_ptr.cc b/cpp/src/vereign/ncrypt/unique_ptr.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a177839f4ec6c2a12780a5e204db45e92965ecd1
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/unique_ptr.cc
@@ -0,0 +1,82 @@
+#include <vereign/ncrypt/unique_ptr.hh>
+
+#include <vereign/ncrypt/errors.hh>
+#include <utility>
+#include <iostream>
+
+namespace vereign::ncrypt {
+
+UniquePtr::UniquePtr() noexcept
+  : ptr_{0}
+{
+}
+
+UniquePtr::UniquePtr(ULONG_PTR handle) noexcept
+  : ptr_{handle}
+{
+}
+
+UniquePtr::UniquePtr(UniquePtr&& other) noexcept
+  : ptr_{other.ptr_}
+{
+  other.ptr_ = 0;
+}
+
+auto UniquePtr::operator=(UniquePtr&& other) noexcept -> UniquePtr& {
+  std::swap(ptr_, other.ptr_);
+
+  return *this;
+}
+
+UniquePtr::~UniquePtr() noexcept {
+  if (ptr_ == 0) {
+    return;
+  }
+
+  NCryptFreeObject(ptr_);
+}
+
+auto UniquePtr::Get() const noexcept -> ULONG_PTR {
+  return ptr_;
+}
+
+auto UniquePtr::Ref() noexcept -> ULONG_PTR* {
+  return &ptr_;
+}
+
+void UniquePtr::Reset() {
+  if (ptr_ == 0) {
+    return;
+  }
+
+  auto status = NCryptFreeObject(ptr_);
+  if (status == NTE_INVALID_HANDLE) {
+    throw Error(status, "free object failed");
+  }
+
+  ptr_ = 0;
+}
+
+auto UniquePtr::Release() -> ULONG_PTR {
+  if (ptr_ == 0) {
+    return 0;
+  }
+
+  auto result = ptr_;
+
+  auto status = NCryptFreeObject(ptr_);
+  if (status == NTE_INVALID_HANDLE) {
+    throw Error(status, "free object failed");
+  }
+
+  ptr_ = 0;
+
+  return result;
+}
+
+
+UniquePtr::operator bool() const noexcept {
+  return ptr_ != 0;
+}
+
+} // vereign::ncrypt
diff --git a/cpp/src/vereign/ncrypt/unique_ptr.hh b/cpp/src/vereign/ncrypt/unique_ptr.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ae4475300c7399f136810082988e4c19d3495666
--- /dev/null
+++ b/cpp/src/vereign/ncrypt/unique_ptr.hh
@@ -0,0 +1,98 @@
+#ifndef __VEREIGN_NCRYPT_UNIQUE_PTR_HH
+#define __VEREIGN_NCRYPT_UNIQUE_PTR_HH
+
+#include <windows.h>
+
+namespace vereign::ncrypt {
+
+/**
+ * Holds ownership of ncrypt handles like NCRYPT_PROV_HANDLE, NCRYPT_PROV_HANDLE.
+ *
+ * The UniquePtr should be used any time a ncrypt API returns a handle in order to ensure exception
+ * safety and resource cleanup.
+ *
+ * The owned handler is typically cleaned up in the UniquePtr::~UniquePtr() by using
+ * NCryptFreeObject function.
+ */
+class UniquePtr {
+public:
+  /**
+   * Constructs a default empty object.
+   */
+  UniquePtr() noexcept;
+
+  /**
+   * Constructs a UniquePtr that becomes an owner of the passed handle.
+   *
+   * @param handle The handle that becomes owned by the crated UniquePtr.
+   */
+  explicit UniquePtr(ULONG_PTR handle) noexcept;
+
+  /**
+   * Move constructor.
+   */
+  UniquePtr(UniquePtr&& other) noexcept;
+
+  /**
+   * Move assignment operator.
+   */
+  auto operator=(UniquePtr&& other) noexcept -> UniquePtr&;
+
+  // Disable copying
+  UniquePtr(const UniquePtr&) = delete;
+  auto operator=(const UniquePtr&) -> UniquePtr&  = delete;
+
+  /**
+   * Upon destruction the owned handle if any is freed with NCryptFreeObject function.
+   */
+  ~UniquePtr() noexcept;
+
+  /**
+   * Retrieve the owned handle.
+   */
+  auto Get() const noexcept -> ULONG_PTR;
+
+  /**
+   * Retrieve a pointer to the owned handle.
+   *
+   * Example:
+   * @code
+   * UniquePtr key{};
+   * NCryptOpenKey(provider, key.Ref(), L"test_key", 0, 0);
+   * assert(bool(key) == true);
+   * @endcode
+   *
+   * This makes it possible for a ncrypt API to inject its output handle directly into the
+   * UniquePtr object.
+   */
+  auto Ref() noexcept -> ULONG_PTR*;
+
+  /**
+   * Free the owned handler.
+   *
+   * If the owned handler is not null, it frees the handle by calling NCryptFreeObject, and sets
+   * the owned handler to null.
+   */
+  void Reset();
+
+  /**
+   * Releases the ownership of the owned handler.
+   *
+   * @returns the owned handler, before releasing the ownership.
+   */
+  auto Release() -> ULONG_PTR;
+
+  /**
+   * Checks if *this owns a non-null handler.
+   *
+   * @returns true if *this owns a handler, false otherwise.
+   */
+  operator bool() const noexcept;
+
+private:
+  ULONG_PTR ptr_;
+};
+
+} // vereign::ncrypt
+
+#endif // __VEREIGN_NCRYPT_UNIQUE_PTR_HH
diff --git a/cpp/src/vereign/restapi/client.cc b/cpp/src/vereign/restapi/client.cc
index 1dd1b8cc9e874ce972ff6725af6ce3768759a4c5..38f023c008b9a9c357fc430efa95fdc04083cebf 100644
--- a/cpp/src/vereign/restapi/client.cc
+++ b/cpp/src/vereign/restapi/client.cc
@@ -1,5 +1,3 @@
-#include "vereign/restapi/detail/post_task.hh"
-#include <chrono>
 #include <vereign/restapi/client.hh>
 
 #include <vereign/restapi/detail/http_reader.hh>
@@ -10,23 +8,27 @@
 #include <boost/beast/version.hpp>
 #include <boost/asio/dispatch.hpp>
 
-namespace vereign {
-namespace restapi {
+namespace {
+  constexpr std::string_view httpUserAgent = "Vereign Client Library";
+}
+
+namespace vereign::restapi {
 
 constexpr auto defaultTimeout = std::chrono::seconds(30);
 
 Client::Client(
   boost::asio::io_context& ioc,
   boost::asio::ssl::context& ssl_ctx,
-  const std::string& host,
-  const std::string& port
+  std::string host,
+  std::string port
 )
-  : executor_{asio::make_strand(ioc)},
+  : user_agent_{httpUserAgent},
+    executor_{asio::make_strand(ioc)},
     ssl_ctx_{ssl_ctx},
     resolver_{ioc},
     reader_{nullptr},
-    host_{host},
-    port_{port},
+    host_{std::move(host)},
+    port_{std::move(port)},
     expiry_time_{defaultTimeout},
     connecting_{false},
     writing_{false},
@@ -38,16 +40,17 @@ Client::Client(
 Client::Client(
   boost::asio::io_context& ioc,
   boost::asio::ssl::context& ssl_ctx,
-  const std::string& host,
-  const std::string& port,
+  std::string host,
+  std::string port,
   std::chrono::nanoseconds expiry_time
 )
-  : executor_{asio::make_strand(ioc)},
+  : user_agent_{httpUserAgent},
+    executor_{asio::make_strand(ioc)},
     ssl_ctx_{ssl_ctx},
     resolver_{ioc},
     reader_{nullptr},
-    host_{host},
-    port_{port},
+    host_{std::move(host)},
+    port_{std::move(port)},
     expiry_time_{expiry_time},
     connecting_{false},
     writing_{false},
@@ -60,6 +63,10 @@ Client::~Client() noexcept {
   Close();
 }
 
+auto Client::UserAgent() const -> const std::string& {
+  return user_agent_;
+}
+
 void Client::Close() {
   asio::post(
     executor_,
@@ -77,7 +84,7 @@ void Client::Close() {
   );
 }
 
-const asio::executor& Client::GetExecutor() const {
+auto Client::GetExecutor() const -> const asio::executor& {
   return executor_;
 }
 
@@ -153,7 +160,9 @@ void Client::connect(tcp::resolver::results_type results) {
   auto stream = stream_;
   beast::get_lowest_layer(*stream_).async_connect(
     results,
-    [this, stream](beast::error_code ec, endpoint_t) {
+    [this, stream](beast::error_code ec, endpoint_t endpoint) {
+      boost::ignore_unused(endpoint);
+
       if (ec) {
         connecting_ = false;
         handleError(ec.message());
@@ -213,7 +222,7 @@ void Client::doPost() {
   req_->version(11);
   req_->target(task->Path());
   req_->set(beast::http::field::host, host_);
-  req_->set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING);
+  req_->set(beast::http::field::user_agent, user_agent_);
   req_->set(beast::http::field::content_type, "application/json");
 
   auto err = task->EncodeRequest(*req_);
@@ -278,5 +287,4 @@ void Client::readResponse() {
     });
 }
 
-}
-}
+} // namespace vereign::restapi
diff --git a/cpp/src/vereign/restapi/client.hh b/cpp/src/vereign/restapi/client.hh
index 1bb1821f37dacbffdac972e6a5d3e75fe3415be0..c03304c3eb97b650b1635cf2cbbbe49e496baa2f 100644
--- a/cpp/src/vereign/restapi/client.hh
+++ b/cpp/src/vereign/restapi/client.hh
@@ -1,8 +1,6 @@
 #ifndef __VEREIGN_RESTAPI_CLIENT_HH
 #define __VEREIGN_RESTAPI_CLIENT_HH
 
-#include <chrono>
-#include <type_traits>
 #include <vereign/restapi/detail/post_task.hh>
 #include <vereign/restapi/http_header.hh>
 
@@ -16,9 +14,10 @@
 #include <string>
 #include <future>
 #include <deque>
+#include <chrono>
+#include <type_traits>
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 namespace beast = boost::beast;
 namespace asio = boost::asio;
@@ -69,8 +68,8 @@ public:
   Client(
     asio::io_context& ioc,
     asio::ssl::context& ssl_ctx,
-    const std::string& host,
-    const std::string& port
+    std::string host,
+    std::string port
   );
 
   /**
@@ -86,8 +85,8 @@ public:
   Client(
     asio::io_context& ioc,
     asio::ssl::context& ssl_ctx,
-    const std::string& host,
-    const std::string& port,
+    std::string host,
+    std::string port,
     std::chrono::nanoseconds expiry_time
   );
 
@@ -100,7 +99,16 @@ public:
 
   // Disable copying
   Client(const Client&) = delete;
-  Client& operator=(const Client&) = delete;
+  auto operator=(const Client&) -> Client& = delete;
+
+  /**
+   * Retrieve client http user agent string.
+   *
+   * This is what the client puts in the requests as `User-Agent` header.
+   *
+   * @returns the client http user agent string.
+   */
+  auto UserAgent() const -> const std::string&;
 
   /**
    * Close closes the connection.
@@ -117,7 +125,7 @@ public:
    *
    * @returns the client's strand executor.
    */
-  const asio::executor& GetExecutor() const;
+  auto GetExecutor() const -> const asio::executor&;
 
   /**
    * Post makes a blocking post request.
@@ -310,6 +318,9 @@ private:
   void cancelAllTasks(const detail::PostError& err);
 
 private:
+  // http user agent.
+  std::string user_agent_;
+
   // a strand that calls all completion handlers in single thread.
   asio::executor executor_;
 
@@ -353,7 +364,6 @@ private:
   bool closed_;
 };
 
-} // namespace restapi
-} // namespace vereign
+} // namespace vereign::restapi
 
 #endif // __VEREIGN_RESTAPI_CLIENT_HH
diff --git a/cpp/src/vereign/restapi/client_session.hh b/cpp/src/vereign/restapi/client_session.hh
index 28e848a7e270c52f09e083132a862c51e08caded..55ee67962299230e8a280016a8e5631c69f0a676 100644
--- a/cpp/src/vereign/restapi/client_session.hh
+++ b/cpp/src/vereign/restapi/client_session.hh
@@ -3,15 +3,12 @@
 
 #include <vereign/restapi/client.hh>
 #include <vereign/restapi/http_header.hh>
-
+#include <vereign/client_library/common_types.pb.h>
 #include <vereign/client_library/identity_types.pb.h>
-#include <boost/none.hpp>
 
-#include <vector>
-#include <future>
+#include <boost/none.hpp>
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 namespace detail {
 struct AuthErrorType {
@@ -29,9 +26,9 @@ struct AuthErrorType {
 
 using AuthError = boost::optional<AuthErrorType>;
 
-inline AuthError MakeAuthError(
+inline auto MakeAuthError(
   const client_library::LoginFormPreviousAddedDeviceResponse& resp
-) {
+) -> AuthError {
   if (resp.code() == "200") {
     return boost::none;
   }
@@ -43,18 +40,26 @@ inline AuthError MakeAuthError(
   };
 }
 
-inline AuthError NoAuthError() {
+inline auto NoAuthError() -> AuthError {
   return boost::none;
 }
 
+struct Session {
+  std::string PublicKey;
+  std::string DeviceHash;
+  std::string Token;
+  std::string UUID;
+};
+
 } // namespace detail
 
 /**
  * ClientSession is a Client decorator, that maintains authenticated session.
+ *
+ * All public methods are thread safe.
  */
 class ClientSession {
 public:
-  // FIXME: the key should come from a storage
   /**
    * Constructs ClientSession instance with default API base path.
    *
@@ -63,13 +68,15 @@ public:
    * @param client Restapi client.
    * @returns ClientSession instance.
    */
-  ClientSession(Client& client, const std::string& pub_key)
+  ClientSession(Client& client)
     : client_{client},
-      pub_key_{pub_key},
-      base_path_{"/api"}
+      base_path_{"/api"},
+      uuid_{""},
+      session_token_{""},
+      pub_key_{""},
+      device_hash_{""}
   {}
 
-  // FIXME: the key should come from a storage
   /**
    * Constructs ClientSession instance.
    *
@@ -77,18 +84,18 @@ public:
    * @param base_path API base path, for example `/api`
    * @returns ClientSession instance.
    */
-  ClientSession(
-    Client& client,
-    const std::string& base_path,
-    const std::string& pub_key
-  ) : client_{client},
-      pub_key_{pub_key},
-      base_path_{base_path}
+  ClientSession(Client& client, std::string base_path)
+    : client_{client},
+      base_path_{std::move(base_path)},
+      uuid_{""},
+      session_token_{""},
+      pub_key_{""},
+      device_hash_{""}
   {}
 
   // Disable copying.
   ClientSession(const ClientSession&) = delete;
-  ClientSession& operator=(const ClientSession&) = delete;
+  auto operator=(const ClientSession&) -> ClientSession& = delete;
 
   /**
    * Closes the connection.
@@ -97,6 +104,40 @@ public:
     Close();
   }
 
+  /**
+   * Set public key used for authentication.
+   *
+   * This effectively resets the current session (if any).
+   *
+   * @param key The public key that will be used for authentication.
+   */
+  void SetPubKey(std::string key, std::string hash) {
+    std::lock_guard<std::mutex> l{mu_};
+
+    pub_key_ = std::move(key);
+    device_hash_ = std::move(hash);
+    session_token_ = "";
+    uuid_ = "";
+  }
+
+  /**
+   * Checks if the client has identity.
+   *
+   * @returns true when there device identity exits, false otherwise.
+   */
+  auto HasIdentity() -> bool {
+    std::lock_guard<std::mutex> l{mu_};
+
+    return pub_key_.size() != 0;
+  }
+
+  /**
+   * Retrieve the underlying http client.
+   */
+  auto GetClient() -> Client& {
+    return client_;
+  }
+
   /**
    * Close closes the connection.
    *
@@ -110,7 +151,7 @@ public:
   }
 
   /**
-   * Post makes a blocking post request.
+   * Post makes an authenticated blocking post request.
    *
    * The passed `req` and `resp` parameters are moved in and returned back once,
    * the returned future is resolved.
@@ -152,7 +193,7 @@ public:
   }
 
   /**
-   * PostAsync makes non blocking post request.
+   * PostAsync makes an authenticated non blocking post request.
    *
    * The passed `req` and `resp` parameters are moved in and returned back once
    * the CompletionFunc is called.
@@ -196,8 +237,180 @@ public:
     );
   }
 
+  /**
+   * PublicPostAsync makes a non authenticated non blocking post request.
+   *
+   * The request is without session, only the device identity - public key, device hash are used
+   * for this request.
+   * It is suitable for the public API.
+   *
+   * The passed `req` and `resp` parameters are moved in and returned back once
+   * the CompletionFunc is called.
+   *
+   * @tparam CompletionFunc A completion functor - `void (Result&&)`, where
+   *    the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where
+   *    RequestPtr is `std::remove_reference<RequestPtrType>::type` and
+   *    ResponsePtr is `std::remove_reference<RequestPtrType>::type`.
+   *
+   * @param path HTTP path, for example `/passport/listPassports`.
+   * @param req Request object that will be serialized as JSON body.
+   *    It can be a const pointer or std::unique_ptr to protobuf message.
+   *
+   * @param resp Response object that will be used to decode the response.
+   *    It can be a  pointer or std::unique_ptr to protobuf message.
+   *
+   * @param func Completion func, that will be called when the post request
+   *    is finished.
+   */
+  template <class RequestPtrType, class ResponsePtrType, class CompletionFunc>
+  void PublicPostAsync(
+    const std::string& path,
+    RequestPtrType&& req,
+    ResponsePtrType&& resp,
+    CompletionFunc&& cf
+  ) {
+
+    auto session = getSession();
+
+    client_.PostAsync(
+      base_path_ + path,
+      std::move(req),
+      std::move(resp),
+      std::vector<vereign::restapi::HttpHeader>{
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash}
+      },
+      std::move(cf)
+    );
+  }
+
+  /**
+   * PublicPost makes a non authenticated blocking post request.
+   *
+   * The request is without session, only the device identity - public key, device hash are used
+   * for this request.
+   * It is suitable for the public API.
+   *
+   * The passed `req` and `resp` parameters are moved in and returned back once,
+   * the returned future is resolved.
+   *
+   * @param path HTTP path, for example `/passport/listPassports`.
+   *    The path will be added to the ClientSession base path.
+   * @param req Request object that will be serialized as JSON body.
+   *    It can be a const pointer or std::unique_ptr to protobuf message.
+   *
+   * @param resp Response object that will be used to decode the response.
+   *    It can be a  pointer or std::unique_ptr to protobuf message.
+   *
+   * @returns future that will be resolved with a result containing both
+   *    the request and response objects originally passed to the `Post` call.
+   */
+  template <class RequestPtrType, class ResponsePtrType>
+  auto PublicPost(
+    const std::string& path,
+    RequestPtrType&& req,
+    ResponsePtrType&& resp
+  ) {
+    auto session = getSession();
+
+    return client_.Post(
+      base_path_ + path,
+      std::move(req),
+      std::move(resp),
+      std::vector<vereign::restapi::HttpHeader>{
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash}
+      }
+    );
+  }
+
+  /**
+   * Initiate authentication.
+   *
+   * This method is blocking.
+   *
+   * @param pub_key The public key to authenticate with.
+   * @param resp Authentication response.
+   */
+  auto Authenticate(
+    const std::string& pub_key,
+    const std::string& device_hash,
+    client_library::EmptyResponse* resp
+  ) {
+    std::promise<void> promise;
+    auto future = promise.get_future();
+
+    {
+      std::lock_guard<std::mutex> l{mu_};
+      pub_key_ = pub_key;
+      device_hash_ = device_hash;
+      session_token_ = "";
+      uuid_ = "";
+    }
+
+    withAuthentication(
+      [resp, promise = std::move(promise)](const detail::AuthError& err) mutable {
+        if (err) {
+          err->AssignTo(*resp);
+        } else {
+          resp->set_code("200");
+          resp->set_status("OK");
+        }
+
+        promise.set_value();
+      }
+    );
+
+    future.wait();
+  }
+
+  /**
+   * Checks if the client is authenticated.
+   *
+   * @returns true when there is successfully authenticated session, false otherwise.
+   */
+  auto HasSession() -> bool {
+    std::lock_guard<std::mutex> l{mu_};
+
+    return session_token_.size() != 0;
+  }
+
+  /**
+   * Retrieve the restful-api base path.
+   *
+   * @returns the base path used for http requests, for example: `api/`.
+   */
+  auto BasePath() const -> const std::string& {
+    return base_path_;
+  }
+
 private:
 
+  void destroySession() {
+    std::lock_guard<std::mutex> l{mu_};
+
+    session_token_ = "";
+    uuid_ = "";
+  }
+
+  void updateSession(const std::string& token, const std::string& uuid) {
+    std::lock_guard<std::mutex> l{mu_};
+
+    session_token_ = token;
+    uuid_ = uuid;
+  }
+
+  auto getSession() -> detail::Session {
+    std::lock_guard<std::mutex> l{mu_};
+
+    return detail::Session{
+      pub_key_,
+      device_hash_,
+      session_token_,
+      uuid_
+    };
+  }
+
   template <class RequestPtr, class ResponsePtr, class CompletionFunc>
   void authPostAsync(
     const detail::AuthError& err,
@@ -216,15 +429,17 @@ private:
     }
 
     using namespace std::placeholders;
+    auto session = getSession();
 
     client_.PostAsync(
       path,
       std::move(req),
       std::move(resp),
       std::vector<vereign::restapi::HttpHeader>{
-        {"publicKey", pub_key_},
-        {"token", session_token_},
-        {"uuid", uuid_}
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash},
+        {"token", session.Token},
+        {"uuid", session.UUID}
       },
       std::bind(
         &ClientSession::handlePostResult<RequestPtr, ResponsePtr, CompletionFunc>,
@@ -254,15 +469,17 @@ private:
     }
 
     using namespace std::placeholders;
+    auto session = getSession();
 
     client_.PostAsync(
       path,
       std::move(req),
       std::move(resp),
       std::vector<vereign::restapi::HttpHeader>{
-        {"publicKey", pub_key_},
-        {"token", session_token_},
-        {"uuid", uuid_}
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash},
+        {"token", session.Token},
+        {"uuid", session.UUID}
       },
       std::move(cf)
     );
@@ -276,7 +493,10 @@ private:
   ) {
     using namespace std::placeholders;
     auto& resp = result.Response;
+
     if (resp->code() == "400" && resp->status() == "Bad session") {
+      destroySession();
+
       withAuthentication(
         std::bind(
           &ClientSession::authPostRetryAsync<RequestPtr, ResponsePtr, CompletionFunc>,
@@ -300,7 +520,8 @@ private:
     asio::post(
       client_.GetExecutor(),
       [this, cf = std::move(cf)]() mutable {
-        if (session_token_.size() != 0) {
+        auto session = getSession();
+        if (!session.Token.empty()) {
           cf(detail::NoAuthError());
           return;
         }
@@ -314,7 +535,8 @@ private:
           std::make_unique<client_library::EmptyRequest>(),
           std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>(),
           std::vector<vereign::restapi::HttpHeader>{
-            {"publicKey", pub_key_}
+            {"publicKey", session.PublicKey},
+            {"deviceHash", session.DeviceHash}
           },
           [this, cf = std::move(cf)] (ResultType&& result) mutable {
             if (result.Response->code() != "200") {
@@ -335,8 +557,7 @@ private:
               return;
             }
 
-            session_token_ = result.Response->data().session();
-            uuid_ = result.Response->data().uuid();
+            updateSession(result.Response->data().session(), result.Response->data().uuid());
 
             cf(detail::MakeAuthError(*result.Response));
           }
@@ -346,23 +567,28 @@ private:
   }
 
 private:
-  // http client.
+  // http client
   Client& client_;
 
-  // public key used for creating the authenticated sessions.
-  std::string pub_key_;
-
   // http base path of the api calls, for example `/api`
   std::string base_path_;
 
-  // session uuid.
+  // protects the data that follows
+  std::mutex mu_;
+
+  // session uuid
   std::string uuid_;
 
-  // session token.
+  // session token
   std::string session_token_;
+
+  // public key used for creating the authenticated sessions
+  std::string pub_key_;
+
+  // public key hash
+  std::string device_hash_;
 };
 
-} // namespace restapi
 } // namespace vereign
 
 #endif // __VEREIGN_RESTAPI_CLIENT_SESSION_HH
diff --git a/cpp/src/vereign/restapi/detail/post_task.hh b/cpp/src/vereign/restapi/detail/post_task.hh
index 74f1ea70ae57b105207529f64c86aa27aaaa48bc..88b84794f1329bf69743cf4d69c6bab4472fe104 100644
--- a/cpp/src/vereign/restapi/detail/post_task.hh
+++ b/cpp/src/vereign/restapi/detail/post_task.hh
@@ -12,8 +12,7 @@
 #include <google/protobuf/util/json_util.h>
 #include <fmt/core.h>
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 namespace beast = boost::beast;
 
@@ -109,6 +108,8 @@ public:
 
   void Complete(const PostError& err) override {
     if (err) {
+      resp_->set_code("500");
+      resp_->set_status("http client error");
       resp_->set_error(err.value());
     }
 
@@ -125,6 +126,14 @@ public:
 private:
 
   void decodeErrorResponse(const HttpResponse& httpResp) {
+    if (httpResp.result() == beast::http::status::not_found) {
+      resp_->set_code("404");
+      resp_->set_status(httpResp.body());
+      resp_->set_error(httpResp.body());
+
+      return;
+    }
+
     google::protobuf::util::JsonParseOptions options;
 
     client_library::ErrorStringResponse stringResp;
@@ -168,7 +177,6 @@ private:
 };
 
 } // namespace detail
-} // namespace restapi
 } // namespace vereign
 
 #endif // __VEREIGN_RESTAPI_DETAIL_POST_TASK_HH
diff --git a/cpp/src/vereign/service/identity_service.cc b/cpp/src/vereign/service/identity_service.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9a23b616b7b843afaf409989f530c6fd5a68faeb
--- /dev/null
+++ b/cpp/src/vereign/service/identity_service.cc
@@ -0,0 +1,66 @@
+#include <vereign/service/identity_service.hh>
+
+#include <vereign/service/gen/identity_service.hh>
+#include <vereign/client_library/common_types.pb.h>
+#include <vereign/restapi/http_header.hh>
+#include <vereign/restapi/client_session.hh>
+#include <vereign/encoding/base64.hh>
+
+#include <memory>
+
+namespace {
+  const std::string loginWithPreviouslyAddedDevicePath = "loginWithPreviouslyAddedDevice";
+  const std::string loginWithNewDevicePath = "loginWithNewDevice";
+}
+
+namespace vereign::service {
+
+IdentityService::IdentityService(
+  restapi::ClientSession& client_session,
+  identity::Provider& identity_provider
+)
+  : gen::IdentityService{client_session},
+    client_session_{client_session},
+    identity_provider_{identity_provider}
+{}
+
+void IdentityService::LoginWithExistingPubKey(
+  const client_library::LoginWithExistingPubKeyForm* req,
+  client_library::EmptyResponse* resp
+) {
+  client_session_.Authenticate(req->pubkey(), "", resp);
+}
+
+void IdentityService::LoginWithNewDevice(
+  const client_library::LoginFormNewDevice* req,
+  client_library::LoginFormNewDeviceResponse* resp
+) {
+  auto public_key = identity_provider_.RecreateIdentity(req->pin());
+
+  auto result = client_session_.GetClient().Post(
+    client_session_.BasePath() + gen::IdentityService::ServicePath + loginWithNewDevicePath,
+    std::make_unique<client_library::EmptyRequest>(),
+    resp,
+    std::vector<vereign::restapi::HttpHeader>{
+      {"publicKey", public_key},
+      {"deviceHash", identity_provider_.GetDeviceHash()}
+    }
+  );
+
+  result.wait();
+
+  if (resp->code() == "200") {
+    client_session_.SetPubKey(public_key, identity_provider_.GetDeviceHash());
+  }
+}
+
+
+void IdentityService::LoginWithPreviouslyAddedDevice(
+  const client_library::LoginFormPreviousAddedDevice* req,
+  client_library::EmptyResponse* resp
+) {
+  auto public_key = identity_provider_.LoadIdentity(req->pin());
+  client_session_.Authenticate(public_key, identity_provider_.GetDeviceHash(), resp);
+}
+
+} // namespace vereign
diff --git a/cpp/src/vereign/service/identity_service.hh b/cpp/src/vereign/service/identity_service.hh
new file mode 100644
index 0000000000000000000000000000000000000000..11de3e0f5318535d3d7307d126aa19d590141ea6
--- /dev/null
+++ b/cpp/src/vereign/service/identity_service.hh
@@ -0,0 +1,107 @@
+#ifndef __VEREIGN_SERVICE_IDENITY_SERVICE_HH
+#define __VEREIGN_SERVICE_IDENITY_SERVICE_HH
+
+#include <vereign/identity/provider.hh>
+#include <vereign/client_library/common_types.pb.h>
+#include <vereign/client_library/identity_types.pb.h>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/restapi/post_result.hh>
+#include <vereign/service/gen/identity_service.hh>
+
+#include <future>
+
+namespace vereign {
+
+namespace restapi {
+  class ClientSession;
+}
+
+namespace service {
+
+template <class Request, class Response>
+using Result = restapi::PostResult<Request, Response>;
+
+class IdentityService : public gen::IdentityService {
+public:
+  /**
+   * Creates IdentityService instance.
+   *
+   * @param client_session HTTP client used for communicating with the Vereign Restful API.
+   * @param identity_provider Local identity provider (manager).
+   */
+  IdentityService(
+    restapi::ClientSession& client_session,
+    identity::Provider& identity_provider
+  );
+
+  // disable copying
+  IdentityService(const IdentityService&) = delete;
+  auto operator=(const IdentityService&) -> IdentityService& = delete;
+
+  /**
+   * Login with existing identity public key.
+   *
+   * This API is for test purposes only. It is not exposed under the gRPC API, and thus is not
+   * accessible by the integrators.
+   *
+   * **WARN: do not use this in production code**
+   */
+  void LoginWithExistingPubKey(
+    const client_library::LoginWithExistingPubKeyForm* req,
+    client_library::EmptyResponse* resp
+  );
+
+  /**
+   * Registers a new device.
+   *
+   * req.pin is required only under Linux.
+   *
+   * Under windows the system cypto storage is used.
+   * When the device is registered a master key is created and the user will be asked for his
+   * consent by showing a dialog window.
+   *
+   * @param req Login request.
+   * @param resp Operation response.
+   *
+   * @throws kvstore::InvalidPinCodeError Only under Linux. Thrown when the provided pin is invalid,
+   *    currently that is when the pin is empty.
+   */
+  void LoginWithNewDevice(
+    const client_library::LoginFormNewDevice* req,
+    client_library::LoginFormNewDeviceResponse* resp
+  );
+
+  /**
+   * Login with already registered device.
+   *
+   * req.pin is required only under Linux.
+   *
+   * Under windows the system cypto storage is used.
+   * When the device is registered a master key is created and the user will be asked for his
+   * consent by showing a dialog window.
+   *
+   * @param req Login request.
+   * @param resp Operation response.
+   *
+   * @throws kvstore::StorageNotInitializedError when the crypto storage is empty, meaning that
+   *    the device is not registered.
+   * @throws kvstore::InvalidPinCodeError under Linux, when the provided pin is invalid, meaning
+   *    that the pin does not match the pin used during the registration.
+   * @throws kvstore::InvalidIdentityError under windows, when for some reason the RSA master key
+   *    has been changed.
+   */
+  void LoginWithPreviouslyAddedDevice(
+    const client_library::LoginFormPreviousAddedDevice* req,
+    client_library::EmptyResponse* resp
+  );
+
+private:
+  restapi::ClientSession& client_session_;
+  identity::Provider& identity_provider_;
+};
+
+} // namespace service
+} // namespace vereign
+
+
+#endif // __VEREIGN_SERVICE_IDENITY_SERVICE_HH
diff --git a/cpp/src/vereign/service/passport_service.cc b/cpp/src/vereign/service/passport_service.cc
deleted file mode 100644
index e6cf88372cecd5c47c002b893fa035dfb5a8da8f..0000000000000000000000000000000000000000
--- a/cpp/src/vereign/service/passport_service.cc
+++ /dev/null
@@ -1,32 +0,0 @@
-#include "vereign/service/gen/passport_service.hh"
-#include <vereign/service/passport_service.hh>
-#include <vereign/restapi/client_session.hh>
-
-namespace {
-  const std::string listPassportsPath = "listPassports";
-}
-
-namespace vereign {
-namespace service {
-
-PassportService::PassportService(restapi::ClientSession& client_session)
-  : gen::PassportService{client_session},
-    client_session_{client_session}
-{}
-
-
-std::future<PassportService::ListPassportsResult>
-PassportService::ListPassportsManually(
-    const client_library::ListPassportsForm* req,
-    client_library::ListPassportsFormResponse* resp
-) {
-  return client_session_.Post(
-    gen::PassportService::ServicePath + listPassportsPath,
-    req,
-    resp
-  );
-}
-
-
-} // namespace service
-} // namespace vereign
diff --git a/cpp/src/vereign/service/passport_service.hh b/cpp/src/vereign/service/passport_service.hh
deleted file mode 100644
index f59b4b27290676e1c3ae9c5cd2b1a7714cce31b0..0000000000000000000000000000000000000000
--- a/cpp/src/vereign/service/passport_service.hh
+++ /dev/null
@@ -1,45 +0,0 @@
-#ifndef __VEREIGN_SERVICE_PASSPORT_SERVICE_HH
-#define __VEREIGN_SERVICE_PASSPORT_SERVICE_HH
-
-#include <vereign/restapi/client_session.hh>
-#include <vereign/restapi/post_result.hh>
-#include <vereign/client_library/types.gen.pb.h>
-#include <vereign/service/gen/passport_service.hh>
-
-#include <future>
-
-namespace vereign {
-
-namespace restapi {
-  class ClientSession;
-}
-
-namespace service {
-
-template <class Request, class Response>
-using Result = restapi::PostResult<Request, Response>;
-
-class PassportService : public gen::PassportService {
-public:
-  explicit PassportService(restapi::ClientSession& client_session);
-
-  PassportService(const PassportService&) = delete;
-  PassportService& operator=(const PassportService&) = delete;
-
-  using ListPassportsResult = Result<
-    const client_library::ListPassportsForm*,
-    client_library::ListPassportsFormResponse*>;
-
-  std::future<ListPassportsResult> ListPassportsManually(
-    const client_library::ListPassportsForm* req,
-    client_library::ListPassportsFormResponse* resp
-  );
-
-private:
-  restapi::ClientSession& client_session_;
-};
-
-} // namespace service
-} // namespace vereign
-
-#endif // __VEREIGN_SERVICE_PASSPORT_SERVICE_HH
diff --git a/cpp/src/vereign/sqlite/connection.cc b/cpp/src/vereign/sqlite/connection.cc
new file mode 100644
index 0000000000000000000000000000000000000000..eefe6a7de2ff004c777f88deb81eb3cee64f9271
--- /dev/null
+++ b/cpp/src/vereign/sqlite/connection.cc
@@ -0,0 +1,75 @@
+#include <vereign/sqlite/connection.hh>
+
+#include <vereign/sqlite/errors.hh>
+#include <vereign/core/scope_guard.hh>
+
+#include <fmt/format.h>
+#include <sqlite3.h>
+#include <iostream>
+
+namespace vereign::sqlite {
+
+Connection::Connection(const std::string& path)
+  : db_{nullptr}
+{
+  auto rc = sqlite3_open(path.data(), &db_);
+  if (rc != SQLITE_OK) {
+    throw Error{rc, fmt::format("open db failed, err: {}", sqlite3_errmsg(db_))};
+  }
+}
+
+Connection::~Connection() noexcept {
+  sqlite3_close(db_);
+}
+
+void Connection::BeginExplicitTransaction() {
+  char* errMsg = nullptr;
+  auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); });
+  auto rc = sqlite3_exec(db_, "BEGIN EXCLUSIVE", nullptr, nullptr, &errMsg);
+  if (rc != SQLITE_OK) {
+    throw Error(rc, fmt::format("starting transaction failed, err: {}", errMsg));
+  }
+}
+
+void Connection::Commit() {
+  char* errMsg = nullptr;
+  auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); });
+
+  auto rc = sqlite3_exec(db_, "COMMIT", nullptr, nullptr, &errMsg);
+  if (rc != SQLITE_OK) {
+    throw Error(rc, fmt::format("commit transaction failed, err: {}", errMsg));
+  }
+}
+
+void Connection::Rollback() {
+  char* errMsg = nullptr;
+  auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); });
+
+  auto rc = sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, &errMsg);
+  if (rc != SQLITE_OK) {
+    throw Error(rc, fmt::format("rollback transaction failed, err: {}", errMsg));
+  }
+}
+
+void Connection::Execute(const std::string& sql) {
+  char* errMsg = nullptr;
+  auto freeErr = vereign::core::MakeScopeGuard([&errMsg]{ sqlite3_free(errMsg); });
+
+  auto rc = sqlite3_exec(db_, sql.data(), nullptr, nullptr, &errMsg);
+  if (rc != SQLITE_OK) {
+    throw Error(rc, fmt::format("query execution failed, err: {}", errMsg));
+  }
+}
+
+auto Connection::Prepare(const std::string& sql) -> Statement {
+  sqlite3_stmt *stmt = nullptr;
+
+  auto rc = sqlite3_prepare_v2(db_, sql.data(), -1, &stmt, nullptr);
+  if (rc != SQLITE_OK) {
+    throw Error(rc, fmt::format("preparing statement failed, err: {}", sqlite3_errmsg(db_)));
+  }
+
+  return Statement{db_, stmt};
+}
+
+} // namespace vereign::sqlite
diff --git a/cpp/src/vereign/sqlite/connection.hh b/cpp/src/vereign/sqlite/connection.hh
new file mode 100644
index 0000000000000000000000000000000000000000..d1134045dc4c2434ff20ad5b2841e58a51c5c253
--- /dev/null
+++ b/cpp/src/vereign/sqlite/connection.hh
@@ -0,0 +1,32 @@
+#ifndef __VEREIGN_SQLITE_CONNECTION_HH
+#define __VEREIGN_SQLITE_CONNECTION_HH
+
+#include <vereign/sqlite/statement.hh>
+
+struct sqlite3;
+struct sqlite3_stmt;
+
+namespace vereign::sqlite {
+
+class Connection {
+public:
+  Connection(const std::string& path);
+  ~Connection() noexcept;
+
+  Connection(const Connection&) = delete;
+  auto operator=(const Connection&) -> Connection& = delete;
+
+  void BeginExplicitTransaction();
+  void Commit();
+  void Rollback();
+
+  void Execute(const std::string& sql);
+  auto Prepare(const std::string& sql) -> Statement;
+
+private:
+  sqlite3* db_;
+};
+
+} // namespace vereign::sqlite
+
+#endif // __VEREIGN_SQLITE_CONNECTION_HH
diff --git a/cpp/src/vereign/sqlite/errors.hh b/cpp/src/vereign/sqlite/errors.hh
new file mode 100644
index 0000000000000000000000000000000000000000..00622311b42921a41500d7d7b3cc1f5b0e54f9a2
--- /dev/null
+++ b/cpp/src/vereign/sqlite/errors.hh
@@ -0,0 +1,26 @@
+#ifndef __VEREIGN_SQLITE_ERROR_HH
+#define __VEREIGN_SQLITE_ERROR_HH
+
+#include <stdexcept>
+
+namespace vereign::sqlite {
+
+class Error : public std::runtime_error {
+public:
+  Error(int code, const std::string& msg)
+    : std::runtime_error(msg),
+      code_{code}
+  {
+  }
+
+  auto code() const noexcept -> int {
+    return code_;
+  }
+
+private:
+  int code_;
+};
+
+} // namespace vereign::sqlite
+
+#endif // __VEREIGN_SQLITE_CONNECTION_HH
diff --git a/cpp/src/vereign/sqlite/statement.cc b/cpp/src/vereign/sqlite/statement.cc
new file mode 100644
index 0000000000000000000000000000000000000000..0a3d00795383e4498580c1a56265b1ce55d87918
--- /dev/null
+++ b/cpp/src/vereign/sqlite/statement.cc
@@ -0,0 +1,78 @@
+#include <vereign/sqlite/statement.hh>
+#include <vereign/sqlite/errors.hh>
+
+#include <fmt/format.h>
+#include <sqlite3.h>
+
+namespace vereign::sqlite {
+
+Statement::Statement(sqlite3* db, sqlite3_stmt* stmt)
+  : db_{db},
+    stmt_{stmt}
+{}
+
+void Statement::Finalize() {
+  sqlite3_finalize(stmt_);
+}
+
+Statement::~Statement() {
+  sqlite3_finalize(stmt_);
+}
+
+void Statement::BindBlob(int index, bytes::View blob) {
+  auto rc = sqlite3_bind_blob64(stmt_, index, blob.CharData(), blob.Size(), SQLITE_STATIC);
+  if (rc != SQLITE_OK) {
+    throw Error{rc, fmt::format("bind blob parameter failed, err: {}", sqlite3_errmsg(db_))};
+  }
+}
+
+void Statement::BindText(int index, const std::string& text) {
+  auto rc = sqlite3_bind_text(stmt_, index, text.data(), text.size(), SQLITE_STATIC);
+  if (rc != SQLITE_OK) {
+    throw Error{rc, fmt::format("bind text parameter failed, err: {}", sqlite3_errmsg(db_))};
+  }
+}
+
+auto Statement::Step() -> bool {
+  auto rc = sqlite3_step(stmt_);
+  switch (rc) {
+  case SQLITE_DONE:
+    return true;
+  case SQLITE_ROW:
+    return false;
+  default:
+    throw Error{rc, fmt::format("executing statement failed, err: {}", sqlite3_errmsg(db_))};
+  }
+}
+
+auto Statement::GetColumnBlob(int index) -> bytes::View {
+  auto size = sqlite3_column_bytes(stmt_, index);
+  auto blob = sqlite3_column_blob(stmt_, index);
+
+  return bytes::View{blob, static_cast<size_t>(size)};
+}
+
+auto Statement::GetColumnText(int index) -> std::string_view {
+  std::size_t size = sqlite3_column_bytes(stmt_, index);
+  auto blob = sqlite3_column_text(stmt_, index);
+
+  return std::string_view{reinterpret_cast<const char*>(blob), size};
+}
+
+void Statement::ResetAndClearBindings() {
+  auto rc = sqlite3_clear_bindings(stmt_);
+  if (rc != SQLITE_OK) {
+    throw Error{rc, fmt::format("statement reset bindings failed, err: {}", sqlite3_errmsg(db_))};
+  }
+
+  Reset();
+}
+
+void Statement::Reset() {
+  auto rc = sqlite3_reset(stmt_);
+  if (rc != SQLITE_OK) {
+    throw Error{rc, fmt::format("statement reset failed, err: {}", sqlite3_errmsg(db_))};
+  }
+}
+
+} // namespace vereign::sqlite
diff --git a/cpp/src/vereign/sqlite/statement.hh b/cpp/src/vereign/sqlite/statement.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2b8c336d735a5ec33eb673dffa8df0ef87cce676
--- /dev/null
+++ b/cpp/src/vereign/sqlite/statement.hh
@@ -0,0 +1,42 @@
+#ifndef __VEREIGN_SQLITE_STATEMENT_HH
+#define __VEREIGN_SQLITE_STATEMENT_HH
+
+#include <vereign/bytes/view.hh>
+#include <vereign/bytes/bytes.hh>
+
+#include <string>
+
+struct sqlite3;
+struct sqlite3_stmt;
+
+namespace vereign::sqlite {
+
+class Statement {
+private:
+  friend class Connection;
+
+  explicit Statement(sqlite3* db, sqlite3_stmt* stmt);
+
+public:
+  ~Statement();
+
+  void BindBlob(int index, bytes::View blob);
+  void BindText(int index, const std::string& text);
+
+  auto Step() -> bool;
+
+  auto GetColumnBlob(int index) -> bytes::View;
+  auto GetColumnText(int index) -> std::string_view;
+
+  void Reset();
+  void ResetAndClearBindings();
+  void Finalize();
+
+private:
+  sqlite3* db_;
+  sqlite3_stmt* stmt_;
+};
+
+} // namespace vereign::sqlite
+
+#endif // __VEREIGN_SQLITE_STATEMENT_HH
diff --git a/cpp/src/vereign/sync/channel.hh b/cpp/src/vereign/sync/channel.hh
index ff6ff439de10fa058ee1a80460cfc8feb1e69dd2..0e1544b29957820a0bd9218b2a8736618c3b9d27 100644
--- a/cpp/src/vereign/sync/channel.hh
+++ b/cpp/src/vereign/sync/channel.hh
@@ -1,13 +1,12 @@
 #ifndef __VEREIGN_SYNC_CHANNEL_HH
 #define __VEREIGN_SYNC_CHANNEL_HH
 
-#include <deque>
+#include <vereign/container/bounded_queue.hh>
+
 #include <condition_variable>
 #include <boost/optional.hpp>
-#include <iostream>
 
-namespace vereign {
-namespace sync {
+namespace vereign::sync {
 
 /**
  * ChannelAddResult is used as return value by Channel TryAdd methods.
@@ -20,7 +19,8 @@ public:
   /**
    * Default constructor - the result is ok.
    */
-  ChannelAddResult(): closed_(false), full_(false) {}
+  ChannelAddResult() = default;
+
   /**
    * Creates ChannelAddResult.
    *
@@ -34,7 +34,7 @@ public:
    *
    * @returns true when the add operation was successful.
    */
-  bool IsOk() const noexcept {
+  auto IsOk() const noexcept -> bool {
     return !closed_ && !full_;
   }
 
@@ -52,7 +52,7 @@ public:
    *
    * @returns true when the channel was closed.
    */
-  bool IsClosed() const noexcept {
+  auto IsClosed() const noexcept -> bool {
     return closed_;
   }
 
@@ -63,13 +63,13 @@ public:
    *
    * @returns true when the channel was full.
    */
-  bool IsFull() const noexcept {
+  auto IsFull() const noexcept -> bool {
     return full_;
   }
 
 private:
-  bool closed_;
-  bool full_;
+  bool closed_ = false;
+  bool full_ = false;
 };
 
 /**
@@ -102,31 +102,31 @@ public:
 
   // The Channel Value is only move constructible and move assignable.
   ChannelValue(ChannelValue&&) = default;
-  ChannelValue& operator=(ChannelValue&&) = default;
+  auto operator=(ChannelValue&&) -> ChannelValue& = default;
   ChannelValue(const ChannelValue&) = delete;
-  ChannelValue& operator=(const ChannelValue&) = delete;
+  auto operator=(const ChannelValue&) -> ChannelValue& = delete;
 
   operator bool() const noexcept {
     return HasValue();
   }
 
-  bool HasValue() const noexcept {
+  auto HasValue() const noexcept -> bool {
     return value_.has_value();
   }
 
-  const ValueType& Value() const {
+  auto Value() const -> const ValueType& {
     return value_.value();
   }
 
-  ValueType& Value() {
+  auto Value() -> ValueType& {
     return value_.value();
   }
 
-  bool IsClosed() const noexcept {
+  auto IsClosed() const noexcept -> bool {
     return closed_;
   }
 
-  bool IsEmpty() const noexcept {
+  auto IsEmpty() const noexcept -> bool {
     return empty_;
   }
 
@@ -164,7 +164,7 @@ public:
    */
   explicit Channel(std::size_t capacity)
     : closed_{false},
-      capacity_{capacity}
+      queue_{capacity}
   {
   }
 
@@ -177,17 +177,17 @@ public:
    * @param value The value is moved into the new element.
    * @returns false when the channel is closed, and the value could not be pushed.
    */
-  bool Add(ValueType&& value) {
+  auto Add(ValueType&& value) -> bool {
     std::unique_lock<std::mutex> lock(mu_);
     writers_cv_.wait(lock, [this]() {
-      return queue_.size() < capacity_ || closed_;
+      return !queue_.IsFull() || closed_;
     });
 
     if (closed_) {
       return false;
     }
 
-    queue_.push_back(std::move(value));
+    queue_.PushBack(std::move(value));
     lock.unlock();
 
     readers_cv_.notify_one();
@@ -204,17 +204,17 @@ public:
    * @param value The value is copied and added to the channel.
    * @returns false when the channel is closed, and the value could not be pushed.
    */
-  bool Add(const ValueType& value) {
+  auto Add(const ValueType& value) -> bool {
     std::unique_lock<std::mutex> lock(mu_);
     writers_cv_.wait(lock, [this]() {
-      return queue_.size() < capacity_ || closed_;
+      return !queue_.IsFull() || closed_;
     });
 
     if (closed_) {
       return false;
     }
 
-    queue_.push_back(value);
+    queue_.PushBack(value);
     lock.unlock();
 
     readers_cv_.notify_one();
@@ -233,16 +233,16 @@ public:
    * @returns a add result, that can be used to check if the operation was
    *    successful, and what was the channel state - closed, full.
    */
-  ChannelAddResult TryAdd(ValueType&& value) {
+  auto TryAdd(ValueType&& value) -> ChannelAddResult {
     ChannelAddResult result;
     {
       std::lock_guard<std::mutex> lock(mu_);
-      result = ChannelAddResult{closed_, queue_.size() >= capacity_};
+      result = ChannelAddResult{closed_, queue_.IsFull()};
       if (!result) {
         return result;
       }
 
-      queue_.push_back(std::move(value));
+      queue_.PushBack(std::move(value));
     }
 
     readers_cv_.notify_one();
@@ -261,16 +261,16 @@ public:
    * @returns a add result, that can be used to check if the operation was
    *    successful, and what was the channel state - closed, full.
    */
-  ChannelAddResult TryAdd(const ValueType& value) {
+  auto TryAdd(const ValueType& value) -> ChannelAddResult {
     ChannelAddResult result;
     {
       std::lock_guard<std::mutex> lock(mu_);
-      result = ChannelAddResult{closed_, queue_.size() >= capacity_};
+      result = ChannelAddResult{closed_, queue_.IsFull()};
       if (!result) {
         return result;
       }
 
-      queue_.push_back(value);
+      queue_.PushBack(value);
     }
 
     readers_cv_.notify_one();
@@ -280,30 +280,31 @@ public:
 
   /**
    * Get retrieves a value from the channel.
-   * If the channel is empty, this call blocks until there is something added
-   * into the channel or the channel is empty but closed.
+   *
+   * If the channel is empty, this call blocks until there is something added into the channel or
+   * the channel is empty but closed.
    *
    * @returns the retrieved value.
    *    The retrieved value has optional semantics.
    *    One must check if the value exists before using it.
    *    See the ChannelValue class docs.
    */
-  ChannelValue<ValueType> Get() {
+  auto Get() -> ChannelValue<ValueType> {
     std::unique_lock<std::mutex> lock(mu_);
     readers_cv_.wait(lock, [this]() {
-      return queue_.size() > 0 || closed_;
+      return !queue_.IsEmpty() || closed_;
     });
 
-    if (queue_.size() == 0) {
-      return ChannelValue<ValueType>{closed_, queue_.size() == 0};
+    if (queue_.IsEmpty()) {
+      return ChannelValue<ValueType>{closed_, true};
     }
 
     auto result = ChannelValue<ValueType>{
-      std::move(queue_.front()),
+      std::move(queue_.Front()),
       closed_,
-      queue_.size() == 0,
+      queue_.IsEmpty(),
     };
-    queue_.pop_front();
+    queue_.PopFront();
 
     lock.unlock();
     writers_cv_.notify_one();
@@ -339,16 +340,10 @@ private:
   // signify if the channel is closed.
   bool closed_;
 
-  // maximum number of elements buffered inside the channel.
-  std::size_t capacity_;
-
   // the internal queue used by the channel.
-  // TODO: replace the deque with circular buffer since the channel's capacity
-  //       is fixed and known during the construction time.
-  std::deque<ValueType> queue_;
+  container::BoundedQueue<ValueType> queue_;
 };
 
-}
-}
+} // namespace vereign::sync
 
-#endif
+#endif // __VEREIGN_SYNC_CHANNEL_HH
diff --git a/cpp/src/vereign/vereign.cc b/cpp/src/vereign/vereign.cc
index 5bbc7e934fe7b9ff917e4b4b75a4a236c8b671f7..6095a55db57e77a6c11feeae3ba8f08217d223f8 100644
--- a/cpp/src/vereign/vereign.cc
+++ b/cpp/src/vereign/vereign.cc
@@ -27,8 +27,7 @@ auto vereign_service_start(
   const char* listen_address,
   const char* vereign_host,
   const char* vereign_port,
-  // FIXME: public_key must come from a storage internally.
-  const char* public_key,
+  const char* storage_path,
   vereign_error** err
 ) -> vereign_service* {
   if (err != nullptr) {
@@ -42,7 +41,7 @@ auto vereign_service_start(
       listen_address,
       vereign_host,
       vereign_port,
-      public_key
+      storage_path != nullptr ? storage_path : ""
     );
 
     return new vereign_service{std::move(serviceImpl)};
@@ -59,7 +58,6 @@ auto vereign_service_start(
   return nullptr;
 }
 
-
 auto vereign_service_selected_port(vereign_service* service) -> int {
   return service->impl->SelectedPort();
 }
@@ -72,4 +70,3 @@ void vereign_service_shutdown(vereign_service* service) {
   service->impl->Shutdown();
   delete service;
 }
-
diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt
index 62bd6f48b027be1b1605a1f083be7c52d0ecaf00..c7bbbfcf2f6008b9cefbca1edce83566eed8f885 100644
--- a/cpp/tests/CMakeLists.txt
+++ b/cpp/tests/CMakeLists.txt
@@ -1,7 +1,20 @@
 
 include_directories(
   ${CMAKE_CURRENT_SOURCE_DIR}
-  "${VENDOR_INSTALL_DIR}/boost/include"
+)
+
+set(TESTUTIL_SRC
+  experiment/array.cc
+
+  testutil/protobuf.cc
+  testutil/golden.cc
+)
+add_library(testutil STATIC ${TESTUTIL_SRC})
+target_link_libraries(testutil PRIVATE
+  vereignlib
+)
+target_include_directories(testutil PRIVATE
+  ${CMAKE_SOURCE_DIR}/src
 )
 
 add_subdirectory("protobuf")
diff --git a/cpp/tests/integration/CMakeLists.txt b/cpp/tests/integration/CMakeLists.txt
index 9efb1023b1885e94c729c2d0259cd1963e188273..885f35651888dbf67bbe1dfbd30a00ef915eaabc 100644
--- a/cpp/tests/integration/CMakeLists.txt
+++ b/cpp/tests/integration/CMakeLists.txt
@@ -1,7 +1,6 @@
 include_directories(
   ${CMAKE_SOURCE_DIR}/src
   ${CMAKE_SOURCE_DIR}/include
-  # ${VENDOR_INSTALL_DIR}/include
   ${CMAKE_SOURCE_DIR}/proto/cpp
 )
 
@@ -24,3 +23,18 @@ add_test(
   NAME integration_test
   COMMAND integration_test
 )
+
+list(APPEND INIT_INTEGRATION_TEST_STORAGE_SRC
+  ../vereign/test/device.cc
+  ../vereign/test/service_context.cc
+
+  init_integration_test_storage.cc
+)
+
+add_executable(init_integration_test_storage ${INIT_INTEGRATION_TEST_STORAGE_SRC})
+
+target_link_libraries(init_integration_test_storage
+  vereignlib
+  Threads::Threads
+)
+
diff --git a/cpp/tests/integration/init_integration_test_storage.cc b/cpp/tests/integration/init_integration_test_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..ff234034cd50f576d43b70263a48f724013b4292
--- /dev/null
+++ b/cpp/tests/integration/init_integration_test_storage.cc
@@ -0,0 +1,34 @@
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/fs/operations.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+#include <util/env.hh>
+
+using namespace vereign;
+
+/**
+ * The init_integration_test_storage tool is used for creating a new device with
+ * initialized storage at ${HOME}/vereign_integration_test dir.
+ *
+ * FIXME: create a command line utility for this and other commands like starting a gRPC
+ *        server for integration tests.
+ */
+auto main(int argc, char** argv) -> int {
+  boost::ignore_unused(argc);
+  boost::ignore_unused(argv);
+
+  auto public_key = test::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = test::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto storage_path = test::GetEnv("TEST_VEREIGN_INTEGRATION_STORAGE", "");
+  auto pin = test::GetEnv("TEST_VEREIGN_PIN", "foo");
+
+  if (storage_path == "") {
+    storage_path = fs::path::Join(fs::HomePath(), "vereign_integration_test");
+  }
+
+  fs::CreateDir(storage_path);
+
+  test::PrepareNewDevice(host, port, public_key, pin, storage_path);
+}
diff --git a/cpp/tests/integration/integration_test.cc b/cpp/tests/integration/integration_test.cc
index ef42c7201cd946c0b613616ab45813340c351dd8..482e13d76750910dcf20cf7a3c6e23142142d27a 100644
--- a/cpp/tests/integration/integration_test.cc
+++ b/cpp/tests/integration/integration_test.cc
@@ -1,16 +1,20 @@
 #include <vereign/vereign.h>
-#include <util/env.hh>
+
 #include <vereign/client_library/types.gen.pb.h>
 #include <vereign/client_library/passport_api.gen.grpc.pb.h>
+#include <vereign/client_library/identity_api.gen.grpc.pb.h>
 #include <vereign/core/scope_guard.hh>
-#include <grpcpp/create_channel.h>
+#include <util/env.hh>
 
+#include <grpcpp/create_channel.h>
 #include <catch2/catch.hpp>
 
 TEST_CASE("C API integration", "[.integration]") {
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  // NOTE: use the init_integration_storage tool to create a new test storage
+  auto storage_path = vereign::test::RequireEnv("TEST_VEREIGN_INTEGRATION_STORAGE");
   auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
   auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto pin = vereign::test::GetEnv("TEST_VEREIGN_PIN", "foo");
 
   // start the service
   vereign_error* err = nullptr;
@@ -18,7 +22,7 @@ TEST_CASE("C API integration", "[.integration]") {
     "localhost:",
     host.data(),
     port.data(),
-    publicKey.data(),
+    storage_path.data(),
     &err
   );
   CHECK(service != nullptr);
@@ -35,28 +39,24 @@ TEST_CASE("C API integration", "[.integration]") {
     ::grpc::InsecureChannelCredentials()
   );
 
+  auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+  auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+  login_req.set_pin(pin);
+  auto login_resp = vereign::client_library::EmptyResponse{};
+  ::grpc::ClientContext login_ctx;
+  auto status = identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+  REQUIRE(status.error_message() == "");
+  REQUIRE(status.ok() == true);
+  REQUIRE(login_resp.error() == "");
+  CHECK(login_resp.status() == "OK");
+  CHECK(login_resp.code() == "200");
+
   auto client = vereign::client_library::PassportAPI::NewStub(channel);
 
   vereign::client_library::ListPassportsForm req;
   vereign::client_library::ListPassportsFormResponse resp;
   ::grpc::ClientContext ctx;
-  auto status = client->ListPassports(&ctx, req, &resp);
-
-  // std::cout << vereign::test::ProtobufToJson(resp) << std::endl;
-
-  REQUIRE(status.error_message() == "");
-  REQUIRE(resp.error() == "");
-  CHECK(resp.status() == "OK");
-  CHECK(resp.code() == "200");
-  CHECK(resp.data().size() > 0);
-  for (auto& passport : resp.data()) {
-    CHECK(passport.uuid().size() == 36);
-  }
-
-  req.Clear();
-  resp.Clear();
-  ::grpc::ClientContext manually_ctx;
-  status = client->ListPassportsManually(&manually_ctx, req, &resp);
+  status = client->ListPassports(&ctx, req, &resp);
 
   // std::cout << vereign::test::ProtobufToJson(resp) << std::endl;
 
@@ -85,8 +85,8 @@ TEST_CASE("C API integration", "[.integration]") {
 
   // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl;
 
-  grpc_shutdown();
-  google::protobuf::ShutdownProtobufLibrary();
+  // grpc_shutdown();
+  // google::protobuf::ShutdownProtobufLibrary();
 }
 
 TEST_CASE("vereign_service_start") {
diff --git a/cpp/tests/testutil/env.hh b/cpp/tests/testutil/env.hh
new file mode 100644
index 0000000000000000000000000000000000000000..fdb3d27635542fc84c448408fdd2ab152872f11e
--- /dev/null
+++ b/cpp/tests/testutil/env.hh
@@ -0,0 +1,29 @@
+#ifndef __VEREIGN_TESTUTIL_ENV_HH
+#define __VEREIGN_TESTUTIL_ENV_HH
+
+#include <string>
+#include <fmt/core.h>
+
+namespace vereign::testutil {
+
+inline auto RequireEnv(const std::string& name) -> std::string {
+  auto var = std::getenv(name.c_str());
+  if (var == nullptr) {
+    throw std::runtime_error{fmt::format("{} env variable is required", name)};
+  }
+
+  return var;
+}
+
+inline auto GetEnv(const std::string& name, const std::string& default_) -> std::string {
+  auto var = std::getenv(name.c_str());
+  if (var == nullptr) {
+    return default_;
+  }
+
+  return var;
+}
+
+} // namespace vereign::testutil
+
+#endif // __VEREIGN_TESTUTIL_ENV_HH
diff --git a/cpp/tests/testutil/error.hh b/cpp/tests/testutil/error.hh
new file mode 100644
index 0000000000000000000000000000000000000000..90a1fdb1beead8a790005b22c57baefc8cfcf08f
--- /dev/null
+++ b/cpp/tests/testutil/error.hh
@@ -0,0 +1,22 @@
+#ifndef __VEREIGN_TESTUTIL_ERROR_HH
+#define __VEREIGN_TESTUTIL_ERROR_HH
+
+#include <optional>
+
+namespace vereign::testutil {
+
+template <class Error, class Fun>
+auto CatchError(Fun fn) -> std::optional<Error> {
+  try {
+    fn();
+
+    return {};
+  } catch (const Error& err) {
+
+    return err;
+  }
+}
+
+} // namespace vereign::testutil
+
+#endif // __VEREIGN_TESTUTIL_ERROR_HH
diff --git a/cpp/tests/testutil/golden.cc b/cpp/tests/testutil/golden.cc
new file mode 100644
index 0000000000000000000000000000000000000000..e5a011263d8cf9211b0082da94c5b67c898e8204
--- /dev/null
+++ b/cpp/tests/testutil/golden.cc
@@ -0,0 +1,26 @@
+#include <testutil/golden.hh>
+
+#include <vereign/fs/util.hh>
+#include <testutil/env.hh>
+
+#include <catch2/catch.hpp>
+
+namespace {
+  constexpr const char* GoldenUpdateEnvVar = "GOLDEN_UPDATE";
+}
+
+namespace vereign::testutil::golden {
+
+void Assert(const std::string& golden_file_path, bytes::View src) {
+  auto update = vereign::testutil::GetEnv(GoldenUpdateEnvVar, "");
+  if (!update.empty()) {
+    fs::WriteFile(golden_file_path, src);
+  }
+
+  auto data = fs::ReadFile(golden_file_path);
+
+  // TODO: create nice patch like diff on failure
+  CHECK(data.View().String() == src.String());
+}
+
+} // namespace vereign::test
diff --git a/cpp/tests/testutil/golden.hh b/cpp/tests/testutil/golden.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2fb33bc97c6a742d35a6f569fa026431ece91a11
--- /dev/null
+++ b/cpp/tests/testutil/golden.hh
@@ -0,0 +1,13 @@
+#ifndef __VEREIGN_TESTUTIL_GOLDEN_HH
+#define __VEREIGN_TESTUTIL_GOLDEN_HH
+
+#include <vereign/bytes/view.hh>
+#include <string>
+
+namespace vereign::testutil::golden {
+
+void Assert(const std::string& golden_file, bytes::View src);
+
+} // namespace vereign::test
+
+#endif // __VEREIGN_TESTUTIL_GOLDEN_HH
diff --git a/cpp/tests/util/protobuf.cc b/cpp/tests/testutil/protobuf.cc
similarity index 74%
rename from cpp/tests/util/protobuf.cc
rename to cpp/tests/testutil/protobuf.cc
index 05e1c791a91878f1d09a4c0068227de89e7937e1..c6ecf9991f052196651cb71af76dbf7598b1ac94 100644
--- a/cpp/tests/util/protobuf.cc
+++ b/cpp/tests/testutil/protobuf.cc
@@ -1,12 +1,11 @@
 #include <vereign/grpc/json/encoder.hh>
-#include <util/protobuf.hh>
+#include <testutil/protobuf.hh>
 
 #include <google/protobuf/util/json_util.h>
 
 #include <catch2/catch.hpp>
 
-namespace vereign {
-namespace test {
+namespace vereign::testutil {
 
 auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string {
   std::string result;
@@ -15,5 +14,4 @@ auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string {
   return result;
 }
 
-} // namespace vereign
-} // namespace test
+} // namespace vereign::testutil
diff --git a/cpp/tests/testutil/protobuf.hh b/cpp/tests/testutil/protobuf.hh
new file mode 100644
index 0000000000000000000000000000000000000000..264f7f6e51011c8678e68e0d943176ab0217e497
--- /dev/null
+++ b/cpp/tests/testutil/protobuf.hh
@@ -0,0 +1,13 @@
+#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH
+#define __VEREIGN_TEST_UTIL_PROTOBUF_HH
+
+#include <string>
+#include <google/protobuf/message.h>
+
+namespace vereign::testutil {
+
+auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string;
+
+} // namespace vereign::testutil
+
+#endif // __VEREIGN_TESTUTIL_PROTOBUF_HH
diff --git a/cpp/tests/util/env.hh b/cpp/tests/util/env.hh
deleted file mode 100644
index cb01a8c1de742a59e95935f5342de7e68fbf900c..0000000000000000000000000000000000000000
--- a/cpp/tests/util/env.hh
+++ /dev/null
@@ -1,34 +0,0 @@
-#ifndef __VEREIGN_TEST_UTIL_ENV_HH
-#define __VEREIGN_TEST_UTIL_ENV_HH
-
-#include <string>
-
-#include <catch2/catch.hpp>
-#include <fmt/core.h>
-
-namespace vereign {
-namespace test {
-
-inline std::string RequireEnv(const std::string& name) {
-  auto var = std::getenv(name.c_str());
-  if (var == nullptr) {
-    FAIL(fmt::format("{} env variable is required", name));
-    return "";
-  }
-
-  return var;
-}
-
-inline std::string GetEnv(const std::string& name, const std::string& default_) {
-  auto var = std::getenv(name.c_str());
-  if (var == nullptr) {
-    return default_;
-  }
-
-  return var;
-}
-
-} // namespace vereign
-} // namespace test
-
-#endif // __VEREIGN_TEST_UTIL_ENV_HH
diff --git a/cpp/tests/util/protobuf.hh b/cpp/tests/util/protobuf.hh
deleted file mode 100644
index 09ca9be1f1b36d9d1444160333d0017ffc396b8e..0000000000000000000000000000000000000000
--- a/cpp/tests/util/protobuf.hh
+++ /dev/null
@@ -1,15 +0,0 @@
-#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH
-#define __VEREIGN_TEST_UTIL_PROTOBUF_HH
-
-#include <string>
-#include <google/protobuf/message.h>
-
-namespace vereign {
-namespace test {
-
-std::string ProtobufToJson(const google::protobuf::Message& msg);
-
-} // namespace vereign
-} // namespace test
-
-#endif // __VEREIGN_TEST_UTIL_PROTOBUF_HH
diff --git a/cpp/tests/vereign/CMakeLists.txt b/cpp/tests/vereign/CMakeLists.txt
index 2c2b6fce316e7721ed05a80e53330200081a4486..913e3148a3e61291ac1214e1bf07aa0bfdb7a10d 100644
--- a/cpp/tests/vereign/CMakeLists.txt
+++ b/cpp/tests/vereign/CMakeLists.txt
@@ -1,6 +1,6 @@
 
 if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
-  add_definitions(-DNOGDI)
+  add_definitions(-DNOGDI -DNOMINMAX)
 endif()
 
 include_directories(
@@ -10,19 +10,52 @@ include_directories(
   ${Boost_INCLUDE_DIRS}
 )
 
-list(APPEND tests_src
+list(APPEND TESTS_SRC
   init_tests.cc
-  ../util/protobuf.cc
-  ../experiment/array.cc
+  test/device.cc
+  test/service_context.cc
 
+  core/time_test.cc
   sync/channel_test.cc
+  container/bounded_queue_test.cc
+
+  encoding/base64_test.cc
+  encoding/hex_test.cc
+
+  bytes/buffer_test.cc
+  bytes/view_test.cc
+
+  crypto/errors_test.cc
+  crypto/aes_test.cc
+  crypto/rsa_test.cc
+  crypto/digest_test.cc
+  crypto/cert_test.cc
+
   restapi/client_test.cc
   restapi/client_session_test.cc
+
+  kvstore/lock_test.cc
+  kvstore/sqlite_storage_test.cc
+  kvstore/crypto_storage_test.cc
+
+  identity/provider_test.cc
+
   service/gen/passport_service_test.cc
+  service/identity_service_test.cc
+
   grpc/server_test.cc
   grpc/json/encoder_test.cc
+  grpc/identity_api_test.cc
+  grpc/event_api_test.cc
 )
-add_executable(tests ${tests_src})
+
+if (WIN32)
+  list(APPEND TESTS_SRC
+    ncrypt/rsa_test.cc
+  )
+endif()
+
+add_executable(tests ${TESTS_SRC})
 
 target_proto_generate(
   TARGET tests
@@ -34,7 +67,7 @@ target_proto_generate(
 
 target_link_libraries(tests
   vereignlib
-  Threads::Threads
+  testutil
 )
 
 if (VEREIGN_ENABLE_BENCHMARKING)
diff --git a/cpp/tests/vereign/bytes/buffer_test.cc b/cpp/tests/vereign/bytes/buffer_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..710c3c184d2111aae21dc02c9b29a9939850842f
--- /dev/null
+++ b/cpp/tests/vereign/bytes/buffer_test.cc
@@ -0,0 +1,491 @@
+#include <vereign/bytes/buffer.hh>
+
+#include <vereign/bytes/errors.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("bytes::Buffer::Buffer", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.Size() == 0);
+    CHECK(buf.Cap() == 0);
+    CHECK(buf.FreeCap() == 0);
+  }
+
+  SECTION("buffer with predefined capacity") {
+    auto buf = bytes::Buffer{16};
+
+    CHECK(buf.Size() == 0);
+    CHECK(buf.Cap() == 16);
+    CHECK(buf.FreeCap() == 16);
+  }
+
+  SECTION("buffer from a view") {
+    auto buf = bytes::Buffer{bytes::View{"foo bar"}};
+
+    CHECK(buf.Size() == 7);
+    CHECK(buf.Cap() == 7);
+    CHECK(buf.FreeCap() == 0);
+    CHECK(buf.View().String() == "foo bar");
+  }
+
+  SECTION("move construct") {
+    auto old_buf = bytes::Buffer{bytes::View{"foo bar"}};
+    auto buf = std::move(old_buf);
+
+    CHECK(old_buf.Size() == 0);
+    CHECK(old_buf.Cap() == 0);
+    CHECK(old_buf.FreeCap() == 0);
+
+    CHECK(buf.Size() == 7);
+    CHECK(buf.Cap() == 7);
+    CHECK(buf.FreeCap() == 0);
+    CHECK(buf.View().String() == "foo bar");
+
+    auto empty = bytes::Buffer{};
+    auto new_empty = std::move(empty);
+
+    CHECK(empty.Size() == 0);
+    CHECK(empty.Cap() == 0);
+    CHECK(empty.FreeCap() == 0);
+
+    CHECK(new_empty.Size() == 0);
+    CHECK(new_empty.Cap() == 0);
+    CHECK(new_empty.FreeCap() == 0);
+  }
+
+  SECTION("move assign") {
+    auto foo = bytes::Buffer{bytes::View{"foo"}};
+    auto bar = bytes::Buffer{bytes::View{"bar"}};
+
+    foo = std::move(bar);
+
+    CHECK(foo.Size() == 3);
+    CHECK(foo.Cap() == 3);
+    CHECK(foo.FreeCap() == 0);
+    CHECK(foo.View().String() == "bar");
+
+    CHECK(bar.Size() == 3);
+    CHECK(bar.Cap() == 3);
+    CHECK(bar.FreeCap() == 0);
+    CHECK(bar.View().String() == "foo");
+
+    auto empty = bytes::Buffer{};
+    foo = std::move(empty);
+
+    CHECK(foo.Size() == 0);
+    CHECK(foo.Cap() == 0);
+    CHECK(foo.FreeCap() == 0);
+    CHECK(foo.View().String() == "");
+
+    CHECK(empty.Size() == 3);
+    CHECK(empty.Cap() == 3);
+    CHECK(empty.FreeCap() == 0);
+    CHECK(empty.View().String() == "bar");
+
+    empty = std::move(foo);
+
+    CHECK(empty.Size() == 0);
+    CHECK(empty.Cap() == 0);
+    CHECK(empty.FreeCap() == 0);
+    CHECK(empty.View().String() == "");
+
+    CHECK(foo.Size() == 3);
+    CHECK(foo.Cap() == 3);
+    CHECK(foo.FreeCap() == 0);
+    CHECK(foo.View().String() == "bar");
+  }
+}
+
+TEST_CASE("bytes::Buffer::begin", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.begin() == nullptr);
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo bar")};
+
+    CHECK(buf.begin() != nullptr);
+
+    std::string s;
+    for (auto& it : buf) {
+      s += it;
+    }
+
+    CHECK(s == "foo bar");
+
+    for (auto& it : buf) {
+      it = 'x';
+    }
+
+    CHECK(buf.View().String() == "xxxxxxx");
+  }
+}
+
+TEST_CASE("bytes::Buffer::end", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.end() == nullptr);
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo bar")};
+
+    CHECK(buf.end() != nullptr);
+
+    std::string s;
+    for (const auto& it : buf) {
+      s += it;
+    }
+
+    CHECK(s == "foo bar");
+
+    for (auto& it : buf) {
+      it = 'x';
+    }
+
+    CHECK(buf.View().String() == "xxxxxxx");
+  }
+
+  SECTION("append to the end") {
+    auto buf = bytes::Buffer{bytes::View("foo bar")};
+
+    buf.Reserve(4);
+    std::strncpy((char*)buf.end(), " baz", 4);
+    buf.IncSize(4);
+
+    CHECK(buf.Size() == 11);
+    CHECK(buf.View().String() == "foo bar baz");
+  }
+}
+
+TEST_CASE("bytes::Buffer::operator[]", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK_THROWS_AS(
+      buf[0],
+      bytes::IndexOutOfBounds
+    );
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+    buf.Reserve(16);
+
+    CHECK(buf.Cap() == 19);
+    CHECK(buf[0] == 'f');
+    CHECK(buf[1] == 'o');
+    CHECK(buf[2] == 'o');
+
+    CHECK_THROWS_AS(
+      buf[19],
+      bytes::IndexOutOfBounds
+    );
+  }
+
+  SECTION("modify buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    buf[0] = 'b';
+    buf[1] = 'a';
+    buf[2] = 'r';
+
+    CHECK_THROWS_AS(
+      buf[3] = 'x',
+      bytes::IndexOutOfBounds
+    );
+
+    CHECK(buf.View().String() == "bar");
+  }
+}
+
+TEST_CASE("bytes::Buffer::Size", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.Size() == 0);
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    CHECK(buf.Size() == 3);
+  }
+
+  SECTION("append to buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+    buf.Write(bytes::View(" bar"));
+
+    CHECK(buf.Size() == 7);
+  }
+}
+
+TEST_CASE("bytes::Buffer::Cap", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.Cap() == 0);
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    CHECK(buf.Cap() == 3);
+  }
+
+  SECTION("append to buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    // capacity is doubled
+    buf.Write(bytes::View("b"));
+
+    CHECK(buf.Cap() == 6);
+  }
+}
+
+TEST_CASE("bytes::Buffer::FreeCap", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    CHECK(buf.FreeCap() == 0);
+  }
+
+  SECTION("non empty buffer") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    CHECK(buf.Cap() == 3);
+    CHECK(buf.FreeCap() == 0);
+
+    buf.Reserve(1);
+
+    CHECK(buf.Cap() - buf.Size() == buf.FreeCap());
+  }
+}
+
+TEST_CASE("bytes::Buffer::Reserve", "[vereign/bytes]") {
+  SECTION("empty buffer") {
+    auto buf = bytes::Buffer{};
+
+    buf.Reserve(8);
+    CHECK(buf.Cap() == 8);
+    CHECK(buf.Size() == 0);
+  }
+
+  SECTION("when the required reservation is less than the old capacity*2, it reserves 2*capacity") {
+    {
+      auto buf = bytes::Buffer{bytes::View("foo")};
+
+      CHECK(buf.Cap() == 3);
+      CHECK(buf.Size() == 3);
+
+      // capacity is doubled
+      buf.Reserve(1);
+
+      CHECK(buf.Cap() == 6);
+      CHECK(buf.FreeCap() == 3);
+      CHECK(buf.Size() == 3);
+    }
+
+    {
+      auto buf = bytes::Buffer{bytes::View("foo")};
+
+      CHECK(buf.Cap() == 3);
+      CHECK(buf.Size() == 3);
+
+      // capacity is doubled
+      buf.Reserve(2);
+
+      CHECK(buf.Cap() == 6);
+      CHECK(buf.FreeCap() == 3);
+      CHECK(buf.Size() == 3);
+    }
+
+    {
+      auto buf = bytes::Buffer{bytes::View("foo")};
+
+      CHECK(buf.Cap() == 3);
+      CHECK(buf.Size() == 3);
+
+      // capacity is doubled
+      buf.Reserve(3);
+
+      CHECK(buf.Cap() == 6);
+      CHECK(buf.FreeCap() == 3);
+      CHECK(buf.Size() == 3);
+    }
+  }
+
+  SECTION("when the required reservation is greater than the old capacity*2, "
+          "it reserves capacity+required size") {
+    auto buf = bytes::Buffer{bytes::View("foo")};
+
+    CHECK(buf.Cap() == 3);
+    CHECK(buf.Size() == 3);
+
+    buf.Reserve(4);
+
+    CHECK(buf.Cap() == 7);
+    CHECK(buf.FreeCap() == 4);
+    CHECK(buf.Size() == 3);
+  }
+}
+
+TEST_CASE("bytes::Buffer::IncSize", "[vereign/bytes]") {
+  SECTION("when IncSize is called on empty buffer, it throws bytes::IncrementOutOfBounds") {
+    auto buf = bytes::Buffer{};
+
+    CHECK_THROWS_AS(
+      buf.IncSize(10),
+      bytes::IncrementOutOfBounds
+    );
+  }
+
+  SECTION("when IncSize is called with value bigger than the free cap, "
+          "it throws bytes::IncrementOutOfBounds") {
+
+    auto buf = bytes::Buffer{3};
+
+    std::strncpy((char*)buf.end(), "bar", 3);
+
+    CHECK_THROWS_AS(
+      buf.IncSize(4),
+      bytes::IncrementOutOfBounds
+    );
+  }
+
+  SECTION("when IncSize is called with zero, it does nothing") {
+    bytes::Buffer buf;
+    buf.IncSize(0);
+
+    CHECK(buf.Size() == 0);
+  }
+
+  SECTION("success") {
+    auto buf = bytes::Buffer{3};
+
+    std::strncpy((char*)buf.end(), "bar", 3);
+
+    buf.IncSize(3);
+
+    CHECK(buf.Size() == 3);
+    CHECK(buf.View().String() == "bar");
+  }
+}
+
+TEST_CASE("bytes::Buffer::WriteWithinCap", "[vereign/bytes]") {
+  SECTION("when WriteWithinCap is called on empty buffer, it does nothing") {
+    auto buf = bytes::Buffer{};
+    buf.WriteWithinCap(bytes::View("foo"));
+
+    CHECK(buf.Size() == 0);
+    CHECK(buf.Cap() == 0);
+  }
+
+  SECTION("when WriteWithinCap is called with empty source, it does nothing") {
+    auto buf = bytes::Buffer{3};
+    buf.WriteWithinCap(bytes::View());
+
+    CHECK(buf.Size() == 0);
+    CHECK(buf.Cap() == 3);
+  }
+
+  SECTION("when WriteWithinCap is called with source bigger than the free cap, "
+          "it copies only free cap amount of bytes") {
+
+    auto buf = bytes::Buffer{3};
+    buf.WriteWithinCap(bytes::View("foo bar"));
+
+    CHECK(buf.Size() == 3);
+    CHECK(buf.View().String() == "foo");
+  }
+
+  SECTION("when WriteWithinCap is called with source smaller than the free cap, "
+          "it copies the full source of bytes") {
+
+    auto buf = bytes::Buffer{7};
+    buf.WriteWithinCap(bytes::View("foo bar"));
+
+    CHECK(buf.Size() == 7);
+    CHECK(buf.View().String() == "foo bar");
+  }
+}
+
+TEST_CASE("bytes::Buffer::Write", "[vereign/bytes]") {
+  SECTION("when Write is called on empty buffer, "
+          "it expands the buffer and writes all the source bytes") {
+    auto buf = bytes::Buffer{};
+    buf.Write(bytes::View("foo"));
+
+    CHECK(buf.Size() == 3);
+    CHECK(buf.Cap() == 3);
+    CHECK(buf.View().String() == "foo");
+  }
+
+  SECTION("when Write is called with empty source, it does nothing") {
+    auto buf = bytes::Buffer{3};
+    buf.Write(bytes::View());
+
+    CHECK(buf.Size() == 0);
+    CHECK(buf.Cap() == 3);
+  }
+
+  SECTION("when Write is called with source bigger than the free cap, "
+          "it expands the buffer and writes all the source bytes") {
+
+    auto buf = bytes::Buffer{3};
+    buf.Write(bytes::View("foo bar"));
+
+    CHECK(buf.Size() == 7);
+    CHECK(buf.View().String() == "foo bar");
+  }
+
+  SECTION("when Write is called with source smaller than the free cap, "
+          "it copies the full source of bytes") {
+
+    auto buf = bytes::Buffer{7};
+    buf.Write(bytes::View("foo bar"));
+
+    CHECK(buf.Size() == 7);
+    CHECK(buf.View().String() == "foo bar");
+  }
+}
+
+TEST_CASE("bytes::Buffer::View", "[vereign/bytes]") {
+  SECTION("when the buffer is empty, it returns empty view") {
+    auto buf = bytes::Buffer{};
+
+    auto v = buf.View();
+    CHECK(v.Size() == 0);
+  }
+
+  SECTION("when the buffer is not empty, it returns a view of the buffer") {
+    auto buf = bytes::Buffer{bytes::View("123")};
+
+    auto v = buf.View();
+    CHECK(v.Size() == 3);
+    CHECK(v.String() == "123");
+
+    v = buf.View(1);
+    CHECK(v.Size() == 2);
+    CHECK(v.String() == "23");
+
+    v = buf.View(2);
+    CHECK(v.Size() == 1);
+    CHECK(v.String() == "3");
+
+    v = buf.View(3);
+    CHECK(v.Size() == 0);
+    CHECK(v.String() == "");
+
+    v = buf.View(4);
+    CHECK(v.Size() == 0);
+    CHECK(v.String() == "");
+  }
+}
diff --git a/cpp/tests/vereign/bytes/view_test.cc b/cpp/tests/vereign/bytes/view_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1f29ab454d9d40225c45c89adddf1dce7f06036d
--- /dev/null
+++ b/cpp/tests/vereign/bytes/view_test.cc
@@ -0,0 +1,236 @@
+#include <vereign/bytes/view.hh>
+
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/errors.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("bytes::View::View", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.Size() == 0);
+  }
+
+  SECTION("view from a pointer and a size") {
+    const uint8_t* data = (uint8_t*)"foo";
+    auto v = bytes::View(data, 3);
+
+    CHECK(v.Size() == 3);
+    CHECK(v.String() == "foo");
+  }
+
+  SECTION("view from a void pointer and a size") {
+    const void* data = (void*)"foo";
+    auto v = bytes::View(data, 3);
+
+    CHECK(v.Size() == 3);
+    CHECK(v.String() == "foo");
+  }
+
+  SECTION("view from a string") {
+    std::string data = "foo";
+    auto v = bytes::View(data);
+
+    CHECK(v.Size() == 3);
+    CHECK(v.String() == "foo");
+  }
+
+  SECTION("view from a wide string") {
+    std::wstring data = L"foo";
+    auto v = bytes::View(data);
+
+    CHECK(v.Size() == sizeof(wchar_t)*3);
+    CHECK(v.WideString() == L"foo");
+  }
+}
+
+TEST_CASE("bytes::View::Slice", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.Slice(1).Size() == 0);
+  }
+
+  SECTION("non-empty view") {
+    std::string s = "123";
+    auto v = bytes::View(s);
+
+    CHECK(v.Slice(0).Size() == 3);
+    CHECK(v.Slice(0).String() == "123");
+
+    CHECK(v.Slice(1).Size() == 2);
+    CHECK(v.Slice(1).String() == "23");
+
+    CHECK(v.Slice(1, 2).Size() == 1);
+    CHECK(v.Slice(1, 2).String() == "2");
+
+    CHECK(v.Slice(1, 3).Size() == 2);
+    CHECK(v.Slice(1, 3).String() == "23");
+
+    CHECK(v.Slice(1, 4).Size() == 2);
+    CHECK(v.Slice(1, 4).String() == "23");
+  }
+
+  SECTION("start/end out of bounds") {
+    std::string s = "123";
+    auto v = bytes::View(s);
+
+    CHECK(v.Slice(0).Size() == 3);
+    CHECK(v.Slice(0).String() == "123");
+
+    CHECK(v.Slice(3).Size() == 0);
+    CHECK(v.Slice(3).String() == "");
+
+    CHECK(v.Slice(4).Size() == 0);
+    CHECK(v.Slice(4).String() == "");
+
+    CHECK(v.Slice(1, 4).Size() == 2);
+    CHECK(v.Slice(1, 4).String() == "23");
+  }
+}
+
+TEST_CASE("bytes::View::Data", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.Data() == nullptr);
+  }
+
+  SECTION("non-empty view") {
+    std::string s = "123";
+    auto v = bytes::View(s);
+
+    CHECK(v.Data()[0] == '1');
+    CHECK(v.Data()[1] == '2');
+  }
+}
+
+TEST_CASE("bytes::View::CharData", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.CharData() == nullptr);
+  }
+
+  SECTION("non-empty view") {
+    std::string s = "123";
+    auto v = bytes::View(s);
+
+    const char* data = v.CharData();
+
+    CHECK(data[0] == '1');
+    CHECK(data[1] == '2');
+  }
+}
+
+TEST_CASE("bytes::View::WideCharData", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.WideCharData() == nullptr);
+  }
+
+  SECTION("non-empty view") {
+    std::wstring s = L"123";
+    auto v = bytes::View(s);
+
+    const wchar_t* data = v.WideCharData();
+
+    CHECK(data[0] == wchar_t('1'));
+    CHECK(data[1] == wchar_t('2'));
+  }
+}
+
+TEST_CASE("bytes::View::String", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.String() == "");
+  }
+
+  SECTION("non-empty view") {
+    std::string s = "123";
+    auto v = bytes::View(s);
+
+    CHECK(v.String() == "123");
+  }
+}
+
+TEST_CASE("bytes::View::WideString", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK(v.WideString() == L"");
+  }
+
+  SECTION("non-empty view") {
+    std::wstring s = L"123";
+    auto v = bytes::View(s);
+
+    CHECK(v.WideString() == L"123");
+  }
+}
+
+TEST_CASE("bytes::View::operator==", "[vereign/bytes]") {
+  auto s1 = std::string{"foo"};
+  auto s2 = std::string{"foo"};
+
+  CHECK(bytes::View(s1) == bytes::View(s2));
+
+  s2 = "bar";
+  CHECK((bytes::View(s1) == bytes::View(s2)) == false);
+
+  s2 = "";
+  CHECK((bytes::View(s1) == bytes::View(s2)) == false);
+
+  auto b1 = crypto::Rand(16);
+  auto b2 = bytes::Buffer{b1.View()};
+
+  CHECK(bytes::View(b1.begin(), 16) == bytes::View(b2.begin(), 16));
+}
+
+TEST_CASE("bytes::View::operator!=", "[vereign/bytes]") {
+  auto s1 = std::string{"foo"};
+  auto s2 = std::string{"foo"};
+
+  CHECK((bytes::View(s1) != bytes::View(s2)) == false);
+
+  s2 = "bar";
+  CHECK(bytes::View(s1) != bytes::View(s2));
+
+  s2 = "";
+  CHECK(bytes::View(s1) != bytes::View(s2));
+
+  auto b1 = crypto::Rand(16);
+  auto b2 = bytes::Buffer{b1.View()};
+
+  CHECK((bytes::View(b1.begin(), 16) != bytes::View(b2.begin(), 16)) == false);
+}
+
+TEST_CASE("bytes::View::operator[]", "[vereign/bytes]") {
+  SECTION("empty view") {
+    auto v = bytes::View{};
+
+    CHECK_THROWS_AS(
+      v[0],
+      bytes::IndexOutOfBounds
+    );
+  }
+
+  SECTION("non-empty view") {
+    auto s = std::string{"123"};
+    auto v = bytes::View(s);
+
+    CHECK(v[0] == '1');
+    CHECK(v[1] == '2');
+    CHECK(v[2] == '3');
+
+    CHECK_THROWS_AS(
+      v[3],
+      bytes::IndexOutOfBounds
+    );
+  }
+}
diff --git a/cpp/tests/vereign/container/bounded_queue_test.cc b/cpp/tests/vereign/container/bounded_queue_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c4aff6ddeb20233180c8e05bdb26b45b94e692b8
--- /dev/null
+++ b/cpp/tests/vereign/container/bounded_queue_test.cc
@@ -0,0 +1,81 @@
+#include <vereign/container/bounded_queue.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("container::BoundedQueue", "[vereign/container]") {
+  // a test type, that can confirm that the queue properly calls its destructor and there
+  // are no memory leaks
+  class Value {
+  public:
+    Value(int v) : v_{std::make_unique<int>(v)} {}
+
+    auto Get() -> int {
+      return *v_;
+    }
+  private:
+    std::unique_ptr<int> v_;
+  };
+
+  auto q = container::BoundedQueue<Value>{3};
+
+  CHECK(q.Size() == 0);
+  CHECK(q.IsEmpty() == true);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(1);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(2);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(3);
+  CHECK(q.Size() == 3);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == true);
+
+  auto v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 1);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 2);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(4);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 3);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 4);
+  CHECK(q.Size() == 0);
+  CHECK(q.IsEmpty() == true);
+  CHECK(q.IsFull() == false);
+
+  // leave something in the queue, for memory sanitizer check of the queue destruction
+  q.PushBack(5);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+}
+
diff --git a/cpp/tests/vereign/core/time_test.cc b/cpp/tests/vereign/core/time_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..09637b9fb892cc8a86a27281587d003326c15642
--- /dev/null
+++ b/cpp/tests/vereign/core/time_test.cc
@@ -0,0 +1,42 @@
+#include <vereign/core/time.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("time::MakePosixTime", "[vereign/time][vereign/core]") {
+  auto ptime = time::MakePosixTime(2020, 1, 1, 0, 1, 1);
+
+  CHECK(ptime.date().year() == 2020);
+  CHECK(ptime.date().month() == boost::gregorian::Jan);
+  CHECK(ptime.date().day() == 1);
+  CHECK(ptime.time_of_day().hours() == 0);
+  CHECK(ptime.time_of_day().minutes() == 1);
+  CHECK(ptime.time_of_day().seconds() == 1);
+
+  CHECK(time::PosixTimeToTime(ptime) == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == time::PosixTimeToTime(ptime));
+
+  ptime = time::MakePosixTime(2020, 1, 1, 24, 60, 60);
+  CHECK(boost::posix_time::to_iso_string(ptime) == "20200102T010100");
+}
+
+TEST_CASE("time::MakeTimeUTC", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTC(2020, 1, 1, 0, 1, 1);
+
+  CHECK(timestamp == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp);
+}
+
+TEST_CASE("time::MakeTimeUTCFromString", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTCFromString("2020-1-1 0:1:1");
+
+  CHECK(timestamp == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp);
+}
+
+TEST_CASE("time::MakeTimeUTCFromISO", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTCFromISO("20200101T000101");
+
+  CHECK(timestamp == 1577836861);
+}
diff --git a/cpp/tests/vereign/crypto/aes_test.cc b/cpp/tests/vereign/crypto/aes_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..b9f4c41bd0334cd29d4a75e839572bf1f4ea8ba5
--- /dev/null
+++ b/cpp/tests/vereign/crypto/aes_test.cc
@@ -0,0 +1,25 @@
+#include <vereign/crypto/aes.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+
+#include <catch2/catch.hpp>
+
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("crypto::aes::GCM256Encrypt", "[vereign/crypto/aes][vereign/crypto]") {
+  const std::string data{"foo bar"};
+  auto key = crypto::Rand(32);
+
+  bytes::Buffer iv;
+  bytes::Buffer tag;
+  bytes::Buffer encrypted;
+
+  crypto::aes::GCM256Encrypt(bytes::View(data), key.View(), iv, tag, encrypted);
+
+  bytes::Buffer decrypted;
+  crypto::aes::GCM256Decrypt(encrypted.View(), key.View(), iv.View(), tag.View(), decrypted);
+
+  CHECK(decrypted.View().String() == data);
+}
diff --git a/cpp/tests/vereign/crypto/cert_test.cc b/cpp/tests/vereign/crypto/cert_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..da6bec076b79072fff0f925e9f20965b3ec84927
--- /dev/null
+++ b/cpp/tests/vereign/crypto/cert_test.cc
@@ -0,0 +1,84 @@
+#include <vereign/crypto/cert.hh>
+
+#include <vereign/core/time.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/bio.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/fs/util.hh>
+#include <testutil/golden.hh>
+
+#include <catch2/catch.hpp>
+
+#include <iostream>
+#include <fstream>
+
+using namespace vereign;
+
+namespace {
+   const std::string testDataPath = fs::path::Join("..", "tests", "vereign", "crypto", "test_data");
+}
+
+TEST_CASE("crypto::cert::CreateSelfSignedCert", "[vereign/crypto/cert][vereign/crypto]") {
+  auto key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View()
+  );
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Email = "ca@example.com";
+  cert_data.Subject.CommonName = "ca-cn.example.com";
+  cert_data.Subject.Country = "BG";
+  cert_data.Subject.State = "Sofia";
+  cert_data.Subject.Locality = "Sofia";
+  cert_data.Subject.Organization = "Vereign AG";
+  cert_data.Subject.OrganizationUnit = "CA";
+  cert_data.Url = "ca.example.com";
+  cert_data.Validity.ValidYears = 5;
+  cert_data.SerialNumber = 42;
+  cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-01 00:00:00");
+  cert_data.IsCA = true;
+
+  auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get());
+
+  std::stringstream ss;
+  crypto::cert::PrintCert(ss, cert.get());
+  testutil::golden::Assert(fs::path::Join(testDataPath, "ca_cert.txt"), bytes::View(ss.str()));
+
+  auto pem = crypto::cert::ExportCertToPEM(cert.get());
+  testutil::golden::Assert(fs::path::Join(testDataPath, "ca.crt"), crypto::bio::View(pem.get()));
+}
+
+TEST_CASE("crypto::cert::CreateCert", "[vereign/crypto/cert][vereign/crypto]") {
+  auto ca_key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View()
+  );
+
+  auto ca_cert = crypto::cert::ImportCertFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca.crt")).View()
+  );
+
+  auto key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "leaf_key.pem")).View()
+  );
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Email = "test@example.com";
+  cert_data.Subject.CommonName = "example.com";
+  cert_data.Subject.Country = "BG";
+  cert_data.Subject.State = "Sofia";
+  cert_data.Subject.Locality = "Sofia";
+  cert_data.Subject.Organization = "Vereign AG";
+  cert_data.Subject.OrganizationUnit = "Engineering";
+  cert_data.Url = "api.example.com";
+  cert_data.Validity.ValidYears = 5;
+  cert_data.SerialNumber = 73;
+  cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-02 00:00:00");
+
+  auto cert = crypto::cert::CreateCert(cert_data, ca_cert.get(), ca_key.get(), key.get());
+
+  std::stringstream ss;
+  crypto::cert::PrintCert(ss, cert.get());
+
+  testutil::golden::Assert(fs::path::Join(testDataPath, "leaf_cert.txt"), bytes::View(ss.str()));
+}
diff --git a/cpp/tests/vereign/crypto/digest_test.cc b/cpp/tests/vereign/crypto/digest_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..cea92371b19c2290900f0047a587a54522ad537b
--- /dev/null
+++ b/cpp/tests/vereign/crypto/digest_test.cc
@@ -0,0 +1,22 @@
+#include <vereign/crypto/digest.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/encoding/hex.hh>
+
+#include <catch2/catch.hpp>
+
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("crypto::digest::sha1", "[vereign/crypto/digest][vereign/crypto]") {
+  const std::string data{"foo bar"};
+
+  bytes::Buffer hash;
+  crypto::digest::sha1(bytes::View(data), hash);
+
+  bytes::Buffer encoded;
+  encoding::hex::Encode(hash.View(), encoded);
+
+  CHECK(encoded.View().String() == "3773dea65156909838fa6c22825cafe090ff8030");
+}
diff --git a/cpp/tests/vereign/crypto/errors_test.cc b/cpp/tests/vereign/crypto/errors_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1b9f32a3523e27b14a9985047ccb0caca2f1dd46
--- /dev/null
+++ b/cpp/tests/vereign/crypto/errors_test.cc
@@ -0,0 +1,24 @@
+#include <vereign/crypto/errors.hh>
+#include <openssl/evp.h>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("crypto::OpenSSLError", "[vereign/crypto]") {
+  SECTION("it must return error with message that included the last OpenSSL error") {
+    EVP_PKEY_CTX* ctx = nullptr;
+    auto r = EVP_PKEY_encrypt_init(ctx);
+    CHECK(r == 0);
+
+    auto err = crypto::OpenSSLError{"pkey encrypt failed"};
+    CHECK(std::string(err.what()) == "pkey encrypt failed: OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE");
+  }
+
+  SECTION("when there was no OpenSSL error,"
+          " it must create an error with `no openssl error in the error queue`") {
+    auto err = crypto::OpenSSLError{"test error"};
+
+    CHECK(std::string(err.what()) == "test error: no openssl error in the error queue");
+  }
+}
diff --git a/cpp/tests/vereign/crypto/rsa_test.cc b/cpp/tests/vereign/crypto/rsa_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f9f27acb9b3a20cf0621dd886144a6a7f2b7306b
--- /dev/null
+++ b/cpp/tests/vereign/crypto/rsa_test.cc
@@ -0,0 +1,100 @@
+#include <vereign/crypto/rsa.hh>
+
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/bio.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/bytes/buffer.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("crypto::rsa PublicKeyEncrypt/PrivateKeyDecrypt", "[vereign/crypto/rsa][vereign/crypto]") {
+  SECTION("small input") {
+    auto key = crypto::rsa::GenerateKey(2048);
+
+    const std::string input{"foo bar"};
+    bytes::Buffer encrypted;
+
+    crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted);
+
+    bytes::Buffer decrypted;
+    crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+
+    CHECK(decrypted.View() == bytes::View(input));
+  }
+
+  SECTION("zero input") {
+    auto key = crypto::rsa::GenerateKey(2048);
+
+    const std::string input;
+    bytes::Buffer encrypted;
+
+    crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted);
+
+    bytes::Buffer decrypted;
+    crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+
+    CHECK(decrypted.View() == bytes::View(input));
+  }
+
+  SECTION("max size input") {
+    auto key = crypto::rsa::GenerateKey(2048);
+
+    auto input = crypto::Rand(214);
+    bytes::Buffer encrypted;
+
+    crypto::rsa::PublicKeyEncrypt(key.get(), input.View(), encrypted);
+
+    bytes::Buffer decrypted;
+    crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+
+    CHECK(decrypted.View() == input.View());
+  }
+
+  SECTION("invalid big input") {
+    auto key = crypto::rsa::GenerateKey(2048);
+
+    auto input = crypto::Rand(215);
+    bytes::Buffer encrypted;
+
+    CHECK_THROWS_WITH(
+      crypto::rsa::PublicKeyEncrypt(key.get(), input.View(), encrypted),
+      "encrypting failed: DATA_TOO_LARGE_FOR_KEY_SIZE"
+    );
+
+    CHECK(encrypted.Size() == 0);
+  }
+}
+
+TEST_CASE("crypto::rsa::ExportPublicKeyToPEM", "[vereign/crypto/rsa][vereign/crypto]") {
+  const std::string input{"foo bar"};
+  auto key = crypto::rsa::GenerateKey(2048);
+
+  auto bio = crypto::rsa::ExportPublicKeyToPEM(key.get());
+  auto imported_key = crypto::rsa::ImportPublicKeyFromPEM(crypto::bio::View(bio.get()));
+
+  bytes::Buffer encrypted;
+  crypto::rsa::PublicKeyEncrypt(imported_key.get(), bytes::View(input), encrypted);
+
+  bytes::Buffer decrypted;
+  crypto::rsa::PrivateKeyDecrypt(key.get(), encrypted.View(), decrypted);
+
+  CHECK(decrypted.View() == bytes::View(input));
+}
+
+TEST_CASE("crypto::rsa::ExportPrivateKeyToPEM", "[vereign/crypto/rsa][vereign/crypto]") {
+  const std::string input{"foo bar"};
+  auto key = crypto::rsa::GenerateKey(2048);
+
+  auto bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+  auto imported_key = crypto::rsa::ImportPrivateKeyFromPEM(crypto::bio::View(bio.get()));
+
+  bytes::Buffer encrypted;
+  crypto::rsa::PublicKeyEncrypt(key.get(), bytes::View(input), encrypted);
+
+  bytes::Buffer decrypted;
+  crypto::rsa::PrivateKeyDecrypt(imported_key.get(), encrypted.View(), decrypted);
+
+  CHECK(decrypted.View() == bytes::View(input));
+}
diff --git a/cpp/tests/vereign/crypto/test_data/.gitattributes b/cpp/tests/vereign/crypto/test_data/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..fa1385d99a319b43c06f5309d1aae9fdd3adea46
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/cpp/tests/vereign/crypto/test_data/ca.crt b/cpp/tests/vereign/crypto/test_data/ca.crt
new file mode 100644
index 0000000000000000000000000000000000000000..affc0044449d58fa031c69a13afa931baf587d3e
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca.crt
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEvDCCA6SgAwIBAgIBKjANBgkqhkiG9w0BAQsFADCBijEaMBgGA1UEAwwRY2Et
+Y24uZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwG
+A1UECAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0w
+GwYJKoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbTAeFw0yMDA3MDEwMDAwMDBaFw0y
+NTA2MzAwMDAwMDBaMIGKMRowGAYDVQQDDBFjYS1jbi5leGFtcGxlLmNvbTELMAkG
+A1UEBhMCQkcxDjAMBgNVBAcMBVNvZmlhMQ4wDAYDVQQIDAVTb2ZpYTETMBEGA1UE
+CgwKVmVyZWlnbiBBRzELMAkGA1UECwwCQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4
+YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxINphs0i
+1HrDcvsG1mUnNrcnwhyitN5ZX8FDgtk+r5RXNk+zRvmlDz+kKq/UNXgIH5v1LbRi
+Vcmh9njnLqh4uPW2Khkxna3yjnWF3NqV780uOCkUEZp0CC2B2RRXCjmxF3wBtggb
+FjBSoh5nhYUtJZkeDgY+AaFlfBKHWWQpzLJWxdHFW/Ej33MubilPTKOaP2Nh9U9x
+bxiwAHS3ijnhpW2Xh6uwf1XMQVwwsbPtEd81BBU86KIeQzOQrYkUEl81nqOzt9Ct
+67fUJs0yPPTtoEekWAYCdFD0hrVLxz1ilQtYF+shXL/6GyrKz0putQhO2wOqINXV
+rHUv1SgEMwD/TwIDAQABo4IBKTCCASUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
+Af8EBAMCAYYwKQYDVR0RBCIwIIEOY2FAZXhhbXBsZS5jb22CDmNhLmV4YW1wbGUu
+Y29tMB0GA1UdDgQWBBR+ivGqlvajtAUhWQthghHP6zZrdjCBtwYDVR0jBIGvMIGs
+gBR+ivGqlvajtAUhWQthghHP6zZrdqGBkKSBjTCBijEaMBgGA1UEAwwRY2EtY24u
+ZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwGA1UE
+CAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0wGwYJ
+KoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbYIBKjANBgkqhkiG9w0BAQsFAAOCAQEA
+wHKy0Gmj5Gs+DglsqzOfmUeqy8EsmaS6ufxV084umpnNmaQrdBSQFH0P8sQJROpH
+1bgibRLQFwQLKvAC6dxCvnH6njW0lCZ/53OkrNyky53eZ6I6DHrT0kjkC9KqShEf
++MhHBniVCX2w8HxFgZgMKGiwL+D8qo5QzHmR+AzXAZGlKKZReKuPUehfO02cVWPH
+uz/dSl2gd7xdAz3lwayVPiNNCu4r0/gwvEiHuwCvjIQPIxsUBlm5BwB0ga0OlMJu
+nGVv3dNkS2KjIEBms4jvXxk9w51OQqrcVUhuj8qHenTtM6ho5QTtDi2z/NbM0oW1
+zAmLUoPW97IgXtp4ghOQ7w==
+-----END CERTIFICATE-----
diff --git a/cpp/tests/vereign/crypto/test_data/ca_cert.txt b/cpp/tests/vereign/crypto/test_data/ca_cert.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9ed51e64c82cf5d0d352d31b632f74c18daa62e8
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca_cert.txt
@@ -0,0 +1,63 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 42 (0x2a)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Validity
+            Not Before: Jul  1 00:00:00 2020 GMT
+            Not After : Jun 30 00:00:00 2025 GMT
+        Subject: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:c4:83:69:86:cd:22:d4:7a:c3:72:fb:06:d6:65:
+                    27:36:b7:27:c2:1c:a2:b4:de:59:5f:c1:43:82:d9:
+                    3e:af:94:57:36:4f:b3:46:f9:a5:0f:3f:a4:2a:af:
+                    d4:35:78:08:1f:9b:f5:2d:b4:62:55:c9:a1:f6:78:
+                    e7:2e:a8:78:b8:f5:b6:2a:19:31:9d:ad:f2:8e:75:
+                    85:dc:da:95:ef:cd:2e:38:29:14:11:9a:74:08:2d:
+                    81:d9:14:57:0a:39:b1:17:7c:01:b6:08:1b:16:30:
+                    52:a2:1e:67:85:85:2d:25:99:1e:0e:06:3e:01:a1:
+                    65:7c:12:87:59:64:29:cc:b2:56:c5:d1:c5:5b:f1:
+                    23:df:73:2e:6e:29:4f:4c:a3:9a:3f:63:61:f5:4f:
+                    71:6f:18:b0:00:74:b7:8a:39:e1:a5:6d:97:87:ab:
+                    b0:7f:55:cc:41:5c:30:b1:b3:ed:11:df:35:04:15:
+                    3c:e8:a2:1e:43:33:90:ad:89:14:12:5f:35:9e:a3:
+                    b3:b7:d0:ad:eb:b7:d4:26:cd:32:3c:f4:ed:a0:47:
+                    a4:58:06:02:74:50:f4:86:b5:4b:c7:3d:62:95:0b:
+                    58:17:eb:21:5c:bf:fa:1b:2a:ca:cf:4a:6e:b5:08:
+                    4e:db:03:aa:20:d5:d5:ac:75:2f:d5:28:04:33:00:
+                    ff:4f
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: critical
+                CA:TRUE
+            X509v3 Key Usage: critical
+                Digital Signature, Certificate Sign, CRL Sign
+            X509v3 Subject Alternative Name: 
+                email:ca@example.com, DNS:ca.example.com
+            X509v3 Subject Key Identifier: 
+                7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+            X509v3 Authority Key Identifier: 
+                keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+                DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com
+                serial:2A
+
+    Signature Algorithm: sha256WithRSAEncryption
+         c0:72:b2:d0:69:a3:e4:6b:3e:0e:09:6c:ab:33:9f:99:47:aa:
+         cb:c1:2c:99:a4:ba:b9:fc:55:d3:ce:2e:9a:99:cd:99:a4:2b:
+         74:14:90:14:7d:0f:f2:c4:09:44:ea:47:d5:b8:22:6d:12:d0:
+         17:04:0b:2a:f0:02:e9:dc:42:be:71:fa:9e:35:b4:94:26:7f:
+         e7:73:a4:ac:dc:a4:cb:9d:de:67:a2:3a:0c:7a:d3:d2:48:e4:
+         0b:d2:aa:4a:11:1f:f8:c8:47:06:78:95:09:7d:b0:f0:7c:45:
+         81:98:0c:28:68:b0:2f:e0:fc:aa:8e:50:cc:79:91:f8:0c:d7:
+         01:91:a5:28:a6:51:78:ab:8f:51:e8:5f:3b:4d:9c:55:63:c7:
+         bb:3f:dd:4a:5d:a0:77:bc:5d:03:3d:e5:c1:ac:95:3e:23:4d:
+         0a:ee:2b:d3:f8:30:bc:48:87:bb:00:af:8c:84:0f:23:1b:14:
+         06:59:b9:07:00:74:81:ad:0e:94:c2:6e:9c:65:6f:dd:d3:64:
+         4b:62:a3:20:40:66:b3:88:ef:5f:19:3d:c3:9d:4e:42:aa:dc:
+         55:48:6e:8f:ca:87:7a:74:ed:33:a8:68:e5:04:ed:0e:2d:b3:
+         fc:d6:cc:d2:85:b5:cc:09:8b:52:83:d6:f7:b2:20:5e:da:78:
+         82:13:90:ef
diff --git a/cpp/tests/vereign/crypto/test_data/ca_key.pem b/cpp/tests/vereign/crypto/test_data/ca_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..2c73e6ead4478b7c4d32a2ad3b3031c7eff054e0
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca_key.pem
@@ -0,0 +1,29 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDEg2mGzSLUesNy
++wbWZSc2tyfCHKK03llfwUOC2T6vlFc2T7NG+aUPP6Qqr9Q1eAgfm/UttGJVyaH2
+eOcuqHi49bYqGTGdrfKOdYXc2pXvzS44KRQRmnQILYHZFFcKObEXfAG2CBsWMFKi
+HmeFhS0lmR4OBj4BoWV8EodZZCnMslbF0cVb8SPfcy5uKU9Mo5o/Y2H1T3FvGLAA
+dLeKOeGlbZeHq7B/VcxBXDCxs+0R3zUEFTzooh5DM5CtiRQSXzWeo7O30K3rt9Qm
+zTI89O2gR6RYBgJ0UPSGtUvHPWKVC1gX6yFcv/obKsrPSm61CE7bA6og1dWsdS/V
+KAQzAP9PAgMBAAECggEAOBj9hGZF9ZsP9WBUrNpWbeeuGVscVX6Ny+h7UbybiPrT
+RKVO28mDRY2Y2zizXwojY4adYI04bx3uttH/yNF+GOrHtE/Z2pXgAqvo6Uma+Mg9
+U/niCT5qtjTrduE4Eaqzc61KPcElnllwFWiRi8ufPjbuHfnJNKTyuA8ABVQzHIQi
+Z+Rhw/IASS2aMuD1A4XicneZ43Vo83mz7xfiebVW7WRyIIcxlWruHQM+BYCTR+y5
+vGdq+P0e4ZjAdwudxB6MfmF5lO16M8CiH5SAJghmBkbEpwdkZuMX6YQLGnw2+C6m
+NHRRb0WSGX12PEJcxA4mvM4L2+m7ihZTSJg5fYSLAQKBgQDmV77TXc6xAfq1O3es
+AV69a5rNgtJ3I7wnv42ZLpQLVSVQ1eqRzhaMIItbX3hwb9STWagIuuGbTG590k00
+lsHRD0/Z4mapyk43b9Zs90ug1L0ojqrVfmjb5uJDzj4fbHO1QtJ2Dt2llMveGCaE
+j5LYz+AKdBjZdxxU6NDws8W1AQKBgQDaZwQjMAx7usp+prVVe33QchTxeziaFh+L
+q53zimoCsXjTSz040dkdk+yhj0DOT3z4Of41+PkWmbLoZh1ar9yFSQPC3fVvz4Sx
+mY5gCIy1D9a3SSPYy6+V6PiCSkYB3JsGONKARl1oxNswewGcBVSxEYMH0INGccoc
+Aqv0WYokTwKBgE/QtdHd3oIdUnc8HPKgIuj2AVUW8MDRxB/t0y3yIuBuZ5jEFxzg
+0ZVKrZ9CzKQBTCKm3X/w6b37VQoKNjBz0MMVUDEKr1JERKgW+7EUW8NmFAmarf1+
+aO1R6rNeIs6WsN1tQyofapQ2pBHYQniKm3K/TT2yDu5DxLBzX/sX//kBAoGAfNsL
+uHoNiOZ+QO1ZGSdiA7pUUsDY2O3w1s4jnEjy1dtTcae/ti3xa0FXs/zLVaaHkPjG
+ZWCSNeNkLgvCmEeVT/Uv20U8vNDr0QQyKqB4oEIPw/RiM9wt4wuulf2UDBhr36C4
+ZnDTkcOIcyy+/MznX9rdB6iig7Elk+HLIAPYP20CgYAL+/AizILmdI7lBvderLfw
+2oslABhYJCjaFX5XWTvOJpPdZPZSqZUgSvzgBM57f1gtRmn6NS1oM6qagoYlOpF4
+rjIcuz86j8tFTp45TjMz1Sf3afyi+iiFhdjTeDTdyjcWK0sBghyljc4jq5scQsYH
+SUOdYY70Kenpzx6iOaWe/g==
+-----END PRIVATE KEY-----
+
diff --git a/cpp/tests/vereign/crypto/test_data/leaf_cert.txt b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fb718f515d1240b1ea27e79859552b2d87d7c06e
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt
@@ -0,0 +1,65 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 73 (0x49)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Validity
+            Not Before: Jul  2 00:00:00 2020 GMT
+            Not After : Jul  1 00:00:00 2025 GMT
+        Subject: CN=example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=Engineering/emailAddress=test@example.com
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:be:ab:51:25:32:3c:75:bb:49:25:1b:b9:9f:6e:
+                    af:ff:38:07:f0:2c:49:e7:3b:c8:3a:19:02:f1:aa:
+                    96:8d:9d:bd:37:b0:97:2a:11:05:71:65:94:ee:32:
+                    6b:4b:d7:94:43:a1:8c:bb:ed:fe:94:d6:93:f8:7b:
+                    ca:48:7d:fc:99:00:88:d5:d1:73:55:31:ca:eb:69:
+                    5f:0d:1b:6a:24:39:db:b9:f1:7a:34:c7:1f:8e:a4:
+                    12:ab:1d:e8:29:b6:aa:05:d3:fb:7d:bb:7e:2b:db:
+                    c8:18:15:ba:8e:6d:b4:df:98:3c:34:6c:50:07:e9:
+                    7a:7a:57:99:8b:cf:6d:d7:c3:e1:c5:90:ae:d3:94:
+                    dc:b9:fc:90:20:ec:63:24:e5:8b:42:d3:bb:4d:e6:
+                    e2:f0:b6:48:a9:c0:27:2e:89:bc:45:db:b6:25:be:
+                    7d:55:40:6d:72:e4:5b:49:81:d4:aa:e0:3a:33:93:
+                    f9:4b:b2:82:b4:45:e1:5e:0c:14:b0:6e:78:f9:1a:
+                    98:72:8c:ef:c5:10:ab:7f:9d:e7:9b:68:17:30:21:
+                    f0:95:bf:ab:c6:45:bb:3c:cf:f1:96:1f:8d:22:88:
+                    72:b3:52:ff:20:c0:40:5b:11:0d:62:24:ec:5c:53:
+                    81:fa:ca:ec:a5:00:b8:7c:1e:e2:a8:b7:01:42:5e:
+                    1d:01
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: critical
+                CA:FALSE
+            X509v3 Key Usage: critical
+                Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
+            X509v3 Extended Key Usage: 
+                E-mail Protection
+            X509v3 Subject Alternative Name: 
+                email:test@example.com, DNS:api.example.com
+            X509v3 Subject Key Identifier: 
+                A5:BD:8A:35:84:98:B6:6C:13:51:A2:4A:5D:C9:51:C6:61:7E:5D:AE
+            X509v3 Authority Key Identifier: 
+                keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+                DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com
+                serial:2A
+
+    Signature Algorithm: sha256WithRSAEncryption
+         8d:90:e7:a7:a6:57:ec:b3:db:38:37:87:bc:1e:0b:e2:bd:c4:
+         88:e3:95:f0:35:01:a7:fe:51:92:b1:4c:37:d7:21:cf:80:77:
+         51:98:62:61:2c:99:58:60:89:01:35:3e:56:8c:dd:07:21:ad:
+         a1:e3:e5:02:3c:e5:cf:c8:ee:46:11:07:16:f5:37:8c:75:22:
+         e0:af:85:e2:85:31:21:a2:f3:e4:b5:1b:de:ef:31:86:88:84:
+         18:11:bc:67:5a:a7:0b:d1:16:fc:5c:12:51:8d:1c:26:d5:45:
+         f8:55:28:7f:85:a2:c7:a7:e4:bc:6b:98:b4:c4:d9:3e:f6:72:
+         a8:f9:24:f5:9b:38:bf:aa:76:9c:68:c8:67:4f:fa:79:9c:7c:
+         52:f2:45:6c:05:26:4f:64:4c:ea:52:d0:c2:75:45:21:e5:8e:
+         d7:60:0a:b2:6c:d2:f8:1c:01:9b:b1:72:1d:4e:95:79:18:34:
+         b6:1c:bc:ea:5d:12:1f:33:90:63:dc:01:b9:9b:33:dd:62:31:
+         69:c8:b2:1d:ce:3d:38:57:60:c5:75:7d:84:95:3d:db:36:0a:
+         a3:77:7c:67:b6:e3:7a:c0:b4:a5:51:05:78:6b:29:28:43:b6:
+         74:d8:74:18:8e:4f:44:62:55:49:bb:f9:f3:1b:37:63:49:ff:
+         c9:2e:73:a2
diff --git a/cpp/tests/vereign/crypto/test_data/leaf_key.pem b/cpp/tests/vereign/crypto/test_data/leaf_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..5d83ade2c640588ac9df74b38aa7dd62dc23600c
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/leaf_key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+q1ElMjx1u0kl
+G7mfbq//OAfwLEnnO8g6GQLxqpaNnb03sJcqEQVxZZTuMmtL15RDoYy77f6U1pP4
+e8pIffyZAIjV0XNVMcrraV8NG2okOdu58Xo0xx+OpBKrHegptqoF0/t9u34r28gY
+FbqObbTfmDw0bFAH6Xp6V5mLz23Xw+HFkK7TlNy5/JAg7GMk5YtC07tN5uLwtkip
+wCcuibxF27Ylvn1VQG1y5FtJgdSq4Dozk/lLsoK0ReFeDBSwbnj5GphyjO/FEKt/
+neebaBcwIfCVv6vGRbs8z/GWH40iiHKzUv8gwEBbEQ1iJOxcU4H6yuylALh8HuKo
+twFCXh0BAgMBAAECggEAAuEIMLktYl4MLh2DpBrng5bMwI1mpfFy5fUD5J1zDbCJ
++iSLj8oh1yhrGIgAiJdjfibwaVO1Z6Qa0rfJzOZBcWz+4/L4RxshSYSLN8yIFy5x
+eSwNMJf+IROsqCf43g0LobAQU4HIggXc+Qd2TEQZ+lNXO2Jpo0jyUr/DtNxSYrLk
+PQOiZUeTRApEFMH/KR7E3CINnFWEsyBktr4UGNT+KbuS+h24QFgeMZS03ELCER/E
+GtYgjYE8Njb+LLC82ZB0jVobo7sXqEu4xhunsysJMID7XF7cRbVH4jS4owBik+MM
+0C4x3IWcUlPTRwCwz7ThdNmuaWysdcm6YKmDTjkg7QKBgQDgObGyYADOBVC986xD
+rHQ6HsHjJoS7UA98OKtpwUfoHgmzCn787bL+28payYLjBQg8Lu0V4wz6IzryCjEY
+DBDWT376lsIElYQBVO80JR+sQHveRKHj3nKxin8RFv+P1iR5l06MLFuI7lpgnfjf
+zAJAMtob6hXTfhKd6ttw3LMJtQKBgQDZsEsv3iWEtJOlEk32b+cBtC9m3+3BRrsl
+vhMqJRRiz3q8ESw4UeYnjHTMJPKixsNCTwJ+swLz1HzPjAPOn8J5g+ehx9szx04g
++SOjO0hYxiuMyEY1RcgQDz8W2eDIY79vQHK6eIdsDX9aDM8TKl0SVkX8yeZVH/l4
+ZrS0GcIlnQKBgHwO78YlX8ydHEM8or7+l3/grJ2lHiMU9LxiDlUA32wJ0owA7qm6
+SiLAMIFypLw+eIPz4rENYoyzGVX5VZhcdKReb37uEoK+xr6F46MSNyfytZYQmxGc
+bhDU8+DcvKE0dhHgKVFc6XgliFOUk7IfCc/DUvHG3jSZuGsvGiKjuVtRAoGAL5DF
+3P+9UvHiEP+e6WoqH8UvVls3qDO8UNLnfR39Blovh0h5URUUJTAblT+9hLPJloON
+GAIlcwHLA86kRlHCPKsFwePZkcbK24mUr+YUONGUUn6wWJOW02uBbYzOAPtKqRR7
+/SK49HM+kH6cAKQREiCnykSTf6Ys7JIEihXWxkECgYEAo/PjT9L+QuYn9muv4p50
+UMJLGQ6kE+63n2dvcEUhXRtEx/i4KzgTkdJ0gBOiaBhJYjzI4BZt/DgtSrzqNC1Y
+UUk5i9sJJF5Ypw0n3dX1mYEFlsJ1xFKDU+lwMakdY+X6b+VECKGIQZsh5DOrFRrv
+QzRzF4x09HztPUADv4gCL40=
+-----END PRIVATE KEY-----
diff --git a/cpp/tests/vereign/encoding/base64_test.cc b/cpp/tests/vereign/encoding/base64_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..0e6316b90d2de75e8f9cefcf9c4772259355982b
--- /dev/null
+++ b/cpp/tests/vereign/encoding/base64_test.cc
@@ -0,0 +1,86 @@
+#include <vereign/encoding/base64.hh>
+
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+
+#include <catch2/catch.hpp>
+
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("base64::Encode", "[vereign/encoding/base64][vereign/encoding]") {
+  SECTION("RFC 4648 test cases plus some extras") {
+    struct Test {
+      std::string Input;
+      std::string Expected;
+    };
+
+    using namespace std::string_literals;
+
+    std::vector<Test> tests{
+      // taken from RFC 4648
+      {"f", "Zg=="},
+      {"fo", "Zm8="},
+      {"foo", "Zm9v"},
+      {"foob", "Zm9vYg=="},
+      {"fooba", "Zm9vYmE="},
+      {"foobar", "Zm9vYmFy"},
+
+      // additional test cases
+      {"", ""},
+      {"\x00\x42"s "foobar", "AEJmb29iYXI="}
+    };
+
+    for (auto& test : tests) {
+      bytes::Buffer encoded;
+      encoding::base64::Encode(bytes::View(test.Input), encoded);
+      CHECK(test.Expected == encoded.View().String());
+    }
+  }
+
+  SECTION("big input must not contain new lines") {
+    auto input = crypto::Rand(1024);
+    bytes::Buffer encoded;
+    encoding::base64::Encode(input.View(), encoded);
+
+    CHECK(encoded.View().String().find('\n') == std::string::npos);
+    CHECK(encoded.View().String().find('\r') == std::string::npos);
+
+    bytes::Buffer decoded;
+    encoding::base64::Decode(encoded.View(), decoded);
+
+    CHECK(input.View() == decoded.View());
+  }
+}
+
+TEST_CASE("base64::Decode", "[vereign/encoding/base64][vereign/encoding]") {
+  SECTION("RFC 4648 test cases plus some extras") {
+    struct Test {
+      std::string Expected;
+      std::string Input;
+    };
+
+    using namespace std::string_literals;
+
+    std::vector<Test> tests{
+      // taken from RFC 4648
+      {"f", "Zg=="},
+      {"fo", "Zm8="},
+      {"foo", "Zm9v"},
+      {"foob", "Zm9vYg=="},
+      {"fooba", "Zm9vYmE="},
+      {"foobar", "Zm9vYmFy"},
+
+      // additional test cases
+      {"", ""},
+      {"\x00\x42"s "foobar", "AEJmb29iYXI="}
+    };
+
+    for (auto& test : tests) {
+      bytes::Buffer encoded;
+      encoding::base64::Decode(bytes::View(test.Input), encoded);
+      CHECK(test.Expected == encoded.View().String());
+    }
+  }
+}
diff --git a/cpp/tests/vereign/encoding/hex_test.cc b/cpp/tests/vereign/encoding/hex_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..9b4fefea84b7de42c502de478613888a736578d9
--- /dev/null
+++ b/cpp/tests/vereign/encoding/hex_test.cc
@@ -0,0 +1,96 @@
+#include <vereign/encoding/hex.hh>
+
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("encoding::hex::Encode", "[vereign/encoding/hex][vereign/encoding]") {
+  struct Test {
+    std::string Input;
+    std::string Expected;
+  };
+
+  using namespace std::string_literals;
+
+  std::vector<Test> tests{
+    {"", ""},
+    {"f", "66"},
+    {"fo", "666f"},
+    {"foo", "666f6f"},
+    {"foob", "666f6f62"},
+    {"fooba", "666f6f6261"},
+    {"foobar", "666f6f626172"},
+    {"\x00\x42"s "foobar", "0042666f6f626172"}
+  };
+
+  for (auto& test : tests) {
+    bytes::Buffer encoded;
+    encoding::hex::Encode(bytes::View(test.Input), encoded);
+    CHECK(test.Expected == encoded.View().String());
+  }
+}
+
+TEST_CASE("encoding::hex::Decode", "[vereign/encoding/hex][vereign/encoding]") {
+  struct Test {
+    std::string Expected;
+    std::string Input;
+  };
+
+  using namespace std::string_literals;
+
+  std::vector<Test> tests{
+    {"", ""},
+    {"f", "66"},
+    {"fo", "666f"},
+    {"foo", "666f6f"},
+    {"foob", "666f6f62"},
+    {"fooba", "666f6f6261"},
+    {"foobar", "666f6f626172"},
+    {"\x00\x42"s "foobar", "0042666f6f626172"}
+  };
+
+  for (auto& test : tests) {
+    bytes::Buffer decoded;
+    encoding::hex::Decode(bytes::View(test.Input), decoded);
+    CHECK(test.Expected == decoded.View().String());
+  }
+
+  auto input = crypto::Rand(16);
+
+  bytes::Buffer encoded;
+  encoding::hex::Encode(input.View(), encoded);
+
+  bytes::Buffer decoded;
+  encoding::hex::Decode(encoded.View(), decoded);
+
+  CHECK(input.View() == decoded.View());
+}
+
+TEST_CASE("encoding::hex::EncodeReverse", "[vereign/encoding/hex][vereign/encoding]") {
+  struct Test {
+    std::string Input;
+    std::string Expected;
+  };
+
+  using namespace std::string_literals;
+
+  std::vector<Test> tests{
+    {"", ""},
+    {"f", "66"},
+    {"fo", "6f66"},
+    {"foo", "6f6f66"},
+    {"foob", "626f6f66"},
+    {"fooba", "61626f6f66"},
+    {"foobar", "7261626f6f66"},
+    {"\x00\x42"s "foobar", "7261626f6f664200"}
+  };
+
+  for (auto& test : tests) {
+    bytes::Buffer encoded;
+    encoding::hex::EncodeReverse(bytes::View(test.Input), encoded);
+    CHECK(test.Expected == encoded.View().String());
+  }
+}
diff --git a/cpp/tests/vereign/filesystem/temp.hh b/cpp/tests/vereign/filesystem/temp.hh
new file mode 100644
index 0000000000000000000000000000000000000000..62c03d0c08c812e86ae7f9268d6936989db3ac3b
--- /dev/null
+++ b/cpp/tests/vereign/filesystem/temp.hh
@@ -0,0 +1,31 @@
+#ifndef __VEREIGN_FILESYSTEM_TEMP_HH
+#define __VEREIGN_FILESYSTEM_TEMP_HH
+
+namespace vereign::filesystem {
+
+std::filesystem::path CreateTmpDir() {
+  // auto tmp_dir = std::filesystem::temp_directory_path();
+  // unsigned long long i = 0;
+  // std::random_device dev;
+  // std::mt19937 prng(dev());
+  // std::uniform_int_distribution<uint64_t> rand(0);
+  // std::filesystem::path path;
+  // while (true) {
+    // std::stringstream ss;
+    // ss << std::hex << rand(prng);
+    // path = tmp_dir / ss.str();
+    // // true if the directory was created.
+    // if (std::filesystem::create_directory(path)) {
+      // break;
+    // }
+    // if (i == max_tries) {
+      // throw std::runtime_error("could not find non-existing directory");
+    // }
+    // i++;
+  // }
+  return path;
+}
+
+} // namespace vereign::filesystem
+
+#endif // __VEREIGN_FILESYSTEM_TEMP_HH
diff --git a/cpp/tests/vereign/grpc/event_api_test.cc b/cpp/tests/vereign/grpc/event_api_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a1220024fe11cb51c2af899fc2779420e178f928
--- /dev/null
+++ b/cpp/tests/vereign/grpc/event_api_test.cc
@@ -0,0 +1,96 @@
+#include <vereign/grpc/server.hh>
+
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/grpc/error_code.hh>
+#include <vereign/core/scope_guard.hh>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/client_library/passport_api.gen.grpc.pb.h>
+#include <vereign/client_library/identity_api.gen.grpc.pb.h>
+#include <vereign/client_library/event_api.gen.grpc.pb.h>
+#include <vereign/service/identity_service.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+
+#ifdef _WIN32
+# include <vereign/ncrypt/rsa.hh>
+#endif
+
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
+#include <grpcpp/create_channel.h>
+
+#include <catch2/catch.hpp>
+
+TEST_CASE("grpc::EventAPI::GetNewEvents", "[vereign/grpc][.integration]") {
+  SECTION("get events while not logged in") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    const std::string pin = "foo";
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    // start listening for events, and exit when the device is confirmed
+    std::thread t1([&channel]() {
+      auto event_client = vereign::client_library::EventAPI::NewStub(channel);
+      auto req = vereign::client_library::GetNewEventsForm{};
+      auto ctx = ::grpc::ClientContext{};
+      auto resp_stream = event_client->GetNewEvents(&ctx, req);
+
+      auto resp = vereign::client_library::GetNewEventsFormResponse{};
+      for (;;) {
+        resp.Clear();
+        auto ok = resp_stream->Read(&resp);
+        if (!ok) {
+          break;
+        }
+
+        REQUIRE(resp.data().size() > 0);
+        for (auto& event : resp.data()) {
+          if (event.type() == "DeviceConfirmed") {
+            return;
+          }
+        }
+
+        // std::cout << vereign::testutil::ProtobufToJson(resp) << std::endl;
+      }
+    });
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormNewDevice{};
+    auto login_resp = vereign::client_library::LoginFormNewDeviceResponse{};
+    login_req.set_pin(pin);
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithNewDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "");
+    CHECK(login_resp.status() == "OK");
+    REQUIRE(login_resp.code() == "200");
+
+    // the old device is used for new device confirmation and authorization
+    auto old_storage_path = vereign::fs::TempFilePath("test_db_");
+    auto rm_old_storage_path = vereign::fs::RemoveFileGuard{old_storage_path};
+    auto old_device_ctx = vereign::test::ServiceContext{host, port, old_storage_path};
+    auto old_device = vereign::test::Device{old_device_ctx};
+    old_device.Login(public_key);
+
+    // confirm the new device using an old device
+    old_device.ConfirmNewDevice(login_resp.data().qrcode(), login_resp.data().actionid());
+
+    t1.join();
+  }
+}
diff --git a/cpp/tests/vereign/grpc/identity_api_test.cc b/cpp/tests/vereign/grpc/identity_api_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..2593b8f0781a19ee0886945e5ec15d8f1e23aca5
--- /dev/null
+++ b/cpp/tests/vereign/grpc/identity_api_test.cc
@@ -0,0 +1,337 @@
+#include <vereign/grpc/server.hh>
+
+#include <vereign/grpc/error_code.hh>
+#include <vereign/core/scope_guard.hh>
+#include <vereign/client_library/passport_api.gen.grpc.pb.h>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/client_library/identity_api.gen.grpc.pb.h>
+#include <vereign/service/identity_service.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+
+#ifdef _WIN32
+# include <vereign/ncrypt/rsa.hh>
+#endif
+
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
+#include <grpcpp/create_channel.h>
+
+#include <catch2/catch.hpp>
+
+TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.integration]") {
+#ifdef _WIN32
+  SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") {
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+
+    auto provider = vereign::ncrypt::rsa::OpenStorageProvider();
+    auto key = vereign::ncrypt::rsa::LoadKey(
+      provider.Get(),
+      std::string(vereign::kvstore::VereignKeyName)
+    );
+    if (key) {
+      vereign::ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+    auto login_resp = vereign::client_library::EmptyResponse{};
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() != "");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::DeviceNotRegistered
+    ));
+  }
+
+  SECTION("when the device key has been changed, it must fail with InvalidIdentity error") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+    vereign::test::PrepareNewDevice(host, port, public_key, "", storage_path);
+
+    auto provider = vereign::ncrypt::rsa::OpenStorageProvider();
+    auto key = vereign::ncrypt::rsa::LoadKey(
+      provider.Get(),
+      std::string(vereign::kvstore::VereignKeyName)
+    );
+    if (key) {
+      vereign::ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    vereign::ncrypt::rsa::CreateKey(
+      provider.Get(),
+      2048,
+      std::string(vereign::kvstore::VereignKeyName),
+      {}
+    );
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+    auto login_resp = vereign::client_library::EmptyResponse{};
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "invalid identity");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::InvalidIdentity
+    ));
+  }
+#else
+  SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") {
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+    auto login_resp = vereign::client_library::EmptyResponse{};
+    login_req.set_pin("foo");
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() != "");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::DeviceNotRegistered
+    ));
+  }
+
+  SECTION("when the pin is wrong, it must fail with InvalidPinCode error") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    const std::string pin = "foo";
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+    vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+    auto login_resp = vereign::client_library::EmptyResponse{};
+    login_req.set_pin("invalid_pin");
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "invalid pin code");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::InvalidPinCode
+    ));
+  }
+
+  SECTION("when the pin is empty, it must fail with InvalidPinCode error") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    const std::string pin = "foo";
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+    vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+    auto login_resp = vereign::client_library::EmptyResponse{};
+    login_req.set_pin("");
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "invalid pin code");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::InvalidPinCode
+    ));
+  }
+
+#endif
+
+  SECTION("when the device is not authorized, it must fail with DeviceNotRegistered error") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    // the old device is used later for new device confirmation and authorization
+    auto old_storage_path = vereign::fs::TempFilePath("test_db_");
+    auto rm_old_storage_path = vereign::fs::RemoveFileGuard{old_storage_path};
+    auto old_device_ctx = vereign::test::ServiceContext{host, port, old_storage_path};
+    auto old_device = vereign::test::Device{old_device_ctx};
+    old_device.Login(public_key);
+
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+    const std::string pin = "foo";
+
+    std::string qr_code;
+    std::string action_id;
+
+    {
+
+      vereign::grpc::Server server{"localhost:", host, port, storage_path};
+      auto on_exit = vereign::core::MakeScopeGuard([&server] {
+        server.Shutdown();
+      });
+
+      auto channel = ::grpc::CreateChannel(
+        "localhost:" + std::to_string(server.SelectedPort()),
+        ::grpc::InsecureChannelCredentials()
+      );
+
+      auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+
+      auto register_req = vereign::client_library::LoginFormNewDevice{};
+      auto register_resp = vereign::client_library::LoginFormNewDeviceResponse{};
+      register_req.set_pin(pin);
+
+      ::grpc::ClientContext register_ctx;
+      identity_client->LoginWithNewDevice(&register_ctx, register_req, &register_resp);
+
+      CHECK(register_resp.error() == "");
+      CHECK(register_resp.status() == "OK");
+      REQUIRE(register_resp.code() == "200");
+
+      qr_code = register_resp.data().qrcode();
+      action_id = register_resp.data().actionid();
+    }
+
+    {
+      vereign::grpc::Server server{"localhost:", host, port, storage_path};
+      auto on_exit = vereign::core::MakeScopeGuard([&server] {
+        server.Shutdown();
+      });
+
+      auto channel = ::grpc::CreateChannel(
+        "localhost:" + std::to_string(server.SelectedPort()),
+        ::grpc::InsecureChannelCredentials()
+      );
+
+      auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+      auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+      auto login_resp = vereign::client_library::EmptyResponse{};
+      login_req.set_pin(pin);
+
+      ::grpc::ClientContext login_ctx;
+      identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
+
+      CHECK(login_resp.error() == "Error retrieving entity ");
+      CHECK(login_resp.status() == "Error while login");
+      REQUIRE(login_resp.code() == "400");
+
+      old_device.ConfirmNewDevice(qr_code, action_id);
+
+      ::grpc::ClientContext login_ctx_confirmed;
+      identity_client->LoginWithPreviouslyAddedDevice(&login_ctx_confirmed, login_req, &login_resp);
+
+      CHECK(login_resp.error() == "Device unauthorized");
+      CHECK(login_resp.status() == "Error while login");
+      REQUIRE(login_resp.code() == "400");
+    }
+  }
+}
+
+TEST_CASE("grpc::IdentityAPI::LoginWithNewDevice", "[vereign/grpc][.integration]") {
+#ifdef _WIN32
+#else
+  SECTION("when the pin is empty, it must fail with InvalidPinCode error") {
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormNewDevice{};
+    auto login_resp = vereign::client_library::LoginFormNewDeviceResponse{};
+    login_req.set_pin("");
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithNewDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "invalid pin code");
+    CHECK(login_resp.status() == vereign::grpc::ClientErrorStatus);
+    REQUIRE(login_resp.code() == vereign::grpc::ErrorCodeAsString(
+      vereign::grpc::ErrorCode::InvalidPinCode
+    ));
+  }
+#endif // _WIN32
+}
diff --git a/cpp/tests/vereign/grpc/json/encoder_test.cc b/cpp/tests/vereign/grpc/json/encoder_test.cc
index c11b41e95013417b294c28f454a9722c50c510d5..7cd3095d85dff462f18cf8b8765a4fdbe1d485bb 100644
--- a/cpp/tests/vereign/grpc/json/encoder_test.cc
+++ b/cpp/tests/vereign/grpc/json/encoder_test.cc
@@ -2,8 +2,6 @@
 #include <vereign/grpc/json/pb/messages.pb.h>
 #include <boost/math/constants/constants.hpp>
 
-#include <vereign/core/hex.hh>
-
 #include <catch2/catch.hpp>
 
 using namespace vereign;
diff --git a/cpp/tests/vereign/grpc/server_test.cc b/cpp/tests/vereign/grpc/server_test.cc
index 61e2ea3874131d4aaff740fea19b6364c26b7273..de3c47db053a0494fbaeb059d1f3535fc0bdcaee 100644
--- a/cpp/tests/vereign/grpc/server_test.cc
+++ b/cpp/tests/vereign/grpc/server_test.cc
@@ -1,20 +1,32 @@
 #include <vereign/grpc/server.hh>
+
 #include <vereign/core/scope_guard.hh>
 #include <vereign/client_library/passport_api.gen.grpc.pb.h>
-#include <util/env.hh>
-#include <util/protobuf.hh>
-
-#include <catch2/catch.hpp>
-#include "vereign/client_library/types.gen.pb.h"
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/client_library/identity_api.gen.grpc.pb.h>
+#include <vereign/service/identity_service.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
 
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 #include <grpcpp/create_channel.h>
 
+#include <catch2/catch.hpp>
+
 TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
-  vereign::grpc::Server server{"localhost:", host, port, publicKey};
+  const std::string pin = "foo";
+  auto storage_path = vereign::fs::TempDir("test_db_");
+  auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+  vereign::test::PrepareNewDevice(host, port, public_key, pin, storage_path);
+
+  vereign::grpc::Server server{"localhost:", host, port, storage_path};
   auto on_exit = vereign::core::MakeScopeGuard([&server] {
     server.Shutdown();
   });
@@ -24,35 +36,33 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
     ::grpc::InsecureChannelCredentials()
   );
 
-  auto client = vereign::client_library::PassportAPI::NewStub(channel);
+  // register new device
+  auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+  auto login_req = vereign::client_library::LoginFormPreviousAddedDevice{};
+  auto login_resp = vereign::client_library::EmptyResponse{};
+  login_req.set_pin(pin);
 
-  vereign::client_library::ListPassportsForm req;
-  vereign::client_library::ListPassportsFormResponse resp;
-  ::grpc::ClientContext ctx;
-  auto status = client->ListPassports(&ctx, req, &resp);
+  ::grpc::ClientContext login_ctx;
+  identity_client->LoginWithPreviouslyAddedDevice(&login_ctx, login_req, &login_resp);
 
-  // std::cout << vereign::test::ProtobufToJson(resp) << std::endl;
+  CHECK(login_resp.error() == "");
+  CHECK(login_resp.status() == "OK");
+  REQUIRE(login_resp.code() == "200");
 
-  REQUIRE(status.error_message() == "");
-  REQUIRE(resp.error() == "");
-  CHECK(resp.status() == "OK");
-  CHECK(resp.code() == "200");
-  CHECK(resp.data().size() > 0);
-  for (auto& passport : resp.data()) {
-    CHECK(passport.uuid().size() == 36);
-  }
+  auto passport_client = vereign::client_library::PassportAPI::NewStub(channel);
 
-  req.Clear();
-  resp.Clear();
-  ::grpc::ClientContext manually_ctx;
-  status = client->ListPassportsManually(&manually_ctx, req, &resp);
+  vereign::client_library::ListPassportsForm req;
+  vereign::client_library::ListPassportsFormResponse resp;
+  ::grpc::ClientContext ctx;
+  auto status = passport_client->ListPassports(&ctx, req, &resp);
 
   // std::cout << vereign::test::ProtobufToJson(resp) << std::endl;
 
   REQUIRE(status.error_message() == "");
-  REQUIRE(resp.error() == "");
+  REQUIRE(status.ok() == true);
+  CHECK(resp.error() == "");
   CHECK(resp.status() == "OK");
-  CHECK(resp.code() == "200");
+  REQUIRE(resp.code() == "200");
   CHECK(resp.data().size() > 0);
   for (auto& passport : resp.data()) {
     CHECK(passport.uuid().size() == 36);
@@ -62,7 +72,7 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
   getInterReq.set_uuid(resp.data().at(0).uuid());
   vereign::client_library::GetInteractionsFormResponse getInterResp;
   ::grpc::ClientContext getInterCtx;
-  status = client->GetInteractions(&getInterCtx, getInterReq, &getInterResp);
+  status = passport_client->GetInteractions(&getInterCtx, getInterReq, &getInterResp);
   CHECK(status.error_message() == "");
   CHECK(getInterResp.error() == "");
   CHECK(getInterResp.status() == "OK");
@@ -74,6 +84,6 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
 
   // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl;
 
-  grpc_shutdown();
-  google::protobuf::ShutdownProtobufLibrary();
+  // grpc_shutdown();
+  // google::protobuf::ShutdownProtobufLibrary();
 }
diff --git a/cpp/tests/vereign/identity/provider_test.cc b/cpp/tests/vereign/identity/provider_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c4de48c2568c74315d2894410f4ff301b939d207
--- /dev/null
+++ b/cpp/tests/vereign/identity/provider_test.cc
@@ -0,0 +1,215 @@
+#include <vereign/identity/provider.hh>
+
+#include <vereign/fs/util.hh>
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/kvstore/crypto_storage.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+#include <vereign/service/gen/passport_service.hh>
+#include <vereign/crypto/cert.hh>
+#include <vereign/crypto/rsa.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
+
+#include <catch2/catch.hpp>
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("identity::Provider::RecreateIdentity", "[vereign/identity]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+
+  auto device_ctx = test::ServiceContext{"", "", storage_path};
+
+  std::string expected;
+  std::string actual;
+
+  {
+    kvstore::SqliteStorage kvstorage{storage_path};
+    kvstore::CryptoStorage storage{kvstorage, true};
+
+    identity::Provider provider{device_ctx.ClientSession(), storage};
+    expected = provider.RecreateIdentity("foo");
+  }
+
+  {
+    kvstore::SqliteStorage kvstorage{storage_path};
+    kvstore::CryptoStorage storage{kvstorage, true};
+
+    identity::Provider provider{device_ctx.ClientSession(), storage};
+    actual = provider.LoadIdentity("foo");
+  }
+
+  CHECK(expected.size() > 0);
+  CHECK(actual.size() > 0);
+  CHECK(expected == actual);
+}
+
+TEST_CASE("identity::Provider::GetProfileCertificate", "[vereign/identity][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  // prepare new device
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  old_device.CreateNewDevice(service_context, "pin");
+
+  // retrieve a profile uuid
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+  auto& list = result.Response;
+  REQUIRE(list->code() == "200");
+  REQUIRE(list->data().size() > 0);
+
+  auto profileUUID = list->data().at(0).uuid();
+
+  auto& identity_provider = service_context.IdentityProvider();
+
+  auto cert = identity_provider.GetProfileCertificate(profileUUID);
+
+  REQUIRE(cert.get() != nullptr);
+  CHECK(cert->PrivateKeyPEM.Size() > 0);
+  CHECK(cert->CertificatePEM.Size() > 0);
+  CHECK(cert->CertificateUUID.size() > 0);
+  CHECK(cert->Chain.size() == 2);
+
+  SECTION("the certificate private key must be importable") {
+    bssl::UniquePtr<EVP_PKEY> cert_pkey;
+    CHECK_NOTHROW(
+      cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View())
+    );
+    CHECK(cert_pkey.get() != nullptr);
+  }
+
+  SECTION("the certificate and the certificates from the chain must be importable") {
+    bssl::UniquePtr<X509> x509_cert;
+    CHECK_NOTHROW(
+      x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View())
+    );
+    CHECK(x509_cert.get() != nullptr);
+
+    for (auto& cert_pem : cert->Chain) {
+      x509_cert.reset();
+
+      CHECK_NOTHROW(
+        x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View())
+      );
+      CHECK(x509_cert.get() != nullptr);
+    }
+  }
+
+
+  SECTION("it must reuse already created profile certificates") {
+    auto new_cert = identity_provider.GetProfileCertificate(profileUUID);
+
+    REQUIRE(new_cert.get() != nullptr);
+    CHECK(new_cert->PrivateKeyPEM.View() == cert->PrivateKeyPEM.View());
+    CHECK(new_cert->CertificatePEM.View() == cert->CertificatePEM.View());
+    CHECK(new_cert->CertificateUUID == cert->CertificateUUID);
+    REQUIRE(new_cert->Chain.size() == cert->Chain.size());
+    for (std::size_t i = 0; i < new_cert->Chain.size(); i++) {
+      CHECK(new_cert->Chain[i].View() == cert->Chain[i].View());
+    }
+  }
+}
+
+TEST_CASE("identity::Provider::GetProfileOneTimeCertificate", "[vereign/identity][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  // prepare new device
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  old_device.CreateNewDevice(service_context, "pin");
+
+  // retrieve a profile uuid
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+  auto& list = result.Response;
+  REQUIRE(list->code() == "200");
+  REQUIRE(list->data().size() > 0);
+
+  auto profileUUID = list->data().at(0).uuid();
+
+  auto& identity_provider = service_context.IdentityProvider();
+
+  auto cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "foo@example.com");
+
+  REQUIRE(cert.get() != nullptr);
+  CHECK(cert->PrivateKeyPEM.Size() > 0);
+  CHECK(cert->CertificatePEM.Size() > 0);
+  CHECK(cert->CertificateUUID.size() == 0);
+  CHECK(cert->Chain.size() == 3);
+
+  SECTION("the certificate private key must be importable") {
+    bssl::UniquePtr<EVP_PKEY> cert_pkey;
+    CHECK_NOTHROW(
+      cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View())
+    );
+    CHECK(cert_pkey.get() != nullptr);
+  }
+
+  // fs::WriteFile("onetime", cert->CertificatePEM.View());
+  // fs::WriteFile("userdevice", cert->Chain[0].View());
+  // fs::WriteFile("serverside", cert->Chain[1].View());
+  // fs::WriteFile("vereign", cert->Chain[2].View());
+
+  SECTION("the certificate and the certificates from the chain must be importable") {
+    bssl::UniquePtr<X509> x509_cert;
+    CHECK_NOTHROW(
+      x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View())
+    );
+    CHECK(x509_cert.get() != nullptr);
+
+    for (auto& cert_pem : cert->Chain) {
+      x509_cert.reset();
+
+      CHECK_NOTHROW(
+        x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View())
+      );
+      CHECK(x509_cert.get() != nullptr);
+    }
+  }
+
+  SECTION("it must create new certificate all the time, but the issuers chain must be the same") {
+    auto new_cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "bar@example.com");
+
+    REQUIRE(new_cert.get() != nullptr);
+    CHECK(new_cert->PrivateKeyPEM.View() != cert->PrivateKeyPEM.View());
+    CHECK(new_cert->CertificatePEM.View() != cert->CertificatePEM.View());
+    CHECK(new_cert->CertificateUUID.size() == 0);
+    REQUIRE(new_cert->Chain.size() == cert->Chain.size());
+    for (std::size_t i = 0; i < new_cert->Chain.size(); i++) {
+      CHECK(new_cert->Chain[i].View() == cert->Chain[i].View());
+    }
+  }
+}
diff --git a/cpp/tests/vereign/kvstore/crypto_storage_test.cc b/cpp/tests/vereign/kvstore/crypto_storage_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..01524db2f8dc171002b49e71addfa33289b20737
--- /dev/null
+++ b/cpp/tests/vereign/kvstore/crypto_storage_test.cc
@@ -0,0 +1,99 @@
+#include <vereign/kvstore/crypto_storage.hh>
+
+#include <vereign/encoding/errors.hh>
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/core/scope_guard.hh>
+
+#include <catch2/catch.hpp>
+#include <boost/filesystem.hpp>
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("kvstore::CryptoStorage::Recreate", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  // put value
+  {
+    auto kvstorage = kvstore::SqliteStorage(storage_path);
+    kvstore::CryptoStorage storage{kvstorage, true};
+
+    storage.Recreate("foo");
+    std::string v{"test value"};
+    storage.PutBytes("test", bytes::View(v));
+  }
+
+  // with another storage instance get the value
+  {
+    auto kvstorage = kvstore::SqliteStorage(storage_path);
+    kvstore::CryptoStorage storage{kvstorage, true};
+
+    bytes::Buffer v;
+    storage.Open("foo");
+    storage.GetBytes("test", v);
+
+    CHECK(v.View().String() == "test value");
+  }
+}
+
+TEST_CASE("kvstore::CryptoStorage::PutBytes", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  auto big_value = crypto::Rand(100000);
+  auto kvstorage = kvstore::SqliteStorage(storage_path);
+  kvstore::CryptoStorage storage{kvstorage, true};
+
+  storage.Recreate("foo");
+  storage.PutBytes("test", big_value.View());
+
+  bytes::Buffer v;
+  storage.GetBytes("test", v);
+
+  REQUIRE(v.Size() == big_value.Size());
+
+  auto cmp = std::memcmp(v.View().Data(), big_value.View().Data(), v.Size());
+  CHECK(cmp == 0);
+}
+
+TEST_CASE("kvstore::CryptoStorage::GetBytes", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  auto kvstorage = kvstore::SqliteStorage(storage_path);
+  kvstore::CryptoStorage storage{kvstorage, true};
+
+  storage.Recreate("foo");
+
+  SECTION("when the value does not exists, it must return false") {
+    bytes::Buffer v;
+
+    CHECK(storage.GetBytes("does_not_exists", v) == false);
+
+    CHECK(v.Size() == 0);
+  }
+
+  SECTION("when the value cannot be decoded, it must throw encoding::Error") {
+    kvstorage.PutInt64("foo", 1);
+
+    bytes::Buffer v;
+
+    CHECK_THROWS_AS(storage.GetBytes("foo", v), encoding::Error);
+
+    CHECK(v.Size() == 0);
+  }
+
+  SECTION("when the value exists, it must retrieve the value") {
+    storage.PutBytes("foo", bytes::View("bar"));
+
+    bytes::Buffer v;
+    storage.GetBytes("foo", v);
+
+    CHECK(v.View() == bytes::View("bar"));
+  }
+}
diff --git a/cpp/tests/vereign/kvstore/lock_test.cc b/cpp/tests/vereign/kvstore/lock_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7dd70ed7943d1ec7400c2e3db7534875d979a6a0
--- /dev/null
+++ b/cpp/tests/vereign/kvstore/lock_test.cc
@@ -0,0 +1,61 @@
+#include <vereign/kvstore/sqlite_storage.hh>
+
+#include <vereign/kvstore/lock.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/core/lock_guard.hh>
+#include <vereign/core/scope_guard.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/sqlite/errors.hh>
+#include <testutil/error.hh>
+#include <sqlite3.h>
+
+#include <catch2/catch.hpp>
+#include <boost/filesystem.hpp>
+#include <thread>
+#include <chrono>
+#include <limits>
+#include <optional>
+
+using namespace vereign;
+
+TEST_CASE("kvstore::Lock", "[vereign/kvstore]") {
+
+  SECTION("when the lock is released within the allowed retrials, the lock succeeds") {
+    auto storage_path = fs::TempFilePath("test_db_");
+    fs::RemoveFileGuard rm{storage_path};
+
+    auto foo_storage = kvstore::SqliteStorage(storage_path);
+    auto bar_storage = kvstore::SqliteStorage(storage_path);
+
+    foo_storage.Lock();
+
+    bool err = false;
+    auto th = std::thread{[&bar_storage, &err]() {
+      try {
+        kvstore::Lock l{bar_storage, std::numeric_limits<int>::max(), std::chrono::milliseconds{10}};
+      } catch (...) {
+        err = true;
+      }
+    }};
+
+    foo_storage.Unlock();
+    th.join();
+  }
+
+  SECTION("when the lock is not released within the allowed retrials, the lock fails") {
+    auto storage_path = fs::TempFilePath("test_db_");
+    fs::RemoveFileGuard rm{storage_path};
+
+    auto foo_storage = kvstore::SqliteStorage(storage_path);
+    auto bar_storage = kvstore::SqliteStorage(storage_path);
+
+    foo_storage.Lock();
+
+    CHECK_THROWS_AS(
+      kvstore::Lock(bar_storage, 2, std::chrono::milliseconds{1}),
+      kvstore::LockError
+    );
+  }
+}
diff --git a/cpp/tests/vereign/kvstore/sqlite_storage_test.cc b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..dfe2d3d117038493c6338a3e202b8a1a2e8eec99
--- /dev/null
+++ b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc
@@ -0,0 +1,157 @@
+#include <vereign/kvstore/sqlite_storage.hh>
+
+#include <vereign/kvstore/lock.hh>
+#include <vereign/kvstore/errors.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/core/scope_guard.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/sqlite/errors.hh>
+#include <testutil/error.hh>
+#include <sqlite3.h>
+
+#include <catch2/catch.hpp>
+#include <boost/filesystem.hpp>
+#include <thread>
+#include <optional>
+
+using namespace vereign;
+
+TEST_CASE("kvstore::SqliteStorage::GetBytes", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  auto kvstorage = kvstore::SqliteStorage(storage_path);
+
+  SECTION("when the value does not exists, it must return false") {
+    bytes::Buffer v;
+
+    auto found = kvstorage.GetBytes("does_not_exists", v);
+
+    CHECK(found == false);
+    CHECK(v.Size() == 0);
+  }
+
+  SECTION("when the value exists, it must retrieve the value") {
+    kvstorage.PutBytes("foo", bytes::View("bar"));
+
+    bytes::Buffer v;
+    auto found = kvstorage.GetBytes("foo", v);
+
+    CHECK(found == true);
+    CHECK(v.View() == bytes::View("bar"));
+  }
+}
+
+TEST_CASE("kvstore::SqliteStorage::GetInt64", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  auto kvstorage = kvstore::SqliteStorage(storage_path);
+
+  SECTION("when the value does not exists, it must return false") {
+    int64_t value = 0;
+    auto found = kvstorage.GetInt64("does_not_exists", value);
+
+    CHECK(found == false);
+    CHECK(value == 0);
+  }
+
+  SECTION("when the value exists, it must retrieve the value") {
+    kvstorage.PutInt64("foo", 42);
+
+    int64_t v = 0;
+    auto result = kvstorage.GetInt64("foo", v);
+
+    CHECK(result == true);
+    CHECK(v == 42);
+  }
+}
+
+TEST_CASE("kvstore::SqliteStorage::DeleteAll", "[vereign/kvstore]") {
+  auto storage_path = fs::TempFilePath("test_db_");
+  fs::RemoveFileGuard rm{storage_path};
+
+  auto kvstorage = kvstore::SqliteStorage(storage_path);
+
+  kvstorage.PutInt64("foo", 42);
+  kvstorage.PutInt64("bar", 422);
+
+  int64_t v = 0;
+  kvstorage.GetInt64("foo", v);
+  CHECK(v == 42);
+  kvstorage.GetInt64("bar", v);
+  CHECK(v == 422);
+
+  kvstorage.DeleteAll();
+
+  CHECK(kvstorage.GetInt64("foo", v) == false);
+  CHECK(kvstorage.GetInt64("bar", v) == false);
+}
+
+TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") {
+
+  SECTION("when locked using lock guard, it must unlock on scope exit") {
+    auto storage_path = fs::TempFilePath("test_db_");
+    fs::RemoveFileGuard rm{storage_path};
+
+    {
+      auto kvstorage = kvstore::SqliteStorage(storage_path);
+
+      kvstore::Lock l{kvstorage};
+
+      kvstorage.PutInt64("foo", 42);
+      kvstorage.PutInt64("bar", 422);
+    }
+
+    {
+      auto kvstorage = kvstore::SqliteStorage(storage_path);
+
+      kvstore::Lock l{kvstorage};
+
+      int64_t v = 0;
+      kvstorage.GetInt64("foo", v);
+      CHECK(v == 42);
+
+      kvstorage.GetInt64("bar", v);
+      CHECK(v == 422);
+    }
+  }
+
+  SECTION("when locked, it must unlock on scope exit") {
+    auto storage_path = fs::TempFilePath("test_db_");
+    fs::RemoveFileGuard rm{storage_path};
+
+    {
+      auto kvstorage = kvstore::SqliteStorage(storage_path);
+      kvstorage.Lock();
+
+      kvstorage.PutInt64("foo", 42);
+      kvstorage.PutInt64("bar", 422);
+    }
+
+    {
+      auto kvstorage = kvstore::SqliteStorage(storage_path);
+      kvstorage.Lock();
+
+      int64_t v = 0;
+      kvstorage.GetInt64("foo", v);
+      CHECK(v == 42);
+
+      kvstorage.GetInt64("bar", v);
+      CHECK(v == 422);
+    }
+  }
+
+  SECTION("when the storage is already locked, it must fail with LockError") {
+    auto storage_path = fs::TempFilePath("test_db_");
+    fs::RemoveFileGuard rm{storage_path};
+
+    auto foo_storage = kvstore::SqliteStorage(storage_path);
+    auto bar_storage = kvstore::SqliteStorage(storage_path);
+
+    foo_storage.Lock();
+
+    CHECK_THROWS_AS(bar_storage.Lock(), kvstore::LockError);
+  }
+}
diff --git a/cpp/tests/vereign/ncrypt/rsa_test.cc b/cpp/tests/vereign/ncrypt/rsa_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..116ed579ca8a3dd518c5e04c40585d34096d706e
--- /dev/null
+++ b/cpp/tests/vereign/ncrypt/rsa_test.cc
@@ -0,0 +1,209 @@
+#include <vereign/crypto/rand.hh>
+#include <vereign/ncrypt/rsa.hh>
+
+#include <vereign/core/scope_guard.hh>
+#include <vereign/ncrypt/unique_ptr.hh>
+#include <vereign/bytes/view_dump.hh>
+
+#include <catch2/catch.hpp>
+#include <iostream>
+
+using namespace vereign;
+
+TEST_CASE("ncrypt::rsa::OpenStorageProvider", "[vereign/ncrypt/rsa][vereign/ncrypt]") {
+  auto provider = ncrypt::rsa::OpenStorageProvider();
+  CHECK(provider.Get() != 0);
+}
+
+TEST_CASE("ncrypt::CreateKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") {
+  const auto test_key = std::string{"vereign_test_key"};
+
+  SECTION("when the key does not exists, it must create a new key") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+    CHECK(new_key.Get() != 0);
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() != 0);
+
+    // cleanup
+    ncrypt::rsa::DeleteKey(key.Get());
+  }
+
+  SECTION("when the key already exists, it must fail with NTE_EXISTS") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+    CHECK(new_key.Get() != 0);
+
+    CHECK_THROWS_WITH(
+      ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {}),
+      "creating rsa key failed: NTE_EXISTS"
+    );
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() != 0);
+
+    // cleanup
+    ncrypt::rsa::DeleteKey(key.Get());
+  }
+}
+
+TEST_CASE("ncrypt::rsa::LoadKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") {
+  const auto test_key = std::string{"vereign_test_key"};
+
+  SECTION("when the key exists, it must load the key") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+    CHECK(new_key.Get() != 0);
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() != 0);
+
+    // cleanup
+    ncrypt::rsa::DeleteKey(key.Get());
+  }
+
+  SECTION("when the key does not exists, it must fail") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() == 0);
+  }
+}
+
+TEST_CASE("ncrypt::rsa::DeleteKey", "[vereign/ncrypt/rsa][vereign/ncrypt]") {
+  const auto test_key = std::string{"vereign_test_key"};
+
+  SECTION("when the key exists, it must delete the key") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+    CHECK(new_key.Get() != 0);
+
+    ncrypt::rsa::DeleteKey(new_key.Get());
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() == 0);
+  }
+
+  SECTION("when the key does not exists, it must fail") {
+    auto provider = ncrypt::rsa::OpenStorageProvider();
+
+    // cleanup
+    auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    if (key) {
+      ncrypt::rsa::DeleteKey(key.Get());
+    }
+
+    auto new_key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+    CHECK(new_key.Get() != 0);
+
+    key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+    CHECK(key.Get() != 0);
+
+    ncrypt::rsa::DeleteKey(new_key.Get());
+
+    CHECK_THROWS_WITH(
+      ncrypt::rsa::DeleteKey(key.Get()),
+      "deleting key failed: NTE_BAD_KEYSET"
+    );
+  }
+}
+
+TEST_CASE("ncrypt::rsa PublicKeyEncrypt/PrivateKeyDecrypt", "[vereign/ncrypt/rsa][vereign/ncrypt]") {
+  const auto test_key = std::string{"vereign_test_key"};
+  auto provider = ncrypt::rsa::OpenStorageProvider();
+  auto key = ncrypt::rsa::LoadKey(provider.Get(), test_key);
+  if (key) {
+    ncrypt::rsa::DeleteKey(key.Get());
+  }
+
+  key = ncrypt::rsa::CreateKey(provider.Get(), 2048, test_key, {});
+  REQUIRE(key.Get() != 0);
+  auto delete_key = core::ScopeGuard([&key] { ncrypt::rsa::DeleteKey(key.Get()); });
+
+  SECTION("small input") {
+    const std::string input{"foo bar"};
+    bytes::Buffer encrypted;
+
+    ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted);
+
+    bytes::Buffer decrypted;
+    ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted);
+
+    CHECK(decrypted.View() == bytes::View(input));
+  }
+
+  SECTION("zero input") {
+    const std::string input;
+    bytes::Buffer encrypted;
+
+    CHECK_THROWS_WITH(
+      ncrypt::rsa::PublicKeyEncrypt(key.Get(), bytes::View(input), encrypted),
+      "encryption failed: NTE_NULL_REFERENCE_POINTER"
+    );
+
+    bytes::Buffer decrypted;
+    CHECK_THROWS_WITH(
+      ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted),
+      "decryption failed: NTE_NULL_REFERENCE_POINTER"
+    );
+  }
+
+  SECTION("max size input") {
+    auto input = crypto::Rand(214);
+    bytes::Buffer encrypted;
+
+    ncrypt::rsa::PublicKeyEncrypt(key.Get(), input.View(), encrypted);
+
+    bytes::Buffer decrypted;
+    ncrypt::rsa::PrivateKeyDecrypt(key.Get(), encrypted.View(), decrypted);
+
+    CHECK(decrypted.View() == input.View());
+  }
+
+  SECTION("invalid big input") {
+    auto input = crypto::Rand(215);
+    bytes::Buffer encrypted;
+
+    CHECK_THROWS_WITH(
+      ncrypt::rsa::PublicKeyEncrypt(key.Get(), input.View(), encrypted),
+      "encryption failed: NTE_INVALID_PARAMETER"
+    );
+
+    CHECK(encrypted.Size() == 0);
+  }
+}
diff --git a/cpp/tests/vereign/restapi/client_session_test.cc b/cpp/tests/vereign/restapi/client_session_test.cc
index 36a824999a5c725e0d5443d91c91369fac619c51..1730d834665459f72009e2b08e1ce702fa24f7cc 100644
--- a/cpp/tests/vereign/restapi/client_session_test.cc
+++ b/cpp/tests/vereign/restapi/client_session_test.cc
@@ -2,8 +2,8 @@
 #include <vereign/restapi/client_session.hh>
 #include <vereign/core/scope_guard.hh>
 #include <vereign/client_library/types.gen.pb.h>
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
 #include <catch2/catch.hpp>
 
@@ -11,22 +11,20 @@
 #include <boost/asio/ssl/context.hpp>
 #include <fmt/core.h>
 
-TEST_CASE("ClientSession::Post", "[vereign/restapi/client_session][.integration]") {
+TEST_CASE("restapi::ClientSession::Post", "[vereign/restapi/client_session][.integration]") {
   namespace asio = boost::asio;
   namespace beast = boost::beast;
   asio::io_context ioc;
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
-
-  // Verify the remote server's certificate
-  // ctx.set_verify_mode(ssl::verify_peer);
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   vereign::restapi::Client client{ioc, ctx, host, port};
-  vereign::restapi::ClientSession client_session{client, publicKey};
+  vereign::restapi::ClientSession client_session{client};
+  client_session.SetPubKey(publicKey, "");
 
   std::thread ioc_thread([&ioc]{
     ioc.run();
diff --git a/cpp/tests/vereign/restapi/client_test.cc b/cpp/tests/vereign/restapi/client_test.cc
index 175501b247e0b321e04bd7aa39649094212f2783..27b71a716c6d4a7c22e3e8fbef88f8ae4dd83147 100644
--- a/cpp/tests/vereign/restapi/client_test.cc
+++ b/cpp/tests/vereign/restapi/client_test.cc
@@ -4,7 +4,7 @@
 #include <vereign/client_library/identity_types.pb.h>
 #include <vereign/client_library/passport_api.gen.pb.h>
 #include <vereign/core/scope_guard.hh>
-#include <util/env.hh>
+#include <testutil/env.hh>
 
 #include "boost/asio/executor_work_guard.hpp"
 #include "boost/beast/core/error.hpp"
@@ -25,9 +25,9 @@ TEST_CASE("Client::Post", "[vereign/restapi/client][.integration]") {
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   // Verify the remote server's certificate
   // ctx.set_verify_mode(ssl::verify_peer);
@@ -102,9 +102,9 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") {
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   host = "localhost";
   port = "9292";
@@ -129,7 +129,7 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") {
 
     threads.emplace_back(
       [&client, th]() mutable {
-        auto req = std::make_unique<client_library::LoginForm>();
+        auto req = std::make_unique<client_library::EmptyRequest>();
         auto resp = std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>();
 
         for (int i = 0; i < 1000; i++) {
@@ -137,7 +137,6 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") {
 
           req->Clear();
           resp->Clear();
-          req->set_mode(id);
 
           auto result = client.Post(
             "/",
diff --git a/cpp/tests/vereign/service/gen/passport_service_test.cc b/cpp/tests/vereign/service/gen/passport_service_test.cc
index ec0375b559d44d09d6452604b38af0660932da7e..2953fe0e466c60bba2797104b7e556b970d21bf4 100644
--- a/cpp/tests/vereign/service/gen/passport_service_test.cc
+++ b/cpp/tests/vereign/service/gen/passport_service_test.cc
@@ -1,11 +1,17 @@
 #include <vereign/service/gen/passport_service.hh>
+
+#include <vereign/fs/util.hh>
 #include <vereign/core/scope_guard.hh>
 #include <vereign/client_library/types.gen.pb.h>
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <vereign/client_library/identity_types.pb.h>
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/service/identity_service.hh>
+#include <vereign/identity/provider.hh>
 
-#include <catch2/catch.hpp>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
+#include <catch2/catch.hpp>
 #include <boost/asio/io_context.hpp>
 #include <boost/asio/ssl/context.hpp>
 #include <fmt/core.h>
@@ -17,17 +23,25 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration]
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   // Verify the remote server's certificate
   // ctx.set_verify_mode(ssl::verify_peer);
 
   vereign::restapi::Client client{ioc, ctx, host, port};
-  vereign::restapi::ClientSession client_session{client, publicKey};
+  vereign::restapi::ClientSession client_session{client};
   vereign::service::gen::PassportService service{client_session};
 
+  auto storage_path = vereign::fs::TempFilePath("test_db_");
+  vereign::fs::RemoveFileGuard rm{storage_path};
+
+  auto kvstorage = vereign::kvstore::SqliteStorage(storage_path);
+  vereign::kvstore::CryptoStorage storage{kvstorage};
+  vereign::identity::Provider provider{client_session, storage};
+  vereign::service::IdentityService idenity_service{client_session, provider};
+
   std::thread ioc_thread([&ioc]{
     ioc.run();
   });
@@ -39,6 +53,15 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration]
     }
   );
 
+  // login
+  auto req = std::make_unique<vereign::client_library::LoginWithExistingPubKeyForm>();
+  auto resp = std::make_unique<vereign::client_library::EmptyResponse>();
+  req->set_pubkey(publicKey);
+  idenity_service.LoginWithExistingPubKey(req.get(), resp.get());
+  CHECK(resp->error() == "");
+  CHECK(resp->status() == "OK");
+  REQUIRE(resp->code() == "200");
+
   for (int i = 0; i < 2; i++) {
     auto req = std::make_unique<vereign::client_library::ListPassportsForm>();
     auto resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
@@ -50,9 +73,9 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration]
     // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl;
 
     auto& list = result.Response;
-    REQUIRE(list->error() == "");
+    CHECK(list->error() == "");
     CHECK(list->status() == "OK");
-    CHECK(list->code() == "200");
+    REQUIRE(list->code() == "200");
     CHECK(list->data().size() > 0);
     for (auto& passport : list->data()) {
       CHECK(passport.uuid().size() == 36);
diff --git a/cpp/tests/vereign/service/identity_service_test.cc b/cpp/tests/vereign/service/identity_service_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4f48b025e33140f7a77bd8897ed393f7476c5ed3
--- /dev/null
+++ b/cpp/tests/vereign/service/identity_service_test.cc
@@ -0,0 +1,129 @@
+#include <vereign/service/identity_service.hh>
+
+#include <vereign/client_library/common_types.pb.h>
+#include <vereign/client_library/identity_types.pb.h>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/service/gen/passport_service.hh>
+#include <vereign/restapi/client_session.hh>
+#include <vereign/identity/provider.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+#include <vereign/fs/util.hh>
+
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("service::IdentityService::LoginWithNewDevice", "[vereign/service][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  // the old device is used later for new device confirmation and authorization
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  auto service = service::IdentityService{
+    service_context.ClientSession(),
+    service_context.IdentityProvider()
+  };
+
+  // register new device
+  auto req = std::make_unique<vereign::client_library::LoginFormNewDevice>();
+  auto resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>();
+  req->set_pin("foo");
+
+  service.LoginWithNewDevice(req.get(), resp.get());
+
+  CHECK(resp->error() == "");
+  CHECK(resp->status() == "OK");
+  REQUIRE(resp->code() == "200");
+  REQUIRE(resp->has_data() == true);
+
+  // confirm and authorize the new device using an old device
+  old_device.ConfirmNewDevice(resp->data().qrcode(), resp->data().actionid());
+  old_device.AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash());
+
+  // list passports with the new device
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+
+  // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl;
+
+  auto& list = result.Response;
+  CHECK(list->error() == "");
+  CHECK(list->status() == "OK");
+  REQUIRE(list->code() == "200");
+  CHECK(list->data().size() > 0);
+  for (const auto& passport : list->data()) {
+    CHECK(passport.uuid().size() == 36);
+  }
+}
+
+TEST_CASE("service::IdentityService::LoginWithPreviouslyAddedDevice", "[vereign/service][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+
+  // prepare new device
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+  auto old_service_context = test::ServiceContext{host, port, storage_path};
+  old_device.CreateNewDevice(old_service_context, "pin");
+
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  auto service = service::IdentityService{
+    service_context.ClientSession(),
+    service_context.IdentityProvider()
+  };
+
+  auto req = std::make_unique<vereign::client_library::LoginFormPreviousAddedDevice>();
+  req->set_pin("pin");
+  auto resp = std::make_unique<vereign::client_library::EmptyResponse>();
+
+  service.LoginWithPreviouslyAddedDevice(req.get(), resp.get());
+
+  CHECK(resp->error() == "");
+  CHECK(resp->status() == "OK");
+  REQUIRE(resp->code() == "200");
+
+  // list passports with the logged device
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+
+  // std::cout << vereign::test::ProtobufToJson(*result.Response) << std::endl;
+
+  auto& list = result.Response;
+  CHECK(list->error() == "");
+  CHECK(list->status() == "OK");
+  REQUIRE(list->code() == "200");
+  CHECK(list->data().size() > 0);
+  for (const auto& passport : list->data()) {
+    CHECK(passport.uuid().size() == 36);
+  }
+}
diff --git a/cpp/tests/vereign/test/device.cc b/cpp/tests/vereign/test/device.cc
new file mode 100644
index 0000000000000000000000000000000000000000..bc47ef4c530a24b7f77e06fd1e9b305fe9b8b2a7
--- /dev/null
+++ b/cpp/tests/vereign/test/device.cc
@@ -0,0 +1,143 @@
+#include <vereign/test/device.hh>
+
+#include <vereign/client_library/common_types.pb.h>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/test/service_context.hh>
+#include <vereign/restapi/client_session.hh>
+#include <vereign/service/identity_service.hh>
+#include <vereign/identity/provider.hh>
+#include <vereign/client_library/identity_types.pb.h>
+
+#include <boost/filesystem/operations.hpp>
+
+namespace vereign::test {
+
+Device::Device(ServiceContext& service_context)
+  : service_context_{service_context},
+    identity_service_{std::make_unique<service::IdentityService>(
+      service_context_.ClientSession(),
+      service_context_.IdentityProvider()
+    )}
+{
+}
+
+Device::~Device() = default;
+
+void Device::Login(const std::string& public_key) {
+  auto req = std::make_unique<vereign::client_library::LoginWithExistingPubKeyForm>();
+  auto resp = std::make_unique<vereign::client_library::EmptyResponse>();
+  req->set_pubkey(public_key);
+
+  identity_service_->LoginWithExistingPubKey(req.get(), resp.get());
+
+  if (resp->code() != "200") {
+    throw std::runtime_error("login to test old device failed with: " + resp->error());
+  }
+}
+
+void Device::ConfirmNewDevice(const std::string& qr_code, const std::string& action_id) {
+  auto req = std::make_unique<vereign::client_library::ConfirmNewDeviceForm>();
+  req->set_code(qr_code);
+  req->set_actionid(action_id);
+  auto resp = std::make_unique<vereign::client_library::EmptyResponse>();
+
+  auto result = identity_service_->ConfirmNewDevice(req.get(), resp.get());
+  result.wait();
+
+  if (resp->code() != "200") {
+    throw std::runtime_error("confirm new device failed with: " + resp->error());
+  }
+}
+
+void Device::AuthorizeDevice(const std::string& device_hash) {
+  auto req = std::make_unique<client_library::EmptyRequest>();
+  auto resp = std::make_unique<client_library::ListDevicesHandlerFormResponse>();
+
+  auto result = identity_service_->ListDevices(req.get(), resp.get());
+  result.wait();
+  if (resp->code() != "200") {
+    throw std::runtime_error("authorize device failed with: " + resp->error());
+  }
+
+  std::string device_id;
+  for (auto& device : resp->data()) {
+    if (device.fingerprint() == device_hash) {
+      device_id = device.deviceid();
+      break;
+    }
+  }
+
+  if (device_id.empty()) {
+    throw std::runtime_error("authorize device failed with: device not found");
+  }
+
+  auto auth_req = std::make_unique<client_library::AuthorizeDeviceForm>();
+  auth_req->set_deviceid(device_id);
+  auto auth_resp = std::make_unique<client_library::EmptyResponse>();
+
+  auto auth_result = identity_service_->AuthorizeDevice(auth_req.get(), auth_resp.get());
+  auth_result.wait();
+  if (auth_resp->code() != "200") {
+    throw std::runtime_error("authorize device failed with: " + auth_resp->error());
+  }
+}
+
+void Device::CreateNewDevice(ServiceContext& service_context, const std::string& pin) {
+  auto service = service::IdentityService{
+    service_context.ClientSession(),
+    service_context.IdentityProvider()
+  };
+
+  // register new device
+  auto req = std::make_unique<vereign::client_library::LoginFormNewDevice>();
+  auto resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>();
+  req->set_pin(pin);
+
+  service.LoginWithNewDevice(req.get(), resp.get());
+  if (resp->code() != "200") {
+    throw std::runtime_error("creating new device failed with: " + resp->error());
+  }
+
+  // confirm and authorize the new device using an old device
+  ConfirmNewDevice(resp->data().qrcode(), resp->data().actionid());
+  AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash());
+}
+
+void PrepareNewDevice(
+  const std::string& host,
+  const std::string& port,
+  const std::string& public_key,
+  const std::string& pin,
+  const std::string& storage_path
+) {
+  // the old device is used later for new device confirmation and authorization
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto service_context = test::ServiceContext{host, port, fs::path::Join(storage_path, "db")};
+  auto identity_service = service::IdentityService{
+    service_context.ClientSession(),
+    service_context.IdentityProvider()
+  };
+
+  // register new device
+  auto register_req = std::make_unique<vereign::client_library::LoginFormNewDevice>();
+  auto register_resp = std::make_unique<vereign::client_library::LoginFormNewDeviceResponse>();
+  register_req->set_pin(pin);
+
+  identity_service.LoginWithNewDevice(register_req.get(), register_resp.get());
+  if (register_resp->code() != "200") {
+    throw std::runtime_error("register new device failed with: " + register_resp->error());
+  }
+
+  // confirm and authorize the new device using an old device
+  old_device.ConfirmNewDevice(register_resp->data().qrcode(), register_resp->data().actionid());
+  old_device.AuthorizeDevice(service_context.IdentityProvider().GetDeviceHash());
+}
+
+} // namespace vereign::test
diff --git a/cpp/tests/vereign/test/device.hh b/cpp/tests/vereign/test/device.hh
new file mode 100644
index 0000000000000000000000000000000000000000..9ad85bd8a42142f35f7c9a589244c2a8b25fd294
--- /dev/null
+++ b/cpp/tests/vereign/test/device.hh
@@ -0,0 +1,43 @@
+#ifndef __TESTS_VEREIGN_TEST_DEVICE_HH
+#define __TESTS_VEREIGN_TEST_DEVICE_HH
+
+#include <memory>
+#include <string>
+
+namespace vereign::service {
+class IdentityService;
+}
+
+namespace vereign::test {
+
+class ServiceContext;
+
+class Device {
+public:
+  Device(ServiceContext& service_context);
+  ~Device();
+
+  Device(const Device&) = delete;
+  auto operator=(const Device&) -> Device& = delete;
+
+  void Login(const std::string& public_key);
+  void ConfirmNewDevice(const std::string& qr_code, const std::string& action_id);
+  void AuthorizeDevice(const std::string& device_hash);
+  void CreateNewDevice(ServiceContext& service_context, const std::string& pin);
+
+private:
+  ServiceContext& service_context_;
+  std::unique_ptr<service::IdentityService> identity_service_;
+};
+
+void PrepareNewDevice(
+  const std::string& host,
+  const std::string& port,
+  const std::string& public_key,
+  const std::string& pin,
+  const std::string& storage_path
+);
+
+} // namespace vereign::test
+
+#endif // __TESTS_VEREIGN_TEST_DEVICE_HH
diff --git a/cpp/tests/vereign/test/service_context.cc b/cpp/tests/vereign/test/service_context.cc
new file mode 100644
index 0000000000000000000000000000000000000000..934ff9084eb440a3c536d4e0f58baaf9c3518b0a
--- /dev/null
+++ b/cpp/tests/vereign/test/service_context.cc
@@ -0,0 +1,54 @@
+#include <vereign/test/service_context.hh>
+
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/kvstore/crypto_storage.hh>
+#include <vereign/restapi/client.hh>
+#include <vereign/restapi/client_session.hh>
+#include <vereign/identity/provider.hh>
+
+#include <boost/filesystem/operations.hpp>
+
+namespace vereign::test {
+
+ServiceContext::ServiceContext(
+  const std::string& vereign_host,
+  const std::string& vereign_port,
+  std::string storage_path
+) : work_guard_{boost::asio::make_work_guard(ioc_)},
+    ssl_context_{boost::asio::ssl::context::tlsv12_client},
+    client_{std::make_unique<restapi::Client>(
+        ioc_, ssl_context_, vereign_host, vereign_port
+    )},
+    client_session_{std::make_unique<restapi::ClientSession>(*client_)},
+    storage_path_{std::move(storage_path)},
+    sqlite_storage_{std::make_unique<kvstore::SqliteStorage>(storage_path_)},
+    storage_{std::make_unique<kvstore::CryptoStorage>(*sqlite_storage_, true)},
+    identity_provider_{std::make_unique<identity::Provider>(*client_session_, *storage_)}
+{
+  service_thread_ = std::thread([this]() {
+    ioc_.run();
+  });
+}
+
+auto ServiceContext::IdentityProvider() -> identity::Provider& {
+  return *identity_provider_;
+}
+
+auto ServiceContext::ClientSession() -> restapi::ClientSession& {
+  return *client_session_;
+}
+
+void ServiceContext::Shutdown() {
+  client_session_->Close();
+
+  work_guard_.reset();
+  if (service_thread_.joinable()) {
+    service_thread_.join();
+  }
+}
+
+ServiceContext::~ServiceContext() {
+  Shutdown();
+}
+
+}
diff --git a/cpp/tests/vereign/test/service_context.hh b/cpp/tests/vereign/test/service_context.hh
new file mode 100644
index 0000000000000000000000000000000000000000..7666e29eba7aa135f3a048ef13d1162fd346ce4d
--- /dev/null
+++ b/cpp/tests/vereign/test/service_context.hh
@@ -0,0 +1,63 @@
+#ifndef __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH
+#define __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/asio/executor_work_guard.hpp>
+#include <boost/filesystem/path.hpp>
+
+#include <thread>
+
+namespace vereign {
+
+namespace restapi {
+class Client;
+class ClientSession;
+}
+
+namespace kvstore {
+class SqliteStorage;
+class CryptoStorage;
+}
+
+namespace identity {
+class Provider;
+}
+
+}
+
+namespace vereign::test {
+
+class ServiceContext {
+public:
+  ServiceContext(
+    const std::string& vereign_host,
+    const std::string& vereign_port,
+    std::string storage_path
+  );
+  ~ServiceContext();
+
+  ServiceContext(const ServiceContext&) = delete;
+  auto operator=(const ServiceContext&) -> ServiceContext& = delete;
+
+  auto IdentityProvider() -> identity::Provider&;
+  auto ClientSession() -> restapi::ClientSession&;
+
+  void Shutdown();
+
+private:
+  boost::asio::io_context ioc_;
+  boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_guard_;
+  boost::asio::ssl::context ssl_context_;
+  std::unique_ptr<restapi::Client> client_;
+  std::unique_ptr<restapi::ClientSession> client_session_;
+  std::string storage_path_;
+  std::unique_ptr<kvstore::SqliteStorage> sqlite_storage_;
+  std::unique_ptr<kvstore::CryptoStorage> storage_;
+  std::unique_ptr<identity::Provider> identity_provider_;
+  std::thread service_thread_;
+};
+
+} // namespace vereign::test
+
+#endif // __TESTS_VEREIGN_TEST_SERVICE_CONTEXT_HH
diff --git a/cpp/vendor/CMakeLists.txt b/cpp/vendor/CMakeLists.txt
index 7d4cb218c8cc4ae5f18c2a2d6a792071df35ab0d..c0f4ec13688b907308716fb24ca8e50b1f074f5a 100644
--- a/cpp/vendor/CMakeLists.txt
+++ b/cpp/vendor/CMakeLists.txt
@@ -53,6 +53,7 @@ include(boring_ssl.cmake)
 include(boost.cmake)
 include(grpc.cmake)
 include(nlohmann.cmake)
+include(sqlite3.cmake)
 
 string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type)
 message(STATUS "Summary:
diff --git a/cpp/vendor/boost.cmake b/cpp/vendor/boost.cmake
index 430902d781c72e6b107b4709b13a84f7160a1220..4d49081cc71229db423067033cdcd9551195644a 100644
--- a/cpp/vendor/boost.cmake
+++ b/cpp/vendor/boost.cmake
@@ -1,6 +1,6 @@
 include(ExternalProject)
 
-set(_boost_libs regex system thread date_time)
+set(_boost_libs regex system thread date_time filesystem)
 
 if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
   list(TRANSFORM _boost_libs PREPEND --with-)
diff --git a/cpp/vendor/cmake/sqlite3/CMakeLists.txt b/cpp/vendor/cmake/sqlite3/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c203d8643bb166579d9785a215cedc7ddfa5b104
--- /dev/null
+++ b/cpp/vendor/cmake/sqlite3/CMakeLists.txt
@@ -0,0 +1,10 @@
+cmake_minimum_required (VERSION 3.16.5)
+
+project (sqlite3)
+
+add_library(sqlite3 STATIC
+  sqlite3.c
+)
+
+install(TARGETS sqlite3 DESTINATION lib)
+install(FILES sqlite3.h sqlite3ext.h DESTINATION include)
diff --git a/cpp/vendor/sqlite3.cmake b/cpp/vendor/sqlite3.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..6c39ac93514e8c9795628a98d9007df8181fddf0
--- /dev/null
+++ b/cpp/vendor/sqlite3.cmake
@@ -0,0 +1,29 @@
+
+ExternalProject_Add(sqlite3lib
+  PREFIX sqlite3
+  URL https://www.sqlite.org/2020/sqlite-amalgamation-3320300.zip
+  URL_HASH SHA1=0c805bea134712a903290a26b2a61c3a8a3bd8cc
+  INSTALL_DIR ${VENDOR_INSTALL_DIR}/sqlite3
+
+  USES_TERMINAL_DOWNLOAD ON
+  USES_TERMINAL_UPDATE ON
+  USES_TERMINAL_CONFIGURE ON
+  USES_TERMINAL_BUILD ON
+  USES_TERMINAL_INSTALL ON
+
+  UPDATE_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/cmake/sqlite3/CMakeLists.txt <SOURCE_DIR>/CMakeLists.txt
+
+  CMAKE_CACHE_ARGS
+  -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+  -DCMAKE_MSVC_RUNTIME_LIBRARY:STRING=${CMAKE_MSVC_RUNTIME_LIBRARY}
+  -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER}
+  -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER}
+  -DCMAKE_C_FLAGS:STRING=${CMAKE_C_FLAGS}
+  -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
+  -DCMAKE_C_FLAGS_DEBUG:STRING=${CMAKE_C_FLAGS_DEBUG}
+  -DCMAKE_CXX_FLAGS_DEBUG:STRING=${CMAKE_CXX_FLAGS_DEBUG}
+  -DCMAKE_C_FLAGS_RELEASE:STRING=${CMAKE_C_FLAGS_RELEASE}
+  -DCMAKE_CXX_FLAGS_RELEASE:STRING=${CMAKE_CXX_FLAGS_RELEASE}
+  -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON
+  -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
+)
diff --git a/javascript/src/constants/secrets.js b/javascript/src/constants/secrets.js
new file mode 100644
index 0000000000000000000000000000000000000000..41b9ef5ee76b93cffcc5141a9534e013ff065dbb
--- /dev/null
+++ b/javascript/src/constants/secrets.js
@@ -0,0 +1,2 @@
+export const RECOMMENDED_TRUSTEES = 3;
+export const THRESHOLD = 2;
diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js
index ce44402be8618dc5cdfa925a0d991ab0b2f9ee37..ecfa3dc105d501b7d5a6ba572142a667e4bb0544 100644
--- a/javascript/src/iframe/viamapi-iframe.js
+++ b/javascript/src/iframe/viamapi-iframe.js
@@ -41,6 +41,12 @@ import {
   STATUS_USER_BLOCKED
 } from "../constants/statuses";
 import generateQrCode from "../utilities/generateQrCode";
+import {
+  generateRecoveryKey,
+  getRecoveryKeyShares,
+  checkRecoveryKeyCombine,
+  encryptShare
+} from "../utilities/secrets";
 
 const penpalMethods = require("../../temp/penpal-methods").default;
 const WopiAPI = require("./wopiapi-iframe");
@@ -271,14 +277,14 @@ window.lastTimeGetProfile = 0;
 let iframeParent = null;
 
 const handleIdentityLogin = (identity, uuid, token) => {
-  const { loadedIdentities, viamApi } = window;
+  const { viamApi, loadedIdentities } = window;
   const { publicKey } = identity.authentication;
-
   viamApi.setSessionData(uuid, token);
   localStorage.setItem("uuid", uuid);
   localStorage.setItem("token", token);
   localStorage.setItem("authenticatedIdentity", publicKey);
-  window.currentlyAuthenticatedIdentity = loadedIdentities[publicKey];
+  window.currentlyAuthenticatedIdentity =
+    loadedIdentities[publicKey] || identity;
   window.lastTimeGetProfile = 0;
   setKeyForUUID(uuid, publicKey);
 };
@@ -348,6 +354,7 @@ async function executeRestfulFunction(type, that, fn, config, ...args) {
     null,
     "previousaddeddevice"
   );
+
   if (loginResponse.data.code !== "200") {
     return loginResponse.data;
   }
@@ -434,7 +441,7 @@ function getCertificateForPassport(passportUUID, internal) {
     const passportIdentity = window.currentlyAuthenticatedIdentity;
     const passport = passportIdentity.getPassport(passportUUID);
     if (passport === undefined || passport === null) {
-      createPassportCertificate(passportUUID).then(function(keys) {
+      createPassportCertificate(passportUUID).then(function (keys) {
         const cryptoData = new CryptoData();
         cryptoData.setPublicKey(keys["publicKeyPEM"]);
         cryptoData.setPrivateKey(keys["privateKeyPEM"]);
@@ -552,7 +559,7 @@ const connection = Penpal.connectToParent({
     ...penpalMethods,
     createIdentity(pinCode) {
       return new Penpal.Promise(result => {
-        createPassportCertificate(makeid()).then(function(keys) {
+        createPassportCertificate(makeid()).then(function (keys) {
           const newIdentity = new Identity();
           const cryptoData = new CryptoData();
           cryptoData.setPublicKey(keys["publicKeyPEM"]);
@@ -562,6 +569,10 @@ const connection = Penpal.connectToParent({
           newIdentity.setPinCode(pinCode);
 
           window.currentlyLoadedIdentity = newIdentity;
+          localStorage.setItem(
+            "currentlyLoadedIdentity",
+            JSON.stringify(newIdentity)
+          );
           const { publicKey, x509Certificate } = newIdentity.authentication;
 
           window.loadedIdentities[publicKey] = newIdentity;
@@ -733,10 +744,7 @@ const connection = Penpal.connectToParent({
         });
       });
     },
-    finalizeEmployeeRegistration: async (
-      identity,
-      identifier
-    ) => {
+    finalizeEmployeeRegistration: async (identity, identifier) => {
       viamApi.setIdentity(identity.authentication.publicKey);
       return executeRestfulFunction(
         "public",
@@ -821,16 +829,9 @@ const connection = Penpal.connectToParent({
       const responseToClient = Object.assign({}, identityLoginResponse);
 
       if (code === "200") {
-        if (
-          mode === LOGIN_MODES.SMS ||
-          mode === LOGIN_MODES.PREVIOUSLY_ADDED_DEVICE
-        ) {
+        if (mode === LOGIN_MODES.PREVIOUSLY_ADDED_DEVICE) {
           handleIdentityLogin(loginIdentity, data.Uuid, data.Session);
           await getProfileData(loginIdentity);
-
-          if (mode === LOGIN_MODES.SMS) {
-            await setIdentityInLocalStorage(loginIdentity);
-          }
         } else if (mode === LOGIN_MODES.NEW_DEVICE) {
           const dataUrl = await generateQrCode(
             `${data.ActionID},${data.QrCode}`
@@ -970,7 +971,7 @@ const connection = Penpal.connectToParent({
         };
       }
     },
-    identityRestoreAccess(restoreAccessIdentity, identificator) {
+    identityRestoreAccess(restoreAccessIdentity, identificator, restoreType) {
       return new Penpal.Promise(result => {
         viamApi.setSessionData("", "");
         viamApi.setIdentity(restoreAccessIdentity.authentication.publicKey);
@@ -980,12 +981,164 @@ const connection = Penpal.connectToParent({
           viamApi,
           viamApi.identityRestoreAccess,
           null,
-          identificator
+          identificator,
+          restoreType
         ).then(executeResult => {
           result(executeResult);
         });
       });
     },
+    identityInitiateSocialRecovery: async accessToken => {
+      const response = await executeRestfulFunction(
+        "public",
+        viamApi,
+        viamApi.identityInitiateSocialRecovery,
+        null,
+        accessToken
+      );
+
+      return response;
+    },
+    contactsCheckAccountRecoveryStatus: async () => {
+      const currentlyLoadedIdentity = localStorage.getItem(
+        "currentlyLoadedIdentity"
+      );
+      const identity = new Identity(currentlyLoadedIdentity);
+      window.currentlyLoadedIdentity = identity;
+      const { publicKey } = identity.authentication;
+      window.loadedIdentities[publicKey] = identity;
+      window.viamAnonymousApi.setIdentity(publicKey);
+      window.viamApi.setSessionData("", "");
+      window.viamApi.setIdentity(publicKey);
+
+      const timeout = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+      async function checkAccountRecoveryStatus() {
+        const response = await executeRestfulFunction(
+          "public",
+          viamApi,
+          viamApi.contactsCheckAccountRecoveryStatus,
+          null
+        );
+
+        if (response.data === 0) {
+          await timeout(1000);
+          await checkAccountRecoveryStatus();
+          return;
+        }
+
+        const deviceHash = await createDeviceHash(publicKey);
+        window.viamApi.setDeviceHash(deviceHash);
+
+        const identityLoginResponse = await executeRestfulFunction(
+          "public",
+          window.viamApi,
+          window.viamApi.identityLogin,
+          null,
+          "previousaddeddevice"
+        );
+
+        const { code, data } = identityLoginResponse;
+
+        if (code === "200") {
+          await setIdentityInLocalStorage(identity);
+          handleIdentityLogin(identity, data.Uuid, data.Session);
+          await getProfileData(identity);
+          localStorage.removeItem("currentlyLoadedIdentity");
+        }
+      }
+
+      await checkAccountRecoveryStatus();
+    },
+    contactsGetTrusteeContactsPublicKeys: async () => {
+      try {
+        const response = await executeRestfulFunction(
+          "private",
+          window.viamApi,
+          window.viamApi.contactsGetTrusteeContactsPublicKeys,
+          null
+        );
+
+        if (response.code !== "200") {
+          return response;
+        }
+
+        const responseData = response.data;
+        const trusteesDevices = Object.values(responseData);
+
+        /** Check if there are new trustees without added secret part */
+        const hasNewTrustees = trusteesDevices.some(device => {
+          const deviceData = Object.values(device);
+          return deviceData.some(data => data.hasShamir === "0");
+        });
+
+        if (!hasNewTrustees) {
+          return response;
+        }
+
+        // Generate and split recovery key
+        const trusteesUuids = Object.keys(responseData);
+        const trusteesToDevices = Object.entries(responseData);
+        const sharesNumber = trusteesUuids.length;
+        const recoveryKey = generateRecoveryKey();
+        let recoveryKeyShares = [recoveryKey];
+        // Split the secret when sharesNumber is more than 1 because VereignPublicKey is always returned
+        if (sharesNumber > 1) {
+          recoveryKeyShares = getRecoveryKeyShares(recoveryKey, sharesNumber);
+          const sanityCheckResponse = checkRecoveryKeyCombine(
+            recoveryKey,
+            recoveryKeyShares
+          );
+
+          if (sanityCheckResponse.code !== "200") {
+            return sanityCheckResponse;
+          }
+        }
+
+        // Encrypt each share with every publicKey of each contact device
+        const shamirPartsList = await Promise.all(
+          trusteesToDevices.map(async ([contactUuid, device], index) => {
+            const deviceIdsToPublicKeys = Object.entries(device);
+            // Encrypt secret shares in parallel
+            const deviceIdsToEncryptedPartsList = await Promise.all(
+              deviceIdsToPublicKeys.map(async ([deviceId, { content }]) => {
+                const encryptedShare = await encryptShare(
+                  recoveryKeyShares[index],
+                  content
+                );
+
+                return [deviceId, encryptedShare];
+              })
+            );
+            // Turn deviceIdsToEncryptedPartsList array to object
+            const deviceIdsToEncryptedParts = Object.fromEntries(
+              deviceIdsToEncryptedPartsList
+            );
+
+            return [contactUuid, deviceIdsToEncryptedParts];
+          })
+        );
+        // Turn shamirPartsList array to object
+        const shamirParts = Object.fromEntries(shamirPartsList);
+
+        // Save Shamir parts to database
+        const saveShamirPartsResponse = await executeRestfulFunction(
+          "private",
+          window.viamApi,
+          window.viamApi.contactsSaveShamirParts,
+          null,
+          shamirParts
+        );
+
+        if (saveShamirPartsResponse.code !== "200") {
+          return saveShamirPartsResponse;
+        }
+
+        return response;
+      } catch (error) {
+        return encodeResponse("400", "", error.message);
+      }
+    },
     parseSMIME,
     getCurrentlyLoggedInUUID() {
       return new Penpal.Promise(result => {
@@ -1083,7 +1236,7 @@ const connection = Penpal.connectToParent({
                 emailArg,
                 passportPrivateKey,
                 passportCertificate
-              ).then(function(keys) {
+              ).then(function (keys) {
                 const publicKeyOneTime = keys["publicKeyPEM"];
                 const privateKeyOneTime = keys["privateKeyPEM"];
                 const certificateOneTime = keys["certificatePEM"];
@@ -1426,10 +1579,7 @@ const connection = Penpal.connectToParent({
         vCardImageClaimValue = vCardClaimResponse.data;
       }
 
-      if (
-        vCardImageClaimValue &&
-        "state" in vCardImageClaimValue
-      ) {
+      if (vCardImageClaimValue && "state" in vCardImageClaimValue) {
         return encodeResponse("200", vCardImageClaimValue.state, "OK");
       }
 
@@ -1489,14 +1639,13 @@ message SignatureData {
         return encodeResponse("400", "", "Identity not authenticated");
       }
 
-
       // Get vCard and QR Code Coordinates
 
       let vCardImageData;
       let vCardImageClaimValue;
 
       let qrCodeImageData;
-      let qrCodeCoordinates = {fromL: -1, fromR: -1, toL: -1, toR: -1};
+      let qrCodeCoordinates = { fromL: -1, fromR: -1, toL: -1, toR: -1 };
 
       if (signatureData) {
         const vCardImageClaimName = "vCardImage";
@@ -1682,7 +1831,7 @@ message SignatureData {
       let vCardImageClaimValue;
 
       let qrCodeImageData;
-      let qrCodeCoordinates = {fromL: -1, fromR: -1, toL: -1, toR: -1};
+      let qrCodeCoordinates = { fromL: -1, fromR: -1, toL: -1, toR: -1 };
 
       const vCardImageClaimName = "vCardImage";
       const defaultTagName = "notag";
@@ -1818,7 +1967,6 @@ message SignatureData {
 
       passportChain.reverse();
 
-
       const signVCardResponse = await executeRestfulFunction(
         "private",
         window.viamApi,
@@ -1936,7 +2084,7 @@ message SignatureData {
 
       return encodeResponse("200", response.data, "Document created");
     },
-    getVcardWithQrCode: async (passportUUID, QRCodeContent = null) =>{
+    getVcardWithQrCode: async (passportUUID, QRCodeContent = null) => {
       //TODO: IMPLEMENT QR CODE backend method needed
       const authenticationPublicKey = localStorage.getItem(
         "authenticatedIdentity"
@@ -2004,7 +2152,7 @@ message SignatureData {
           );
         }
       }
-      return  encodeResponse("200",vCardImageData, 'vCard got');
+      return encodeResponse("200", vCardImageData, "vCard got");
     },
     documentPutDocument: async (
       passportUUID,
@@ -2422,7 +2570,7 @@ connection.promise.then(parent => {
   let previousLocalStorageToken;
   let previousLocalStorageIdentity;
 
-  setInterval(async function() {
+  setInterval(async function () {
     if (window.currentlyAuthenticatedIdentity) {
       const { authentication } = window.currentlyAuthenticatedIdentity;
       const pinCode = getPincode(authentication.publicKey);
@@ -2467,6 +2615,8 @@ connection.promise.then(parent => {
         identityAuthenticatedEvent = false;
         window.currentlyLoadedIdentity = null;
       }
+
+      localStorage.removeItem("currentlyLoadedIdentity");
     }
 
     if (window.currentlyLoadedIdentity) {
diff --git a/javascript/src/lib/secrets.js b/javascript/src/lib/secrets.js
new file mode 100644
index 0000000000000000000000000000000000000000..9da60b9782fa0494d2827ae9c0b2193994a7d70d
--- /dev/null
+++ b/javascript/src/lib/secrets.js
@@ -0,0 +1,1043 @@
+// @preserve author Alexander Stetsyuk
+// @preserve author Glenn Rempe <glenn@rempe.us>
+// @license MIT
+
+/*jslint passfail: false, bitwise: true, nomen: true, plusplus: true, todo: false, maxerr: 1000 */
+/*global define, require, module, exports, window, Uint32Array */
+
+// eslint : http://eslint.org/docs/configuring/
+/*eslint-env node, browser, jasmine */
+/*eslint no-underscore-dangle:0 */
+
+// UMD (Universal Module Definition)
+// Uses Node, AMD or browser globals to create a module. This module creates
+// a global even when AMD is used. This is useful if you have some scripts
+// that are loaded by an AMD loader, but they still want access to globals.
+// See : https://github.com/umdjs/umd
+// See : https://github.com/umdjs/umd/blob/master/returnExportsGlobal.js
+//
+(function(root, factory) {
+  "use strict";
+
+  if (typeof define === "function" && define.amd) {
+    // AMD. Register as an anonymous module.
+    define([], function() {
+      /*eslint-disable no-return-assign */
+      return (root.secrets = factory());
+      /*eslint-enable no-return-assign */
+    });
+  } else if (typeof exports === "object") {
+    // Node. Does not work with strict CommonJS, but
+    // only CommonJS-like environments that support module.exports,
+    // like Node.
+    module.exports = factory(require("crypto"));
+  } else {
+    // Browser globals (root is window)
+    root.secrets = factory(root.crypto);
+  }
+})(this, function() {
+  "use strict";
+  let crypto;
+  try {
+    crypto = require("crypto");
+  } catch (err) {
+    console.warn("crypto support is disabled!");
+  }
+
+  if (!crypto) {
+    crypto = window.crypto;
+  }
+
+  var defaults, config, preGenPadding, runCSPRNGTest, CSPRNGTypes;
+
+  function reset() {
+    defaults = {
+      bits: 8, // default number of bits
+      radix: 16, // work with HEX by default
+      minBits: 3,
+      maxBits: 20, // this permits 1,048,575 shares, though going this high is NOT recommended in JS!
+      bytesPerChar: 2,
+      maxBytesPerChar: 6, // Math.pow(256,7) > Math.pow(2,53)
+
+      // Primitive polynomials (in decimal form) for Galois Fields GF(2^n), for 2 <= n <= 30
+      // The index of each term in the array corresponds to the n for that polynomial
+      // i.e. to get the polynomial for n=16, use primitivePolynomials[16]
+      primitivePolynomials: [
+        null,
+        null,
+        1,
+        3,
+        3,
+        5,
+        3,
+        3,
+        29,
+        17,
+        9,
+        5,
+        83,
+        27,
+        43,
+        3,
+        45,
+        9,
+        39,
+        39,
+        9,
+        5,
+        3,
+        33,
+        27,
+        9,
+        71,
+        39,
+        9,
+        5,
+        83
+      ]
+    };
+    config = {};
+    preGenPadding = new Array(1024).join("0"); // Pre-generate a string of 1024 0's for use by padLeft().
+    runCSPRNGTest = true;
+
+    // WARNING : Never use 'testRandom' except for testing.
+    CSPRNGTypes = [
+      "nodeCryptoRandomBytes",
+      "browserCryptoGetRandomValues",
+      "testRandom"
+    ];
+  }
+
+  function isSetRNG() {
+    if (config && config.rng && typeof config.rng === "function") {
+      return true;
+    }
+
+    return false;
+  }
+
+  // Pads a string `str` with zeros on the left so that its length is a multiple of `bits`
+  function padLeft(str, multipleOfBits) {
+    var missing;
+
+    if (multipleOfBits === 0 || multipleOfBits === 1) {
+      return str;
+    }
+
+    if (multipleOfBits && multipleOfBits > 1024) {
+      throw new Error("Padding must be multiples of no larger than 1024 bits.");
+    }
+
+    multipleOfBits = multipleOfBits || config.bits;
+
+    if (str) {
+      missing = str.length % multipleOfBits;
+    }
+
+    if (missing) {
+      return (preGenPadding + str).slice(
+        -(multipleOfBits - missing + str.length)
+      );
+    }
+
+    return str;
+  }
+
+  function hex2bin(str) {
+    var bin = "",
+      num,
+      i;
+
+    for (i = str.length - 1; i >= 0; i--) {
+      num = parseInt(str[i], 16);
+
+      if (isNaN(num)) {
+        throw new Error("Invalid hex character.");
+      }
+
+      bin = padLeft(num.toString(2), 4) + bin;
+    }
+    return bin;
+  }
+
+  function bin2hex(str) {
+    var hex = "",
+      num,
+      i;
+
+    str = padLeft(str, 4);
+
+    for (i = str.length; i >= 4; i -= 4) {
+      num = parseInt(str.slice(i - 4, i), 2);
+      if (isNaN(num)) {
+        throw new Error("Invalid binary character.");
+      }
+      hex = num.toString(16) + hex;
+    }
+
+    return hex;
+  }
+
+  // Browser supports crypto.getRandomValues()
+  function hasCryptoGetRandomValues() {
+    if (
+      crypto &&
+      typeof crypto === "object" &&
+      (typeof crypto.getRandomValues === "function" ||
+        typeof crypto.getRandomValues === "object") &&
+      (typeof Uint32Array === "function" || typeof Uint32Array === "object")
+    ) {
+      return true;
+    }
+
+    return false;
+  }
+
+  // Node.js support for crypto.randomBytes()
+  function hasCryptoRandomBytes() {
+    if (
+      typeof crypto === "object" &&
+      typeof crypto.randomBytes === "function"
+    ) {
+      return true;
+    }
+
+    return false;
+  }
+
+  // Returns a pseudo-random number generator of the form function(bits){}
+  // which should output a random string of 1's and 0's of length `bits`.
+  // `type` (Optional) : A string representing the CSPRNG that you want to
+  // force to be loaded, overriding feature detection. Can be one of:
+  //    "nodeCryptoRandomBytes"
+  //    "browserCryptoGetRandomValues"
+  //
+  function getRNG(type) {
+    function construct(bits, arr, radix, size) {
+      var i = 0,
+        len,
+        str = "",
+        parsedInt;
+
+      if (arr) {
+        len = arr.length - 1;
+      }
+
+      while (i < len || str.length < bits) {
+        // convert any negative nums to positive with Math.abs()
+        parsedInt = Math.abs(parseInt(arr[i], radix));
+        str = str + padLeft(parsedInt.toString(2), size);
+        i++;
+      }
+
+      str = str.substr(-bits);
+
+      // return null so this result can be re-processed if the result is all 0's.
+      if ((str.match(/0/g) || []).length === str.length) {
+        return null;
+      }
+
+      return str;
+    }
+
+    // Node.js : crypto.randomBytes()
+    // Note : Node.js and crypto.randomBytes() uses the OpenSSL RAND_bytes() function for its CSPRNG.
+    //        Node.js will need to have been compiled with OpenSSL for this to work.
+    // See : https://github.com/joyent/node/blob/d8baf8a2a4481940bfed0196308ae6189ca18eee/src/node_crypto.cc#L4696
+    // See : https://www.openssl.org/docs/crypto/rand.html
+    function nodeCryptoRandomBytes(bits) {
+      var buf,
+        bytes,
+        radix,
+        size,
+        str = null;
+
+      radix = 16;
+      size = 4;
+      bytes = Math.ceil(bits / 8);
+
+      while (str === null) {
+        buf = crypto.randomBytes(bytes);
+        str = construct(bits, buf.toString("hex"), radix, size);
+      }
+
+      return str;
+    }
+
+    // Browser : crypto.getRandomValues()
+    // See : https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-Crypto
+    // See : https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues
+    // Supported Browsers : http://caniuse.com/#search=crypto.getRandomValues
+    function browserCryptoGetRandomValues(bits) {
+      var elems,
+        radix,
+        size,
+        str = null;
+
+      radix = 10;
+      size = 32;
+      elems = Math.ceil(bits / 32);
+      while (str === null) {
+        str = construct(
+          bits,
+          crypto.getRandomValues(new Uint32Array(elems)),
+          radix,
+          size
+        );
+      }
+
+      return str;
+    }
+
+    // /////////////////////////////////////////////////////////////
+    // WARNING : DO NOT USE. For testing purposes only.
+    // /////////////////////////////////////////////////////////////
+    // This function will return repeatable non-random test bits. Can be used
+    // for testing only. Node.js does not return proper random bytes
+    // when run within a PhantomJS container.
+    function testRandom(bits) {
+      var arr,
+        elems,
+        int,
+        radix,
+        size,
+        str = null;
+
+      radix = 10;
+      size = 32;
+      elems = Math.ceil(bits / 32);
+      int = 123456789;
+      arr = new Uint32Array(elems);
+
+      // Fill every element of the Uint32Array with the same int.
+      for (var i = 0; i < arr.length; i++) {
+        arr[i] = int;
+      }
+
+      while (str === null) {
+        str = construct(bits, arr, radix, size);
+      }
+
+      return str;
+    }
+
+    // Return a random generator function for browsers that support
+    // crypto.getRandomValues() or Node.js compiled with OpenSSL support.
+    // WARNING : NEVER use testRandom outside of a testing context. Totally non-random!
+    if (type && type === "testRandom") {
+      config.typeCSPRNG = type;
+      return testRandom;
+    } else if (type && type === "nodeCryptoRandomBytes") {
+      config.typeCSPRNG = type;
+      return nodeCryptoRandomBytes;
+    } else if (type && type === "browserCryptoGetRandomValues") {
+      config.typeCSPRNG = type;
+      return browserCryptoGetRandomValues;
+    } else if (hasCryptoRandomBytes()) {
+      config.typeCSPRNG = "nodeCryptoRandomBytes";
+      return nodeCryptoRandomBytes;
+    } else if (hasCryptoGetRandomValues()) {
+      config.typeCSPRNG = "browserCryptoGetRandomValues";
+      return browserCryptoGetRandomValues;
+    }
+  }
+
+  // Splits a number string `bits`-length segments, after first
+  // optionally zero-padding it to a length that is a multiple of `padLength.
+  // Returns array of integers (each less than 2^bits-1), with each element
+  // representing a `bits`-length segment of the input string from right to left,
+  // i.e. parts[0] represents the right-most `bits`-length segment of the input string.
+  function splitNumStringToIntArray(str, padLength) {
+    var parts = [],
+      i;
+
+    if (padLength) {
+      str = padLeft(str, padLength);
+    }
+
+    for (i = str.length; i > config.bits; i -= config.bits) {
+      parts.push(parseInt(str.slice(i - config.bits, i), 2));
+    }
+
+    parts.push(parseInt(str.slice(0, i), 2));
+
+    return parts;
+  }
+
+  // Polynomial evaluation at `x` using Horner's Method
+  // NOTE: fx=fx * x + coeff[i] ->  exp(log(fx) + log(x)) + coeff[i],
+  //       so if fx===0, just set fx to coeff[i] because
+  //       using the exp/log form will result in incorrect value
+  function horner(x, coeffs) {
+    var logx = config.logs[x],
+      fx = 0,
+      i;
+
+    for (i = coeffs.length - 1; i >= 0; i--) {
+      if (fx !== 0) {
+        fx =
+          config.exps[(logx + config.logs[fx]) % config.maxShares] ^ coeffs[i];
+      } else {
+        fx = coeffs[i];
+      }
+    }
+
+    return fx;
+  }
+
+  // Evaluate the Lagrange interpolation polynomial at x = `at`
+  // using x and y Arrays that are of the same length, with
+  // corresponding elements constituting points on the polynomial.
+  function lagrange(at, x, y) {
+    var sum = 0,
+      len,
+      product,
+      i,
+      j;
+
+    for (i = 0, len = x.length; i < len; i++) {
+      if (y[i]) {
+        product = config.logs[y[i]];
+
+        for (j = 0; j < len; j++) {
+          if (i !== j) {
+            if (at === x[j]) {
+              // happens when computing a share that is in the list of shares used to compute it
+              product = -1; // fix for a zero product term, after which the sum should be sum^0 = sum, not sum^1
+              break;
+            }
+            product =
+              (product +
+                config.logs[at ^ x[j]] -
+                config.logs[x[i] ^ x[j]] +
+                config.maxShares) %
+              config.maxShares; // to make sure it's not negative
+          }
+        }
+
+        // though exps[-1] === undefined and undefined ^ anything = anything in
+        // chrome, this behavior may not hold everywhere, so do the check
+        sum = product === -1 ? sum : sum ^ config.exps[product];
+      }
+    }
+
+    return sum;
+  }
+
+  // This is the basic polynomial generation and evaluation function
+  // for a `config.bits`-length secret (NOT an arbitrary length)
+  // Note: no error-checking at this stage! If `secret` is NOT
+  // a NUMBER less than 2^bits-1, the output will be incorrect!
+  function getShares(secret, numShares, threshold) {
+    var shares = [],
+      coeffs = [secret],
+      i,
+      len;
+
+    for (i = 1; i < threshold; i++) {
+      coeffs[i] = parseInt(config.rng(config.bits), 2);
+    }
+
+    for (i = 1, len = numShares + 1; i < len; i++) {
+      shares[i - 1] = {
+        x: i,
+        y: horner(i, coeffs)
+      };
+    }
+
+    return shares;
+  }
+
+  function constructPublicShareString(bits, id, data) {
+    var bitsBase36, idHex, idMax, idPaddingLen, newShareString;
+
+    id = parseInt(id, config.radix);
+    bits = parseInt(bits, 10) || config.bits;
+    bitsBase36 = bits.toString(36).toUpperCase();
+    idMax = Math.pow(2, bits) - 1;
+    idPaddingLen = idMax.toString(config.radix).length;
+    idHex = padLeft(id.toString(config.radix), idPaddingLen);
+
+    if (typeof id !== "number" || id % 1 !== 0 || id < 1 || id > idMax) {
+      throw new Error(
+        "Share id must be an integer between 1 and " + idMax + ", inclusive."
+      );
+    }
+
+    newShareString = bitsBase36 + idHex + data;
+
+    return newShareString;
+  }
+
+  // EXPORTED FUNCTIONS
+  // //////////////////
+
+  var secrets = {
+    init: function(bits, rngType) {
+      var logs = [],
+        exps = [],
+        x = 1,
+        primitive,
+        i;
+
+      // reset all config back to initial state
+      reset();
+
+      if (
+        bits &&
+        (typeof bits !== "number" ||
+          bits % 1 !== 0 ||
+          bits < defaults.minBits ||
+          bits > defaults.maxBits)
+      ) {
+        throw new Error(
+          "Number of bits must be an integer between " +
+            defaults.minBits +
+            " and " +
+            defaults.maxBits +
+            ", inclusive."
+        );
+      }
+
+      if (rngType && CSPRNGTypes.indexOf(rngType) === -1) {
+        throw new Error("Invalid RNG type argument : '" + rngType + "'");
+      }
+
+      config.radix = defaults.radix;
+      config.bits = bits || defaults.bits;
+      config.size = Math.pow(2, config.bits);
+      config.maxShares = config.size - 1;
+
+      // Construct the exp and log tables for multiplication.
+      primitive = defaults.primitivePolynomials[config.bits];
+
+      for (i = 0; i < config.size; i++) {
+        exps[i] = x;
+        logs[x] = i;
+        x = x << 1; // Left shift assignment
+        if (x >= config.size) {
+          x = x ^ primitive; // Bitwise XOR assignment
+          x = x & config.maxShares; // Bitwise AND assignment
+        }
+      }
+
+      config.logs = logs;
+      config.exps = exps;
+
+      if (rngType) {
+        this.setRNG(rngType);
+      }
+
+      if (!isSetRNG()) {
+        this.setRNG();
+      }
+
+      if (
+        !isSetRNG() ||
+        !config.bits ||
+        !config.size ||
+        !config.maxShares ||
+        !config.logs ||
+        !config.exps ||
+        config.logs.length !== config.size ||
+        config.exps.length !== config.size
+      ) {
+        throw new Error("Initialization failed.");
+      }
+    },
+
+    // Evaluates the Lagrange interpolation polynomial at x=`at` for
+    // individual config.bits-length segments of each share in the `shares`
+    // Array. Each share is expressed in base `inputRadix`. The output
+    // is expressed in base `outputRadix'.
+    combine: function(shares, at) {
+      var i,
+        j,
+        len,
+        len2,
+        result = "",
+        setBits,
+        share,
+        splitShare,
+        x = [],
+        y = [];
+
+      at = at || 0;
+
+      for (i = 0, len = shares.length; i < len; i++) {
+        share = this.extractShareComponents(shares[i]);
+
+        // All shares must have the same bits settings.
+        if (setBits === undefined) {
+          setBits = share.bits;
+        } else if (share.bits !== setBits) {
+          throw new Error("Mismatched shares: Different bit settings.");
+        }
+
+        // Reset everything to the bit settings of the shares.
+        if (config.bits !== setBits) {
+          this.init(setBits);
+        }
+
+        // Proceed if this share.id is not already in the Array 'x' and
+        // then split each share's hex data into an Array of Integers,
+        // then 'rotate' those arrays where the first element of each row is converted to
+        // its own array, the second element of each to its own Array, and so on for all of the rest.
+        // Essentially zipping all of the shares together.
+        //
+        // e.g.
+        //   [ 193, 186, 29, 150, 5, 120, 44, 46, 49, 59, 6, 1, 102, 98, 177, 196 ]
+        //   [ 53, 105, 139, 49, 187, 240, 91, 92, 98, 118, 12, 2, 204, 196, 127, 149 ]
+        //   [ 146, 211, 249, 167, 209, 136, 118, 114, 83, 77, 10, 3, 170, 166, 206, 81 ]
+        //
+        // becomes:
+        //
+        // [ [ 193, 53, 146 ],
+        //   [ 186, 105, 211 ],
+        //   [ 29, 139, 249 ],
+        //   [ 150, 49, 167 ],
+        //   [ 5, 187, 209 ],
+        //   [ 120, 240, 136 ],
+        //   [ 44, 91, 118 ],
+        //   [ 46, 92, 114 ],
+        //   [ 49, 98, 83 ],
+        //   [ 59, 118, 77 ],
+        //   [ 6, 12, 10 ],
+        //   [ 1, 2, 3 ],
+        //   [ 102, 204, 170 ],
+        //   [ 98, 196, 166 ],
+        //   [ 177, 127, 206 ],
+        //   [ 196, 149, 81 ] ]
+        //
+        if (x.indexOf(share.id) === -1) {
+          x.push(share.id);
+          splitShare = splitNumStringToIntArray(hex2bin(share.data));
+          for (j = 0, len2 = splitShare.length; j < len2; j++) {
+            y[j] = y[j] || [];
+            y[j][x.length - 1] = splitShare[j];
+          }
+        }
+      }
+
+      // Extract the secret from the 'rotated' share data and return a
+      // string of Binary digits which represent the secret directly. or in the
+      // case of a newShare() return the binary string representing just that
+      // new share.
+      for (i = 0, len = y.length; i < len; i++) {
+        result = padLeft(lagrange(at, x, y[i]).toString(2)) + result;
+      }
+
+      // If 'at' is non-zero combine() was called from newShare(). In this
+      // case return the result (the new share data) directly.
+      //
+      // Otherwise find the first '1' which was added in the share() function as a padding marker
+      // and return only the data after the padding and the marker. Convert this Binary string
+      // to hex, which represents the final secret result (which can be converted from hex back
+      // to the original string in user space using `hex2str()`).
+      return bin2hex(at >= 1 ? result : result.slice(result.indexOf("1") + 1));
+    },
+
+    getConfig: function() {
+      var obj = {};
+      obj.radix = config.radix;
+      obj.bits = config.bits;
+      obj.maxShares = config.maxShares;
+      obj.hasCSPRNG = isSetRNG();
+      obj.typeCSPRNG = config.typeCSPRNG;
+      return obj;
+    },
+
+    // Given a public share, extract the bits (Integer), share ID (Integer), and share data (Hex)
+    // and return an Object containing those components.
+    extractShareComponents: function(share) {
+      var bits,
+        id,
+        idLen,
+        max,
+        obj = {},
+        regexStr,
+        shareComponents;
+
+      // Extract the first char which represents the bits in Base 36
+      bits = parseInt(share.substr(0, 1), 36);
+
+      if (
+        bits &&
+        (typeof bits !== "number" ||
+          bits % 1 !== 0 ||
+          bits < defaults.minBits ||
+          bits > defaults.maxBits)
+      ) {
+        throw new Error(
+          "Invalid share : Number of bits must be an integer between " +
+            defaults.minBits +
+            " and " +
+            defaults.maxBits +
+            ", inclusive."
+        );
+      }
+
+      // calc the max shares allowed for given bits
+      max = Math.pow(2, bits) - 1;
+
+      // Determine the ID length which is variable and based on the bit count.
+      idLen = (Math.pow(2, bits) - 1).toString(config.radix).length;
+
+      // Extract all the parts now that the segment sizes are known.
+      regexStr = "^([a-kA-K3-9]{1})([a-fA-F0-9]{" + idLen + "})([a-fA-F0-9]+)$";
+      shareComponents = new RegExp(regexStr).exec(share);
+
+      // The ID is a Hex number and needs to be converted to an Integer
+      if (shareComponents) {
+        id = parseInt(shareComponents[2], config.radix);
+      }
+
+      if (typeof id !== "number" || id % 1 !== 0 || id < 1 || id > max) {
+        throw new Error(
+          "Invalid share : Share id must be an integer between 1 and " +
+            config.maxShares +
+            ", inclusive."
+        );
+      }
+
+      if (shareComponents && shareComponents[3]) {
+        obj.bits = bits;
+        obj.id = id;
+        obj.data = shareComponents[3];
+        return obj;
+      }
+
+      throw new Error("The share data provided is invalid : " + share);
+    },
+
+    // Set the PRNG to use. If no RNG function is supplied, pick a default using getRNG()
+    setRNG: function(rng) {
+      var errPrefix = "Random number generator is invalid ",
+        errSuffix =
+          " Supply an CSPRNG of the form function(bits){} that returns a string containing 'bits' number of random 1's and 0's.";
+
+      if (rng && typeof rng === "string" && CSPRNGTypes.indexOf(rng) === -1) {
+        throw new Error("Invalid RNG type argument : '" + rng + "'");
+      }
+
+      // If RNG was not specified at all,
+      // try to pick one appropriate for this env.
+      if (!rng) {
+        rng = getRNG();
+      }
+
+      // If `rng` is a string, try to forcibly
+      // set the RNG to the type specified.
+      if (rng && typeof rng === "string") {
+        rng = getRNG(rng);
+      }
+
+      if (runCSPRNGTest) {
+        if (rng && typeof rng !== "function") {
+          throw new Error(errPrefix + "(Not a function)." + errSuffix);
+        }
+
+        if (rng && typeof rng(config.bits) !== "string") {
+          throw new Error(errPrefix + "(Output is not a string)." + errSuffix);
+        }
+
+        if (rng && !parseInt(rng(config.bits), 2)) {
+          throw new Error(
+            errPrefix +
+              "(Binary string output not parseable to an Integer)." +
+              errSuffix
+          );
+        }
+
+        if (rng && rng(config.bits).length > config.bits) {
+          throw new Error(
+            errPrefix +
+              "(Output length is greater than config.bits)." +
+              errSuffix
+          );
+        }
+
+        if (rng && rng(config.bits).length < config.bits) {
+          throw new Error(
+            errPrefix + "(Output length is less than config.bits)." + errSuffix
+          );
+        }
+      }
+
+      config.rng = rng;
+
+      return true;
+    },
+
+    // Converts a given UTF16 character string to the HEX representation.
+    // Each character of the input string is represented by
+    // `bytesPerChar` bytes in the output string which defaults to 2.
+    str2hex: function(str, bytesPerChar) {
+      var hexChars,
+        max,
+        out = "",
+        neededBytes,
+        num,
+        i,
+        len;
+
+      if (typeof str !== "string") {
+        throw new Error("Input must be a character string.");
+      }
+
+      if (!bytesPerChar) {
+        bytesPerChar = defaults.bytesPerChar;
+      }
+
+      if (
+        typeof bytesPerChar !== "number" ||
+        bytesPerChar < 1 ||
+        bytesPerChar > defaults.maxBytesPerChar ||
+        bytesPerChar % 1 !== 0
+      ) {
+        throw new Error(
+          "Bytes per character must be an integer between 1 and " +
+            defaults.maxBytesPerChar +
+            ", inclusive."
+        );
+      }
+
+      hexChars = 2 * bytesPerChar;
+      max = Math.pow(16, hexChars) - 1;
+
+      for (i = 0, len = str.length; i < len; i++) {
+        num = str[i].charCodeAt();
+
+        if (isNaN(num)) {
+          throw new Error("Invalid character: " + str[i]);
+        }
+
+        if (num > max) {
+          neededBytes = Math.ceil(Math.log(num + 1) / Math.log(256));
+          throw new Error(
+            "Invalid character code (" +
+              num +
+              "). Maximum allowable is 256^bytes-1 (" +
+              max +
+              "). To convert this character, use at least " +
+              neededBytes +
+              " bytes."
+          );
+        }
+
+        out = padLeft(num.toString(16), hexChars) + out;
+      }
+      return out;
+    },
+
+    // Converts a given HEX number string to a UTF16 character string.
+    hex2str: function(str, bytesPerChar) {
+      var hexChars,
+        out = "",
+        i,
+        len;
+
+      if (typeof str !== "string") {
+        throw new Error("Input must be a hexadecimal string.");
+      }
+      bytesPerChar = bytesPerChar || defaults.bytesPerChar;
+
+      if (
+        typeof bytesPerChar !== "number" ||
+        bytesPerChar % 1 !== 0 ||
+        bytesPerChar < 1 ||
+        bytesPerChar > defaults.maxBytesPerChar
+      ) {
+        throw new Error(
+          "Bytes per character must be an integer between 1 and " +
+            defaults.maxBytesPerChar +
+            ", inclusive."
+        );
+      }
+
+      hexChars = 2 * bytesPerChar;
+
+      str = padLeft(str, hexChars);
+
+      for (i = 0, len = str.length; i < len; i += hexChars) {
+        out =
+          String.fromCharCode(parseInt(str.slice(i, i + hexChars), 16)) + out;
+      }
+
+      return out;
+    },
+
+    // Generates a random bits-length number string using the PRNG
+    random: function(bits) {
+      if (
+        typeof bits !== "number" ||
+        bits % 1 !== 0 ||
+        bits < 2 ||
+        bits > 65536
+      ) {
+        throw new Error(
+          "Number of bits must be an Integer between 1 and 65536."
+        );
+      }
+
+      return bin2hex(config.rng(bits));
+    },
+
+    // Divides a `secret` number String str expressed in radix `inputRadix` (optional, default 16)
+    // into `numShares` shares, each expressed in radix `outputRadix` (optional, default to `inputRadix`),
+    // requiring `threshold` number of shares to reconstruct the secret.
+    // Optionally, zero-pads the secret to a length that is a multiple of padLength before sharing.
+    share: function(secret, numShares, threshold, padLength) {
+      var neededBits,
+        subShares,
+        x = new Array(numShares),
+        y = new Array(numShares),
+        i,
+        j,
+        len;
+
+      // Security:
+      // For additional security, pad in multiples of 128 bits by default.
+      // A small trade-off in larger share size to help prevent leakage of information
+      // about small-ish secrets and increase the difficulty of attacking them.
+      padLength = padLength || 128;
+
+      if (typeof secret !== "string") {
+        throw new Error("Secret must be a string.");
+      }
+
+      if (
+        typeof numShares !== "number" ||
+        numShares % 1 !== 0 ||
+        numShares < 2
+      ) {
+        throw new Error(
+          "Number of shares must be an integer between 2 and 2^bits-1 (" +
+            config.maxShares +
+            "), inclusive."
+        );
+      }
+
+      if (numShares > config.maxShares) {
+        neededBits = Math.ceil(Math.log(numShares + 1) / Math.LN2);
+        throw new Error(
+          "Number of shares must be an integer between 2 and 2^bits-1 (" +
+            config.maxShares +
+            "), inclusive. To create " +
+            numShares +
+            " shares, use at least " +
+            neededBits +
+            " bits."
+        );
+      }
+
+      if (
+        typeof threshold !== "number" ||
+        threshold % 1 !== 0 ||
+        threshold < 2
+      ) {
+        throw new Error(
+          "Threshold number of shares must be an integer between 2 and 2^bits-1 (" +
+            config.maxShares +
+            "), inclusive."
+        );
+      }
+
+      if (threshold > config.maxShares) {
+        neededBits = Math.ceil(Math.log(threshold + 1) / Math.LN2);
+        throw new Error(
+          "Threshold number of shares must be an integer between 2 and 2^bits-1 (" +
+            config.maxShares +
+            "), inclusive.  To use a threshold of " +
+            threshold +
+            ", use at least " +
+            neededBits +
+            " bits."
+        );
+      }
+
+      if (threshold > numShares) {
+        throw new Error(
+          "Threshold number of shares was " +
+            threshold +
+            " but must be less than or equal to the " +
+            numShares +
+            " shares specified as the total to generate."
+        );
+      }
+
+      if (
+        typeof padLength !== "number" ||
+        padLength % 1 !== 0 ||
+        padLength < 0 ||
+        padLength > 1024
+      ) {
+        throw new Error(
+          "Zero-pad length must be an integer between 0 and 1024 inclusive."
+        );
+      }
+
+      secret = "1" + hex2bin(secret); // prepend a 1 as a marker so that we can preserve the correct number of leading zeros in our secret
+      secret = splitNumStringToIntArray(secret, padLength);
+
+      for (i = 0, len = secret.length; i < len; i++) {
+        subShares = getShares(secret[i], numShares, threshold);
+        for (j = 0; j < numShares; j++) {
+          x[j] = x[j] || subShares[j].x.toString(config.radix);
+          y[j] = padLeft(subShares[j].y.toString(2)) + (y[j] || "");
+        }
+      }
+
+      for (i = 0; i < numShares; i++) {
+        x[i] = constructPublicShareString(config.bits, x[i], bin2hex(y[i]));
+      }
+
+      return x;
+    },
+
+    // Generate a new share with id `id` (a number between 1 and 2^bits-1)
+    // `id` can be a Number or a String in the default radix (16)
+    newShare: function(id, shares) {
+      var share, radid;
+
+      if (id && typeof id === "string") {
+        id = parseInt(id, config.radix);
+      }
+
+      radid = id.toString(config.radix);
+
+      if (id && radid && shares && shares[0]) {
+        share = this.extractShareComponents(shares[0]);
+        return constructPublicShareString(
+          share.bits,
+          radid,
+          this.combine(shares, id)
+        );
+      }
+
+      throw new Error("Invalid 'id' or 'shares' Array argument to newShare().");
+    },
+
+    /* test-code */
+    // export private functions so they can be unit tested directly.
+    _reset: reset,
+    _padLeft: padLeft,
+    _hex2bin: hex2bin,
+    _bin2hex: bin2hex,
+    _hasCryptoGetRandomValues: hasCryptoGetRandomValues,
+    _hasCryptoRandomBytes: hasCryptoRandomBytes,
+    _getRNG: getRNG,
+    _isSetRNG: isSetRNG,
+    _splitNumStringToIntArray: splitNumStringToIntArray,
+    _horner: horner,
+    _lagrange: lagrange,
+    _getShares: getShares,
+    _constructPublicShareString: constructPublicShareString
+    /* end-test-code */
+  };
+
+  // Always initialize secrets with default settings.
+  secrets.init();
+
+  return secrets;
+});
diff --git a/javascript/src/utilities/numberUtilities.js b/javascript/src/utilities/numberUtilities.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbee13839522df1f54fe79ff2fa57befd736ae68
--- /dev/null
+++ b/javascript/src/utilities/numberUtilities.js
@@ -0,0 +1,14 @@
+export function getRandomInt(max) {
+  return Math.floor(Math.random() * Math.floor(max));
+}
+
+export function getSliceRange(max) {
+  const beginIndex = getRandomInt(max);
+  const endIndex = getRandomInt(max);
+
+  if (beginIndex === endIndex) {
+    return getSliceRange(max);
+  }
+
+  return { beginIndex, endIndex };
+}
diff --git a/javascript/src/utilities/secrets.js b/javascript/src/utilities/secrets.js
new file mode 100644
index 0000000000000000000000000000000000000000..2223ae6533e57d2573b3a473e94f0c7e8287adfd
--- /dev/null
+++ b/javascript/src/utilities/secrets.js
@@ -0,0 +1,98 @@
+import secrets from "../lib/secrets";
+import { encryptMessage } from "./signingUtilities";
+import { encodeResponse } from "./appUtility";
+import { getSliceRange } from "./numberUtilities";
+import { THRESHOLD } from "../constants/secrets";
+
+/** Initialize
+ */
+export const initSecrets = (bits, rngType) => secrets.init(bits, rngType);
+
+export const setRNG = rngType => secrets.setRNG(rngType);
+export const getSecretsConfig = () => secrets.getConfig();
+
+/**
+ * Function generates a random bits length string, and output it in hexadecimal format
+ *
+ * @param {number} bits
+ * @returns {string} hex
+ */
+export const generateSecret = bits => secrets.random(bits);
+
+/**
+ * Divide a secret expressed in hexadecimal form into numShares number of shares, requiring that threshold number of shares be present for reconstructing the secret
+ *
+ * @param {string} secret
+ * @param {number} numShares
+ * @param {number} threshold
+ * @param {number} [padLength=128]
+ * @returns {array}
+ */
+export const divideSecretToShares = (
+  secret,
+  numShares,
+  threshold,
+  padLength = 128
+) => secrets.share(secret, numShares, threshold, padLength);
+
+/**
+ * Reconstructs a secret from shares
+ *
+ * @param {array} shares
+ * @returns {string}
+ */
+export const combineSecret = shares => secrets.combine(shares);
+
+export const encryptShare = async (share, publicKey) =>
+  await encryptMessage(share, publicKey, "secretPart");
+
+/** Account Recovery key management */
+
+export const generateRecoveryKey = () => {
+  const recoveryKey = generateSecret(512);
+  return recoveryKey;
+};
+
+export const getRecoveryKeyShares = (recoveryKey, sharesNumber) => {
+  return divideSecretToShares(recoveryKey, sharesNumber, THRESHOLD);
+};
+
+function getSecretSliceRange(max) {
+  const { beginIndex, endIndex } = getSliceRange(max);
+  if (endIndex - beginIndex < THRESHOLD) {
+    return getSecretSliceRange(max);
+  }
+
+  return { beginIndex, endIndex };
+}
+
+export const checkRecoveryKeyCombine = (recoveryKey, recoveryKeyShares) => {
+  let checkKey;
+
+  const { beginIndex, endIndex } = getSecretSliceRange(
+    recoveryKeyShares.length + 1
+  );
+
+  checkKey = combineSecret(recoveryKeyShares.slice(beginIndex, endIndex));
+  if (checkKey !== recoveryKey) {
+    return encodeResponse(
+      "400",
+      "",
+      "Sanity check with required number of shares failed"
+    );
+  }
+  checkKey = combineSecret(recoveryKeyShares.slice(0, 1));
+  if (checkKey === recoveryKey) {
+    return encodeResponse(
+      "400",
+      "",
+      "Sanity check with less than required shares failed"
+    );
+  }
+  checkKey = combineSecret(recoveryKeyShares);
+  if (checkKey !== recoveryKey) {
+    return encodeResponse("400", "", "Sanity check with all shares failed");
+  }
+
+  return encodeResponse("200", "", "Check passed");
+};