diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 3814a223c..d7caf5c80 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -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: diff --git a/SessionMessagingKit/Database/Migrations/_011_SharedUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_011_SharedUtilChanges.swift index 548f1f0d1..c9b33cb43 100644 --- a/SessionMessagingKit/Database/Migrations/_011_SharedUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_011_SharedUtilChanges.swift @@ -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? = try SessionUtil.loadState( - for: .userProfile + for: .userProfile, + secretKey: secretKey, + cachedData: nil ) let confResult: SessionUtil.ConfResult = try SessionUtil.update( profile: Profile.fetchOrCreateCurrentUser(db), diff --git a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift index cafc4645f..02bb36173 100644 --- a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift +++ b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift @@ -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 diff --git a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserProfile.swift b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserProfile.swift index 38a616b59..35fa57e27 100644 --- a/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserProfile.swift +++ b/SessionMessagingKit/LibSessionUtil/Config Handling/SessionUtil+UserProfile.swift @@ -36,7 +36,7 @@ internal extension SessionUtil { if profilePic.keylen > 0, let profilePictureUrlPtr: UnsafePointer = profilePic.url, - let profilePictureKeyPtr: UnsafePointer = profilePic.key + let profilePictureKeyPtr: UnsafePointer = 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( diff --git a/SessionMessagingKit/LibSessionUtil/SessionUtil.swift b/SessionMessagingKit/LibSessionUtil/SessionUtil.swift index 1d3247ee4..32c29557d 100644 --- a/SessionMessagingKit/LibSessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/LibSessionUtil/SessionUtil.swift @@ -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? { + private static func loadState( + for variant: ConfigDump.Variant, + secretKey ed25519SecretKey: [UInt8]? + ) -> UnsafeMutablePointer? { + 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? { // Setup initial variables (including getting the memory address for any cached data) var conf: UnsafeMutablePointer? = 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? = nil + var dumpResult: UnsafeMutablePointer? = nil var dumpResultLen: Int = 0 config_dump(conf, &dumpResult, &dumpResultLen) - guard let dumpResult: UnsafeMutablePointer = dumpResult else { return } + guard let dumpResult: UnsafeMutablePointer = 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? = nil + var toPush: UnsafeMutablePointer? = nil var toPushLen: Int = 0 let seqNo: Int64 = conf.mutate { config_push($0, &toPush, &toPushLen) } - guard let toPush: UnsafeMutablePointer = toPush else { return nil } + guard let toPush: UnsafeMutablePointer = 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?] = next.value - .map { message -> [CChar] in - message.data - .bytes - .map { CChar(bitPattern: $0) } - } + var mergeData: [UnsafePointer?] = 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) diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64/libsession-util.a b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64/libsession-util.a index 6d20b8a98..5e58f5b33 100644 Binary files a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64/libsession-util.a and b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64/libsession-util.a differ diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64_x86_64-simulator/libsession-util.a b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64_x86_64-simulator/libsession-util.a index 284fc504f..8c1e89f23 100644 Binary files a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64_x86_64-simulator/libsession-util.a and b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/ios-arm64_x86_64-simulator/libsession-util.a differ diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/module.modulemap b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/module.modulemap index a9e908ab1..5580079fa 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/module.modulemap +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/module.modulemap @@ -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 * diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.h b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.h new file mode 100644 index 000000000..eea54c1dd --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef int64_t seqno_t; + +#ifdef __cplusplus +} +#endif diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.hpp index 90ab8676a..c59809861 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.hpp +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config.hpp @@ -11,8 +11,11 @@ #include #include +#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(v); } -using seqno_t = std::int64_t; using hash_t = std::array; using seqno_hash_t = std::pair; -using ustring = std::basic_string; -using ustring_view = std::basic_string_view; - class MutableConfigMessage; /// Base type for all errors that can happen during config parsing @@ -103,7 +102,7 @@ class ConfigMessage { using verify_callable = std::function; /// Signing function: this is passed the data to be signed and returns the 64-byte signature. - using sign_callable = std::function; + using sign_callable = std::function; 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& configs, + const std::vector& 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& configs, + const std::vector& 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 { diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.h b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.h index 55649ecc8..2619511dd 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.h +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.h @@ -8,14 +8,7 @@ extern "C" { #include #include -#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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.hpp index dc14a37bc..86f42533d 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.hpp +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/base.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include +#include #include "base.h" #include "namespaces.hpp" @@ -23,7 +25,7 @@ static constexpr bool is_dict_value = is_dict_subtype || is_one_of; // 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; + 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 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 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 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& configs); + // parseable). This should not happen (the current config, at least, should always be + // re-parseable). + virtual int merge(const std::vector& configs); + + // Same as above but takes a vector of ustring's as sometimes that is more convenient. + int merge(const std::vector& 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 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 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 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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.h b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.h new file mode 100644 index 000000000..b29848929 --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/// 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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.hpp new file mode 100644 index 000000000..75c9f9aff --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/encrypt.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include + +#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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.h b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.h index 6a0123027..8b438591e 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.h +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.h @@ -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; diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.hpp index b4cdba80b..f13a83867 100644 --- a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.hpp +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/config/user_profile.hpp @@ -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 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 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 get_profile_pic() const; + std::optional 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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/export.h b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/export.h new file mode 100644 index 000000000..ab307f6f2 --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/export.h @@ -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 diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/types.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/types.hpp new file mode 100644 index 000000000..d63fe470e --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/types.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace session { + +using ustring = std::basic_string; +using ustring_view = std::basic_string_view; + +namespace config { + + using seqno_t = std::int64_t; + +} // namespace config + +} // namespace session diff --git a/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/util.hpp b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/util.hpp new file mode 100644 index 000000000..8348d908e --- /dev/null +++ b/SessionMessagingKit/LibSessionUtil/libsession-util.xcframework/session/util.hpp @@ -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(x); +} +inline unsigned char* to_unsigned(char* x) { + return reinterpret_cast(x); +} +inline const char* from_unsigned(const unsigned char* x) { + return reinterpret_cast(x); +} +inline char* from_unsigned(unsigned char* x) { + return reinterpret_cast(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 diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index d59cc1621..ac1160084 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -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.pending() - let userConfigMessageChanges: [SharedConfigMessage] = SessionUtil.getChanges() + let userConfigMessageChanges: [SharedConfigMessage] = SessionUtil.getChanges( + ed25519SecretKey: ed25519SecretKey + ) let destination: Message.Destination = Message.Destination.contact( publicKey: publicKey, namespace: .userProfileConfig diff --git a/SessionMessagingKitTests/LibSessionUtil/ConfigUserProfileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/ConfigUserProfileSpec.swift index e08840408..93ba46791 100644 --- a/SessionMessagingKitTests/LibSessionUtil/ConfigUserProfileSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/ConfigUserProfileSpec.swift @@ -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? = nil var conf: UnsafeMutablePointer? = 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? = user_profile_get_name(conf) expect(namePtr).to(beNil()) - var toPush: UnsafeMutablePointer? = nil + var toPush: UnsafeMutablePointer? = 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:? = 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:? = nil + var toPush2: UnsafeMutablePointer? = 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? = 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? = nil + var dump1: UnsafeMutablePointer? = 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? = nil + var dump2: UnsafeMutablePointer? = 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? = nil var conf2: UnsafeMutablePointer? = 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?] = [expPush1].unsafeCopy() - var mergeSize: [Int] = [expPush1.count] - config_merge(conf2, &mergeData, &mergeSize, 1) + var mergeData: [UnsafePointer?] = [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? = nil + var dump3: UnsafeMutablePointer? = 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? = nil + var toPush3: UnsafeMutablePointer? = 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? = nil + var toPush4: UnsafeMutablePointer? = 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? = nil + var dump4: UnsafeMutablePointer? = nil var dump4Len: Int = 0 config_dump(conf, &dump4, &dump4Len); - var dump5: UnsafeMutablePointer? = nil + var dump5: UnsafeMutablePointer? = 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?] = [UnsafePointer(toPush3)] + var mergeData2: [UnsafePointer?] = [UnsafePointer(toPush3)] var mergeSize2: [Int] = [toPush3Len] config_merge(conf2, &mergeData2, &mergeSize2, 1) toPush3?.deallocate() - var mergeData3: [UnsafePointer?] = [UnsafePointer(toPush4)] + var mergeData3: [UnsafePointer?] = [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? = nil + var dump6: UnsafeMutablePointer? = nil var dump6Len: Int = 0 config_dump(conf, &dump6, &dump6Len); - var dump7: UnsafeMutablePointer? = nil + var dump7: UnsafeMutablePointer? = nil var dump7Len: Int = 0 config_dump(conf2, &dump7, &dump7Len); // (store in db) diff --git a/SessionSnodeKit/Models/SendMessageRequest.swift b/SessionSnodeKit/Models/SendMessageRequest.swift index 541768a64..94c5ab0a9 100644 --- a/SessionSnodeKit/Models/SendMessageRequest.swift +++ b/SessionSnodeKit/Models/SendMessageRequest.swift @@ -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 diff --git a/SessionUtilitiesKit/General/Collection+Utilities.swift b/SessionUtilitiesKit/General/Collection+Utilities.swift index 234de4c1a..f757a9a8d 100644 --- a/SessionUtilitiesKit/General/Collection+Utilities.swift +++ b/SessionUtilitiesKit/General/Collection+Utilities.swift @@ -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?] { + return self.map { value in + let copy = UnsafeMutableBufferPointer.allocate(capacity: value.count) + _ = copy.initialize(from: value) + return UnsafePointer(copy.baseAddress) + } + } +} diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index 9e70fdbfa..f3dcac986 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -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))