commit 5377cfb1bf5123f48d5e95a447092dcc1e1cfe36
parent 51d5c9b214674b244f90baa88b45c537c05cd2ee
Author: MTRNord <mtrnord1@gmail.com>
Date: Wed, 7 Aug 2024 02:30:28 +0200
Implement basic olm account class
Diffstat:
12 files changed, 1367 insertions(+), 9 deletions(-)
diff --git a/include/account.hpp b/include/account.hpp
@@ -0,0 +1,226 @@
+#pragma once
+
+#include <numeric>
+#include <botan/x25519.h>
+#include <botan/ed25519.h>
+#include <botan/base64.h>
+
+#include "list.hpp"
+
+namespace spank_olm
+{
+ /**
+ * \brief Represents identity keys containing both Ed25519 and Curve25519 key pairs.
+ *
+ * This struct aggregates an Ed25519 key pair for signing and a Curve25519 key pair
+ * for encryption and key exchange.
+ */
+ struct IdentityKeys
+ {
+ Botan::Ed25519_PrivateKey ed25519_key;
+ ///< The Ed25519 key pair for signing. The public key can be obtained using the public_key() method.
+ Botan::X25519_PrivateKey curve25519_key;
+ ///< The Curve25519 key pair for encryption and key exchange. The public key can be obtained using the public_key() method.
+ };
+
+ /**
+ * \brief Represents a one-time key used in the encryption process.
+ *
+ * This struct contains an identifier, a publication status, and a Curve25519 key pair.
+ */
+ struct OneTimeKey
+ {
+ std::uint32_t id; ///< The unique identifier for the one-time key.
+ bool published; ///< Indicates whether the key has been published.
+ Botan::X25519_PrivateKey key; ///< The Curve25519 key pair for encryption and key exchange.
+ };
+
+ constexpr std::size_t MAX_ONE_TIME_KEYS(100); ///< The maximum number of one-time keys.
+
+ struct Account
+ {
+ Account() : num_fallback_keys(0),
+ next_one_time_key_id(0)
+ {
+ }
+
+ Account(Account const&);
+
+ std::optional<IdentityKeys> identity_keys; ///< The identity keys for the account.
+ FixedSizeArray<OneTimeKey, MAX_ONE_TIME_KEYS> one_time_keys; ///< The one-time keys for the account.
+ std::uint8_t num_fallback_keys; ///< The number of fallback keys.
+ std::optional<OneTimeKey> current_fallback_key; ///< The current fallback key.
+ std::optional<OneTimeKey> prev_fallback_key; ///< The previous fallback key.
+ std::uint32_t next_one_time_key_id; ///< The identifier for the next one-time key.
+
+ /**
+ * \brief Generates a new account with the given identity keys.
+ *
+ * \param rng The botan random number generator to use.
+ * \return The result of the operation.
+ * \throws SpankOlmErrorKeyGeneration if the key generation fails.
+ */
+ void new_account(Botan::RandomNumberGenerator& rng);
+
+ /**
+ * \brief Output the identity keys for this account as JSON.
+ *
+ * The output JSON will have the following format:
+ *
+ * ```json
+ * {
+ * "curve25519": "<43 base64 characters>",
+ * "ed25519": "<43 base64 characters>"
+ * }
+ * ```
+ *
+ * \return The JSON representation of the identity keys.
+ */
+ [[nodiscard]] constexpr std::string get_identity_json() const
+ {
+ auto curve25519_key = identity_keys->curve25519_key.public_key()->raw_public_key_bits();
+ auto ed25519_key = identity_keys->ed25519_key.public_key()->raw_public_key_bits();
+
+ const auto curve25519_base64 = Botan::base64_encode(curve25519_key);
+ const auto ed25519_base64 = Botan::base64_encode(ed25519_key);
+
+ return R"({"curve25519": ")" + curve25519_base64 +
+ R"(", "ed25519": ")" + ed25519_base64 + "\"}";
+ }
+
+ /**
+ * \brief Signs a message using the Ed25519 key.
+ *
+ * @param message The message to sign.
+ * @return The signature of the message.
+ */
+ [[nodiscard]] std::vector<uint8_t> sign(std::string_view message) const;
+
+
+ /**
+ * \brief Output the identity keys for this account as JSON.
+ *
+ * The output JSON will have the following format:
+ *
+ * ```json
+ * {
+ * "curve25519": [
+ * "<6 byte key id>": "<43 base64 characters>",
+ * "<6 byte key id>": "<43 base64 characters>",
+ * ...
+ * ]
+ * }
+ * ```
+ *
+ * @return Returns the JSON representation of the one time keys which haven't been published yet.
+ */
+ [[nodiscard]] constexpr std::string get_one_time_keys_json()
+ {
+ std::vector<std::string> stringified_keys;
+
+ for (const auto& key : one_time_keys)
+ {
+ if (!key->published)
+ {
+ auto key_base64 = Botan::base64_encode(key->key.public_key()->raw_public_key_bits());
+ stringified_keys.push_back(R"(")" + std::to_string(key->id) + R"(": ")" + key_base64 + "\"");
+ }
+ }
+
+ const std::string keys_json = std::accumulate(
+ stringified_keys.begin(), stringified_keys.end(), std::string(),
+ [](const std::string& acc, const std::string& key)
+ {
+ return acc.empty() ? key : acc + ", " + key;
+ });
+
+ return R"({"curve25519": {)" + keys_json + "}}";
+ }
+
+ /**
+ * \brief Mark the curent list of one_time_keys and the current_fallback_key as published.
+ *
+ * The current one time keys will no longer be returned by
+ * get_one_time_keys_json() and the current fallback key will no longer be
+ * returned by get_unpublished_fallback_key_json().
+ *
+ * \return The count of keys marked as published.
+ */
+ std::size_t mark_keys_as_published();
+
+ /**
+ * \brief Returns the maximum number of one-time keys.
+ *
+ * This function provides the maximum number of one-time keys that can be stored
+ * in the account. This value is defined by the constant MAX_ONE_TIME_KEYS.
+ *
+ * \return The maximum number of one-time keys.
+ */
+ [[nodiscard]] static constexpr std::size_t max_number_of_one_time_keys()
+ {
+ return MAX_ONE_TIME_KEYS;
+ }
+
+ /**
+ * \brief Generates a number of new one-time keys.
+ *
+ * Generates a number of new one time keys. If the total number of keys
+ * stored by this account exceeds max_number_of_one_time_keys() then the
+ * old keys are discarded.
+ */
+ void generate_one_time_keys(Botan::RandomNumberGenerator& rng, std::size_t number_of_keys);
+
+ /**
+ * \brief Generates a new fallback key.
+ */
+ void generate_fallback_key(Botan::RandomNumberGenerator& rng);
+
+ /**
+ * \brief Output the currentöy unpublished fallback key as JSON.
+ *
+ * The output JSON will have the following format:
+ *
+ * ```json
+ * {
+ * "curve25519": [
+ * "<6 byte key id>": "<43 base64 characters>",
+ * "<6 byte key id>": "<43 base64 characters>",
+ * ...
+ * ]
+ * }
+ * ```
+ */
+ [[nodiscard]] std::string constexpr get_unpublished_fallback_key_json() const
+ {
+ if (!current_fallback_key || current_fallback_key->published)
+ {
+ return R"({"curve25519": {}})";
+ }
+
+ const auto key_base64 = Botan::base64_encode(current_fallback_key->key.public_key()->raw_public_key_bits());
+ return R"({"curve25519": {")" + std::to_string(current_fallback_key->id) + R"(": ")" + key_base64 + "\"}}";
+ }
+
+ /**
+ * \brief Forget about the old fallback key.
+ */
+ void forget_old_fallback_key();
+
+ /**
+ * \brief Lookup a one time key with the given public key
+ */
+ [[nodiscard]] std::optional<OneTimeKey const*> lookup_key(Botan::X25519_PublicKey const& key) const;
+
+ /**
+ * \brief Remove a one time key with the given public key
+ */
+ void remove_key(Botan::X25519_PublicKey const& key);
+
+ [[nodiscard]] std::vector<uint8_t> pickle() const;
+
+ static Account unpickle(std::vector<uint8_t> const& data);
+ };
+}
+
+
+
diff --git a/include/errors.hpp b/include/errors.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <exception>
+#include <string>
+#include <utility>
+
+// Base exception class for SpankOlm
+class SpankOlmException : public std::exception
+{
+public:
+ explicit SpankOlmException(std::string message) : message_(std::move(message))
+ {
+ }
+
+ [[nodiscard]] const char* what() const noexcept override
+ {
+ return message_.c_str();
+ }
+
+private:
+ std::string message_;
+};
+
+// Specific exception for key generation errors
+class SpankOlmErrorKeyGeneration final : public SpankOlmException
+{
+public:
+ SpankOlmErrorKeyGeneration() : SpankOlmException("Error generating key pair.")
+ {
+ }
+};
+
+// Specific exception for unknown pickle version errors
+class SpankOlmErrorUnknownPickleVersion final : public SpankOlmException
+{
+public:
+ SpankOlmErrorUnknownPickleVersion() : SpankOlmException("Unknown pickle version.")
+ {
+ }
+};
+
+// Specific exception for not finding the version in the pickle
+class SpankOlmErrorVersionNotFound final : public SpankOlmException
+{
+public:
+ SpankOlmErrorVersionNotFound() : SpankOlmException("Version not found in pickle.")
+ {
+ }
+};
+
+// Specific exception for a bad legacy account pickle
+class SpankOlmErrorBadLegacyAccountPickle final : public SpankOlmException
+{
+public:
+ SpankOlmErrorBadLegacyAccountPickle() : SpankOlmException("Bad legacy account pickle.")
+ {
+ }
+};
+
+// Specific exception for a corrupted account pickle
+class SpankOlmErrorCorruptedAccountPickle final : public SpankOlmException
+{
+public:
+ SpankOlmErrorCorruptedAccountPickle() : SpankOlmException("Corrupted account pickle.")
+ {
+ }
+};
diff --git a/include/export.hpp b/include/export.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#ifdef SPANK_OLM_STATIC_DEFINE
+# define SPANK_OLM_EXPORT
+# define SPANK_OLM_NO_EXPORT
+#else
+# ifndef SPANK_OLM_EXPORT
+# ifdef SPANK_olm_EXPORTS
+ /* We are building this library */
+# define SPANK_OLM_EXPORT __attribute__((visibility("default")))
+# else
+ /* We are using this library */
+# define SPANK_OLM_EXPORT __attribute__((visibility("default")))
+# endif
+# endif
+
+# ifndef SPANK_OLM_NO_EXPORT
+# define SPANK_OLM_NO_EXPORT __attribute__((visibility("hidden")))
+# endif
+#endif
+
+#ifndef SPANK_OLM_DEPRECATED
+# define SPANK_OLM_DEPRECATED __attribute__ ((__deprecated__))
+#endif
+
+#ifndef SPANK_OLM_DEPRECATED_EXPORT
+# define SPANK_OLM_DEPRECATED_EXPORT SPANK_OLM_EXPORT SPANK_OLM_DEPRECATED
+#endif
+
+#ifndef SPANK_OLM_DEPRECATED_NO_EXPORT
+# define SPANK_OLM_DEPRECATED_NO_EXPORT SPANK_OLM_NO_EXPORT SPANK_OLM_DEPRECATED
+#endif
+
+#if 0 /* DEFINE_NO_DEPRECATED */
+# ifndef SPANK_OLM_NO_DEPRECATED
+# define SPANK_OLM_NO_DEPRECATED
+# endif
+#endif
+
diff --git a/include/list.hpp b/include/list.hpp
@@ -0,0 +1,245 @@
+#pragma once
+#include <cstddef>
+#include <iostream>
+#include <iterator>
+#include <utility>
+#include <memory>
+
+namespace spank_olm
+{
+ // Possibly should be replaced by implace_vector. For example https://godbolt.org/z/5P78aG5xE
+
+ /**
+ * \brief A fixed-size array implementation.
+ *
+ * \tparam T The type of elements stored in the array.
+ * \tparam max_size The maximum number of elements the array can hold.
+ */
+ template <typename T, std::size_t max_size>
+ class FixedSizeArray
+ {
+ public:
+ /**
+ * \brief Constructs an empty FixedSizeArray.
+ */
+ FixedSizeArray() : current_size(0)
+ {
+ data = std::make_unique<T*[]>(max_size);
+ }
+
+ /**
+ * \brief Destroys the FixedSizeArray and frees allocated memory.
+ */
+ ~FixedSizeArray()
+ {
+ clear();
+ }
+
+ // Error codes
+ enum ErrorCode
+ {
+ SUCCESS = 0, ///< Operation was successful.
+ INDEX_OUT_OF_RANGE = 1 ///< Index was out of range.
+ };
+
+ /**
+ * \brief Inserts a value at the beginning of the array.
+ *
+ * \param value The value to insert.
+ * \return ErrorCode indicating the result of the operation.
+ */
+ constexpr ErrorCode insert(const T& value)
+ {
+ return insert_at(0, value);
+ }
+
+ /**
+ * \brief Inserts a value at a specified index in the array.
+ *
+ * \param index The index at which to insert the value.
+ * \param value The value to insert.
+ * \return ErrorCode indicating the result of the operation.
+ */
+ constexpr ErrorCode insert_at(std::size_t index, const T& value)
+ {
+ if (index > current_size)
+ {
+ return INDEX_OUT_OF_RANGE;
+ }
+ if (current_size < max_size)
+ {
+ // Shift elements to the right
+ for (std::size_t i = current_size; i > index; --i)
+ {
+ data[i] = std::move(data[i - 1]);
+ }
+ data[index] = new T(value);
+ ++current_size;
+ }
+ else
+ {
+ // Drop the last element and shift others to the right
+ delete data[max_size - 1];
+ for (std::size_t i = max_size - 1; i > index; --i)
+ {
+ data[i] = std::move(data[i - 1]);
+ }
+ data[index] = new T(value);
+ }
+ return SUCCESS;
+ }
+
+ /**
+ * \brief Erases the element at a specified index.
+ *
+ * \param index_given The index of the element to erase.
+ * \return ErrorCode indicating the result of the operation.
+ */
+ constexpr ErrorCode erase_at(const std::size_t index_given)
+ {
+ // The list behaves reversed to the array, so we need to reverse the index
+ const std::size_t index = current_size - index_given - 1;
+
+ if (index >= current_size)
+ {
+ return INDEX_OUT_OF_RANGE;
+ }
+ delete data[index];
+ for (std::size_t i = index; i < current_size - 1; ++i)
+ {
+ data[i] = std::move(data[i + 1]);
+ }
+ --current_size;
+ return SUCCESS;
+ }
+
+ /**
+ * \brief Erases the element at the specified pointer position.
+ *
+ * \param ptr The pointer to the element to erase.
+ * \return ErrorCode indicating the result of the operation.
+ */
+ constexpr ErrorCode erase(T* const ptr)
+ {
+ for (std::size_t i = 0; i < current_size; ++i)
+ {
+ if (data[i] == ptr)
+ {
+ return erase_at(i);
+ }
+ }
+ return INDEX_OUT_OF_RANGE;
+ }
+
+ /**
+ * \brief Accesses the element at a specified index.
+ *
+ * \param index The index of the element to access.
+ * \return A reference to the element at the specified index.
+ */
+ constexpr T& operator[](std::size_t index)
+ {
+ return *data[index];
+ }
+
+ /**
+ * \brief Accesses the element at a specified index (const version).
+ *
+ * \param index The index of the element to access.
+ * \return A const reference to the element at the specified index.
+ */
+ constexpr const T& operator[](std::size_t index) const
+ {
+ return *data[index];
+ }
+
+ /**
+ * \brief Assigns the contents of another FixedSizeArray to this one.
+ *
+ * \param other The FixedSizeArray to copy from.
+ * \return A reference to this FixedSizeArray.
+ */
+ constexpr FixedSizeArray& operator=(const FixedSizeArray& other)
+ {
+ if (this != &other)
+ {
+ clear();
+ data = std::make_unique<T*[]>(max_size);
+ for (std::size_t i = 0; i < other.current_size; ++i)
+ {
+ data[i] = new T(*other.data[i]);
+ }
+ current_size = other.current_size;
+ }
+ return *this;
+ }
+
+ /**
+ * \brief Returns the number of elements in the array.
+ *
+ * \return The number of elements in the array.
+ */
+ [[nodiscard]] constexpr std::size_t size() const
+ {
+ return current_size;
+ }
+
+ /**
+ * \brief Checks if the array is empty.
+ *
+ * \return True if the array is empty, false otherwise.
+ */
+ [[nodiscard]] constexpr bool empty() const
+ {
+ return current_size == 0;
+ }
+
+ // Iterator support
+ using iterator = T**;
+ using const_iterator = const T**;
+
+ /**
+ * \brief Returns an iterator to the beginning of the array.
+ *
+ * \return An iterator to the beginning of the array.
+ */
+ constexpr iterator begin() { return data.get(); }
+
+ /**
+ * \brief Returns a const iterator to the beginning of the array.
+ *
+ * \return A const iterator to the beginning of the array.
+ */
+ constexpr const_iterator begin() const { return const_cast<const_iterator>(data.get()); }
+
+ /**
+ * \brief Returns an iterator to the end of the array.
+ *
+ * \return An iterator to the end of the array.
+ */
+ constexpr iterator end() { return data.get() + current_size; }
+
+ /**
+ * \brief Returns a const iterator to the end of the array.
+ *
+ * \return A const iterator to the end of the array.
+ */
+ constexpr const_iterator end() const { return const_cast<const_iterator>(data.get() + current_size); }
+
+ private:
+ /**
+ * \brief Clears the array and frees allocated memory.
+ */
+ void clear()
+ {
+ for (std::size_t i = 0; i < current_size; ++i)
+ {
+ delete data[i];
+ }
+ current_size = 0;
+ }
+
+ std::unique_ptr<T*[]> data; ///< Pointer to the array data.
+ std::size_t current_size; ///< The current number of elements in the array.
+ };
+}
diff --git a/include/slap-olm.hpp b/include/slap-olm.hpp
@@ -1 +0,0 @@
-#pragma once
-\ No newline at end of file
diff --git a/include/spank-olm.hpp b/include/spank-olm.hpp
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "account.hpp"
+\ No newline at end of file
diff --git a/meson.build b/meson.build
@@ -4,7 +4,7 @@ project('spank-olm', 'cpp',
'cpp_std=c++23',
'b_lto=true',
'b_thinlto_cache=true',
- 'warning_level=3',])
+ 'warning_level=3', ])
# libFuzzer related things
fuzzing_engine = get_option('fuzzing_engine')
@@ -22,15 +22,18 @@ botan_dep = dependency('botan-3', version : '>=3.6.0', required : true, method :
spank_olm_deps = [botan_dep]
incdir = include_directories('include')
-spank_olm = library('spank_olm', 'src/slap-olm.cpp', install : true, dependencies : spank_olm_deps, include_directories : incdir)
+# List of source files
+src_files = files('src/spank-olm.cpp', 'src/account.cpp')
+
+spank_olm = library('spank_olm', src_files, install : true, dependencies : spank_olm_deps, include_directories : incdir)
spank_olm_dep = declare_dependency(
- link_with: spank_olm,
- dependencies: spank_olm_deps,
+ link_with : spank_olm,
+ dependencies : spank_olm_deps,
)
snitch_dep = dependency('snitch')
-test('test', executable('spank-olm-test','tests/test.cpp',dependencies:snitch_dep, link_with : spank_olm))
+test('list_test', executable('spank-olm-test', 'tests/list_test.cpp', dependencies : [snitch_dep, spank_olm_dep], include_directories : incdir))
subdir('fuzz')
\ No newline at end of file
diff --git a/src/account.cpp b/src/account.cpp
@@ -0,0 +1,609 @@
+#include "account.hpp"
+#include "errors.hpp"
+
+#include <botan/pubkey.h>
+#include <botan/auto_rng.h>
+
+/* Convenience macro for checking the return value of internal unpickling
+ * functions and returning early on failure. */
+#ifndef UNPICKLE_OK
+#define UNPICKLE_OK(x) do { if (!(x)) return NULL; } while(0)
+#endif
+
+
+namespace spank_olm
+{
+ /**
+ * Serializes a 32-bit unsigned integer into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The 32-bit unsigned integer to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(std::uint8_t* pos, const std::uint32_t value)
+ {
+ for (int i = 3; i >= 0; --i)
+ {
+ *(pos++) = (value >> (i * 8)) & 0xFF;
+ }
+ return pos;
+ }
+
+ /**
+ * Deserializes a 32-bit unsigned integer from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the 32-bit unsigned integer to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(std::uint8_t const* pos, std::uint8_t const* end, std::uint32_t& value)
+ {
+ value = 0;
+ if (!pos || end < pos + 4) return nullptr;
+ for (unsigned i = 0; i < 4; ++i)
+ {
+ value = (value << 8) | *(pos++);
+ }
+ return pos;
+ }
+
+ /**
+ * Serializes a boolean value into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The boolean value to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(std::uint8_t* pos, const bool value)
+ {
+ *(pos++) = value ? 1 : 0;
+ return pos;
+ }
+
+ /**
+ * Deserializes a boolean value from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the boolean value to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(std::uint8_t const* pos, std::uint8_t const* end, bool& value)
+ {
+ if (!pos || end <= pos) return nullptr;
+ value = *(pos++) != 0;
+ return pos;
+ }
+
+ /**
+ * Serializes a Botan::secure_vector<uint8_t> into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The Botan::secure_vector<uint8_t> to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(std::uint8_t* pos, const Botan::secure_vector<uint8_t>& value)
+ {
+ pos = pickle(pos, static_cast<std::uint32_t>(value.size()));
+ for (const auto byte : value)
+ {
+ *(pos++) = byte;
+ }
+ return pos;
+ }
+
+ /**
+ * Deserializes a Botan::secure_vector<uint8_t> from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the Botan::secure_vector<uint8_t> to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(std::uint8_t const* pos, std::uint8_t const* end, Botan::secure_vector<uint8_t>& value)
+ {
+ std::uint32_t size;
+ pos = unpickle(pos, end, size);
+ if (!pos || end < pos + size) return nullptr;
+ value.assign(pos, pos + size);
+ return pos + size;
+ }
+
+
+ /**
+ * Serializes a std::vector<uint8_t> into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The Botan::secure_vector<uint8_t> to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(std::uint8_t* pos, const std::vector<uint8_t>& value)
+ {
+ pos = pickle(pos, static_cast<std::uint32_t>(value.size()));
+ for (const auto byte : value)
+ {
+ *(pos++) = byte;
+ }
+ return pos;
+ }
+
+ /**
+ * Deserializes a std::vector<uint8_t> from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the Botan::secure_vector<uint8_t> to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(std::uint8_t const* pos, std::uint8_t const* end, std::vector<uint8_t>& value)
+ {
+ std::uint32_t size;
+ pos = unpickle(pos, end, size);
+ if (!pos || end < pos + size) return nullptr;
+ value.assign(pos, pos + size);
+ return pos + size;
+ }
+
+ /**
+ * Serializes a OneTimeKey object into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The OneTimeKey object to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ static std::uint8_t* pickle(
+ std::uint8_t* pos,
+ OneTimeKey const& value
+ )
+ {
+ pos = pickle(pos, value.id);
+ pos = pickle(pos, value.published);
+ pos = pickle(pos, value.key.private_key_bits());
+ return pos;
+ }
+
+ /**
+ * Deserializes a OneTimeKey object from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the OneTimeKey object to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ static std::uint8_t const* unpickle(
+ std::uint8_t const* pos, std::uint8_t const* end,
+ OneTimeKey& value
+ )
+ {
+ pos = unpickle(pos, end, value.id);
+ UNPICKLE_OK(pos);
+ pos = unpickle(pos, end, value.published);
+ UNPICKLE_OK(pos);
+ Botan::secure_vector<uint8_t> key_bits;
+ pos = unpickle(pos, end, key_bits);
+ UNPICKLE_OK(pos);
+ value.key = Botan::X25519_PrivateKey(key_bits);
+ return pos;
+ }
+
+ /**
+ * Serializes a FixedSizeArray object into a byte array.
+ *
+ * @tparam T The type of elements in the FixedSizeArray.
+ * @tparam max_size The maximum size of the FixedSizeArray.
+ * @param pos Pointer to the current position in the byte array.
+ * @param list The FixedSizeArray object to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ template <typename T, std::size_t max_size>
+ std::uint8_t* pickle(
+ std::uint8_t* pos,
+ FixedSizeArray<T, max_size> const& list
+ )
+ {
+ pos = pickle(pos, static_cast<std::uint32_t>(list.size()));
+ for (auto const& value : list)
+ {
+ pos = spank_olm::pickle(pos, *value);
+ }
+ return pos;
+ }
+
+ /**
+ * Deserializes a FixedSizeArray object from a byte array.
+ *
+ * @tparam T The type of elements in the FixedSizeArray.
+ * @tparam max_size The maximum size of the FixedSizeArray.
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param list Reference to the FixedSizeArray object to store the deserialized values.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ template <typename T, std::size_t max_size>
+ std::uint8_t const* unpickle(
+ std::uint8_t const* pos, std::uint8_t const* end,
+ FixedSizeArray<T, max_size>& list
+ )
+ {
+ std::uint32_t size;
+ pos = unpickle(pos, end, size);
+ if (!pos)
+ {
+ return nullptr;
+ }
+
+ while (size-- && pos != end)
+ {
+ T value;
+ pos = spank_olm::unpickle(pos, end, value);
+ if (!pos)
+ {
+ return nullptr;
+ }
+ list.insert(value);
+ }
+
+ return pos;
+ }
+
+ /**
+ * Serializes a uint8_t value into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The uint8_t value to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(
+ std::uint8_t* pos,
+ const std::uint8_t value
+ )
+ {
+ *(pos++) = value;
+ return pos;
+ }
+
+ /**
+ * Deserializes a uint8_t value from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the uint8_t value to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(
+ std::uint8_t const* pos, std::uint8_t const* end,
+ std::uint8_t& value
+ )
+ {
+ if (!pos || pos == end) return nullptr;
+ value = *(pos++);
+ return pos;
+ }
+
+ /**
+ * Serializes an optional IdentityKeys object into a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param value The optional IdentityKeys object to serialize.
+ * @return Pointer to the position in the byte array after the serialized data.
+ */
+ std::uint8_t* pickle(
+ std::uint8_t* pos,
+ const std::optional<IdentityKeys>& value)
+ {
+ pos = pickle(pos, value->ed25519_key.public_key()->raw_public_key_bits());
+ pos = pickle(pos, value->ed25519_key.raw_private_key_bits());
+ pos = pickle(pos, value->curve25519_key.public_key()->raw_public_key_bits());
+ pos = pickle(pos, value->curve25519_key.raw_private_key_bits());
+ return pos;
+ }
+
+ /**
+ * Deserializes an optional IdentityKeys object from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the optional IdentityKeys object to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(
+ std::uint8_t const* pos, std::uint8_t const* end,
+ std::optional<IdentityKeys>& value)
+ {
+ if (!pos || pos == end) return nullptr;
+ Botan::secure_vector<uint8_t> ed25519_public_key_bits;
+ pos = unpickle(pos, end, ed25519_public_key_bits);
+ UNPICKLE_OK(pos);
+ Botan::secure_vector<uint8_t> ed25519_private_key_bits;
+ pos = unpickle(pos, end, ed25519_private_key_bits);
+ UNPICKLE_OK(pos);
+ Botan::secure_vector<uint8_t> curve25519_public_key_bits;
+ pos = unpickle(pos, end, curve25519_public_key_bits);
+ UNPICKLE_OK(pos);
+ Botan::secure_vector<uint8_t> curve25519_private_key_bits;
+ pos = unpickle(pos, end, curve25519_private_key_bits);
+ UNPICKLE_OK(pos);
+ value = IdentityKeys{
+ Botan::Ed25519_PrivateKey(ed25519_private_key_bits),
+ Botan::X25519_PrivateKey(curve25519_private_key_bits)
+ };
+ return pos;
+ }
+
+ /**
+ * Deserializes an optional OneTimeKey object from a byte array.
+ *
+ * @param pos Pointer to the current position in the byte array.
+ * @param end Pointer to the end of the byte array.
+ * @param value Reference to the optional OneTimeKey object to store the deserialized value.
+ * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure.
+ */
+ std::uint8_t const* unpickle(
+ std::uint8_t const* pos, std::uint8_t const* end,
+ std::optional<OneTimeKey>& value)
+ {
+ if (!pos || pos == end) return nullptr;
+ std::uint32_t id;
+ bool published;
+ Botan::secure_vector<uint8_t> key_bits;
+ pos = unpickle(pos, end, id);
+ UNPICKLE_OK(pos);
+ pos = unpickle(pos, end, published);
+ UNPICKLE_OK(pos);
+ pos = unpickle(pos, end, key_bits);
+ UNPICKLE_OK(pos);
+ value = OneTimeKey{id, published, Botan::X25519_PrivateKey(key_bits)};
+ return pos;
+ }
+
+ void Account::new_account(Botan::RandomNumberGenerator& rng)
+ {
+ identity_keys = IdentityKeys{
+ Botan::Ed25519_PrivateKey(rng),
+ Botan::X25519_PrivateKey(rng)
+ };
+
+ // Make sure we check the key pairs.
+ if (!identity_keys->ed25519_key.check_key(rng, false) ||
+ !identity_keys->curve25519_key.check_key(rng, false))
+ {
+ throw SpankOlmErrorKeyGeneration();
+ }
+
+ // Verify the public keys using the respective check_key methods.
+ if (!identity_keys->ed25519_key.public_key()->check_key(rng, false) ||
+ !identity_keys->curve25519_key.public_key()->check_key(rng, false))
+ {
+ throw SpankOlmErrorKeyGeneration();
+ }
+ }
+
+ std::vector<uint8_t> Account::sign(const std::string_view message) const
+ {
+ Botan::AutoSeeded_RNG rng;
+
+ // According to https://botan.randombit.net/handbook/api_ref/pubkey.html#ed25519-ed448-variants
+ const std::string padding_scheme = "Ed25519ph";
+
+ // Use the Ed25519 key to sign the message using the Botan library.
+
+
+ Botan::PK_Signer signer(identity_keys->ed25519_key, rng, padding_scheme);
+ signer.update(message);
+ auto signature = signer.signature(rng);
+
+ return signature;
+ }
+
+ std::size_t Account::mark_keys_as_published()
+ {
+ auto count = 0;
+ for (const auto& key : one_time_keys)
+ {
+ if (!key->published)
+ {
+ key->published = true;
+ count++;
+ }
+ }
+
+ current_fallback_key->published = true;
+ return count;
+ }
+
+ void Account::generate_one_time_keys(Botan::RandomNumberGenerator& rng, const std::size_t number_of_keys)
+ {
+ for (std::size_t i = 0; i < number_of_keys; ++i)
+ {
+ one_time_keys.insert(OneTimeKey{++next_one_time_key_id, false, Botan::X25519_PrivateKey(rng)});
+ }
+ }
+
+ void Account::generate_fallback_key(Botan::RandomNumberGenerator& rng)
+ {
+ if (num_fallback_keys < 2)
+ {
+ num_fallback_keys++;
+ }
+ prev_fallback_key = current_fallback_key;
+ current_fallback_key = OneTimeKey{++next_one_time_key_id, false, Botan::X25519_PrivateKey(rng)};
+ }
+
+ void Account::forget_old_fallback_key()
+ {
+ if (num_fallback_keys >= 2)
+ {
+ num_fallback_keys = 1;
+ // TODO: Verify if this is correct.
+ prev_fallback_key.reset();
+ }
+ }
+
+ std::optional<OneTimeKey const*> Account::lookup_key(Botan::X25519_PublicKey const& key) const
+ {
+ for (const auto& one_time_key : one_time_keys)
+ {
+ if (one_time_key->key.public_key()->raw_public_key_bits() == key.raw_public_key_bits())
+ {
+ return one_time_key;
+ }
+ }
+ if (num_fallback_keys >= 1 && current_fallback_key->key.public_key()->raw_public_key_bits() == key.
+ raw_public_key_bits())
+ {
+ return ¤t_fallback_key.value();
+ }
+ if (num_fallback_keys >= 2 && prev_fallback_key->key.public_key()->raw_public_key_bits() == key.
+ raw_public_key_bits())
+ {
+ return ¤t_fallback_key.value();
+ }
+ return std::nullopt;
+ }
+
+ void Account::remove_key(Botan::X25519_PublicKey const& key)
+ {
+ // Use iterator to find and remove the key.
+ for (const auto& one_time_key : one_time_keys)
+ {
+ if (one_time_key->key.public_key()->raw_public_key_bits() == key.raw_public_key_bits())
+ {
+ one_time_keys.erase(one_time_key);
+ return;
+ }
+ }
+ }
+
+
+ namespace
+ {
+ /**
+ * \brief The current version of the account pickle format.
+ *
+ * \details
+ * - Version 1 used only 32 bytes for the ed25519 private key. Any keys thus used should be considered compromised.
+ * - Version 2 does not have fallback keys.
+ * - Version 3 does not store whether the current fallback key is published.
+ */
+ constexpr std::uint32_t ACCOUNT_PICKLE_VERSION = 4;
+ }
+
+
+ /**
+ * Serializes the Account object into a byte array.
+ *
+ * @return A vector of uint8_t containing the serialized data.
+ */
+ std::vector<uint8_t> Account::pickle() const
+ {
+ std::vector<uint8_t> buffer(1024); // Initial buffer size, can be adjusted
+ auto pos = buffer.data();
+
+ pos = spank_olm::pickle(pos, ACCOUNT_PICKLE_VERSION);
+
+ pos = spank_olm::pickle(pos, identity_keys);
+
+ pos = spank_olm::pickle(pos, one_time_keys);
+
+ pos = spank_olm::pickle(pos, num_fallback_keys);
+
+ if (num_fallback_keys >= 1)
+ {
+ pos = spank_olm::pickle(pos, current_fallback_key->key.raw_private_key_bits());
+ if (num_fallback_keys >= 2)
+ {
+ pos = spank_olm::pickle(pos, prev_fallback_key->key.raw_private_key_bits());
+ }
+ }
+
+ pos = spank_olm::pickle(pos, next_one_time_key_id);
+
+ buffer.resize(pos - buffer.data()); // Adjust buffer size to actual data size
+ return buffer;
+ }
+
+ /**
+ * Deserializes an Account object from a byte array.
+ *
+ * @param data A vector of uint8_t containing the serialized data.
+ * @return The deserialized Account object.
+ * @throws SpankOlmErrorVersionNotFound if the pickle version is not found.
+ * @throws SpankOlmErrorBadLegacyAccountPickle if the pickle version is 1.
+ * @throws SpankOlmErrorUnknownPickleVersion if the pickle version is unknown.
+ * @throws SpankOlmErrorCorruptedAccountPickle if the pickle data is corrupted.
+ */
+ Account Account::unpickle(std::vector<uint8_t> const& data)
+ {
+ Account value;
+ auto pos = data.data();
+ const auto end = data.data() + data.size();
+ uint32_t pickle_version;
+
+ pos = spank_olm::unpickle(pos, end, pickle_version);
+ if (!pos)
+ {
+ throw SpankOlmErrorVersionNotFound();
+ }
+
+ switch (pickle_version)
+ {
+ case ACCOUNT_PICKLE_VERSION:
+ case 3:
+ case 2:
+ break;
+ case 1:
+ throw SpankOlmErrorBadLegacyAccountPickle();
+ default:
+ throw SpankOlmErrorUnknownPickleVersion();
+ }
+
+ pos = spank_olm::unpickle(pos, end, value.identity_keys);
+ pos = spank_olm::unpickle(pos, end, value.one_time_keys);
+
+ if (pickle_version <= 2)
+ {
+ value.num_fallback_keys = 0;
+ }
+ else if (pickle_version == 3)
+ {
+ pos = spank_olm::unpickle(pos, end, value.current_fallback_key);
+ pos = spank_olm::unpickle(pos, end, value.prev_fallback_key);
+ if (value.current_fallback_key->published)
+ {
+ if (value.prev_fallback_key->published)
+ {
+ value.num_fallback_keys = 2;
+ }
+ else
+ {
+ value.num_fallback_keys = 1;
+ }
+ }
+ else
+ {
+ value.num_fallback_keys = 0;
+ }
+ }
+ else
+ {
+ pos = spank_olm::unpickle(pos, end, value.num_fallback_keys);
+ if (value.num_fallback_keys >= 1)
+ {
+ pos = spank_olm::unpickle(pos, end, value.current_fallback_key);
+ if (value.num_fallback_keys >= 2)
+ {
+ pos = spank_olm::unpickle(pos, end, value.prev_fallback_key);
+ if (value.num_fallback_keys >= 3)
+ {
+ throw SpankOlmErrorCorruptedAccountPickle();
+ }
+ }
+ }
+ }
+
+ spank_olm::unpickle(pos, end, value.next_one_time_key_id);
+
+ return value;
+ }
+}
diff --git a/src/slap-olm.cpp b/src/slap-olm.cpp
@@ -1 +0,0 @@
-#include "slap-olm.hpp"
-\ No newline at end of file
diff --git a/src/spank-olm.cpp b/src/spank-olm.cpp
@@ -0,0 +1 @@
+#include "spank-olm.hpp"
+\ No newline at end of file
diff --git a/tests/list_test.cpp b/tests/list_test.cpp
@@ -0,0 +1,167 @@
+#include <iostream>
+#include <snitch/snitch.hpp>
+#include <list.hpp>
+
+TEST_CASE("FixedSizeArray basic operations")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+
+ // Test insertion
+ REQUIRE(array.insert_at(0, 10) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.insert_at(1, 20) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.insert_at(1, 15) == FixedSizeArray<int, 5>::SUCCESS);
+
+ // Test size
+ REQUIRE(array.size() == 3);
+
+ // Test element values
+ // Test element values
+ REQUIRE(array[0] == 10);
+ REQUIRE(array[1] == 15);
+ REQUIRE(array[2] == 20);
+
+ // Test erasure
+ REQUIRE(array.erase_at(1) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.size() == 2);
+ REQUIRE(array[1] == 20);
+
+ // Test boundary conditions
+ REQUIRE(array.insert_at(5, 30) == FixedSizeArray<int, 5>::INDEX_OUT_OF_RANGE);
+ REQUIRE(array.erase_at(5) == FixedSizeArray<int, 5>::INDEX_OUT_OF_RANGE);
+
+ // Ensure iterator works
+ int expected_values[] = {10, 20};
+ int i = 0;
+ for (const auto& value : array)
+ {
+ REQUIRE(*value == expected_values[i++]);
+ }
+
+ // Ensure that adding more elements than the maximum size is possible and the last element is dropped as expected
+ // Loop to insert elements into the array, starting from 0 up to 9
+ // This will cause the array to overflow, and only the last 5 elements will be kept
+ for (int j = 0; j < 10; ++j)
+ {
+ array.insert_at(0, j);
+ }
+
+ // Check that the array size is 5 after the overflow
+ REQUIRE(array.size() == 5);
+
+ // Loop to verify that the elements in the array are as expected
+ // The array should contain the last 5 inserted elements in reverse order
+ for (int j = 0; j < 5; ++j)
+ {
+ REQUIRE(array[j] == 9 - j);
+ }
+}
+
+TEST_CASE("FixedSizeArray erase element at pointer null")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+ array.insert(1);
+ array.insert(2);
+ array.insert(3);
+
+ int* ptr = nullptr;
+ REQUIRE(array.erase(ptr) == FixedSizeArray<int, 5>::INDEX_OUT_OF_RANGE);
+ REQUIRE(array.size() == 3);
+}
+
+TEST_CASE("FixedSizeArray erase first element")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+ array.insert(1);
+ array.insert(2);
+ array.insert(3);
+ REQUIRE(array.size() == 3);
+
+ REQUIRE(array.erase_at(0) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.size() == 2);
+
+ REQUIRE(array[1] == 2);
+ REQUIRE(array[0] == 3);
+}
+
+TEST_CASE("FixedSizeArray erase last element")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+ array.insert(1);
+ array.insert(2);
+ array.insert(3);
+
+ int last_index = array.size() - 1;
+ REQUIRE(array.erase_at(last_index) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.size() == 2);
+ REQUIRE(array[0] == 2);
+ REQUIRE(array[1] == 1);
+}
+
+TEST_CASE("FixedSizeArray erase element in full array")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+ for (int i = 0; i < 5; ++i)
+ {
+ array.insert(i);
+ }
+
+ REQUIRE(array.erase_at(2) == FixedSizeArray<int, 5>::SUCCESS);
+ REQUIRE(array.size() == 4);
+ REQUIRE(array[0] == 4);
+ REQUIRE(array[1] == 3);
+ REQUIRE(array[2] == 1);
+ REQUIRE(array[3] == 0);
+}
+
+TEST_CASE("FixedSizeArray erase element in empty array")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+
+ int value = 1;
+ int* ptr = &value;
+ REQUIRE(array.erase(ptr) == FixedSizeArray<int, 5>::INDEX_OUT_OF_RANGE);
+ REQUIRE(array.size() == 0);
+}
+
+TEST_CASE("FixedSizeArray empty and size match")
+{
+ using namespace spank_olm;
+
+ FixedSizeArray<int, 5> array;
+
+ // Initially, the array should be empty
+ REQUIRE(array.empty() == true);
+ REQUIRE(array.size() == 0);
+
+ // Insert an element and check again
+ array.insert(1);
+ REQUIRE(array.empty() == false);
+ REQUIRE(array.size() == 1);
+
+ // Insert another element and check again
+ array.insert(2);
+ REQUIRE(array.empty() == false);
+ REQUIRE(array.size() == 2);
+
+ // Erase an element and check again
+ array.erase_at(0);
+ REQUIRE(array.empty() == false);
+ REQUIRE(array.size() == 1);
+
+ // Erase the last element and check again
+ array.erase_at(0);
+ REQUIRE(array.empty() == true);
+ REQUIRE(array.size() == 0);
+}
diff --git a/tests/test.cpp b/tests/test.cpp