Updated to the latest version of the shared util library

# Conflicts:
#	Session/Onboarding/Onboarding.swift
pull/856/head
Morgan Pretty 2 years ago
parent 1345e89809
commit f5933bdf75

@ -24,22 +24,25 @@ enum Onboarding {
didApproveMe: true
)
.save(db)
// Create the 'Note to Self' thread (not visible by default)
try SessionThread
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact)
.save(db)
// Create the initial shared util state (won't have been created on
// launch due to lack of ed25519 key)
SessionUtil.loadState(ed25519SecretKey: ed25519KeyPair.secretKey)
// No need to show the seed again if the user is restoring or linking
db[.hasViewedSeed] = (self == .recover || self == .link)
}
switch self {
case .register:
Storage.shared.write { db in db[.hasViewedSeed] = false }
// Set hasSyncedInitialConfiguration to true so that when we hit the
// home screen a configuration sync is triggered (yes, the logic is a
// bit weird). This is needed so that if the user registers and
// immediately links a device, there'll be a configuration in their swarm.
userDefaults[.hasSyncedInitialConfiguration] = true
case .recover, .link:
// No need to show it again if the user is restoring or linking
Storage.shared.write { db in db[.hasViewedSeed] = true }
userDefaults[.hasSyncedInitialConfiguration] = false
}
// Set hasSyncedInitialConfiguration to true so that when we hit the
// home screen a configuration sync is triggered (yes, the logic is a
// bit weird). This is needed so that if the user registers and
// immediately links a device, there'll be a configuration in their swarm.
userDefaults[.hasSyncedInitialConfiguration] = (self == .register)
switch self {
case .register, .recover:

@ -22,9 +22,17 @@ enum _011_SharedUtilChanges: Migration {
.notNull()
}
// If we don't have an ed25519 key then no need to create cached dump data
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else {
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
return
}
// Create a dump for the user profile data
let userProfileConf: UnsafeMutablePointer<config_object>? = try SessionUtil.loadState(
for: .userProfile
for: .userProfile,
secretKey: secretKey,
cachedData: nil
)
let confResult: SessionUtil.ConfResult = try SessionUtil.update(
profile: Profile.fetchOrCreateCurrentUser(db),

@ -102,7 +102,7 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
case is ClosedGroupControlMessage: return .closedGroupControlMessage
case is DataExtractionNotification: return .dataExtractionNotification
case is ExpirationTimerUpdate: return .expirationTimerUpdate
case is ConfigurationMessage: return .configurationMessage
case is ConfigurationMessage, is SharedConfigMessage: return .configurationMessage // TODO: Confirm this is desired
case is UnsendRequest: return .unsendRequest
case is MessageRequestResponse: return .messageRequestResponse
case is CallMessage: return .call

@ -36,7 +36,7 @@ internal extension SessionUtil {
if
profilePic.keylen > 0,
let profilePictureUrlPtr: UnsafePointer<CChar> = profilePic.url,
let profilePictureKeyPtr: UnsafePointer<CChar> = profilePic.key
let profilePictureKeyPtr: UnsafePointer<UInt8> = profilePic.key
{
profilePictureUrl = String(cString: profilePictureUrlPtr)
profilePictureKey = Data(bytes: profilePictureKeyPtr, count: profilePic.keylen)
@ -86,9 +86,7 @@ internal extension SessionUtil {
.bytes
.map { CChar(bitPattern: $0) }
.withUnsafeBufferPointer { profileUrlPtr in
let profileKey: [CChar]? = profile.profileEncryptionKey?
.bytes
.map { CChar(bitPattern: $0) }
let profileKey: [UInt8]? = profile.profileEncryptionKey?.bytes
return profileKey?.withUnsafeBufferPointer { profileKeyPtr in
user_profile_pic(

@ -45,22 +45,30 @@ import SessionUtilitiesKit
// MARK: - Loading
/*internal*/public static func loadState() {
SessionUtil.userProfileConfig.mutate { $0 = loadState(for: .userProfile) }
/*internal*/public static func loadState(ed25519SecretKey: [UInt8]?) {
guard let secretKey: [UInt8] = ed25519SecretKey else { return }
SessionUtil.userProfileConfig.mutate { $0 = loadState(for: .userProfile, secretKey: secretKey) }
}
private static func loadState(for variant: ConfigDump.Variant) -> UnsafeMutablePointer<config_object>? {
private static func loadState(
for variant: ConfigDump.Variant,
secretKey ed25519SecretKey: [UInt8]?
) -> UnsafeMutablePointer<config_object>? {
guard let secretKey: [UInt8] = ed25519SecretKey else { return nil }
// Load any
let storedDump: Data? = Storage.shared
.read { db in try ConfigDump.fetchOne(db, id: variant) }?
.data
return try? loadState(for: variant, cachedData: storedDump)
return try? loadState(for: variant, secretKey: secretKey, cachedData: storedDump)
}
internal static func loadState(
for variant: ConfigDump.Variant,
cachedData: Data? = nil
secretKey ed25519SecretKey: [UInt8],
cachedData: Data?
) throws -> UnsafeMutablePointer<config_object>? {
// Setup initial variables (including getting the memory address for any cached data)
var conf: UnsafeMutablePointer<config_object>? = nil
@ -81,10 +89,11 @@ import SessionUtilitiesKit
}
// Try to create the object
var secretKey: [UInt8] = ed25519SecretKey
let result: Int32 = {
switch variant {
case .userProfile:
return user_profile_init(&conf, cachedDump?.data, (cachedDump?.length ?? 0), error)
return user_profile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
}
}()
@ -107,11 +116,11 @@ import SessionUtilitiesKit
// If it doesn't need a dump then do nothing
guard config_needs_dump(conf) else { return }
var dumpResult: UnsafeMutablePointer<CChar>? = nil
var dumpResult: UnsafeMutablePointer<UInt8>? = nil
var dumpResultLen: Int = 0
config_dump(conf, &dumpResult, &dumpResultLen)
guard let dumpResult: UnsafeMutablePointer<CChar> = dumpResult else { return }
guard let dumpResult: UnsafeMutablePointer<UInt8> = dumpResult else { return }
let dumpData: Data = Data(bytes: dumpResult, count: dumpResultLen)
dumpResult.deallocate()
@ -126,7 +135,8 @@ import SessionUtilitiesKit
// MARK: - Pushes
public static func getChanges(
for variants: [ConfigDump.Variant] = ConfigDump.Variant.allCases
for variants: [ConfigDump.Variant] = ConfigDump.Variant.allCases,
ed25519SecretKey: [UInt8]
) -> [SharedConfigMessage] {
return variants
.compactMap { variant -> SharedConfigMessage? in
@ -135,11 +145,11 @@ import SessionUtilitiesKit
// Check if the config needs to be pushed
guard config_needs_push(conf.wrappedValue) else { return nil }
var toPush: UnsafeMutablePointer<CChar>? = nil
var toPush: UnsafeMutablePointer<UInt8>? = nil
var toPushLen: Int = 0
let seqNo: Int64 = conf.mutate { config_push($0, &toPush, &toPushLen) }
guard let toPush: UnsafeMutablePointer<CChar> = toPush else { return nil }
guard let toPush: UnsafeMutablePointer<UInt8> = toPush else { return nil }
let pushData: Data = Data(bytes: toPush, count: toPushLen)
toPush.deallocate()
@ -185,12 +195,8 @@ import SessionUtilitiesKit
// Block the config while we are merging
atomicConf.mutate { conf in
var mergeData: [UnsafePointer<CChar>?] = next.value
.map { message -> [CChar] in
message.data
.bytes
.map { CChar(bitPattern: $0) }
}
var mergeData: [UnsafePointer<UInt8>?] = next.value
.map { message -> [UInt8] in message.data.bytes }
.unsafeCopy()
var mergeSize: [Int] = messages.map { $0.data.count }
config_merge(conf, &mergeData, &mergeSize, messages.count)

@ -1,7 +1,10 @@
module SessionUtil {
module capi {
header "session/export.h"
header "session/config.h"
header "session/config/error.h"
header "session/config/user_profile.h"
header "session/config/encrypt.h"
header "session/config/base.h"
header "session/xed25519.h"
export *

@ -0,0 +1,13 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef int64_t seqno_t;
#ifdef __cplusplus
}
#endif

@ -11,8 +11,11 @@
#include <variant>
#include <vector>
#include "types.hpp"
namespace session::config {
// FIXME: for multi-message we encode to longer and then split it up
inline constexpr int MAX_MESSAGE_SIZE = 76800; // 76.8kB = Storage server's limit
// Application data data types:
@ -35,13 +38,9 @@ constexpr inline const dict_variant& unwrap(const dict_value& v) {
return static_cast<const dict_variant&>(v);
}
using seqno_t = std::int64_t;
using hash_t = std::array<unsigned char, 32>;
using seqno_hash_t = std::pair<seqno_t, hash_t>;
using ustring = std::basic_string<unsigned char>;
using ustring_view = std::basic_string_view<unsigned char>;
class MutableConfigMessage;
/// Base type for all errors that can happen during config parsing
@ -103,7 +102,7 @@ class ConfigMessage {
using verify_callable = std::function<bool(ustring_view data, ustring_view signature)>;
/// Signing function: this is passed the data to be signed and returns the 64-byte signature.
using sign_callable = std::function<std::string(ustring_view data)>;
using sign_callable = std::function<ustring(ustring_view data)>;
ConfigMessage();
ConfigMessage(const ConfigMessage&) = default;
@ -116,7 +115,7 @@ class ConfigMessage {
/// Initializes a config message by parsing a serialized message. Throws on any error. See the
/// vector version below for argument descriptions.
explicit ConfigMessage(
std::string_view serialized,
ustring_view serialized,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
@ -155,7 +154,7 @@ class ConfigMessage {
/// parse. A simple handler such as `[](const auto& e) { throw e; }` can be used to make any
/// parse error of any message fatal.
explicit ConfigMessage(
const std::vector<std::string_view>& configs,
const std::vector<ustring_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
@ -218,10 +217,10 @@ class ConfigMessage {
/// typically for a local serialization value that isn't being pushed to the server). Note that
/// signing is always disabled if there is no signing callback set, regardless of the value of
/// this argument.
virtual std::string serialize(bool enable_signing = true);
virtual ustring serialize(bool enable_signing = true);
protected:
std::string serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
ustring serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
};
// Constructor tag
@ -267,7 +266,7 @@ class MutableConfigMessage : public ConfigMessage {
/// constructor only increments seqno once while the indirect version would increment twice in
/// the case of a required merge conflict resolution.
explicit MutableConfigMessage(
const std::vector<std::string_view>& configs,
const std::vector<ustring_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
@ -278,7 +277,7 @@ class MutableConfigMessage : public ConfigMessage {
/// take an error handler and instead always throws on parse errors (the above also throws for
/// an erroneous single message, but with a less specific "no valid config messages" error).
explicit MutableConfigMessage(
std::string_view config,
ustring_view config,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
@ -319,46 +318,10 @@ class MutableConfigMessage : public ConfigMessage {
const hash_t& hash() override;
protected:
const hash_t& hash(std::string_view serialized);
const hash_t& hash(ustring_view serialized);
void increment_impl();
};
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
///
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
/// message can calculate independently (for instance a value derived from a secret key, or a shared
/// random key). This key will be hashed with the message size and domain suffix (see below) to
/// determine the actual encryption key.
///
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
/// config, e.g. "closed-group" or "contacts". The full key will be
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
/// above).
///
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
/// suitable for being passed to decrypt() to authenticate and decrypt.
///
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
/// Same as above but works with strings/string_views instead of ustring/ustring_view
std::string encrypt(std::string_view message, std::string_view key_base, std::string_view domain);
/// Thrown if decrypt() fails.
struct decrypt_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
/// given to encrypt or else decryption fails. Upon decryption failure a std::
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
/// Same as above but using std::string/string_view
std::string decrypt(
std::string_view ciphertext, std::string_view key_base, std::string_view domain);
} // namespace session::config
namespace oxenc::detail {

@ -8,14 +8,7 @@ extern "C" {
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) || defined(WIN32)
#define LIBSESSION_EXPORT __declspec(dllexport)
#else
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
#endif
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT
typedef int64_t seqno_t;
#include "../config.h"
// Config object base type: this type holds the internal object and is initialized by the various
// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions.
@ -33,6 +26,28 @@ typedef struct config_object {
/// user_profile_init).
void config_free(config_object* conf);
typedef enum config_log_level {
LOG_LEVEL_DEBUG = 0,
LOG_LEVEL_INFO,
LOG_LEVEL_WARNING,
LOG_LEVEL_ERROR
} config_log_level;
/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL
/// if not needed). The given function pointer will be invoked with one of the above values, a
/// null-terminated c string containing the log message, and the void* context object given when
/// setting the logger (this is for caller-specific state data and won't be touched).
///
/// The logging function must have signature:
///
/// void log(config_log_level lvl, const char* msg, void* ctx);
///
/// Can be called with callback set to NULL to clear an existing logger.
///
/// The config object itself has no log level: the caller should filter by level as needed.
void config_set_logger(
config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx);
/// Returns the numeric namespace in which config messages of this type should be stored.
int16_t config_storage_namespace(const config_object* conf);
@ -42,7 +57,8 @@ int16_t config_storage_namespace(const config_object* conf);
///
/// `configs` is an array of pointers to the start of the strings; `lengths` is an array of string
/// lengths; `count` is the length of those two arrays.
void config_merge(config_object* conf, const char** configs, const size_t* lengths, size_t count);
int config_merge(
config_object* conf, const unsigned char** configs, const size_t* lengths, size_t count);
/// Returns true if this config object contains updated data that has not yet been confirmed stored
/// on the server.
@ -59,7 +75,7 @@ bool config_needs_push(const config_object* conf);
///
/// NB: The returned buffer belongs to the caller: that is, the caller *MUST* free() it when done
/// with it.
seqno_t config_push(config_object* conf, char** out, size_t* outlen);
seqno_t config_push(config_object* conf, unsigned char** out, size_t* outlen);
/// Reports that data obtained from `config_push` has been successfully stored on the server. The
/// seqno value is the one returned by the config_push call that yielded the config data.
@ -74,12 +90,29 @@ void config_confirm_pushed(config_object* conf, seqno_t seqno);
///
/// Immediately after this is called `config_needs_dump` will start returning true (until the
/// configuration is next modified).
void config_dump(config_object* conf, char** out, size_t* outlen);
void config_dump(config_object* conf, unsigned char** out, size_t* outlen);
/// Returns true if something has changed since the last call to `dump()` that requires calling
/// and saving the `config_dump()` data again.
bool config_needs_dump(const config_object* conf);
/// Config key management; see the corresponding method docs in base.hpp. All `key` arguments here
/// are 32-byte binary buffers (and since fixed-length, there is no keylen argument).
void config_add_key(config_object* conf, const unsigned char* key);
void config_add_key_low_prio(config_object* conf, const unsigned char* key);
int config_clear_keys(config_object* conf);
bool config_remove_key(config_object* conf, const unsigned char* key);
int config_key_count(const config_object* conf);
bool config_has_key(const config_object* conf, const unsigned char* key);
// Returns a pointer to the 32-byte binary key at position i. This is *not* null terminated (and is
// exactly 32 bytes long). `i < config_key_count(conf)` must be satisfied. Ownership of the data
// remains in the object (that is: the caller must not attempt to free it).
const unsigned char* config_key(const config_object* conf, size_t i);
/// Returns the encryption domain C-str used to encrypt values for this config object. (This is
/// here only for debugging/testing).
const char* config_encryption_domain(const config_object* conf);
#ifdef __cplusplus
} // extern "C"
#endif

@ -1,9 +1,11 @@
#pragma once
#include <cassert>
#include <memory>
#include <session/config.hpp>
#include <type_traits>
#include <variant>
#include <vector>
#include "base.h"
#include "namespaces.hpp"
@ -23,7 +25,7 @@ static constexpr bool is_dict_value =
is_dict_subtype<T> || is_one_of<T, dict_value, int64_t, std::string>;
// Levels for the logging callback
enum class LogLevel { debug, info, warning, error };
enum class LogLevel { debug = 0, info, warning, error };
/// Our current config state
enum class ConfigState : int {
@ -52,12 +54,21 @@ class ConfigBase {
// Tracks our current state
ConfigState _state = ConfigState::Clean;
protected:
// Constructs an empty base config with no config settings and seqno set to 0.
ConfigBase();
static constexpr size_t KEY_SIZE = 32;
// Contains the base key(s) we use to encrypt/decrypt messages. If non-empty, the .front()
// element will be used when encrypting a new message to push. When decrypting, we attempt each
// of them, starting with .front(), until decryption succeeds.
using Key = std::array<unsigned char, KEY_SIZE>;
Key* _keys = nullptr;
size_t _keys_size = 0;
size_t _keys_capacity = 0;
// Constructs a base config by loading the data from a dump as produced by `dump()`.
explicit ConfigBase(std::string_view dump);
protected:
// Constructs a base config by loading the data from a dump as produced by `dump()`. If the
// dump is nullopt then an empty base config is constructed with no config settings and seqno
// set to 0.
explicit ConfigBase(std::optional<ustring_view> dump = std::nullopt);
// Tracks whether we need to dump again; most mutating methods should set this to true (unless
// calling set_state, which sets to to true implicitly).
@ -69,10 +80,7 @@ class ConfigBase {
_needs_dump = true;
}
// If set then we log things by calling this callback
std::function<void(LogLevel lvl, std::string msg)> logger;
// Invokes the above if set, does nothing if there is no logger.
// Invokes the `logger` callback if set, does nothing if there is no logger.
void log(LogLevel lvl, std::string msg) {
if (logger)
logger(lvl, std::move(msg));
@ -371,15 +379,24 @@ class ConfigBase {
virtual void load_extra_data(oxenc::bt_dict extra) {}
public:
virtual ~ConfigBase() = default;
virtual ~ConfigBase();
// Proxy class providing read and write access to the contained config data.
const DictFieldRoot data{*this};
// If set then we log things by calling this callback
std::function<void(LogLevel lvl, std::string msg)> logger;
// Accesses the storage namespace where this config type is to be stored/loaded from. See
// namespaces.hpp for the underlying integer values.
virtual Namespace storage_namespace() const = 0;
/// Subclasses must override this to return a constant string that is unique per config type;
/// this value is used for domain separation in encryption. The string length must be between 1
/// and 24 characters; use the class name (e.g. "UserProfile") unless you have something better
/// to use. This is rarely needed externally; it is public merely for testing purposes.
virtual const char* encryption_domain() const = 0;
// How many config lags should be used for this object; default to 5. Implementing subclasses
// can override to return a different constant if desired. More lags require more "diff"
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
@ -392,9 +409,15 @@ class ConfigBase {
// After this call the caller should check `needs_push()` to see if the data on hand was updated
// and needs to be pushed to the server again.
//
// Returns the number of the given config messages that were successfully parsed.
//
// Will throw on serious error (i.e. if neither the current nor any of the given configs are
// parseable).
virtual void merge(const std::vector<std::string_view>& configs);
// parseable). This should not happen (the current config, at least, should always be
// re-parseable).
virtual int merge(const std::vector<ustring_view>& configs);
// Same as above but takes a vector of ustring's as sometimes that is more convenient.
int merge(const std::vector<ustring>& configs);
// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized
// yet).
@ -408,13 +431,13 @@ class ConfigBase {
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
// storage of the most recent serialized push data.
virtual bool needs_push() const;
bool needs_push() const;
// Returns the data to push to the server along with the seqno value of the data. If the config
// is currently dirty (i.e. has previously unsent modifications) then this marks it as
// awaiting-confirmation instead of dirty so that any future change immediately increments the
// seqno.
virtual std::pair<std::string, seqno_t> push();
// Returns the data messages to push to the server along with the seqno value of the data. If
// the config is currently dirty (i.e. has previously unsent modifications) then this marks it
// as awaiting-confirmation instead of dirty so that any future change immediately increments
// the seqno.
std::pair<ustring, seqno_t> push();
// Should be called after the push is confirmed stored on the storage server swarm to let the
// object know the data is stored. (Once this is called `needs_push` will start returning false
@ -431,11 +454,61 @@ class ConfigBase {
// into the constructor to reconstitute the object (including the push/not pushed status). This
// method is *not* virtual: if subclasses need to store extra data they should set it in the
// `subclass_data` field.
std::string dump();
ustring dump();
// Returns true if something has changed since the last call to `dump()` that requires calling
// and saving the `dump()` data again.
virtual bool needs_dump() const { return _needs_dump; }
// Encryption key methods. For classes that have a single, static key (such as user profile
// storage types) these methods typically don't need to be used: the subclass calls them
// automatically.
// Adds an encryption/decryption key, without removing existing keys. They key must be exactly
// 32 bytes long. The newly added key becomes the highest priority key (unless the
// `high_priority` argument is set to false' see below): it will be used for encryption of
// config pushes after the call, and will be tried first when decrypting, followed by keys
// present (if any) before this call. If the given key is already present in the key list then
// this call moves it to the front of the list (if not already at the front).
//
// If the `high_priority` argument is specified and false, then the key is added to the *end* of
// the key list instead of the beginning: that is, it will not replace the current
// highest-priority key used for encryption, but will still be usable for decryption of new
// incoming messages (after trying keys present before the call). If the key already exists
// then nothing happens with `high_priority=false` (in particular, it is *not* repositioned, in
// contrast to high_priority=true behaviour).
//
// Will throw a std::invalid_argument if the key is not 32 bytes.
void add_key(ustring_view key, bool high_priority = true);
// Clears all stored encryption/decryption keys. This is typically immediately followed with
// one or more `add_key` call to replace existing keys. Returns the number of keys removed.
int clear_keys();
// Removes the given encryption/decryption key, if present. Returns true if it was found and
// removed, false if it was not in the key list.
//
// The optional second argument removes the key only from position `from` or higher. It is
// mainly for internal use and is usually omitted.
bool remove_key(ustring_view key, size_t from = 0);
// Returns a vector of encryption keys, in priority order (i.e. element 0 is the encryption key,
// and the first decryption key).
std::vector<ustring_view> get_keys() const;
// Returns the number of encryption keys.
int key_count() const;
// Returns true if the given key is already in the keys list.
bool has_key(ustring_view key) const;
// Accesses the key at position i (0 if omitted). There must be at least one key, and i must be
// less than key_count(). The key at position 0 is used for encryption; for decryption all keys
// are tried in order, starting from position 0.
ustring_view key(size_t i = 0) const {
assert(i < _keys_size);
return {_keys[i].data(), _keys[i].size()};
}
};
// The C++ struct we hold opaquely inside the C internals struct. This is designed so that any
@ -498,6 +571,6 @@ inline int set_error(config_object* conf, int errcode, const std::exception& e)
// Copies a value contained in a string into a new malloced char buffer, returning the buffer and
// size via the two pointer arguments.
void copy_out(const std::string& data, char** out, size_t* outlen);
void copy_out(ustring_view data, unsigned char** out, size_t* outlen);
} // namespace session::config

@ -0,0 +1,36 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
/// Wrapper around session::config::encrypt. message and key_base are binary: message has the
/// length provided, key_base must be exactly 32 bytes. domain is a c string. Returns a newly
/// allocated buffer containing the encrypted data, and sets the data's length into
/// `ciphertext_size`. It is the caller's responsibility to `free()` the returned buffer!
///
/// Returns nullptr on error.
unsigned char* config_encrypt(
const unsigned char* message,
size_t mlen,
const unsigned char* key_base,
const char* domain,
size_t* ciphertext_size);
/// Works just like config_encrypt, but in reverse.
unsigned char* config_decrypt(
const unsigned char* ciphertext,
size_t clen,
const unsigned char* key_base,
const char* domain,
size_t* plaintext_size);
/// Returns the amount of padding needed for a plaintext of size s with encryption overhead
/// `overhead`.
size_t config_padded_size(size_t s, size_t overhead);
#ifdef __cplusplus
}
#endif

@ -0,0 +1,69 @@
#pragma once
#include <stdexcept>
#include "../types.hpp"
namespace session::config {
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
///
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
/// message can calculate independently (for instance a value derived from a secret key, or a shared
/// random key). This key will be hashed with the message size and domain suffix (see below) to
/// determine the actual encryption key.
///
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
/// config, e.g. "closed-group" or "contacts". The full key will be
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
/// above).
///
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
/// suitable for being passed to decrypt() to authenticate and decrypt.
///
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
/// Same as above, but modifies `message` in place. `message` gets encrypted plus has the extra
/// data and nonce appended.
void encrypt_inplace(ustring& message, ustring_view key_base, std::string_view domain);
/// Constant amount of extra bytes required to be appended when encrypting.
constexpr size_t ENCRYPT_DATA_OVERHEAD = 40; // ABYTES + NPUBBYTES
/// Thrown if decrypt() fails.
struct decrypt_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
/// given to encrypt or else decryption fails. Upon decryption failure a `decrypt_error` exception
/// is thrown.
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
/// Same as above, but does in in-place. The string gets shortend to the plaintext after this call.
void decrypt_inplace(ustring& ciphertext, ustring_view key_base, std::string_view domain);
/// Returns the target size of the message with padding, assuming an additional `overhead` bytes of
/// overhead (e.g. from encrypt() overhead) will be appended. Will always return a value >= s +
/// overhead.
///
/// Padding increments we use: 256 byte increments up to 5120; 1024 byte increments up to 20480,
/// 2048 increments up to 40960, then 5120 from there up.
inline constexpr size_t padded_size(size_t s, size_t overhead = ENCRYPT_DATA_OVERHEAD) {
size_t s2 = s + overhead;
size_t chunk = s2 < 5120 ? 256 : s2 < 20480 ? 1024 : s2 < 40960 ? 2048 : 5120;
return (s2 + chunk - 1) / chunk * chunk - overhead;
}
/// Inserts null byte padding to the beginning of a message to make the final message size granular.
/// See the above function for the sizes.
///
/// \param data - the data; this is modified in place.
/// \param overhead - encryption overhead to account for to reach the desired padded size. The
/// default, if omitted, is the space used by the `encrypt()` function defined above.
void pad_message(ustring& data, size_t overhead = ENCRYPT_DATA_OVERHEAD);
} // namespace session::config

@ -6,19 +6,32 @@ extern "C" {
#include "base.h"
/// Constructs a user profile config object and sets a pointer to it in `conf`. To restore an
/// existing dump produced by a past instantiation's call to `dump()` pass the dump value via `dump`
/// and `dumplen`; to construct a new, empty profile pass NULL and 0.
/// Constructs a user profile config object and sets a pointer to it in `conf`.
///
/// `error` must either be NULL or a pointer to a buffer of at least 256 bytes.
/// \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.
///
/// Returns 0 on success; returns a non-zero error code and sets error (if not NULL) to the
/// exception message on failure.
/// \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 profile 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 user_profile_init(config_object** conf, const char* dump, size_t dumplen, char* error)
__attribute__((warn_unused_result));
int user_profile_init(
config_object** conf,
const unsigned char* ed25519_secretkey,
const unsigned char* dump,
size_t dumplen,
char* error) __attribute__((warn_unused_result));
/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at
/// all. Should be copied right away as the pointer may not remain valid beyond other API calls.
@ -34,7 +47,7 @@ typedef struct user_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 char* key;
const unsigned char* key;
size_t keylen;
} user_profile_pic;

@ -14,30 +14,52 @@ 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:
/// Constructs a new, blank user profile.
UserProfile() = default;
// No default constructor
UserProfile() = delete;
/// Constructs a user profile from existing data
explicit UserProfile(std::string_view dumped) : ConfigBase{dumped} {}
/// Constructs a user profile from existing data (stored from `dump()`) and the user's secret
/// 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 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()`.
UserProfile(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
Namespace storage_namespace() const override { return Namespace::UserProfile; }
/// Returns the user profile name, or nullptr if there is no profile name set.
const std::string* get_name() const;
const char* encryption_domain() const override { return "UserProfile"; }
/// Sets the user profile name
/// Returns the user profile name, or std::nullopt if there is no profile name set.
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);
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
/// values if *either* value is unset or empty in the config data.
std::pair<const std::string*, const std::string*> get_profile_pic() const;
std::optional<profile_pic> get_profile_pic() const;
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
/// one is empty.
void set_profile_pic(std::string url, std::string key);
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

@ -0,0 +1,8 @@
#pragma once
#if defined(_WIN32) || defined(WIN32)
#define LIBSESSION_EXPORT __declspec(dllexport)
#else
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
#endif
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT

@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
namespace session {
using ustring = std::basic_string<unsigned char>;
using ustring_view = std::basic_string_view<unsigned char>;
namespace config {
using seqno_t = std::int64_t;
} // namespace config
} // namespace session

@ -0,0 +1,27 @@
#pragma once
#include "types.hpp"
namespace session {
// Helper function to go to/from char pointers to unsigned char pointers:
inline const unsigned char* to_unsigned(const char* x) {
return reinterpret_cast<const unsigned char*>(x);
}
inline unsigned char* to_unsigned(char* x) {
return reinterpret_cast<unsigned char*>(x);
}
inline const char* from_unsigned(const unsigned char* x) {
return reinterpret_cast<const char*>(x);
}
inline char* from_unsigned(unsigned char* x) {
return reinterpret_cast<char*>(x);
}
// Helper function to switch between string_view and ustring_view
inline ustring_view to_unsigned_sv(std::string_view v) {
return {to_unsigned(v.data()), v.size()};
}
inline std::string_view from_unsigned_sv(ustring_view v) {
return {from_unsigned(v.data()), v.size()};
}
} // namespace session

@ -191,7 +191,10 @@ extension MessageSender {
// If we don't have a userKeyPair yet then there is no need to sync the configuration
// as the user doesn't exist yet (this will get triggered on the first launch of a
// fresh install due to the migrations getting run)
guard Identity.userExists(db) else { return Promise(error: StorageError.generic) }
guard
Identity.userExists(db),
let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey
else { return Promise(error: StorageError.generic) }
let publicKey: String = getUserHexEncodedPublicKey(db)
let legacyDestination: Message.Destination = Message.Destination.contact(
@ -201,7 +204,9 @@ extension MessageSender {
let legacyConfigurationMessage = try ConfigurationMessage.getCurrent(db)
let (promise, seal) = Promise<Void>.pending()
let userConfigMessageChanges: [SharedConfigMessage] = SessionUtil.getChanges()
let userConfigMessageChanges: [SharedConfigMessage] = SessionUtil.getChanges(
ed25519SecretKey: ed25519SecretKey
)
let destination: Message.Destination = Message.Destination.contact(
publicKey: publicKey,
namespace: .userProfileConfig

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
@ -13,10 +14,21 @@ class ConfigUserProfileSpec: QuickSpec {
override func spec() {
it("generates 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(user_profile_init(&conf, nil, 0, error)).to(equal(0))
expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// We don't need to push anything, since this is an empty config
@ -28,15 +40,31 @@ class ConfigUserProfileSpec: QuickSpec {
let namePtr: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr).to(beNil())
var toPush: UnsafeMutablePointer<CChar>? = nil
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(String(pointer: toPush, length: toPushLen)).to(equal("d1:#i0e1:&de1:<le1:=dee"))
expect(toPushLen).to(equal(256))
let encDomain: [CChar] = "UserProfile"
.bytes
.map { CChar(bitPattern: $0) }
expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile"))
var toPushDecSize: Int = 0
let toPushDecrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(toPush, toPushLen, edSK, encDomain, &toPushDecSize)
let prefixPadding: String = (0..<193)
.map { _ in "\0" }
.joined()
expect(toPushDecrypted).toNot(beNil())
expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPushDecrypted, length: toPushDecSize))
.to(equal("\(prefixPadding)d1:#i0e1:&de1:<le1:=dee"))
toPush?.deallocate()
toPushDecrypted?.deallocate()
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
@ -49,9 +77,7 @@ class ConfigUserProfileSpec: QuickSpec {
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp"
.bytes
.map { CChar(bitPattern: $0) }
let profileKey: [CChar] = "secretNOTSECRET"
.bytes
.map { CChar(bitPattern: $0) }
let profileKey: [UInt8] = "secretNOTSECRET".bytes
let p: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
profileKey.withUnsafeBufferPointer { profileKeyPtr in
user_profile_pic(
@ -80,7 +106,7 @@ class ConfigUserProfileSpec: QuickSpec {
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
var toPush2: UnsafeMutablePointer<CChar>? = nil
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
@ -90,11 +116,10 @@ class ConfigUserProfileSpec: QuickSpec {
// Note: This hex value differs from the value in the library tests because
// it looks like the library has an "end of cell mark" character added at the
// end (0x07 or '0007') so we need to manually add it to work
let expHash0: [CChar] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
.map { CChar(bitPattern: $0) }
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1: [CChar] = ["""
let expPush1Decrypted: [UInt8] = ["""
d
1:#i1e
1:& d
@ -105,8 +130,7 @@ class ConfigUserProfileSpec: QuickSpec {
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
.map { CChar(bitPattern: $0) },
.bytes,
expHash0,
"""
de e
@ -119,20 +143,44 @@ class ConfigUserProfileSpec: QuickSpec {
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
.map { CChar(bitPattern: $0) }
]
.flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"a2952190dcb9797bc48e48f6dc7b3254d004bde9091cfc9ec3433cbc5939a3726deb04f58a546d7d79e6f8",
"0ea185d43bf93278398556304998ae882304075c77f15c67f9914c4d10005a661f29ff7a79e0a9de7f2172",
"5ba3b5a6c19eaa3797671b8fa4008d62e9af2744629cbb46664c4d8048e2867f66ed9254120371bdb24e95",
"b2d92341fa3b1f695046113a768ceb7522269f937ead5591bfa8a5eeee3010474002f2db9de043f0f0d1cf",
"b1066a03e7b5d6cfb70a8f84a20cd2df5a510cd3d175708015a52dd4a105886d916db0005dbea5706e5a5d",
"c37ffd0a0ca2824b524da2e2ad181a48bb38e21ed9abe136014a4ee1e472cb2f53102db2a46afa9d68"
].joined()).bytes
expect(String(pointer: toPush2, length: toPush2Len, encoding: .ascii))
.to(equal(String(pointer: expPush1, length: expPush1.count, encoding: .ascii)))
.to(equal(String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)))
// Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data)
var toPush2DecSize: Int = 0
let toPush2Decrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(
toPush2,
toPush2Len,
edSK,
encDomain,
&toPush2DecSize
)
let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count))
.map { _ in "\0" }
.joined()
expect(toPush2DecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPush2Decrypted, length: toPush2DecSize, encoding: .ascii))
.to(equal(String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii).map { "\(prefixPadding2)\($0)" }))
toPush2?.deallocate()
toPush2Decrypted?.deallocate()
// We haven't dumped, so still need to dump:
expect(config_needs_dump(conf)).to(beTrue())
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
expect(config_needs_push(conf)).to(beTrue())
var dump1: UnsafeMutablePointer<CChar>? = nil
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
@ -144,12 +192,13 @@ class ConfigUserProfileSpec: QuickSpec {
"""
d
1:! i2e
1:$ \(expPush1.count):
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1,
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
@ -167,7 +216,7 @@ class ConfigUserProfileSpec: QuickSpec {
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
var dump2: UnsafeMutablePointer<CChar>? = nil
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
dump2?.deallocate()
@ -179,20 +228,20 @@ class ConfigUserProfileSpec: QuickSpec {
// Start with an empty config, as above:
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf2, nil, 0, error2)).to(equal(0))
expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
error2?.deallocate()
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
var mergeData: [UnsafePointer<CChar>?] = [expPush1].unsafeCopy()
var mergeSize: [Int] = [expPush1.count]
config_merge(conf2, &mergeData, &mergeSize, 1)
var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
var mergeSize: [Int] = [expPush1Encrypted.count]
expect(config_merge(conf2, &mergeData, &mergeSize, 1)).to(equal(1))
mergeData.forEach { $0?.deallocate() }
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<CChar>? = nil
var dump3: UnsafeMutablePointer<UInt8>? = nil
var dump3Len: Int = 0
config_dump(conf2, &dump3, &dump3Len)
// (store in db)
@ -213,9 +262,7 @@ class ConfigUserProfileSpec: QuickSpec {
let profile2Url: [CChar] = "http://new.example.com/pic"
.bytes
.map { CChar(bitPattern: $0) }
let profile2Key: [CChar] = "qwert\0yuio"
.bytes
.map { CChar(bitPattern: $0) }
let profile2Key: [UInt8] = "qwert\0yuio".bytes
let p2: user_profile_pic = profile2Url.withUnsafeBufferPointer { profile2UrlPtr in
profile2Key.withUnsafeBufferPointer { profile2KeyPtr in
user_profile_pic(
@ -230,20 +277,20 @@ class ConfigUserProfileSpec: QuickSpec {
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
var toPush3: UnsafeMutablePointer<CChar>? = nil
var toPush3: UnsafeMutablePointer<UInt8>? = nil
var toPush3Len: Int = 0
let seqno3: Int64 = config_push(conf, &toPush3, &toPush3Len)
expect(seqno3).to(equal(2)) // incremented, since we made a field change
var toPush4: UnsafeMutablePointer<CChar>? = nil
var toPush4: UnsafeMutablePointer<UInt8>? = nil
var toPush4Len: Int = 0
let seqno4: Int64 = config_push(conf2, &toPush4, &toPush4Len)
expect(seqno4).to(equal(2)) // incremented, since we made a field change
var dump4: UnsafeMutablePointer<CChar>? = nil
var dump4: UnsafeMutablePointer<UInt8>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<CChar>? = nil
var dump5: UnsafeMutablePointer<UInt8>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
@ -260,11 +307,11 @@ class ConfigUserProfileSpec: QuickSpec {
// Feed the new config into each other. (This array could hold multiple configs if we pulled
// down more than one).
var mergeData2: [UnsafePointer<CChar>?] = [UnsafePointer(toPush3)]
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush3)]
var mergeSize2: [Int] = [toPush3Len]
config_merge(conf2, &mergeData2, &mergeSize2, 1)
toPush3?.deallocate()
var mergeData3: [UnsafePointer<CChar>?] = [UnsafePointer(toPush4)]
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(toPush4)]
var mergeSize3: [Int] = [toPush4Len]
config_merge(conf, &mergeData3, &mergeSize3, 1)
toPush4?.deallocate()
@ -301,10 +348,10 @@ class ConfigUserProfileSpec: QuickSpec {
config_confirm_pushed(conf, seqno5)
config_confirm_pushed(conf2, seqno6)
var dump6: UnsafeMutablePointer<CChar>? = nil
var dump6: UnsafeMutablePointer<UInt8>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<CChar>? = nil
var dump7: UnsafeMutablePointer<UInt8>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)

@ -6,7 +6,7 @@ extension SnodeAPI {
public class SendMessageRequest: SnodeAuthenticatedRequestBody {
enum CodingKeys: String, CodingKey {
case namespace
case signatureTimestamp = "sig_timestamp"
case signatureTimestamp = "timestamp"//"sig_timestamp" // TODO: Add this back once the snodes are fixed
}
let message: SnodeMessage

@ -31,3 +31,16 @@ public extension Collection where Element == [CChar] {
}
}
}
public extension Collection where Element == [UInt8] {
/// This creates an array of UnsafePointer types to access data of the C strings in memory. This array provides no automated
/// memory management of it's children so after use you are responsible for handling the life cycle of the child elements and
/// need to call `deallocate()` on each child.
func unsafeCopy() -> [UnsafePointer<UInt8>?] {
return self.map { value in
let copy = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: value.count)
_ = copy.initialize(from: value)
return UnsafePointer(copy.baseAddress)
}
}
}

@ -77,7 +77,9 @@ public enum AppSetup {
// After the migrations have run but before the migration completion we load the
// SessionUtil state and update the 'needsConfigSync' flag based on whether the
// configs also need to be sync'ed
SessionUtil.loadState()
SessionUtil.loadState(
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey
)
DispatchQueue.main.async {
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))

Loading…
Cancel
Save