changeset 1:fc26a8fc54d5

initial release
author Alain Mazy <alain@mazy.be>
date Fri, 03 Jul 2020 10:08:44 +0200
parents d7198e8f0d47
children cd1622edea7f
files .hgignore AUTHORS Aws/AwsS3StoragePlugin.cpp Aws/AwsS3StoragePlugin.h Aws/CMakeLists.txt Azure/AzureBlobStoragePlugin.cpp Azure/AzureBlobStoragePlugin.h Azure/CMakeLists.txt Common/EncryptionConfigurator.cpp Common/EncryptionConfigurator.h Common/EncryptionHelpers.cpp Common/EncryptionHelpers.h Common/IStoragePlugin.h Common/Resources/DownloadOrthancFramework.cmake Common/StoragePlugin.cpp Google/CMakeLists.txt Google/GoogleStoragePlugin.cpp Google/GoogleStoragePlugin.h NEWS README.md UnitTestsSources/EncryptionTests.cpp UnitTestsSources/UnitTestsGcsClient.cpp UnitTestsSources/UnitTestsMain.cpp
diffstat 23 files changed, 3034 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,1 @@
+CMakeLists.txt.user*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AUTHORS	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,11 @@
+Orthanc Object Storage plugin
+=============================
+
+
+Authors
+-------
+
+* Osimis <info@osimis.io>
+  Quai Banning 6
+  4000 Liège
+  Belgium
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Aws/AwsS3StoragePlugin.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,298 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "AwsS3StoragePlugin.h"
+
+#include <aws/core/Aws.h>
+#include <aws/s3/S3Client.h>
+#include <aws/s3/model/PutObjectRequest.h>
+#include <aws/s3/model/GetObjectRequest.h>
+#include <aws/s3/model/ListObjectsRequest.h>
+#include <aws/s3/model/DeleteObjectRequest.h>
+#include <aws/core/auth/AWSCredentialsProvider.h>
+#include <aws/core/utils/memory/stl/AWSStringStream.h>
+
+#include <boost/lexical_cast.hpp>
+#include <iostream>
+#include <fstream>
+
+const char* ALLOCATION_TAG = "OrthancS3";
+
+class AwsS3StoragePlugin : public IStoragePlugin
+{
+public:
+
+  Aws::S3::S3Client       client_;
+  std::string             bucketName_;
+
+public:
+
+  AwsS3StoragePlugin(const Aws::S3::S3Client& client, const std::string& bucketName);
+
+  virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+private:
+  virtual std::string GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+};
+
+
+class Writer : public IStoragePlugin::IWriter
+{
+  std::string             path_;
+  Aws::S3::S3Client       client_;
+  std::string             bucketName_;
+
+public:
+  Writer(const Aws::S3::S3Client& client, const std::string& bucketName, const std::string& path)
+    : path_(path),
+      client_(client),
+      bucketName_(bucketName)
+  {
+  }
+
+  virtual ~Writer()
+  {
+  }
+
+  virtual void Write(const char* data, size_t size)
+  {
+    Aws::S3::Model::PutObjectRequest putObjectRequest;
+
+    putObjectRequest.SetBucket(bucketName_.c_str());
+    putObjectRequest.SetKey(path_.c_str());
+
+    std::shared_ptr<Aws::StringStream> stream = Aws::MakeShared<Aws::StringStream>(ALLOCATION_TAG, std::ios_base::in | std::ios_base::binary);
+
+    stream->rdbuf()->pubsetbuf(const_cast<char*>(data), size);
+    stream->rdbuf()->pubseekpos(size);
+    stream->seekg(0);
+
+    putObjectRequest.SetBody(stream);
+
+    auto result = client_.PutObject(putObjectRequest);
+
+    if (!result.IsSuccess())
+    {
+      throw StoragePluginException(std::string("error while writing file ") + path_ + ": " + result.GetError().GetExceptionName().c_str() + " " + result.GetError().GetMessage().c_str());
+    }
+  }
+};
+
+
+class Reader : public IStoragePlugin::IReader
+{
+  std::string             path_;
+  Aws::S3::S3Client       client_;
+  std::string             bucketName_;
+
+public:
+  Reader(const Aws::S3::S3Client& client, const std::string& bucketName, const std::string& path)
+    : path_(path),
+      client_(client),
+      bucketName_(bucketName)
+  {
+  }
+
+  virtual ~Reader()
+  {
+
+  }
+  virtual size_t GetSize()
+  {
+    Aws::S3::Model::ListObjectsRequest listObjectRequest;
+    listObjectRequest.SetBucket(bucketName_.c_str());
+    listObjectRequest.SetPrefix(path_.c_str());
+
+    auto result = client_.ListObjects(listObjectRequest);
+
+    if (result.IsSuccess())
+    {
+      Aws::Vector<Aws::S3::Model::Object> objectList =
+          result.GetResult().GetContents();
+
+      if (objectList.size() == 1)
+      {
+        return objectList[0].GetSize();
+      }
+      throw StoragePluginException(std::string("error while reading file ") + path_ + ": multiple objet with same name !");
+    }
+    else
+    {
+      throw StoragePluginException(std::string("error while reading file ") + path_ + ": " + result.GetError().GetExceptionName().c_str() + " " + result.GetError().GetMessage().c_str());
+    }
+  }
+
+  virtual void Read(char* data, size_t size)
+  {
+    Aws::S3::Model::GetObjectRequest getObjectRequest;
+    getObjectRequest.SetBucket(bucketName_.c_str());
+    getObjectRequest.SetKey(path_.c_str());
+
+    getObjectRequest.SetResponseStreamFactory(
+          [data, size]()
+    {
+      std::unique_ptr<Aws::StringStream>
+          istream(Aws::New<Aws::StringStream>(ALLOCATION_TAG));
+
+      istream->rdbuf()->pubsetbuf(static_cast<char*>(data),
+                                  size);
+
+      return istream.release();
+    });
+
+    // Get the object
+    auto result = client_.GetObject(getObjectRequest);
+    if (result.IsSuccess())
+    {
+    }
+    else
+    {
+      throw StoragePluginException(std::string("error while reading file ") + path_ + ": " + result.GetError().GetExceptionName().c_str() + " " + result.GetError().GetMessage().c_str());
+    }
+  }
+
+};
+
+
+
+const char* AwsS3StoragePluginFactory::GetStoragePluginName()
+{
+  return "AWS S3 Storage";
+}
+
+IStoragePlugin* AwsS3StoragePluginFactory::CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig)
+{
+  static const char* const PLUGIN_SECTION = "AwsS3Storage";
+  if (!orthancConfig.IsSection(PLUGIN_SECTION))
+  {
+    OrthancPlugins::LogWarning(std::string(GetStoragePluginName()) + " plugin, section missing.  Plugin is not enabled.");
+    return nullptr;
+  }
+
+  OrthancPlugins::OrthancConfiguration pluginSection;
+  orthancConfig.GetSection(pluginSection, PLUGIN_SECTION);
+
+  std::string bucketName;
+  std::string region;
+  std::string accessKey;
+  std::string secretKey;
+
+  if (!pluginSection.LookupStringValue(bucketName, "BucketName"))
+  {
+    OrthancPlugins::LogError("AwsS3Storage/BucketName configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  if (!pluginSection.LookupStringValue(region, "Region"))
+  {
+    OrthancPlugins::LogError("AwsS3Storage/Region configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  if (!pluginSection.LookupStringValue(accessKey, "AccessKey"))
+  {
+    OrthancPlugins::LogError("AwsS3Storage/AccessKey configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  if (!pluginSection.LookupStringValue(secretKey, "SecretKey"))
+  {
+    OrthancPlugins::LogError("AwsS3Storage/SecretKey configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  try
+  {
+    Aws::SDKOptions options;
+    Aws::InitAPI(options);
+
+    Aws::Auth::AWSCredentials credentials(accessKey.c_str(), secretKey.c_str());
+    Aws::Client::ClientConfiguration configuration;
+    configuration.region = region.c_str();
+    Aws::S3::S3Client client(credentials, configuration);
+
+    OrthancPlugins::LogInfo("AWS S3 storage initialized");
+
+    return new AwsS3StoragePlugin(client, bucketName);
+  }
+  catch (const std::exception& e)
+  {
+    OrthancPlugins::LogError(std::string("AzureBlobStorage plugin: failed to initialize plugin: ") + e.what());
+    return nullptr;
+  }
+
+}
+
+AwsS3StoragePlugin::AwsS3StoragePlugin(const Aws::S3::S3Client& client, const std::string& bucketName)
+  : client_(client),
+    bucketName_(bucketName)
+{
+
+}
+
+IStoragePlugin::IWriter* AwsS3StoragePlugin::GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Writer(client_, bucketName_, GetPath(uuid, type, encryptionEnabled));
+}
+
+IStoragePlugin::IReader* AwsS3StoragePlugin::GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Reader(client_, bucketName_, GetPath(uuid, type, encryptionEnabled));
+}
+
+void AwsS3StoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  std::string path = GetPath(uuid, type, encryptionEnabled);
+
+  Aws::S3::Model::DeleteObjectRequest deleteObjectRequest;
+  deleteObjectRequest.SetBucket(bucketName_.c_str());
+  deleteObjectRequest.SetKey(path.c_str());
+
+  auto result = client_.DeleteObject(deleteObjectRequest);
+
+  if (!result.IsSuccess())
+  {
+    throw StoragePluginException(std::string("error while deleting file ") + path + ": " + result.GetError().GetExceptionName().c_str() + " " + result.GetError().GetMessage().c_str());
+  }
+
+}
+
+std::string AwsS3StoragePlugin::GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  std::string path = std::string(uuid);
+
+  if (type == OrthancPluginContentType_Dicom)
+  {
+    path += ".dcm";
+  }
+  else if (type == OrthancPluginContentType_DicomAsJson)
+  {
+    path += ".json";
+  }
+  else
+  {
+    path += ".unk";
+  }
+
+  if (encryptionEnabled)
+  {
+    path += ".enc";
+  }
+  return path;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Aws/AwsS3StoragePlugin.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,29 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../Common/IStoragePlugin.h"
+
+class AwsS3StoragePluginFactory
+{
+public:
+  static const char* GetStoragePluginName();
+  static IStoragePlugin* CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig);
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Aws/CMakeLists.txt	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,91 @@
+cmake_minimum_required(VERSION 2.8)
+
+option(BUILD_SHARED_LIBS "Build shared libraries" ON)
+
+project(OrthancAwsS3Storage)
+
+set(PLUGIN_VERSION "0.9")
+
+include(CheckIncludeFileCXX)
+
+set(ORTHANC_FRAMEWORK_SOURCE "hg" CACHE STRING "orthanc source")
+set(ORTHANC_FRAMEWORK_VERSION "1.7.0" CACHE STRING "orthanc framework version")
+set(ALLOW_DOWNLOADS ON)
+
+# Download and setup the Orthanc framework
+include(${CMAKE_SOURCE_DIR}/../Common/Resources/DownloadOrthancFramework.cmake)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_GOOGLE_TEST ON)
+set(ORTHANC_FRAMEWORK_PLUGIN ON)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+
+add_definitions(
+    -DHAS_ORTHANC_EXCEPTION=1
+    -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+    -DAWS_STORAGE_PLUGIN=1
+    )
+add_definitions(-DPLUGIN_VERSION="${PLUGIN_VERSION}")
+
+include_directories(
+  ${ORTHANC_ROOT}
+  ${ORTHANC_ROOT}/Core
+  ${ORTHANC_ROOT}/Plugins/Include
+  ${ORTHANC_ROOT}/Plugins/Samples/Common
+  )
+
+
+find_package(cryptopp CONFIG REQUIRED)
+find_package(AWSSDK REQUIRED COMPONENTS s3)
+
+include_directories(${WASTORAGE_INCLUDE_DIR})
+
+set(COMMON_SOURCES
+    ${CMAKE_SOURCE_DIR}/../Common/IStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+    ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+
+    ${ORTHANC_CORE_SOURCES}
+  )
+
+add_library(OrthancAwsS3Storage SHARED
+    AwsS3StoragePlugin.cpp
+    AwsS3StoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/StoragePlugin.cpp
+
+    ${COMMON_SOURCES}
+  )
+
+set_target_properties(OrthancAwsS3Storage PROPERTIES
+  VERSION ${PLUGIN_VERSION}
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+target_link_libraries(OrthancAwsS3Storage
+  PRIVATE
+  cryptopp-static
+  ${AWSSDK_LINK_LIBRARIES}
+  )
+
+
+
+add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${COMMON_SOURCES}
+
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/EncryptionTests.cpp
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/UnitTestsMain.cpp
+    )
+
+target_link_libraries(UnitTests
+  PRIVATE
+  cryptopp-static
+  ${GOOGLE_TEST_LIBRARIES}
+  ${AWSSDK_LINK_LIBRARIES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Azure/AzureBlobStoragePlugin.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,294 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "AzureBlobStoragePlugin.h"
+
+#include <was/storage_account.h>
+#include <was/blob.h>
+#include <boost/lexical_cast.hpp>
+#include "cpprest/rawptrstream.h"
+
+
+// Create aliases to make the code easier to read.
+namespace as = azure::storage;
+
+
+class AzureBlobStoragePlugin : public IStoragePlugin
+{
+public:
+
+  as::cloud_blob_client       blobClient_;
+  as::cloud_blob_container    blobContainer_;
+public:
+
+//  AzureBlobStoragePlugin(const std::string& connectionString,
+//                         const std::string& containerName
+//                        );
+  AzureBlobStoragePlugin(const as::cloud_blob_client& blobClient, const as::cloud_blob_container& blobContainer);
+
+  virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+private:
+  virtual std::string GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+};
+
+
+class Writer : public IStoragePlugin::IWriter
+{
+  std::string   path_;
+  as::cloud_blob_client   client_;
+  as::cloud_blob_container container_;
+
+public:
+  Writer(const as::cloud_blob_container& container, const std::string& path, const as::cloud_blob_client& client)
+    : path_(path),
+      client_(client),
+      container_(container)
+  {
+  }
+
+  virtual ~Writer()
+  {
+  }
+
+  virtual void Write(const char* data, size_t size)
+  {
+    try
+    {
+      concurrency::streams::istream inputStream = concurrency::streams::rawptr_stream<uint8_t>::open_istream(reinterpret_cast<const uint8_t*>(data), size);
+      azure::storage::cloud_block_blob blob = container_.get_block_blob_reference(path_);
+      blob.upload_from_stream(inputStream);
+      inputStream.close().wait();
+    }
+    catch (std::exception& ex)
+    {
+      throw StoragePluginException("AzureBlobStorage: error writing file " + std::string(path_) + ": " + ex.what());
+    }
+  }
+};
+
+
+class Reader : public IStoragePlugin::IReader
+{
+  std::string   path_;
+  as::cloud_blob_client   client_;
+  as::cloud_blob_container container_;
+  as::cloud_block_blob block_;
+
+public:
+  Reader(const as::cloud_blob_container& container, const std::string& path, const as::cloud_blob_client& client)
+    : path_(path),
+      client_(client),
+      container_(container)
+  {
+    try
+    {
+      block_ = container_.get_block_blob_reference(path_);
+      block_.download_attributes(); // to retrieve the properties
+    }
+    catch (std::exception& ex)
+    {
+      throw StoragePluginException("AzureBlobStorage: error opening file for reading " + std::string(path_) + ": " + ex.what());
+    }
+  }
+
+  virtual ~Reader()
+  {
+
+  }
+  virtual size_t GetSize()
+  {
+    try
+    {
+      return block_.properties().size();
+    }
+    catch (std::exception& ex)
+    {
+      throw StoragePluginException("AzureBlobStorage: error while reading file " + std::string(path_) + ": " + ex.what());
+    }
+  }
+
+  virtual void Read(char* data, size_t size)
+  {
+    try
+    {
+      concurrency::streams::ostream outputStream = concurrency::streams::rawptr_stream<uint8_t>::open_ostream(reinterpret_cast<uint8_t*>(data), size);
+
+      block_.download_to_stream(outputStream);
+    }
+    catch (std::exception& ex)
+    {
+      throw StoragePluginException("AzureBlobStorage: error while reading file " + std::string(path_) + ": " + ex.what());
+    }
+  }
+
+};
+
+
+
+const char* AzureBlobStoragePluginFactory::GetStoragePluginName()
+{
+  return "Azure Blob Storage";
+}
+
+IStoragePlugin* AzureBlobStoragePluginFactory::CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig)
+{
+  std::string connectionString;
+  std::string containerName;
+
+  static const char* const PLUGIN_SECTION = "AzureBlobStorage";
+  if (orthancConfig.IsSection(PLUGIN_SECTION))
+  {
+    OrthancPlugins::OrthancConfiguration pluginSection;
+    orthancConfig.GetSection(pluginSection, PLUGIN_SECTION);
+
+    if (!pluginSection.LookupStringValue(connectionString, "ConnectionString"))
+    {
+      OrthancPlugins::LogError("AzureBlobStorage/ConnectionString configuration missing.  Unable to initialize plugin");
+      return nullptr;
+    }
+
+    if (!pluginSection.LookupStringValue(containerName, "ContainerName"))
+    {
+      OrthancPlugins::LogError("AzureBlobStorage/ContainerName configuration missing.  Unable to initialize plugin");
+      return nullptr;
+    }
+
+  }
+  else if (orthancConfig.IsSection("BlobStorage")) // backward compatibility with the old plugin:
+  {
+    OrthancPlugins::LogWarning("AzureBlobStorage: you're using an old configuration format for the plugin.");
+
+    OrthancPlugins::OrthancConfiguration pluginSection;
+    orthancConfig.GetSection(pluginSection, "BlobStorage");
+
+    std::string accountName;
+    std::string accountKey;
+
+    if (!pluginSection.LookupStringValue(containerName, "ContainerName"))
+    {
+      OrthancPlugins::LogError("BlobStorage/AccountName configuration missing.  Unable to initialize plugin");
+      return nullptr;
+    }
+
+    if (!pluginSection.LookupStringValue(accountName, "AccountName"))
+    {
+      OrthancPlugins::LogError("BlobStorage/AccountName configuration missing.  Unable to initialize plugin");
+      return nullptr;
+    }
+
+    if (!pluginSection.LookupStringValue(accountKey, "AccountKey"))
+    {
+      OrthancPlugins::LogError("BlobStorage/ContainerName configuration missing.  Unable to initialize plugin");
+      return nullptr;
+    }
+
+    std::ostringstream connectionStringBuilder;
+    connectionStringBuilder << "DefaultEndpointsProtocol=https;AccountName=" << accountName << ";AccountKey=" << accountKey;
+    connectionString = connectionStringBuilder.str();
+  }
+  else
+  {
+    OrthancPlugins::LogWarning(std::string(GetStoragePluginName()) + " plugin, section missing.  Plugin is not enabled.");
+    return nullptr;
+  }
+
+  try
+  {
+    as::cloud_storage_account storageAccount = as::cloud_storage_account::parse(connectionString);
+
+    as::cloud_blob_client blobClient = storageAccount.create_cloud_blob_client();
+    as::cloud_blob_container blobContainer = blobClient.get_container_reference(containerName);
+
+    // Return value is true if the container did not exist and was successfully created.
+    bool containerCreated = blobContainer.create_if_not_exists();
+
+    if (containerCreated)
+    {
+      OrthancPlugins::LogWarning("Blob Storage Area container has been created.  **** check in the Azure console that your container is private ****");
+    }
+
+    OrthancPlugins::LogInfo("Blob storage initialized");
+
+    return new AzureBlobStoragePlugin(blobClient, blobContainer);
+  }
+  catch (const std::exception& e)
+  {
+    OrthancPlugins::LogError(std::string("AzureBlobStorage plugin: failed to initialize plugin: ") + e.what());
+    return nullptr;
+  }
+
+}
+
+AzureBlobStoragePlugin::AzureBlobStoragePlugin(const as::cloud_blob_client& blobClient, const as::cloud_blob_container& blobContainer) //const std::string &containerName) //, google::cloud::storage::Client& mainClient)
+  : blobClient_(blobClient),
+    blobContainer_(blobContainer)
+{
+
+}
+
+IStoragePlugin::IWriter* AzureBlobStoragePlugin::GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Writer(blobContainer_, GetPath(uuid, type, encryptionEnabled), blobClient_);
+}
+
+IStoragePlugin::IReader* AzureBlobStoragePlugin::GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Reader(blobContainer_, GetPath(uuid, type, encryptionEnabled), blobClient_);
+}
+
+void AzureBlobStoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  std::string path = GetPath(uuid, type, encryptionEnabled);
+
+  try
+  {
+    as::cloud_block_blob blockBlob = blobContainer_.get_block_blob_reference(path);
+
+    blockBlob.delete_blob();
+  }
+  catch (std::exception& ex)
+  {
+    throw StoragePluginException("AzureBlobStorage: error while deleting file " + std::string(path) + ": " + ex.what());
+  }
+}
+
+std::string AzureBlobStoragePlugin::GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  std::string path = std::string(uuid);
+
+  if (type == OrthancPluginContentType_Dicom)
+  {
+    path += ".dcm";
+  }
+  else if (type == OrthancPluginContentType_DicomAsJson)
+  {
+    path += ".json";
+  }
+  else
+  {
+    path += ".unk";
+  }
+
+  if (encryptionEnabled)
+  {
+    path += ".enc";
+  }
+  return path;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Azure/AzureBlobStoragePlugin.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,29 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../Common/IStoragePlugin.h"
+
+class AzureBlobStoragePluginFactory
+{
+public:
+  static const char* GetStoragePluginName();
+  static IStoragePlugin* CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig);
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Azure/CMakeLists.txt	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,96 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(OrthancAzureBlobStorage)
+
+set(PLUGIN_VERSION "0.9")
+
+include(CheckIncludeFileCXX)
+
+set(ORTHANC_FRAMEWORK_SOURCE "hg" CACHE STRING "orthanc source")
+set(ORTHANC_FRAMEWORK_VERSION "1.7.0" CACHE STRING "orthanc framework version")
+set(ALLOW_DOWNLOADS ON)
+
+# Download and setup the Orthanc framework
+include(${CMAKE_SOURCE_DIR}/../Common/Resources/DownloadOrthancFramework.cmake)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_GOOGLE_TEST ON)
+set(ORTHANC_FRAMEWORK_PLUGIN ON)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+
+add_definitions(
+    -DHAS_ORTHANC_EXCEPTION=1
+    -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+    -DAZURE_STORAGE_PLUGIN=1
+    )
+add_definitions(-DPLUGIN_VERSION="${PLUGIN_VERSION}")
+
+include_directories(
+  ${ORTHANC_ROOT}
+  ${ORTHANC_ROOT}/Core
+  ${ORTHANC_ROOT}/Plugins/Include
+  ${ORTHANC_ROOT}/Plugins/Samples/Common
+  )
+
+
+find_package(cryptopp CONFIG REQUIRED)
+
+# Azure stuff (from https://github.com/Microsoft/vcpkg/issues/6277)
+find_package(cpprestsdk CONFIG REQUIRED)
+find_path(WASTORAGE_INCLUDE_DIR was/blob.h)
+find_library(WASTORAGE_LIBRARY azurestorage)
+find_package(Boost REQUIRED COMPONENTS log)
+find_library(UUID_LIBRARY uuid)
+find_package(LibXml2 REQUIRED)
+
+include_directories(${WASTORAGE_INCLUDE_DIR})
+
+set(COMMON_SOURCES
+    ${CMAKE_SOURCE_DIR}/../Common/IStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+    ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+
+    ${ORTHANC_CORE_SOURCES}
+  )
+
+add_library(OrthancAzureBlobStorage SHARED
+    AzureBlobStoragePlugin.cpp
+    AzureBlobStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/StoragePlugin.cpp
+
+    ${COMMON_SOURCES}
+  )
+
+set_target_properties(OrthancAzureBlobStorage PROPERTIES
+  VERSION ${PLUGIN_VERSION}
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+target_link_libraries(OrthancAzureBlobStorage
+  PRIVATE
+  cryptopp-static
+  ${WASTORAGE_LIBRARY} ${UUID_LIBRARY} ${Boost_LIBRARIES} ${LIBXML2_LIBRARIES} cpprestsdk::cpprest
+  )
+
+
+
+add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${COMMON_SOURCES}
+
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/EncryptionTests.cpp
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/UnitTestsMain.cpp
+    )
+
+target_link_libraries(UnitTests
+  PRIVATE
+  cryptopp-static
+  ${GOOGLE_TEST_LIBRARIES}
+  ${WASTORAGE_LIBRARY} ${UUID_LIBRARY} ${Boost_LIBRARIES} ${LIBXML2_LIBRARIES} cpprestsdk::cpprest
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/EncryptionConfigurator.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,86 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#include <orthanc/OrthancCPlugin.h>
+#include <OrthancPluginCppWrapper.h>
+
+#include "EncryptionConfigurator.h"
+
+bool ReadMasterKey(uint32_t& id, std::string& keyPath, const Json::Value& node)
+{
+  if (!node.isArray() || node.size() != 2 || !node[0].isUInt() || !node[1].isString())
+  {
+    OrthancPlugins::LogWarning("Encryption: Invalid master key configuration");
+    return false;
+  }
+
+  id = node[0].asUInt();
+  keyPath = node[1].asString();
+
+  return true;
+}
+
+
+EncryptionHelpers* EncryptionConfigurator::CreateEncryptionHelpers(const OrthancPlugins::OrthancConfiguration& cryptoSection)
+{
+  bool enabled = cryptoSection.GetBooleanValue("Enable", true);
+
+  if (!enabled)
+  {
+    return nullptr;
+  }
+
+  Json::Value cryptoJson = cryptoSection.GetJson();
+
+  if (!cryptoJson.isMember("MasterKey") || !cryptoJson["MasterKey"].isArray())
+  {
+    OrthancPlugins::LogWarning("Encryption: MasterKey missing.  Unable to initialize encryption");
+    return nullptr;
+  }
+
+  unsigned int maxConcurrentInputSizeInMb = cryptoSection.GetUnsignedIntegerValue("MaxConcurrentInputSize", 1024);
+
+  std::unique_ptr<EncryptionHelpers> crypto(new EncryptionHelpers(maxConcurrentInputSizeInMb * 1024*1024));
+
+  uint32_t masterKeyId;
+  std::string masterKeyPath;
+
+  if (!ReadMasterKey(masterKeyId, masterKeyPath, cryptoJson["MasterKey"]))
+  {
+    return nullptr;
+  }
+  crypto->SetCurrentMasterKey(masterKeyId, masterKeyPath);
+
+  if (cryptoJson.isMember("PreviousMasterKeys") && cryptoJson["PreviousMasterKeys"].isArray())
+  {
+    for (size_t i = 0; i < cryptoJson["PreviousMasterKeys"].size(); i++)
+    {
+      uint32_t keyId;
+      std::string keyPath;
+      if (!ReadMasterKey(keyId, keyPath, cryptoJson["PreviousMasterKeys"][(unsigned int)i]))
+      {
+          return nullptr;
+      }
+      crypto->AddPreviousMasterKey(keyId, keyPath);
+    }
+  }
+
+  return crypto.release();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/EncryptionConfigurator.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,29 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "EncryptionHelpers.h"
+
+class EncryptionConfigurator
+{
+
+public:
+  static EncryptionHelpers* CreateEncryptionHelpers(const OrthancPlugins::OrthancConfiguration& encryptionSection);
+
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/EncryptionHelpers.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,368 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "EncryptionHelpers.h"
+#include <assert.h>
+
+#include <boost/lexical_cast.hpp>
+#include <iostream>
+#include "cryptopp/cryptlib.h"
+#include "cryptopp/modes.h"
+#include "cryptopp/hex.h"
+#include "cryptopp/gcm.h"
+#include "cryptopp/files.h"
+
+const std::string EncryptionHelpers::HEADER_VERSION = "A1";
+
+using namespace  CryptoPP;
+
+std::string EncryptionHelpers::ToHexString(const byte* block, size_t size)
+{
+  std::string blockAsString = std::string(reinterpret_cast<const char*>(block), size);
+
+  return ToHexString(blockAsString);
+}
+
+std::string EncryptionHelpers::ToHexString(const std::string& block)
+{
+  std::string hexString;
+  StringSource ss(block, true,
+                   new HexEncoder(
+                     new StringSink(hexString)
+                     ) // StreamTransformationFilter
+                   ); // StringSource
+
+  return hexString;
+}
+
+std::string EncryptionHelpers::ToHexString(const SecByteBlock& block)
+{
+  return ToHexString(ToString(block));
+}
+
+std::string EncryptionHelpers::ToString(const CryptoPP::SecByteBlock& block)
+{
+  return std::string(reinterpret_cast<const char*>(block.data()), block.size());
+}
+
+std::string EncryptionHelpers::ToString(uint32_t value)
+{
+  return std::string(reinterpret_cast<const char*>(&value), 4);
+}
+
+void EncryptionHelpers::ReadKey(CryptoPP::SecByteBlock& key, const std::string& path)
+{
+  try
+  {
+    FileSource fs(path.c_str(), true,
+                  new HexDecoder(
+                    new ArraySink(key.begin(), key.size())
+                    )
+                  );
+  }
+  catch (CryptoPP::Exception& ex)
+  {
+    throw EncryptionException("unabled to read key from file '" + path + "': " + ex.what());
+  }
+}
+
+void EncryptionHelpers::SetCurrentMasterKey(uint32_t id, const std::string& path)
+{
+  SecByteBlock key(AES_KEY_SIZE);
+
+  ReadKey(key, path);
+  SetCurrentMasterKey(id, key);
+}
+
+void EncryptionHelpers::AddPreviousMasterKey(uint32_t id, const std::string& path)
+{
+  SecByteBlock key(AES_KEY_SIZE);
+
+  ReadKey(key, path);
+  AddPreviousMasterKey(id, key);
+}
+
+EncryptionHelpers::EncryptionHelpers(size_t maxConcurrentInputSize)
+  : concurrentInputSizeSemaphore_(maxConcurrentInputSize),
+    maxConcurrentInputSize_(maxConcurrentInputSize)
+{
+}
+
+void EncryptionHelpers::SetCurrentMasterKey(uint32_t id, const CryptoPP::SecByteBlock& key)
+{
+  encryptionMasterKey_ = key;
+  encryptionMasterKeyId_ = ToString(id);
+}
+
+void EncryptionHelpers::AddPreviousMasterKey(uint32_t id, const CryptoPP::SecByteBlock& key)
+{
+  previousMasterKeys_[ToString(id)] = key;
+}
+
+const CryptoPP::SecByteBlock& EncryptionHelpers::GetMasterKey(const std::string& keyId)
+{
+  if (encryptionMasterKeyId_ == keyId)
+  {
+    return encryptionMasterKey_;
+  }
+
+  if (previousMasterKeys_.find(keyId) == previousMasterKeys_.end())
+  {
+    throw EncryptionException("The master key whose id is '" + ToHexString(keyId) + "' could not be found.  Unable to decrypt file");
+  }
+
+  return previousMasterKeys_.at(keyId);
+}
+
+void EncryptionHelpers::GenerateKey(CryptoPP::SecByteBlock& key)
+{
+  AutoSeededRandomPool prng;
+
+  SecByteBlock tempKey(AES_KEY_SIZE);
+  prng.GenerateBlock( tempKey, tempKey.size() );
+  key = tempKey;
+}
+
+void EncryptionHelpers::Encrypt(std::string &output, const std::string &input)
+{
+  Encrypt(output, input.data(), input.size());
+}
+
+void EncryptionHelpers::Encrypt(std::string &output, const char* data, size_t size)
+{
+  if (size > maxConcurrentInputSize_)
+  {
+    throw EncryptionException("The file is too large to encrypt: " + boost::lexical_cast<std::string>(size) + " bytes.  Try increasing the MaxConcurrentInputSize");
+  }
+
+  Orthanc::Semaphore::Locker lock(concurrentInputSizeSemaphore_, size);
+
+  EncryptInternal(output, data, size, encryptionMasterKey_);
+}
+
+void EncryptionHelpers::Decrypt(std::string &output, const std::string &input)
+{
+  output.resize(input.size() - OVERHEAD_SIZE);
+  Decrypt(const_cast<char*>(output.data()), input.data(), input.size());
+}
+
+void EncryptionHelpers::Decrypt(char* output, const char* data, size_t size)
+{
+  if (size > maxConcurrentInputSize_)
+  {
+    throw EncryptionException("The file is too large to decrypt: " + boost::lexical_cast<std::string>(size) + " bytes.  Try increasing the MaxConcurrentInputSize");
+  }
+
+  Orthanc::Semaphore::Locker lock(concurrentInputSizeSemaphore_, size);
+
+  if (size < HEADER_VERSION_SIZE)
+  {
+    throw EncryptionException("Unable to decrypt data, no header found");
+  }
+
+  std::string version = std::string(data, HEADER_VERSION_SIZE);
+
+  if (version != "A1")
+  {
+    throw EncryptionException("Unable to decrypt data, version '" + version + "' is not supported");
+  }
+
+  if (size < (HEADER_VERSION_SIZE + MASTER_KEY_ID_SIZE))
+  {
+    throw EncryptionException("Unable to decrypt data, no master key id found");
+  }
+
+  std::string decryptionMasterKeyId = std::string(data + HEADER_VERSION_SIZE, MASTER_KEY_ID_SIZE);
+
+  const SecByteBlock& decryptionMasterKey = GetMasterKey(decryptionMasterKeyId);
+  DecryptInternal(output, data, size, decryptionMasterKey);
+}
+
+void EncryptionHelpers::EncryptPrefixSecBlock(std::string& output, const CryptoPP::SecByteBlock& input, const CryptoPP::SecByteBlock& masterKey)
+{
+  try
+  {
+    SecByteBlock iv(16);
+    memset(iv.data(), 0, iv.size());
+
+    CTR_Mode<AES>::Encryption e;
+    e.SetKeyWithIV(masterKey, masterKey.size(), iv.data(), iv.size());
+
+    std::string inputString = ToString(input);
+
+    // The StreamTransformationFilter adds padding
+    //  as required. ECB and CBC Mode must be padded
+    //  to the block size of the cipher.
+    StringSource ss(inputString, true,
+        new StreamTransformationFilter(e,
+            new StringSink(output)
+        ) // StreamTransformationFilter
+    ); // StringSource
+  }
+  catch (CryptoPP::Exception& e)
+  {
+    throw EncryptionException(e.what());
+  }
+
+  assert(output.size() == input.size());
+}
+
+void EncryptionHelpers::DecryptPrefixSecBlock(CryptoPP::SecByteBlock& output, const std::string& input, const CryptoPP::SecByteBlock& masterKey)
+{
+  try
+  {
+    SecByteBlock iv(16);
+    memset(iv.data(), 0, iv.size());
+
+    CTR_Mode<AES>::Decryption  d;
+    d.SetKeyWithIV(masterKey, masterKey.size(), iv.data(), iv.size());
+
+    std::string outputString;
+
+    // The StreamTransformationFilter adds padding
+    //  as required. ECB and CBC Mode must be padded
+    //  to the block size of the cipher.
+    StringSource ss(input, true,
+        new StreamTransformationFilter(d,
+            new StringSink(outputString)
+        ) // StreamTransformationFilter
+    ); // StringSource
+
+    output.Assign((const byte*)outputString.data(), outputString.size());
+  }
+  catch (CryptoPP::Exception& e)
+  {
+    throw EncryptionException(e.what());
+  }
+
+  assert(output.size() == input.size());
+}
+
+
+void EncryptionHelpers::EncryptInternal(std::string& output, const char* data, size_t size, const CryptoPP::SecByteBlock& masterKey)
+{
+  SecByteBlock iv(IV_SIZE);
+  randomGenerator_.GenerateBlock(iv, iv.size());  // with GCM, the iv is supposed to be a nonce (not a random number).  However, since each dataKey is used only once, we consider a random number is fine.
+
+  SecByteBlock dataKey;
+  GenerateKey(dataKey);
+
+//  std::cout << ToHexString(dataKey) << std::endl;
+//  std::cout << ToHexString(iv) << std::endl;
+  std::string encryptedDataKey;
+  std::string encryptedIv;
+
+  EncryptPrefixSecBlock(encryptedIv, iv, masterKey);
+  EncryptPrefixSecBlock(encryptedDataKey, dataKey, masterKey);
+
+  std::string prefix = HEADER_VERSION + encryptionMasterKeyId_ + encryptedIv + encryptedDataKey;
+
+  try
+  {
+    GCM<AES>::Encryption e;
+    e.SetKeyWithIV(dataKey, dataKey.size(), iv, sizeof(iv));
+
+    // the output text starts with the unencrypted prefix
+    output = prefix;
+
+    AuthenticatedEncryptionFilter ef(e,
+                                      new StringSink(output), false, INTEGRITY_CHECK_TAG_SIZE
+                                      );
+
+
+    // AuthenticatedEncryptionFilter::ChannelPut
+    //  defines two channels: "" (empty) and "AAD"
+    //   channel "" is encrypted and authenticated
+    //   channel "AAD" is authenticated
+    ef.ChannelPut("AAD", (const byte*)prefix.data(), prefix.size());
+    ef.ChannelMessageEnd("AAD");
+
+    // Authenticated data *must* be pushed before
+    //  Confidential/Authenticated data. Otherwise
+    //  we must catch the BadState exception
+    ef.ChannelPut("", (const byte*)data, size);
+    ef.ChannelMessageEnd("");
+  }
+  catch(CryptoPP::Exception& e)
+  {
+    throw EncryptionException(e.what());
+  }
+}
+
+void EncryptionHelpers::DecryptInternal(char* output, const char* data, size_t size, const CryptoPP::SecByteBlock& masterKey)
+{
+  size_t prefixSize = HEADER_VERSION_SIZE + MASTER_KEY_ID_SIZE + IV_SIZE + AES_KEY_SIZE;
+
+  std::string prefix = std::string(data, prefixSize);
+  std::string mac = std::string(data + size - INTEGRITY_CHECK_TAG_SIZE, INTEGRITY_CHECK_TAG_SIZE);
+
+  std::string encryptedIv = prefix.substr(HEADER_VERSION_SIZE + MASTER_KEY_ID_SIZE, IV_SIZE);
+  std::string encryptedDataKey = prefix.substr(HEADER_VERSION_SIZE + MASTER_KEY_ID_SIZE + IV_SIZE, AES_KEY_SIZE);
+
+  SecByteBlock dataKey;
+  SecByteBlock iv;
+
+  DecryptPrefixSecBlock(iv, encryptedIv, masterKey);
+  DecryptPrefixSecBlock(dataKey, encryptedDataKey, masterKey);
+//  std::cout << ToHexString(dataKey) << std::endl;
+//  std::cout << ToHexString(iv) << std::endl;
+
+  GCM<AES>::Decryption d;
+  d.SetKeyWithIV(dataKey, sizeof(dataKey), iv, sizeof(iv));
+
+  try {
+    AuthenticatedDecryptionFilter df(d, NULL,
+                                      AuthenticatedDecryptionFilter::MAC_AT_BEGIN |
+                                      AuthenticatedDecryptionFilter::THROW_EXCEPTION, INTEGRITY_CHECK_TAG_SIZE);
+
+    // The order of the following calls are important
+    df.ChannelPut("", (const byte*)mac.data(), mac.size());
+    df.ChannelPut("AAD", (const byte*)prefix.data(), prefix.size());
+    df.ChannelPut("", (const byte*)(data) + prefixSize, size - INTEGRITY_CHECK_TAG_SIZE - prefixSize);
+
+    // If the object throws, it will most likely occur
+    //  during ChannelMessageEnd()
+    df.ChannelMessageEnd("AAD");
+    df.ChannelMessageEnd("");
+
+    // If the object does not throw, here's the only
+    // opportunity to check the data's integrity
+    if (!df.GetLastResult())
+    {
+      throw EncryptionException("The decryption filter failed for some unknown reason.  Integrity check failed ?");
+    }
+
+    // Remove data from channel
+    size_t n = (size_t)-1;
+
+    // Recover plain text
+    df.SetRetrievalChannel("");
+    n = (size_t)df.MaxRetrievable();
+
+    if(n > 0)
+    {
+      assert(n == size - OVERHEAD_SIZE);
+
+      df.Get((byte*)output, n);
+    }
+  }
+  catch (CryptoPP::Exception& ex)
+  {
+    throw EncryptionException(ex.what());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/EncryptionHelpers.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,112 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#pragma once
+
+#include <memory.h>
+#include <cryptopp/secblock.h>
+#include "cryptopp/osrng.h"
+#include <boost/thread/mutex.hpp>
+#include "Core/MultiThreading/Semaphore.h"
+
+class EncryptionException : public std::runtime_error
+{
+public:
+  EncryptionException(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+class EncryptionHelpers
+{
+public:
+  static const size_t HEADER_VERSION_SIZE = 2;
+  static const size_t MASTER_KEY_ID_SIZE = 4;
+  static const size_t AES_KEY_SIZE = 32;                  // length of AES keys (in bytes)
+  static const size_t IV_SIZE = 32;                       // length of IVs (in bytes)
+  static const size_t INTEGRITY_CHECK_TAG_SIZE = 16;      // length of the TAG that is used to check the integrity of data (in bytes)
+
+  static const size_t OVERHEAD_SIZE = HEADER_VERSION_SIZE + MASTER_KEY_ID_SIZE + AES_KEY_SIZE + IV_SIZE + INTEGRITY_CHECK_TAG_SIZE;
+
+
+  static const std::string HEADER_VERSION;
+
+private:
+  Orthanc::Semaphore                concurrentInputSizeSemaphore_;
+  size_t                            maxConcurrentInputSize_;
+
+  CryptoPP::AutoSeededRandomPool    randomGenerator_;
+
+  CryptoPP::SecByteBlock            encryptionMasterKey_;  // at a given time, there's only one master key that is used for encryption
+  std::string                       encryptionMasterKeyId_;
+
+  std::map<std::string, CryptoPP::SecByteBlock> previousMasterKeys_; // for decryption, we might use older master keys too
+
+public:
+
+  // since the memory used during encryption/decryption can grow up to a bit more than 2 times the input,
+  // we want to limit the number of threads doing concurrent processing according to the available memory
+  // instead of the number of concurrent threads
+  EncryptionHelpers(size_t maxConcurrentInputSize = 1024*1024*1024);
+
+  void SetCurrentMasterKey(uint32_t id, const std::string& path);
+
+  void SetCurrentMasterKey(uint32_t id, const CryptoPP::SecByteBlock& key);
+
+  void AddPreviousMasterKey(uint32_t id, const std::string& path);
+
+  void AddPreviousMasterKey(uint32_t id, const CryptoPP::SecByteBlock& key);
+
+  // input: plain text data
+  // output: prefix/encrypted data/integrity check tag
+  void Encrypt(std::string& output, const std::string& input);
+  void Encrypt(std::string& output, const char* data, size_t size);
+
+  // input: prefix/encrypted data/integrity check tag
+  // output: plain text data
+  void Decrypt(std::string& output, const std::string& input);
+  void Decrypt(char* output, const char* data, size_t size);
+
+  static void GenerateKey(CryptoPP::SecByteBlock& key);
+
+private:
+
+  void EncryptInternal(std::string& output, const char* data, size_t size, const CryptoPP::SecByteBlock& masterKey);
+
+  void DecryptInternal(char* output, const char* data, size_t size, const CryptoPP::SecByteBlock& masterKey);
+
+  void EncryptPrefixSecBlock(std::string& output, const CryptoPP::SecByteBlock& input, const CryptoPP::SecByteBlock& masterKey);
+
+  void DecryptPrefixSecBlock(CryptoPP::SecByteBlock& output, const std::string& input, const CryptoPP::SecByteBlock& masterKey);
+
+  std::string GetMasterKeyIdentifier(const CryptoPP::SecByteBlock& masterKey);
+
+  const CryptoPP::SecByteBlock& GetMasterKey(const std::string& keyId);
+
+public:
+
+  static std::string ToHexString(const CryptoPP::byte* block, size_t size);
+  static std::string ToHexString(const std::string& block);
+  static std::string ToHexString(const CryptoPP::SecByteBlock& block);
+  static std::string ToString(const CryptoPP::SecByteBlock& block);
+  static std::string ToString(uint32_t value);
+
+  static void ReadKey(CryptoPP::SecByteBlock& key, const std::string& path);
+  //static void EncryptionHelpers::Encrypt(std::string& output, const std::string& input, const std::string& key, const std::string& iv);
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/IStoragePlugin.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,73 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <orthanc/OrthancCPlugin.h>
+#include <OrthancPluginCppWrapper.h>
+
+class StoragePluginException : public std::runtime_error
+{
+public:
+  StoragePluginException(const std::string& what)
+    : std::runtime_error(what)
+  {
+  }
+};
+
+
+
+
+// each "plugin" must also provide these global methods
+//class XXXStoragePluginFactory
+//{
+//public:
+//  const char* GetStoragePluginName();
+//  IStoragePlugin* CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig);
+//};
+
+class IStoragePlugin
+{
+public:
+  class IWriter
+  {
+  public:
+    IWriter()
+    {}
+
+    virtual ~IWriter() {}
+    virtual void Write(const char* data, size_t size) = 0;
+  };
+
+  class IReader
+  {
+  public:
+    IReader()
+    {}
+
+    virtual ~IReader() {}
+    virtual size_t GetSize() = 0;
+    virtual void Read(char* data, size_t size) = 0;
+  };
+
+public:
+
+  virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
+  virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/Resources/DownloadOrthancFramework.cmake	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,372 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+##
+## Check whether the parent script sets the mandatory variables
+##
+
+if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR
+    (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND
+     NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path"))
+  message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"hg\", \"web\", \"archive\" or \"path\"")
+endif()
+
+
+##
+## Detection of the requested version
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set")
+  endif()
+
+  if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR
+      DEFINED ORTHANC_FRAMEWORK_MINOR OR
+      DEFINED ORTHANC_FRAMEWORK_REVISION OR
+      DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Some internal variable has been set")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_MD5 "")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH)
+    if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline")
+      set(ORTHANC_FRAMEWORK_BRANCH "default")
+      set(ORTHANC_FRAMEWORK_MAJOR 999)
+      set(ORTHANC_FRAMEWORK_MINOR 999)
+      set(ORTHANC_FRAMEWORK_REVISION 999)
+
+    else()
+      set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+      set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
+      string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION})
+      string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION})
+
+      if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR
+          NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$")
+        message("Bad version of the Orthanc framework: ${ORTHANC_FRAMEWORK_VERSION}")
+      endif()
+
+      if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1")
+        set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
+        set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1")
+        set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0")
+        set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
+        set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
+        set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
+        set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4")
+        set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5")
+        set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
+        set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7")
+        set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8")
+        set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
+        set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1")
+        set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0")
+        set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef")
+
+      # Below this point are development snapshots that were used to
+      # release some plugin, before an official release of the Orthanc
+      # framework was available. Here is the command to be used to
+      # generate a proper archive:
+      #
+      #   $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz
+      #
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df")
+        # DICOMweb 1.1 (framework pre-1.6.0)
+        set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645")
+      endif()
+    endif()
+  endif()
+else()
+  message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.")
+  set(ORTHANC_FRAMEWORK_MAJOR 999)
+  set(ORTHANC_FRAMEWORK_MINOR 999)
+  set(ORTHANC_FRAMEWORK_REVISION 999)
+endif()
+
+
+
+##
+## Detection of the third-party software
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  find_program(ORTHANC_FRAMEWORK_HG hg)
+  
+  if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND")
+    message(FATAL_ERROR "Please install Mercurial")
+  endif()
+endif()
+
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+    find_program(ORTHANC_FRAMEWORK_7ZIP 7z 
+      PATHS 
+      "$ENV{ProgramFiles}/7-Zip"
+      "$ENV{ProgramW6432}/7-Zip"
+      )
+
+    if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND")
+      message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
+    endif()
+
+  else()
+    find_program(ORTHANC_FRAMEWORK_TAR tar)
+    if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'tar' package")
+    endif()
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework specified as a path on the filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT})
+    message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+    message(FATAL_ERROR "Directory not containing the source code of Orthanc: ${ORTHANC_FRAMEWORK_ROOT}")
+  endif()
+  
+  set(ORTHANC_ROOT ${ORTHANC_FRAMEWORK_ROOT})
+endif()
+
+
+
+##
+## Case of the Orthanc framework cloned using Mercurial
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
+  if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+    message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+  endif()
+
+  set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc)
+
+  if (EXISTS ${ORTHANC_ROOT})
+    message("Updating the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} pull
+      WORKING_DIRECTORY ${ORTHANC_ROOT}
+      RESULT_VARIABLE Failure
+      )    
+  else()
+    message("Forking the Orthanc source repository using Mercurial")
+    execute_process(
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/"
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )    
+  endif()
+
+  if (Failure OR NOT EXISTS ${ORTHANC_ROOT})
+    message(FATAL_ERROR "Cannot fork the Orthanc repository")
+  endif()
+
+  message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}")
+
+  execute_process(
+    COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH}
+    WORKING_DIRECTORY ${ORTHANC_ROOT}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while running Mercurial")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework provided as a source archive on the
+## filesystem
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive")
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE)
+    message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc")
+  endif()
+endif()
+
+
+
+##
+## Case of the Orthanc framework downloaded from the Web
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+  if (DEFINED ORTHANC_FRAMEWORK_URL)
+    string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}")
+  else()
+    # Default case: Download from the official Web site
+    set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
+    set(ORTHANC_FRAMEWORK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+  endif()
+
+  set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
+
+  if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}")
+    if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
+      message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
+    endif()
+
+    message("Downloading: ${ORTHANC_FRAMEWORK_URL}")
+
+    file(DOWNLOAD
+      "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" 
+      SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}"
+      TIMEOUT 60
+      INACTIVITY_TIMEOUT 60
+      )
+  else()
+    message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}")
+  endif()  
+endif()
+
+
+
+
+##
+## Uncompressing the Orthanc framework, if it was retrieved from a
+## source archive on the filesystem, or from the official Web site
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
+    ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
+
+  if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR
+      NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR
+      NOT DEFINED ORTHANC_FRAMEWORK_MD5)
+    message(FATAL_ERROR "Internal error")
+  endif()
+
+  if (ORTHANC_FRAMEWORK_MD5 STREQUAL "")
+    message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}")
+  endif()
+
+  file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5)
+
+  if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}")
+    message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+  endif()
+
+  set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
+
+  if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+    if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$")
+      message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+    endif()
+    
+    message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}")
+
+    if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+      # How to silently extract files using 7-zip
+      # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+      
+      if (Failure)
+        message(FATAL_ERROR "Error while running the uncompression tool")
+      endif()
+
+      get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME)
+      string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
+
+      execute_process(
+        COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2}
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        OUTPUT_QUIET
+        )
+
+    else()
+      execute_process(
+        COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}"
+        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+        RESULT_VARIABLE Failure
+        )
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
+      message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endif()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/StoragePlugin.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,230 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#if GOOGLE_STORAGE_PLUGIN==1
+#include "../Google/GoogleStoragePlugin.h"
+#define StoragePluginFactory GoogleStoragePluginFactory
+#elif AZURE_STORAGE_PLUGIN==1
+#include "../Azure/AzureBlobStoragePlugin.h"
+#define StoragePluginFactory AzureBlobStoragePluginFactory
+#elif AWS_STORAGE_PLUGIN==1
+#include "../Aws/AwsS3StoragePlugin.h"
+#define StoragePluginFactory AwsS3StoragePluginFactory
+#else
+#pragma message(error  "define a plugin")
+#endif
+
+#include <string.h>
+#include <stdio.h>
+#include <string>
+
+#include <iostream>
+#include "../Common/EncryptionHelpers.h"
+#include "../Common/EncryptionConfigurator.h"
+
+static std::unique_ptr<IStoragePlugin> plugin;
+
+static std::unique_ptr<EncryptionHelpers> crypto;
+static bool cryptoEnabled = false;
+
+
+static OrthancPluginErrorCode StorageCreate(const char* uuid,
+                                            const void* content,
+                                            int64_t size,
+                                            OrthancPluginContentType type)
+{
+  try
+  {
+    std::unique_ptr<IStoragePlugin::IWriter> writer(plugin->GetWriterForObject(uuid, type, cryptoEnabled));
+
+    if (cryptoEnabled)
+    {
+      std::string encryptedFile;
+
+      try
+      {
+        crypto->Encrypt(encryptedFile, (const char*)content, size);
+      }
+      catch (EncryptionException& ex)
+      {
+        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while encrypting object " + std::string(uuid) + ": " + ex.what());
+        return OrthancPluginErrorCode_StorageAreaPlugin;
+      }
+
+      writer->Write(encryptedFile.data(), encryptedFile.size());
+    }
+    else
+    {
+      writer->Write(reinterpret_cast<const char*>(content), size);
+    }
+  }
+  catch (StoragePluginException& ex)
+  {
+    OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while creating object " + std::string(uuid) + ": " + ex.what());
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+static OrthancPluginErrorCode StorageRead(void** content,
+                                          int64_t* size,
+                                          const char* uuid,
+                                          OrthancPluginContentType type)
+{
+  try
+  {
+    std::unique_ptr<IStoragePlugin::IReader> reader(plugin->GetReaderForObject(uuid, type, cryptoEnabled));
+
+    size_t fileSize = reader->GetSize();
+
+    if (cryptoEnabled)
+    {
+      *size = fileSize - crypto->OVERHEAD_SIZE;
+    }
+    else
+    {
+      *size = fileSize;
+    }
+
+    if (*size <= 0)
+    {
+      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ", size of file is too small: " + boost::lexical_cast<std::string>(fileSize) + " bytes");
+      return OrthancPluginErrorCode_StorageAreaPlugin;
+    }
+
+    *content = malloc(static_cast<uint64_t>(*size));
+    if (*content == nullptr)
+    {
+      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(*size) + " bytes");
+      return OrthancPluginErrorCode_StorageAreaPlugin;
+    }
+
+    if (cryptoEnabled)
+    {
+      std::vector<char> encrypted(fileSize);
+      reader->Read(encrypted.data(), fileSize);
+
+      try
+      {
+        crypto->Decrypt((char*)(*content), encrypted.data(), fileSize);
+      }
+      catch (EncryptionException& ex)
+      {
+        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while decrypting object " + std::string(uuid) + ": " + ex.what());
+        return OrthancPluginErrorCode_StorageAreaPlugin;
+      }
+    }
+    else
+    {
+      reader->Read(*(reinterpret_cast<char**>(content)), fileSize);
+    }
+  }
+  catch (StoragePluginException& ex)
+  {
+    OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while creating object " + std::string(uuid) + ": " + ex.what());
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+
+}
+
+
+static OrthancPluginErrorCode StorageRemove(const char* uuid,
+                                            OrthancPluginContentType type)
+{
+  try
+  {
+    plugin->DeleteObject(uuid, type, cryptoEnabled);
+  }
+  catch (StoragePluginException& ex)
+  {
+    OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + ex.what());
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+
+    OrthancPlugins::OrthancConfiguration orthancConfig;
+
+    OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + " plugin is initializing");
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              context->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPlugins::LogError(info);
+      return -1;
+    }
+
+    plugin.reset(StoragePluginFactory::CreateStoragePlugin(orthancConfig));
+
+    static const char* const ENCRYPTION_SECTION = "StorageEncryption";
+
+    if (orthancConfig.IsSection(ENCRYPTION_SECTION))
+    {
+      OrthancPlugins::OrthancConfiguration cryptoSection;
+      orthancConfig.GetSection(cryptoSection, ENCRYPTION_SECTION);
+
+      crypto.reset(EncryptionConfigurator::CreateEncryptionHelpers(cryptoSection));
+      cryptoEnabled = crypto.get() != nullptr;
+    }
+    else
+    {
+      OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": client-side encryption is disabled");
+    }
+
+    OrthancPluginRegisterStorageArea(context, StorageCreate, StorageRead, StorageRemove);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + " plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return StoragePluginFactory::GetStoragePluginName();
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return PLUGIN_VERSION;
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Google/CMakeLists.txt	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,86 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(OrthancGoogleCloudStorage)
+
+set(PLUGIN_VERSION "0.9")
+
+include(CheckIncludeFileCXX)
+
+set(ORTHANC_FRAMEWORK_SOURCE "hg" CACHE STRING "orthanc source")
+set(ORTHANC_FRAMEWORK_VERSION "1.7.0" CACHE STRING "orthanc framework version")
+set(ALLOW_DOWNLOADS ON)
+
+# Download and setup the Orthanc framework
+include(${CMAKE_SOURCE_DIR}/../Common/Resources/DownloadOrthancFramework.cmake)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_GOOGLE_TEST ON)
+set(ORTHANC_FRAMEWORK_PLUGIN ON)
+
+include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+
+add_definitions(
+    -DHAS_ORTHANC_EXCEPTION=1
+    -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+    -DGOOGLE_STORAGE_PLUGIN=1
+    )
+add_definitions(-DPLUGIN_VERSION="${PLUGIN_VERSION}")
+
+include_directories(
+  ${ORTHANC_ROOT}
+  ${ORTHANC_ROOT}/Core
+  ${ORTHANC_ROOT}/Plugins/Include
+  ${ORTHANC_ROOT}/Plugins/Samples/Common
+  )
+
+find_package(CURL REQUIRED)
+find_package(storage_client REQUIRED)
+find_package(cryptopp CONFIG REQUIRED)
+
+set(COMMON_SOURCES
+    ${CMAKE_SOURCE_DIR}/../Common/IStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
+    ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+    ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+
+    ${ORTHANC_CORE_SOURCES}
+  )
+
+add_library(OrthancGoogleCloudStorage SHARED
+    GoogleStoragePlugin.cpp
+    GoogleStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/StoragePlugin.cpp
+
+    ${COMMON_SOURCES}
+  )
+
+set_target_properties(OrthancGoogleCloudStorage PROPERTIES
+  VERSION ${PLUGIN_VERSION}
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+target_link_libraries(OrthancGoogleCloudStorage
+  PRIVATE
+  storage_client
+  cryptopp-static
+  )
+
+add_executable(UnitTests
+    ${GOOGLE_TEST_SOURCES}
+    ${COMMON_SOURCES}
+
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/EncryptionTests.cpp
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/UnitTestsMain.cpp
+    ${CMAKE_SOURCE_DIR}/../UnitTestsSources/UnitTestsGcsClient.cpp
+    )
+
+target_link_libraries(UnitTests
+  PRIVATE
+  cryptopp-static
+  storage_client
+  ${GOOGLE_TEST_LIBRARIES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Google/GoogleStoragePlugin.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,227 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "GoogleStoragePlugin.h"
+
+#include "google/cloud/storage/client.h"
+
+// Create aliases to make the code easier to read.
+namespace gcs = google::cloud::storage;
+
+class Writer : public IStoragePlugin::IWriter
+{
+  std::string   path_;
+  gcs::Client   client_;
+  std::string   bucketName_;
+  gcs::ObjectWriteStream stream_;
+public:
+  Writer(const std::string& bucketName, const std::string& path, gcs::Client& client)
+    : path_(path),
+      client_(client),
+      bucketName_(bucketName)
+  {
+  }
+
+  virtual ~Writer()
+  {
+  }
+
+  virtual void Write(const char* data, size_t size)
+  {
+    stream_ = client_.WriteObject(bucketName_, path_);
+
+    if (stream_)
+    {
+      stream_.write(data, size);
+      stream_.Close();
+
+      if (!stream_.metadata())
+      {
+        throw StoragePluginException("GoogleCloudStorage: error while writing file " + std::string(path_) + ": " + stream_.metadata().status().message());
+      }
+    }
+    else
+    {
+      throw StoragePluginException("GoogleCloudStorage: error while opening/writing file " + std::string(path_) + ": " + stream_.metadata().status().message());
+    }
+  }
+};
+
+
+class Reader : public IStoragePlugin::IReader
+{
+  std::string   path_;
+  gcs::Client   client_;
+  std::string   bucketName_;
+
+public:
+  Reader(const std::string& bucketName, const std::string& path, gcs::Client& client)
+    : path_(path),
+      client_(client),
+      bucketName_(bucketName)
+  {
+  }
+
+  virtual ~Reader()
+  {
+
+  }
+  virtual size_t GetSize()
+  {
+    auto objectMetadata = client_.GetObjectMetadata(bucketName_, path_);
+
+    if (objectMetadata)
+    {
+      std::uint64_t fileSize = static_cast<int64_t>(objectMetadata->size());
+
+      return fileSize;
+    }
+    else
+    {
+        throw StoragePluginException("error while getting the size of " + std::string(path_) + ": " + objectMetadata.status().message());
+    }
+  }
+
+  virtual void Read(char* data, size_t size)
+  {
+    auto reader = client_.ReadObject(bucketName_, path_);
+
+    if (!reader)
+    {
+      throw StoragePluginException("error while opening/reading file " + std::string(path_) + ": " + reader.status().message());
+    }
+
+    reader.read(data, size);
+
+    if (!reader)
+    {
+      throw StoragePluginException("error while reading file " + std::string(path_) + ": " + reader.status().message());
+    }
+  }
+
+};
+
+
+
+const char* GoogleStoragePluginFactory::GetStoragePluginName()
+{
+  return "Google Cloud Storage";
+}
+
+IStoragePlugin* GoogleStoragePluginFactory::CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig)
+{
+  static const char* const PLUGIN_SECTION = "GoogleCloudStorage";
+  if (!orthancConfig.IsSection(PLUGIN_SECTION))
+  {
+    OrthancPlugins::LogWarning(std::string(GetStoragePluginName()) + " plugin, section missing.  Plugin is not enabled.");
+    return nullptr;
+  }
+
+  OrthancPlugins::OrthancConfiguration pluginSection;
+  orthancConfig.GetSection(pluginSection, PLUGIN_SECTION);
+
+  std::string pathToGoogleCredentials;
+
+  if (!pluginSection.LookupStringValue(pathToGoogleCredentials, "ServiceAccountFile"))
+  {
+    OrthancPlugins::LogError("GoogleCloudStorage/ServiceAccountFile configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  std::string googleBucketName;
+  if (!pluginSection.LookupStringValue(googleBucketName, "BucketName"))
+  {
+    OrthancPlugins::LogError("GoogleCloudStorage/BucketName configuration missing.  Unable to initialize plugin");
+    return nullptr;
+  }
+
+  // Use service account credentials from a JSON keyfile:
+  auto creds = gcs::oauth2::CreateServiceAccountCredentialsFromJsonFilePath(pathToGoogleCredentials);
+  if (!creds)
+  {
+    OrthancPlugins::LogError("GoogleCloudStorage plugin: unable to validate credentials.  Check the ServiceAccountFile: " + creds.status().message());
+    return nullptr;
+  }
+
+  // Create a client to communicate with Google Cloud Storage.
+  google::cloud::StatusOr<gcs::Client> mainClient = gcs::Client(gcs::ClientOptions(*creds));
+
+  if (!mainClient)
+  {
+    OrthancPlugins::LogError("GoogleCloudStorage plugin: unable to create client: " + mainClient.status().message());
+    return nullptr;
+  }
+
+  return new GoogleStoragePlugin(googleBucketName, mainClient.value());
+}
+
+GoogleStoragePlugin::GoogleStoragePlugin(const std::string &bucketName, google::cloud::storage::Client& mainClient)
+  : bucketName_(bucketName),
+    mainClient_(mainClient)
+{
+
+}
+
+IStoragePlugin::IWriter* GoogleStoragePlugin::GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Writer(bucketName_, GetPath(uuid, type, encryptionEnabled), mainClient_);
+}
+
+IStoragePlugin::IReader* GoogleStoragePlugin::GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new Reader(bucketName_, GetPath(uuid, type, encryptionEnabled), mainClient_);
+}
+
+void GoogleStoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  gcs::Client client(mainClient_);
+
+  std::string path = GetPath(uuid, type, encryptionEnabled);
+
+  auto deletionStatus = client.DeleteObject(bucketName_, path);
+
+  if (!deletionStatus.ok())
+  {
+    throw StoragePluginException("GoogleCloudStorage: error while deleting file " + std::string(path) + ": " + deletionStatus.message());
+  }
+
+}
+
+std::string GoogleStoragePlugin::GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  std::string path = std::string(uuid);
+
+  if (type == OrthancPluginContentType_Dicom)
+  {
+    path += ".dcm";
+  }
+  else if (type == OrthancPluginContentType_DicomAsJson)
+  {
+    path += ".json";
+  }
+  else
+  {
+    path += ".unk";
+  }
+
+  if (encryptionEnabled)
+  {
+    path += ".enc";
+  }
+  return path;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Google/GoogleStoragePlugin.h	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,51 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../Common/IStoragePlugin.h"
+
+#include "google/cloud/storage/client.h"
+
+class GoogleStoragePluginFactory
+{
+public:
+  static const char* GetStoragePluginName();
+  static IStoragePlugin* CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig);
+};
+
+
+class GoogleStoragePlugin : public IStoragePlugin
+{
+public:
+
+  std::string         bucketName_;
+  google::cloud::storage::Client mainClient_; // the client that is created at startup.  Each thread should copy it when it needs it. (from the doc: Instances of this class created via copy-construction or copy-assignment share the underlying pool of connections. Access to these copies via multiple threads is guaranteed to work. Two threads operating on the same instance of this class is not guaranteed to work.)
+
+public:
+
+  GoogleStoragePlugin(const std::string& bucketName,
+                      google::cloud::storage::Client& mainClient
+                      );
+
+  virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+private:
+  virtual std::string GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/NEWS	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,10 @@
+Pending changes in the mainline
+===============================
+
+None
+
+
+2020-07-03 - v 0.9.0
+====================
+
+* Initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,169 @@
+# README #
+
+Orthanc object-storages plugin for main cloud providers (Google/Azure/AWS)
+
+## Encryption ##
+
+### Encryption rationale ###
+
+Although all cloud providers already provide encryption at rest, the plugins provide an optional layer of client-side encryption .  It is very important that you understand the scope and benefits of this additional layer of encryption.
+
+Encryption at rest provided by cloud providers basically compares with a file-system disk encryption.  If someone has access to the disk, he won't have access to your data without the encryption key.
+
+With cloud encryption at rest only, if someone has access to the "api-key" of your storage or if one of your admin inadvertently make your storage public, PHI will leak.
+
+Once you use client-side encryption, you'll basically store packets of meaningless bytes on the cloud infrastructure.  So, if an "api-key" leaks or if the storage is misconfigured, packet of bytes will leak but not PHI.
+
+These packets of bytes might eventually not be considered as Personal Health Information (PHI) anymore and eventually help you meet your local regulations (Please check your local regulations).
+
+However, note that, if you're running entirely in a cloud environment, your decryption keys will still be stored on the cloud infrastructure (VM disks - process RAM) and an attacker could still eventually gain access to this keys.  Furthermore, in the scope of the [Cloud Act](https://bitbucket.org/osimis/orthanc-cloud-storages/src/master/UnitTestsSources/EncryptionTests.cpp), the cloud provider might still have the possibility to retrieve your data and encryption key (while it will still be more complex than with standard encryption at rest).
+
+If Orthanc is running in your infrastructure with the Index DB on your infrastructure, and files are store in the cloud, the master keys will remain on your infrastructure only and there's no way the data stored in the cloud could be decrypted outside your infrastructure.
+
+
+Also note that, although the cloud providers also provide client-side encryption, we, as an open-source project, wanted to provide our own implementation on which you'll have full control and extension capabilities.  This also allows us to implement the same logic on all cloud providers.
+
+Our encryption is based on well-known standards (see below).  Since it is documented and the source code is open-source, feel-free to have your security expert review it before using it in a production environment.
+
+### Encryption technical overview ###
+
+Orthanc saves 2 kind of files: DICOM files and JSON summaries of DICOM files.  Both files contain PHI.
+
+When configuring the plugin, you'll have to provide a `Master Key` that we can also call `Key Encryption Key` (KEK).
+
+For each file being saved, the plugin will generate a new `Data Encryption Key` (DEK).  This DEK, encrypted with the KEK will be pre-pended to the file.
+
+If, at any point, your KEK leaks or you want to rotate your KEKs, you'll be able to use a new one to encrypt new files that are being added and still use the old ones to decrypt data.  You could then eventually start a side script to remove usages of the leaked/obsolete KEKs.
+
+To summarize:
+
+- We use (Crypto++)[https://www.cryptopp.com/] to perform all encryptions.  
+- All keys (KEK and DEK) are AES-256 keys.
+- DEKs and IVs are encrypted by KEK using CTR block cipher using a null IV.
+- data is encrypted by DEK using GCM block cipher that will also perform integrity check on the whole file.
+
+The format of data stored on disk is therefore the following:
+- `VERSION HEADER`: 2 bytes: identify the structure of the following data
+- `MASTER KEY ID`: 4 bytes: a numerical ID of the KEK that was used to encrypt the DEK
+- `EIV`: 32 bytes: IV used by DEK for data encryption; encrypted by KEK
+- `EDEK`: 32 bytes: the DEK encrypted by the KEK.
+- `CIPHER TEXT`: variable length: the DICOM/JSON file encrypted by the DEK
+- `TAG`: 16 bytes: integrity check performed on the whole encrypted file (including header, master key id, EIV and EDEK)
+
+### Configuration ###
+
+AES Keys shall be 32 bytes long (256 bits) and encoded in base64.  Here's a sample OpenSSL command to generate such a key:
+
+```
+openssl rand -base64 -out /tmp/test.key 32
+```
+
+Each key must have a unique id that is a uint32 number.
+
+Here's a sample configuration file of the `StorageEncryption` section of the plugins:
+
+```
+{
+    "StorageEncryption" : {
+        "Enable": true,
+        "MasterKey": [3, "/path/to/master.key"], // key id - path to the base64 encoded key
+        "PreviousMasterKeys" : [
+            [ 1, "/path/to/previous1.key"],
+            [ 2, "/path/to/previous2.key"]
+        ],
+        "MaxConcurrentInputSize" : 1024   // size in MB 
+    }
+}
+```
+
+*MaxConcurrentInputSize*: Since the memory used during encryption/decryption can grow up to a bit more than 2 times the input, we want to limit the number of threads doing concurrent processing according to the available memory instead of the number of concurrent threads.  Therefore, if you're currently
+ingesting small files, you can have a lot of thread working together while, if you're ingesting large files, threads might have to wait before receiving a "slot" to access the encryption module.
+
+
+## Google Cloud Storage plugin ##
+
+### Prerequisites ###
+
+* Install [vcpkg](https://github.com/Microsoft/vcpkg) 
+
+### Compile Google plugin ###
+
+* `./vcpkg install google-cloud-cpp`
+* `./vcpkg install cryptopp`
+* `hg clone ...`
+* `mkdir -p build/google`
+* `cd build/google` 
+* `cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]\scripts\buildsystems\vcpkg.cmake ../../orthanc-cloud-storages/google`
+
+### Google plugin configuration ###
+
+```
+    "GoogleCloudStorage" : {
+        "ServiceAccountFile": "/.../googleServiceAccountFile.json",
+        "BucketName": "test-orthanc-storage-plugin"
+    }
+
+```
+
+## Azure Blob Storage plugin ##
+
+### Prerequisites ###
+
+* Install [vcpkg](https://github.com/Microsoft/vcpkg) 
+
+### Compile Azure plugin ###
+
+* `./vcpkg install cpprestsdk`
+* `hg clone ...`
+* `mkdir -p build/azure`
+* `cd build/azure` 
+* `cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]\scripts\buildsystems\vcpkg.cmake ../../orthanc-cloud-storages/Azure`
+
+### Azure plugin configuration ###
+
+```
+    "AzureBlobStorage" : {
+    	"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=xxxxxxxxx;AccountKey=yyyyyyyy===;EndpointSuffix=core.windows.net",
+    	"ContainerName" : "test-orthanc-storage-plugin"
+    }
+```
+
+## AWS S3 Storage plugin ##
+
+### Prerequisites ###
+
+* Install [vcpkg](https://github.com/Microsoft/vcpkg) 
+
+* compile the AWS C++ SDK
+
+```
+
+mkdir ~/aws
+cd ~/aws
+git clone https://github.com/aws/aws-sdk-cpp.git
+
+mkdir -p ~/aws/builds/aws-sdk-cpp
+cd ~/aws/builds/aws-sdk-cpp
+cmake -DBUILD_ONLY="s3;transfer" ~/aws/aws-sdk-cpp 
+make -j 4 
+make install
+```
+
+### Compile AWS S3 plugin ###
+
+* `./vcpkg install cryptopp`
+* `hg clone ...`
+* `mkdir -p build/aws`
+* `cd build/aws` 
+* `cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]\scripts\buildsystems\vcpkg.cmake ../../orthanc-cloud-storages/Aws`
+
+### Azure plugin configuration ###
+
+```
+    "AwsS3Storage" : {
+    	"BucketName": "test-orthanc-s3-plugin",
+        "Region" : "eu-central-1",
+        "AccessKey" : "AKXXX",
+        "SecretKey" : "RhYYYY"
+    }
+```
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/EncryptionTests.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,269 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#include "gtest/gtest.h"
+
+#include "../Common/EncryptionHelpers.h"
+#include <boost/chrono/chrono.hpp>
+#include <boost/date_time.hpp>
+
+TEST(EncryptionHelpers, GenerateKey)
+{
+  CryptoPP::SecByteBlock key1, key2;
+  EncryptionHelpers::GenerateKey(key1);
+  EncryptionHelpers::GenerateKey(key2);
+
+//  std::cout << EncryptionHelpers::ToHexString(key1) << std::endl;
+//  std::cout << EncryptionHelpers::ToHexString(key2) << std::endl;
+
+  ASSERT_NE(key1, key2);
+
+  ASSERT_EQ(32, key1.size()); // right now, we work with 256bits key
+  ASSERT_EQ(32*2, EncryptionHelpers::ToHexString(key1).size());
+}
+
+TEST(EncryptionHelpers, EncryptDecryptSimpleText)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  std::string plainTextMessage = "Plain text message";
+  std::string encryptedMessage;
+
+  crypto.Encrypt(encryptedMessage, plainTextMessage);
+
+  std::string decryptedMessage;
+
+  crypto.Decrypt(decryptedMessage, encryptedMessage);
+
+  ASSERT_EQ(plainTextMessage, decryptedMessage);
+}
+
+TEST(EncryptionHelpers, EncryptDecrypt1byteText)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  std::string plainTextMessage = "P";
+  std::string encryptedMessage;
+
+  crypto.Encrypt(encryptedMessage, plainTextMessage);
+
+  std::string decryptedMessage;
+
+  crypto.Decrypt(decryptedMessage, encryptedMessage);
+
+  ASSERT_EQ(plainTextMessage, decryptedMessage);
+}
+
+TEST(EncryptionHelpers, EncryptDecrypt0byteText)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  std::string plainTextMessage = "";
+  std::string encryptedMessage;
+
+  crypto.Encrypt(encryptedMessage, plainTextMessage);
+
+  std::string decryptedMessage;
+
+  crypto.Decrypt(decryptedMessage, encryptedMessage);
+
+  ASSERT_EQ(plainTextMessage, decryptedMessage);
+}
+
+TEST(EncryptionHelpers, EncryptDecryptTampering)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  std::string plainTextMessage = "Plain text message";
+  std::string encryptedMessage;
+  std::string decryptedMessage;
+
+  crypto.Encrypt(encryptedMessage, plainTextMessage);
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // change the header
+    tamperedEncryptedMessage[0] = 'B';
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // tamper the masterKeyId:
+    tamperedEncryptedMessage[EncryptionHelpers::HEADER_VERSION_SIZE + 2] = 0xAF;
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // tamper the iv:
+    tamperedEncryptedMessage[EncryptionHelpers::HEADER_VERSION_SIZE + EncryptionHelpers::MASTER_KEY_ID_SIZE + 2] = 0;
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // tamper the encrypted text:
+    tamperedEncryptedMessage[EncryptionHelpers::HEADER_VERSION_SIZE + EncryptionHelpers::MASTER_KEY_ID_SIZE + EncryptionHelpers::IV_SIZE + 2] = 0;
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // tamper the mac:
+    tamperedEncryptedMessage[tamperedEncryptedMessage.size() - 2] = 0;
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // extend the file content
+    tamperedEncryptedMessage = tamperedEncryptedMessage + "TAMPER";
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+
+  {
+    std::string tamperedEncryptedMessage = encryptedMessage;
+    // reduce the file content
+    tamperedEncryptedMessage = tamperedEncryptedMessage.substr(0, tamperedEncryptedMessage.size() - 5);
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, tamperedEncryptedMessage), EncryptionException);
+  }
+}
+
+
+TEST(EncryptionHelpers, EncryptDecrypt2TimesSameText)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  std::string plainTextMessage = "Plain text message";
+  std::string encryptedMessage1;
+  std::string encryptedMessage2;
+
+  crypto.Encrypt(encryptedMessage1, plainTextMessage);
+  crypto.Encrypt(encryptedMessage2, plainTextMessage);
+
+  ASSERT_NE(encryptedMessage1, encryptedMessage2);
+
+  std::string decryptedMessage1;
+  std::string decryptedMessage2;
+
+  crypto.Decrypt(decryptedMessage1, encryptedMessage1);
+  crypto.Decrypt(decryptedMessage2, encryptedMessage2);
+
+  ASSERT_EQ(plainTextMessage, decryptedMessage1);
+  ASSERT_EQ(plainTextMessage, decryptedMessage2);
+}
+
+TEST(EncryptionHelpers, RotateMasterKeys)
+{
+  std::string plainTextMessage = "Plain text message";
+  std::string encryptedMessage1;
+  std::string encryptedMessage2;
+  std::string decryptedMessage;
+
+  CryptoPP::SecByteBlock masterKey1;
+  CryptoPP::SecByteBlock masterKey2;
+  EncryptionHelpers::GenerateKey(masterKey1);
+  EncryptionHelpers::GenerateKey(masterKey2);
+
+  {
+    EncryptionHelpers crypto;
+    crypto.SetCurrentMasterKey(1, masterKey1);
+    crypto.Encrypt(encryptedMessage1, plainTextMessage);
+
+    crypto.SetCurrentMasterKey(2, masterKey2);
+    crypto.AddPreviousMasterKey(1, masterKey1);
+
+    crypto.Encrypt(encryptedMessage2, plainTextMessage);
+
+    // ensure that we can decrypt messages encrypted with both master keys
+    crypto.Decrypt(decryptedMessage, encryptedMessage1);
+    ASSERT_EQ(plainTextMessage, decryptedMessage);
+
+    crypto.Decrypt(decryptedMessage, encryptedMessage2);
+    ASSERT_EQ(plainTextMessage, decryptedMessage);
+  }
+
+  {
+    // if we don't know the old key, check we can not decrypt the old message
+    EncryptionHelpers crypto;
+    crypto.SetCurrentMasterKey(2, masterKey2);
+
+    ASSERT_THROW(crypto.Decrypt(decryptedMessage, encryptedMessage1), EncryptionException);
+  }
+}
+
+
+void MeasurePerformance(size_t sizeInMB, EncryptionHelpers& crypto)
+{
+  std::string largePlainText(sizeInMB * 1024 * 1024, 'A');
+  std::string encryptedMessage;
+  std::string decryptedMessage;
+
+  {
+    auto start = boost::posix_time::microsec_clock::local_time();
+    crypto.Encrypt(encryptedMessage, largePlainText);
+
+    auto end = boost::posix_time::microsec_clock::local_time();
+    boost::posix_time::time_duration diff = end - start;
+    std::cout << "encryption of " << sizeInMB << " MB file took " << diff.total_milliseconds() << " ms" << std::endl;
+  }
+
+  {
+    auto start = boost::posix_time::microsec_clock::local_time();
+    crypto.Decrypt(decryptedMessage, encryptedMessage);
+
+    auto end = boost::posix_time::microsec_clock::local_time();
+    boost::posix_time::time_duration diff = end - start;
+    std::cout << "decryption of " << sizeInMB << " MB file took " << diff.total_milliseconds() << " ms" << std::endl;
+  }
+
+}
+
+TEST(EncryptionHelpers, Performance)
+{
+  CryptoPP::SecByteBlock masterKey;
+  EncryptionHelpers::GenerateKey(masterKey);
+
+  EncryptionHelpers crypto;
+  crypto.SetCurrentMasterKey(1, masterKey);
+
+  MeasurePerformance(1, crypto);
+  MeasurePerformance(10, crypto);
+//  MeasurePerformance(100, crypto);
+//  MeasurePerformance(400, crypto);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/UnitTestsGcsClient.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,76 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+#include "gtest/gtest.h"
+#include <threads.h>
+#include <thread>
+#include "google/cloud/storage/client.h"
+
+namespace gcs = google::cloud::storage;
+std::string credentialsPath = "/home/am/builds/orthanc/google-api-key.json"; // TODO: change to your file
+std::string googleBucketName = "test-orthanc-storage-plugin";
+
+// Note: this test actually crashes when run from gdb.  Same happens in the Orthanc plugins (only with GDB again !).
+// Since this does not happen when run from command line (same for Orthanc plugins), we never investigated any further.
+TEST(DISABLED_GcsClient, PruneDeadConnections)
+{
+  static google::cloud::StatusOr<gcs::Client> mainClient; // the client that is created at startup.  Each thread should copy it when it needs it. (from the doc: Instances of this class created via copy-construction or copy-assignment share the underlying pool of connections. Access to these copies via multiple threads is guaranteed to work. Two threads operating on the same instance of this class is not guaranteed to work.)
+
+  // Use service account credentials from a JSON keyfile:
+  auto creds = gcs::oauth2::CreateServiceAccountCredentialsFromJsonFilePath(credentialsPath);
+  ASSERT_TRUE(creds);
+
+  mainClient = gcs::Client(gcs::ClientOptions(*creds));
+
+  ASSERT_TRUE(mainClient);
+
+  std::string content = "MY TEST FILE CONTENT";
+  std::string filePath = "0000_test_file.txt";
+
+  {
+    gcs::Client client(mainClient.value());
+
+    auto writer = client.WriteObject(googleBucketName, filePath);
+    writer.write(content.data(), content.size());
+    writer.Close();
+
+    ASSERT_TRUE(writer.metadata());
+  }
+
+  std::cout << "file written (1)" << std::endl;
+  system("date && netstat -p | grep UnitTests");
+
+  // on my system, I need 4 minutes before the connections go into "CLOSE_WAIT" state
+  std::cout << "waiting 250s" << std::endl;
+  std::this_thread::sleep_for(std::chrono::seconds(250));
+  system("date && netstat -p | grep UnitTests");
+
+  // and an extra 1 minute before the previous connection disappears
+  std::cout << "waiting 60s" << std::endl;
+  std::this_thread::sleep_for(std::chrono::seconds(60));
+  system("date && netstat -p | grep UnitTests");
+
+  {
+    gcs::Client client(mainClient.value());
+
+    auto writer = client.WriteObject(googleBucketName, filePath);
+    writer.write(content.data(), content.size());
+    writer.Close();
+
+    ASSERT_TRUE(writer.metadata());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/UnitTestsMain.cpp	Fri Jul 03 10:08:44 2020 +0200
@@ -0,0 +1,27 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "gtest/gtest.h"
+
+int main(int argc, char **argv)
+{
+  ::testing::InitGoogleTest(&argc, argv);
+  int result = RUN_ALL_TESTS();
+
+  return result;
+}