spank-olm

WIP Do not look
git clone git://archive.git.mtrnord.blog/MTRNord/spank-olm.git
Log | Files | Refs | README | LICENSE

commit 5377cfb1bf5123f48d5e95a447092dcc1e1cfe36
parent 51d5c9b214674b244f90baa88b45c537c05cd2ee
Author: MTRNord <mtrnord1@gmail.com>
Date:   Wed,  7 Aug 2024 02:30:28 +0200

Implement basic olm account class

Diffstat:
Ainclude/account.hpp | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/errors.hpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/export.hpp | 39+++++++++++++++++++++++++++++++++++++++
Ainclude/list.hpp | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dinclude/slap-olm.hpp | 2--
Ainclude/spank-olm.hpp | 4++++
Mmeson.build | 13++++++++-----
Asrc/account.cpp | 609+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/slap-olm.cpp | 2--
Asrc/spank-olm.cpp | 2++
Atests/list_test.cpp | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtests/test.cpp | 0
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 &current_fallback_key.value(); + } + if (num_fallback_keys >= 2 && prev_fallback_key->key.public_key()->raw_public_key_bits() == key. + raw_public_key_bits()) + { + return &current_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