Updated to the latest config lib and added it's unit tests

This commit is contained in:
Morgan Pretty 2022-12-09 13:19:14 +11:00
parent 22130f734e
commit 893967e380
21 changed files with 779 additions and 33 deletions

View File

@ -590,6 +590,7 @@
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
FD2B4AFB29429D1000AB4848 /* ConfigContactsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */; };
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; };
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; };
@ -1705,6 +1706,7 @@
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = "<group>"; };
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigContactsSpec.swift; sourceTree = "<group>"; };
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+ClassicDark.swift"; sourceTree = "<group>"; };
FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = "<group>"; };
@ -4014,6 +4016,7 @@
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
isa = PBXGroup;
children = (
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */,
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
);
path = LibSessionUtil;
@ -6007,6 +6010,7 @@
FD078E5227E1760A000769AF /* OGMDependencyExtensions.swift in Sources */,
FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */,
FDC290A627D860CE005DAE71 /* Mock.swift in Sources */,
FD2B4AFB29429D1000AB4848 /* ConfigContactsSpec.swift in Sources */,
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,

View File

@ -15,6 +15,7 @@ public struct ConfigDump: Codable, Equatable, Hashable, Identifiable, FetchableR
public enum Variant: String, Codable, DatabaseValueConvertible, CaseIterable {
case userProfile
case contacts
}
public var id: Variant { variant }
@ -32,6 +33,7 @@ public extension ConfigDump.Variant {
var configMessageKind: SharedConfigMessage.Kind {
switch self {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}
}

View File

@ -24,6 +24,7 @@ import SessionUtilitiesKit
// MARK: - Configs
private static var userProfileConfig: Atomic<UnsafeMutablePointer<config_object>?> = Atomic(nil)
private static var contactsConfig: Atomic<UnsafeMutablePointer<config_object>?> = Atomic(nil)
// MARK: - Variables
@ -32,6 +33,9 @@ import SessionUtilitiesKit
switch variant {
case .userProfile:
return (userProfileConfig.wrappedValue.map { config_needs_push($0) } ?? false)
case .contacts:
return (contactsConfig.wrappedValue.map { config_needs_push($0) } ?? false)
}
}
}
@ -40,6 +44,7 @@ import SessionUtilitiesKit
private static func config(for variant: ConfigDump.Variant) -> Atomic<UnsafeMutablePointer<config_object>?> {
switch variant {
case .userProfile: return SessionUtil.userProfileConfig
case .contacts: return SessionUtil.contactsConfig
}
}
@ -49,6 +54,7 @@ import SessionUtilitiesKit
guard let secretKey: [UInt8] = ed25519SecretKey else { return }
SessionUtil.userProfileConfig.mutate { $0 = loadState(for: .userProfile, secretKey: secretKey) }
SessionUtil.contactsConfig.mutate { $0 = loadState(for: .contacts, secretKey: secretKey) }
}
private static func loadState(
@ -73,17 +79,17 @@ import SessionUtilitiesKit
// Setup initial variables (including getting the memory address for any cached data)
var conf: UnsafeMutablePointer<config_object>? = nil
let error: UnsafeMutablePointer<CChar>? = nil
let cachedDump: (data: UnsafePointer<CChar>, length: Int)? = cachedData?.withUnsafeBytes { unsafeBytes in
let cachedDump: (data: UnsafePointer<UInt8>, length: Int)? = cachedData?.withUnsafeBytes { unsafeBytes in
return unsafeBytes.baseAddress.map {
(
$0.assumingMemoryBound(to: CChar.self),
$0.assumingMemoryBound(to: UInt8.self),
unsafeBytes.count
)
}
}
// No need to deallocate the `cachedDump.data` as it'll automatically be cleaned up by
// the `cachedData` lifecycle, but need to deallocate the `error` if it gets set
// the `cachedDump` lifecycle, but need to deallocate the `error` if it gets set
defer {
error?.deallocate()
}
@ -94,6 +100,9 @@ import SessionUtilitiesKit
switch variant {
case .userProfile:
return user_profile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
case .contacts:
return contacts_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
}
}()

View File

@ -4,8 +4,10 @@ module SessionUtil {
header "session/config.h"
header "session/config/error.h"
header "session/config/user_profile.h"
header "session/config/contacts.h"
header "session/config/encrypt.h"
header "session/config/base.h"
header "session/config/profile_pic.h"
header "session/xed25519.h"
export *
}

View File

@ -237,6 +237,13 @@ class ConfigBase {
/// otherwise.
const std::string* string() const { return get_clean<std::string>(); }
/// Returns the value as a ustring_view, if it exists and is a string; nullopt otherwise.
std::optional<ustring_view> uview() const {
if (auto* s = get_clean<std::string>())
return ustring_view{reinterpret_cast<const unsigned char*>(s->data()), s->size()};
return std::nullopt;
}
/// returns the value as a string_view or a fallback if the value doesn't exist (or isn't a
/// string). The returned view is directly into the value (or fallback) and so mustn't be
/// used beyond the validity of either.
@ -278,8 +285,12 @@ class ConfigBase {
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
/// they currently exist along the path.
void operator=(std::string value) { assign_if_changed(std::move(value)); }
/// Same as above, but takes a string_view for convenience.
/// Same as above, but takes a string_view for convenience (this makes a copy).
void operator=(std::string_view value) { *this = std::string{value}; }
/// Same as above, but takes a ustring_view
void operator=(ustring_view value) {
*this = std::string{reinterpret_cast<const char*>(value.data()), value.size()};
}
/// Replace the current value with the given integer. See above.
void operator=(int64_t value) { assign_if_changed(value); }
/// Replace the current value with the given set. See above.
@ -378,6 +389,14 @@ class ConfigBase {
// nothing.
virtual void load_extra_data(oxenc::bt_dict extra) {}
// Called to load an ed25519 key for encryption; this is meant for use by single-ownership
// config types, like UserProfile, but not shared config types (closed groups).
//
// Takes a binary string which is either the 32-byte seed, or 64-byte libsodium secret (which is
// just the seed and pubkey concatenated together), and then calls `key(...)` with the seed.
// Throws std::invalid_argument if given something that doesn't match the required input.
void load_key(ustring_view ed25519_secretkey);
public:
virtual ~ConfigBase();

View File

@ -0,0 +1,147 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "base.h"
#include "profile_pic.h"
typedef struct contacts_contact {
char session_id[67]; // in hex; 66 hex chars + null terminator.
// These can be NULL. When setting, either NULL or empty string will clear the setting.
const char* name;
const char* nickname;
user_profile_pic profile_pic;
bool approved;
bool approved_me;
bool blocked;
} contacts_contact;
/// Constructs a contacts config object and sets a pointer to it in `conf`.
///
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
/// bytes of that are the seed). This field cannot be null.
///
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
/// instantiation's call to `dump()`. To construct a new, empty object this should be NULL.
///
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
///
/// \param error - the pointer to a buffer in which we will write an error string if an error
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
/// buffer of at least 256 bytes.
///
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
/// C-string into `error` (if not NULL) on failure.
///
/// When done with the object the `config_object` must be destroyed by passing the pointer to
/// config_free() (in `session/config/base.h`).
int contacts_init(
config_object** conf,
const unsigned char* ed25519_secretkey,
const unsigned char* dump,
size_t dumplen,
char* error) __attribute__((warn_unused_result));
/// Returns true if session_id has the right form (66 hex digits). This is a quick check, not a
/// robust one: it does not check the leading byte prefix, nor the cryptographic properties of the
/// pubkey for actual validity.
bool session_id_is_valid(const char* session_id);
/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex
/// string), if the contact exists, and returns true. If the contact does not exist then `contact`
/// is left unchanged and false is returned.
bool contacts_get(const config_object* conf, contacts_contact* contact, const char* session_id)
__attribute__((warn_unused_result));
/// Same as the above except that when the contact does not exist, this sets all the contact fields
/// to defaults and loads it with the given session_id.
///
/// Returns true as long as it is given a valid session_id. A false return is considered an error,
/// and means the session_id was not a valid session_id.
///
/// This is the method that should usually be used to create or update a contact, followed by
/// setting fields in the contact, and then giving it to contacts_set().
bool contacts_get_or_create(
const config_object* conf, contacts_contact* contact, const char* session_id)
__attribute__((warn_unused_result));
/// Adds or updates a contact from the given contact info struct.
void contacts_set(config_object* conf, const contacts_contact* contact);
// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would
// save very little in actual calling code. The procedure for updating a single field without them
// is simple enough; for example to update `approved` and leave everything else unchanged:
//
// contacts_contact c;
// if (contacts_get_or_create(conf, &c, some_session_id)) {
// const char* new_nickname = "Joe";
// c.approved = new_nickname;
// contacts_set_or_create(conf, &c);
// } else {
// // some_session_id was invalid!
// }
/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was
/// found and removed, false if the contact was not present. You must not call this during
/// iteration; see details below.
bool contacts_erase(config_object* conf, const char* session_id);
/// Functions for iterating through the entire contact list, in sorted order. Intended use is:
///
/// contacts_contact c;
/// contacts_iterator *it = contacts_iterator_new(contacts);
/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) {
/// // c.session_id, c.nickname, etc. are loaded
/// }
/// contacts_iterator_free(it);
///
/// It is permitted to modify records (e.g. with a call to `contacts_set`) and add records while
/// iterating.
///
/// If you need to remove while iterating then usage is slightly different: you must advance the
/// iteration by calling either contacts_iterator_advance if not deleting, or
/// contacts_iterator_erase to erase and advance. Usage looks like this:
///
/// contacts_contact c;
/// contacts_iterator *it = contacts_iterator_new(contacts);
/// while (!contacts_iterator_done(it, &c)) {
/// // c.session_id, c.nickname, etc. are loaded
///
/// bool should_delete = /* ... */;
///
/// if (should_delete)
/// contacts_iterator_erase(it);
/// else
/// contacts_iterator_advance(it);
/// }
/// contacts_iterator_free(it);
///
///
typedef struct contacts_iterator {
void* _internals;
} contacts_iterator;
// Starts a new iterator.
contacts_iterator* contacts_iterator_new(const config_object* conf);
// Frees an iterator once no longer needed.
void contacts_iterator_free(contacts_iterator* it);
// Returns true if iteration has reached the end. Otherwise `c` is populated and false is returned.
bool contacts_iterator_done(contacts_iterator* it, contacts_contact* c);
// Advances the iterator.
void contacts_iterator_advance(contacts_iterator* it);
// Erases the current contact while advancing the iterator to the next contact in the iteration.
void contacts_iterator_erase(config_object* conf, contacts_iterator* it);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,187 @@
#pragma once
#include <cstddef>
#include <iterator>
#include <memory>
#include <session/config.hpp>
#include "base.hpp"
#include "namespaces.hpp"
#include "profile_pic.hpp"
extern "C" struct contacts_contact;
namespace session::config {
/// keys used in this config, either currently or in the past (so that we don't reuse):
///
/// c - dict of contacts; within this dict each key is the session pubkey (binary, 33 bytes) and
/// value is a dict containing keys:
///
/// ! - dummy value that is always set to an empty string. This ensures that we always have at
/// least one key set, which is required to keep the dict value alive (empty dicts get
/// pruned when serialied).
/// n - contact name (string)
/// N - contact nickname (string)
/// p - profile url (string)
/// q - profile decryption key (binary)
/// a - 1 if approved, omitted otherwise (int)
/// A - 1 if remote has approved me, omitted otherwise (int)
/// b - 1 if contact is blocked, omitted otherwise
/// Struct containing contact info. Note that data must be copied/used immediately as the data will
/// not remain valid beyond other calls into the library. When settings things in this externally
/// (e.g. to pass into `set()`), take note that the `name` and `nickname` are string_views: that is,
/// they must reference existing string data that remains valid for the duration of the contact_info
/// instance.
struct contact_info {
std::string session_id; // in hex
std::optional<std::string_view> name;
std::optional<std::string_view> nickname;
std::optional<profile_pic> profile_picture;
bool approved = false;
bool approved_me = false;
bool blocked = false;
contact_info(std::string sid);
// Internal ctor/method for C API implementations:
contact_info(const struct contacts_contact& c); // From c struct
void into(contacts_contact& c); // Into c struct
private:
friend class Contacts;
void load(const dict& info_dict);
};
class Contacts : public ConfigBase {
public:
// No default constructor
Contacts() = delete;
/// Constructs a contact list from existing data (stored from `dump()`) and the user's secret
/// key for generating the data encryption key. To construct a blank list (i.e. with no
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
///
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
/// the secret key.
///
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
/// that was previously dumped from an instance of this class by calling `dump()`.
Contacts(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
Namespace storage_namespace() const override { return Namespace::Contacts; }
const char* encryption_domain() const override { return "Contacts"; }
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
/// not found, otherwise returns a filled out `contact_info`.
std::optional<contact_info> get(std::string_view pubkey_hex) const;
/// Similar to get(), but if the session ID does not exist this returns a filled-out
/// contact_info containing the session_id (all other fields will be empty/defaulted). This is
/// intended to be combined with `set` to set-or-create a record. Note that this does not add
/// the session id to the contact list when called: that requires also calling `set` with this
/// value.
contact_info get_or_create(std::string_view pubkey_hex) const;
/// Sets or updates multiple contact info values at once with the given info. The usual use is
/// to access the current info, change anything desired, then pass it back into set_contact,
/// e.g.:
///
/// auto c = contacts.get_or_create(pubkey);
/// c.name = "Session User 42";
/// c.nickname = "BFF";
/// contacts.set(c);
void set(const contact_info& contact);
/// Alternative to `set()` for setting individual fields.
void set_name(std::string_view session_id, std::string_view name);
void set_nickname(std::string_view session_id, std::string_view nickname);
void set_profile_pic(std::string_view session_id, profile_pic pic);
void set_approved(std::string_view session_id, bool approved);
void set_approved_me(std::string_view session_id, bool approved_me);
void set_blocked(std::string_view session_id, bool blocked);
/// Removes a contact, if present. Returns true if it was found and removed, false otherwise.
/// Note that this removes all fields related to a contact, even fields we do not know about.
bool erase(std::string_view session_id);
struct iterator;
/// This works like erase, but takes an iterator to the contact to remove. The element is
/// removed and the iterator to the next element after the removed one is returned. This is
/// intended for use where elements are to be removed during iteration: see below for an
/// example.
iterator erase(iterator it);
/// Iterators for iterating through all contacts. Typically you access this implicit via a for
/// loop over the `Contacts` object:
///
/// for (auto& contact : contacts) {
/// // use contact.session_id, contact.name, etc.
/// }
///
/// This iterates in sorted order through the session_ids.
///
/// It is permitted to modify and add records while iterating (e.g. by modifying `contact` and
/// then calling set()).
///
/// If you need to erase the current contact during iteration then care is required: you need to
/// advance the iterator via the iterator version of erase when erasing an element rather than
/// incrementing it regularly. For example:
///
/// for (auto it = contacts.begin(); it != contacts.end(); ) {
/// if (should_remove(*it))
/// it = contacts.erase(it);
/// else
/// ++it;
/// }
///
/// Alternatively, you can use the first version with two loops: the first loop through all
/// contacts doesn't erase but just builds a vector of IDs to erase, then the second loops
/// through that vector calling `erase()` for each one.
///
iterator begin() const { return iterator{data["c"].dict()}; }
iterator end() const { return iterator{nullptr}; }
using iterator_category = std::input_iterator_tag;
using value_type = contact_info;
using reference = value_type&;
using pointer = value_type*;
using difference_type = std::ptrdiff_t;
struct iterator {
private:
std::shared_ptr<contact_info> _val;
dict::const_iterator _it;
const dict* _contacts;
void _load_info();
iterator(const dict* contacts) : _contacts{contacts} {
if (_contacts) {
_it = _contacts->begin();
_load_info();
}
}
friend class Contacts;
public:
bool operator==(const iterator& other) const;
bool operator!=(const iterator& other) const { return !(*this == other); }
bool done() const; // Equivalent to comparing against the end iterator
contact_info& operator*() const { return *_val; }
contact_info* operator->() const { return _val.get(); }
iterator& operator++();
iterator operator++(int) {
auto copy{*this};
++*this;
return copy;
}
};
};
} // namespace session::config

View File

@ -6,6 +6,7 @@ namespace session::config {
enum class Namespace : std::int16_t {
UserProfile = 2,
Contacts = 3,
ClosedGroupInfo = 11,
};

View File

@ -0,0 +1,21 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
typedef struct user_profile_pic {
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
// profile pic.
const char* url;
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
// null-terminated C string. Will be NULL if there is no profile pic.
const unsigned char* key;
size_t keylen;
} user_profile_pic;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,26 @@
#pragma once
#include "session/types.hpp"
namespace session::config {
// Profile pic info. Note that `url` is null terminated (though the null lies just beyond the end
// of the string view: that is, it views into a full std::string).
struct profile_pic {
std::string_view url;
ustring_view key;
profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} {}
// Returns true if either url or key are empty
bool empty() const { return url.empty() || key.empty(); }
// Guard against accidentally passing in a temporary string or ustring:
template <
typename UrlType,
typename KeyType,
std::enable_if_t<
std::is_same_v<UrlType, std::string> || std::is_same_v<KeyType, ustring>>>
profile_pic(UrlType&& url, KeyType&& key) = delete;
};
} // namespace session::config

View File

@ -5,6 +5,7 @@ extern "C" {
#endif
#include "base.h"
#include "profile_pic.h"
/// Constructs a user profile config object and sets a pointer to it in `conf`.
///
@ -41,16 +42,6 @@ const char* user_profile_get_name(const config_object* conf);
/// error (and sets the config_object's error string).
int user_profile_set_name(config_object* conf, const char* name);
typedef struct user_profile_pic {
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
// profile pic.
const char* url;
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
// null-terminated C string. Will be NULL if there is no profile pic.
const unsigned char* key;
size_t keylen;
} user_profile_pic;
// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile
// pic is not currently set, and otherwise should be copied right away (they will not be valid
// beyond other API calls on this config object).

View File

@ -5,6 +5,7 @@
#include "base.hpp"
#include "namespaces.hpp"
#include "profile_pic.hpp"
namespace session::config {
@ -14,13 +15,6 @@ namespace session::config {
/// p - user profile url
/// q - user profile decryption key (binary)
// Profile pic info. Note that `url` is null terminated (though the null lies just beyond the end
// of the string view: that is, it views into a full std::string).
struct profile_pic {
std::string_view url;
ustring_view key;
};
class UserProfile final : public ConfigBase {
public:
@ -31,12 +25,13 @@ class UserProfile final : public ConfigBase {
/// key for generating the data encryption key. To construct a blank profile (i.e. with no
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
///
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt user
/// profile messages; these can either be the full 64-byte value (which is technically the
/// 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of the secret key.
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
/// the secret key.
///
/// \param dumped - either `std::nullopt` to construct a new, empty user profile; or binary
/// state data that was previously dumped from a UserProfile object by calling `dump()`.
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
/// that was previously dumped from an instance of this class by calling `dump()`.
UserProfile(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
Namespace storage_namespace() const override { return Namespace::UserProfile; }
@ -44,7 +39,7 @@ class UserProfile final : public ConfigBase {
const char* encryption_domain() const override { return "UserProfile"; }
/// Returns the user profile name, or std::nullopt if there is no profile name set.
const std::optional<std::string_view> get_name() const;
std::optional<std::string_view> get_name() const;
/// Sets the user profile name; if given an empty string then the name is removed.
void set_name(std::string_view new_name);
@ -57,9 +52,6 @@ class UserProfile final : public ConfigBase {
/// one is empty.
void set_profile_pic(std::string_view url, ustring_view key);
void set_profile_pic(profile_pic pic);
private:
void load_key(ustring_view ed25519_secretkey);
};
} // namespace session::config

View File

@ -21,10 +21,12 @@ public final class SharedConfigMessage: ControlMessage {
public enum Kind: CustomStringConvertible, Codable {
case userProfile
case contacts
public var description: String {
switch self {
case .userProfile: return "userProfile"
case .contacts: return "contacts"
}
}
}
@ -74,6 +76,7 @@ public final class SharedConfigMessage: ControlMessage {
kind: {
switch sharedConfigMessage.kind {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}(),
seqNo: sharedConfigMessage.seqno,
@ -87,6 +90,7 @@ public final class SharedConfigMessage: ControlMessage {
kind: {
switch self.kind {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}(),
seqno: self.seqNo,
@ -121,6 +125,7 @@ public extension SharedConfigMessage.Kind {
var configDumpVariant: ConfigDump.Variant {
switch self {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}
}

View File

@ -3714,17 +3714,20 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
@objc public enum SNProtoSharedConfigMessageKind: Int32 {
case userProfile = 1
case contacts = 2
}
private class func SNProtoSharedConfigMessageKindWrap(_ value: SessionProtos_SharedConfigMessage.Kind) -> SNProtoSharedConfigMessageKind {
switch value {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}
private class func SNProtoSharedConfigMessageKindUnwrap(_ value: SNProtoSharedConfigMessageKind) -> SessionProtos_SharedConfigMessage.Kind {
switch value {
case .userProfile: return .userProfile
case .contacts: return .contacts
}
}

View File

@ -1621,6 +1621,7 @@ struct SessionProtos_SharedConfigMessage {
enum Kind: SwiftProtobuf.Enum {
typealias RawValue = Int
case userProfile // = 1
case contacts // = 2
init() {
self = .userProfile
@ -1629,6 +1630,7 @@ struct SessionProtos_SharedConfigMessage {
init?(rawValue: Int) {
switch rawValue {
case 1: self = .userProfile
case 2: self = .contacts
default: return nil
}
}
@ -1636,6 +1638,7 @@ struct SessionProtos_SharedConfigMessage {
var rawValue: Int {
switch self {
case .userProfile: return 1
case .contacts: return 2
}
}
@ -3332,5 +3335,6 @@ extension SessionProtos_SharedConfigMessage: SwiftProtobuf.Message, SwiftProtobu
extension SessionProtos_SharedConfigMessage.Kind: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "USER_PROFILE"),
2: .same(proto: "CONTACTS"),
]
}

View File

@ -275,6 +275,7 @@ message AttachmentPointer {
message SharedConfigMessage {
enum Kind {
USER_PROFILE = 1;
CONTACTS = 2;
}
// @required

View File

@ -0,0 +1,324 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigContactsSpec: QuickSpec {
// MARK: - Spec
override func spec() {
it("generates Contact configs correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
var definitelyRealId: [CChar] = "050000000000000000000000000000000000000000000000000000000000000000"
.bytes
.map { CChar(bitPattern: $0) }
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
expect(contacts_get(conf, contactPtr, &definitelyRealId)).to(beFalse())
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_create(conf, &contact2, &definitelyRealId)).to(beTrue())
expect(contact2.name).to(beNil())
expect(contact2.nickname).to(beNil())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact2.profile_pic.url).to(beNil())
expect(contact2.profile_pic.key).to(beNil())
expect(contact2.profile_pic.keylen).to(equal(0))
// We don't need to push anything, since this is a default contact
expect(config_needs_push(conf)).to(beFalse())
// And we haven't changed anything so don't need to dump to db
expect(config_needs_dump(conf)).to(beFalse())
var toPush: UnsafeMutablePointer<UInt8>? = nil
var toPushLen: Int = 0
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let seqno: Int64 = config_push(conf, &toPush, &toPushLen)
expect(toPush).toNot(beNil())
expect(seqno).to(equal(0))
expect(toPushLen).to(equal(256))
// Update the contact data
let contact2Name: [CChar] = "Joe"
.bytes
.map { CChar(bitPattern: $0) }
let contact2Nickname: [CChar] = "Joey"
.bytes
.map { CChar(bitPattern: $0) }
contact2Name.withUnsafeBufferPointer { contact2.name = $0.baseAddress }
contact2Nickname.withUnsafeBufferPointer { contact2.nickname = $0.baseAddress }
contact2.approved = true
contact2.approved_me = true
// Update the contact
contacts_set(conf, &contact2)
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &definitelyRealId)).to(beTrue())
expect(String(cString: contact3.name)).to(equal("Joe"))
expect(String(cString: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact3.profile_pic.url).to(beNil())
expect(contact3.profile_pic.key).to(beNil())
expect(contact3.profile_pic.keylen).to(equal(0))
expect(contact3.blocked).to(beFalse())
let contact3SessionId: [CChar] = withUnsafeBytes(of: contact3.session_id) { [UInt8]($0) }
.map { CChar($0) }
expect(contact3SessionId).to(equal(definitelyRealId.nullTerminated()))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
var toPush2: UnsafeMutablePointer<UInt8>? = nil
var toPush2Len: Int = 0
let seqno2: Int64 = config_push(conf, &toPush2, &toPush2Len);
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
expect(seqno2).to(equal(1))
toPush2?.deallocate()
// Pretend we uploaded it
config_confirm_pushed(conf, seqno2)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
// NB: Not going to check encrypted data and decryption here because that's general (not
// specific to contacts) and is covered already in the user profile tests.
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error?.deallocate()
dump1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
var toPush3: UnsafeMutablePointer<UInt8>? = nil
var toPush3Len: Int = 0
let seqno3: Int64 = config_push(conf, &toPush3, &toPush3Len);
expect(seqno3).to(equal(1))
toPush3?.deallocate()
// Because we just called dump() above, to load up contacts2
expect(config_needs_dump(conf)).to(beFalse())
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &definitelyRealId)).to(beTrue())
expect(String(cString: contact4.name)).to(equal("Joe"))
expect(String(cString: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact4.profile_pic.url).to(beNil())
expect(contact4.profile_pic.key).to(beNil())
expect(contact4.profile_pic.keylen).to(equal(0))
expect(contact4.blocked).to(beFalse())
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
.bytes
.map { CChar(bitPattern: $0) }
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_create(conf2, &contact5, &anotherId)).to(beTrue())
expect(contact5.name).to(beNil())
expect(contact5.nickname).to(beNil())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact5.profile_pic.url).to(beNil())
expect(contact5.profile_pic.key).to(beNil())
expect(contact5.profile_pic.keylen).to(equal(0))
expect(contact5.blocked).to(beFalse())
// We're not setting any fields, but we should still keep a record of the session id
contacts_set(conf2, &contact5)
expect(config_needs_push(conf2)).to(beTrue())
var toPush4: UnsafeMutablePointer<UInt8>? = nil
var toPush4Len: Int = 0
let seqno4: Int64 = config_push(conf2, &toPush4, &toPush4Len);
expect(seqno4).to(equal(2))
// Check the merging
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush4)]
var mergeSize: [Int] = [toPush4Len]
expect(config_merge(conf, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf2, seqno4)
toPush4?.deallocate()
expect(config_needs_push(conf)).to(beFalse())
var toPush5: UnsafeMutablePointer<UInt8>? = nil
var toPush5Len: Int = 0
let seqno5: Int64 = config_push(conf2, &toPush5, &toPush5Len);
expect(seqno5).to(equal(2))
toPush5?.deallocate()
// Iterate through and make sure we got everything we expected
var sessionIds: [String] = []
var nicknames: [String] = []
var contact6: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(
String(cString: withUnsafeBytes(of: contact6.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
)
nicknames.append(
contact6.nickname.map { String(cString: $0) } ??
"(N/A)"
)
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.first).to(equal(String(cString: definitelyRealId.nullTerminated())))
expect(sessionIds.last).to(equal(String(cString: anotherId.nullTerminated())))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
// Conflict! Oh no!
// On client 1 delete a contact:
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
var thirdId: [CChar] = "052222222222222222222222222222222222222222222222222222222222222222"
.bytes
.map { CChar(bitPattern: $0) }
let nickname7: [CChar] = "Nickname 3"
.bytes
.map { CChar(bitPattern: $0) }
let profileUrl7: [CChar] = "http://example.com/huge.bmp"
.bytes
.map { CChar(bitPattern: $0) }
let profileKey7: [UInt8] = "qwerty".bytes
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_create(conf2, &contact7, &thirdId)).to(beTrue())
nickname7.withUnsafeBufferPointer { contact7.nickname = $0.baseAddress }
contact7.approved = true
contact7.approved_me = true
profileUrl7.withUnsafeBufferPointer { contact7.profile_pic.url = $0.baseAddress }
profileKey7.withUnsafeBufferPointer { contact7.profile_pic.key = $0.baseAddress }
contact7.profile_pic.keylen = 6
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
var toPush6: UnsafeMutablePointer<UInt8>? = nil
var toPush6Len: Int = 0
let seqno6: Int64 = config_push(conf, &toPush6, &toPush6Len);
expect(seqno6).to(equal(3))
var toPush7: UnsafeMutablePointer<UInt8>? = nil
var toPush7Len: Int = 0
let seqno7: Int64 = config_push(conf2, &toPush7, &toPush7Len);
expect(seqno7).to(equal(3))
expect(String(pointer: toPush6, length: toPush6Len, encoding: .ascii))
.toNot(equal(String(pointer: toPush7, length: toPush7Len, encoding: .ascii)))
config_confirm_pushed(conf, seqno6)
config_confirm_pushed(conf2, seqno7)
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush7)]
var mergeSize2: [Int] = [toPush7Len]
expect(config_merge(conf, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(config_needs_push(conf)).to(beTrue())
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush6)]
var mergeSize3: [Int] = [toPush6Len]
expect(config_merge(conf2, &mergeData3, &mergeSize3, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beTrue())
toPush6?.deallocate()
toPush7?.deallocate()
var toPush8: UnsafeMutablePointer<UInt8>? = nil
var toPush8Len: Int = 0
let seqno8: Int64 = config_push(conf, &toPush8, &toPush8Len);
expect(seqno8).to(equal(4))
var toPush9: UnsafeMutablePointer<UInt8>? = nil
var toPush9Len: Int = 0
let seqno9: Int64 = config_push(conf2, &toPush9, &toPush9Len);
expect(seqno9).to(equal(seqno8))
expect(String(pointer: toPush8, length: toPush8Len, encoding: .ascii))
.to(equal(String(pointer: toPush9, length: toPush9Len, encoding: .ascii)))
toPush8?.deallocate()
toPush9?.deallocate()
config_confirm_pushed(conf, seqno8)
config_confirm_pushed(conf2, seqno9)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Validate the changes
var sessionIds2: [String] = []
var nicknames2: [String] = []
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(
String(cString: withUnsafeBytes(of: contact8.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
)
nicknames2.append(
contact8.nickname.map { String(cString: $0) } ??
"(N/A)"
)
contacts_iterator_advance(contactIterator2)
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(String(cString: anotherId.nullTerminated())))
expect(sessionIds2.last).to(equal(String(cString: thirdId.nullTerminated())))
expect(nicknames2.first).to(equal("(N/A)"))
expect(nicknames2.last).to(equal("Nickname 3"))
}
}
}

View File

@ -13,7 +13,7 @@ class ConfigUserProfileSpec: QuickSpec {
// MARK: - Spec
override func spec() {
it("generates configs correctly") {
it("generates UserProfile configs correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
@ -309,11 +309,11 @@ class ConfigUserProfileSpec: QuickSpec {
// down more than one).
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush3)]
var mergeSize2: [Int] = [toPush3Len]
config_merge(conf2, &mergeData2, &mergeSize2, 1)
expect(config_merge(conf2, &mergeData2, &mergeSize2, 1)).to(equal(1))
toPush3?.deallocate()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush4)]
var mergeSize3: [Int] = [toPush4Len]
config_merge(conf, &mergeData3, &mergeSize3, 1)
expect(config_merge(conf, &mergeData3, &mergeSize3, 1)).to(equal(1))
toPush4?.deallocate()
// Now after the merge we *will* want to push from both client, since both will have generated a

View File

@ -63,3 +63,11 @@ public extension Array where Element == String {
return self.reversed()
}
}
public extension Array where Element == CChar {
func nullTerminated() -> [Element] {
guard self.last != CChar(0) else { return self }
return self.appending(CChar(0))
}
}