diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 241dc78..e3f6a18 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.12.0" -kotlin = "2.2.0" +agp = "8.12.1" +kotlin = "2.2.10" [libraries] junit = { module = "junit:junit", version = "4.13.2" } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 23ed558..1597fa6 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -12,7 +12,7 @@ android { compileSdk = 35 defaultConfig { - minSdk = 24 + minSdk = 26 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/library/src/main/cpp/CMakeLists.txt b/library/src/main/cpp/CMakeLists.txt index 8256a10..21863b0 100644 --- a/library/src/main/cpp/CMakeLists.txt +++ b/library/src/main/cpp/CMakeLists.txt @@ -44,6 +44,7 @@ set(SOURCES ed25519.cpp curve25519.cpp hash.cpp + protocol.cpp ) add_library( # Sets the name of the library. diff --git a/library/src/main/cpp/group_keys.cpp b/library/src/main/cpp/group_keys.cpp index 45f87f7..55f43bc 100644 --- a/library/src/main/cpp/group_keys.cpp +++ b/library/src/main/cpp/group_keys.cpp @@ -187,6 +187,13 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, j return jni_utils::jlist_from_collection(env, ptr->group_keys(), util::bytes_from_span); } +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_GroupKeysConfig_groupEncKey(JNIEnv *env, jobject thiz) { + auto ptr = ptrToKeys(env, thiz); + return util::bytes_from_span(env, ptr->group_enc_key()); +} + extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_GroupKeysConfig_activeHashes(JNIEnv *env, diff --git a/library/src/main/cpp/jni_utils.h b/library/src/main/cpp/jni_utils.h index 5aca809..a9cc6f5 100644 --- a/library/src/main/cpp/jni_utils.h +++ b/library/src/main/cpp/jni_utils.h @@ -69,12 +69,18 @@ namespace jni_utils { env_->DeleteLocalRef(ref_); } } + JavaLocalRef(JavaLocalRef&& other) : env_(other.env_), ref_(other.ref_) { + other.ref_ = nullptr; + } + + JavaLocalRef(const JavaLocalRef&) = delete; inline JNIType get() const { return ref_; } }; + /** * Create a Java List from an iterator. * @@ -162,6 +168,8 @@ namespace jni_utils { data = std::span(const_cast(c_str), env->GetStringUTFLength(s)); } + JavaStringRef(const JavaStringRef &) = delete; + ~JavaStringRef() { env->ReleaseStringUTFChars(s, data.data()); } @@ -195,8 +203,18 @@ namespace jni_utils { data = std::span(reinterpret_cast(env->GetByteArrayElements(byte_array, nullptr)), length); } + JavaByteArrayRef(const JavaByteArrayRef &) = delete; + + JavaByteArrayRef(JavaByteArrayRef&& other) : env(other.env), byte_array(other.byte_array), data(other.data) { + other.byte_array = nullptr; + other.data = {}; + } + ~JavaByteArrayRef() { - env->ReleaseByteArrayElements(byte_array, reinterpret_cast(data.data()), 0); + if (byte_array) { + env->ReleaseByteArrayElements(byte_array, + reinterpret_cast(data.data()), 0); + } } // Get the data as a span. Only valid during the lifetime of this object. @@ -208,6 +226,24 @@ namespace jni_utils { return std::vector(data.begin(), data.end()); } }; + + template + static std::optional> java_to_cpp_array(JNIEnv *env, jbyteArray array) { + if (!array) { + return std::nullopt; + } + + JavaByteArrayRef bytes(env, array); + auto span = bytes.get(); + if (span.size() != N) { + throw std::runtime_error("Invalid byte array length from java, expecting " + std::to_string(N) + " got " + std::to_string(span.size())); + } + + std::array out; + std::copy(span.begin(), span.end(), out.begin()); + return out; + } + } #endif //SESSION_ANDROID_JNI_UTILS_H diff --git a/library/src/main/cpp/protocol.cpp b/library/src/main/cpp/protocol.cpp new file mode 100644 index 0000000..68fb27c --- /dev/null +++ b/library/src/main/cpp/protocol.cpp @@ -0,0 +1,189 @@ +#include +#include +#include + +#include "jni_utils.h" + +using namespace jni_utils; + + + +static JavaLocalRef serializeProStatus(JNIEnv *env, const session::DecryptedEnvelope & envelope) { + if (!envelope.pro.has_value()) { + JavaLocalRef noneClass(env, env->FindClass("network/loki/messenger/libsession_util/protocol/ProStatus$None")); + auto fieldId = env->GetStaticFieldID( + noneClass.get(), + "INSTANCE", "Lnetwork/loki/messenger/libsession_util/protocol/ProStatus$None;"); + return {env, env->GetStaticObjectField(noneClass.get(), fieldId)}; + } + + if (envelope.pro->status == session::config::ProStatus::Valid) { + JavaLocalRef validClass(env, env->FindClass("network/loki/messenger/libsession_util/protocol/ProStatus$Valid")); + auto init = env->GetMethodID(validClass.get(), "", "(JJ)V"); + return {env, env->NewObject(validClass.get(), init, + static_cast(envelope.pro->proof.expiry_unix_ts.time_since_epoch().count()), + static_cast(envelope.pro->features))}; + } + + JavaLocalRef invalidClass(env, env->FindClass("network/loki/messenger/libsession_util/protocol/ProStatus$Invalid")); + auto fieldId = env->GetStaticFieldID( + invalidClass.get(), + "INSTANCE", "Lnetwork/loki/messenger/libsession_util/protocol/ProStatus$Invalid;"); + return {env, env->GetStaticObjectField(invalidClass.get(), fieldId)}; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_decryptEnvelope(JNIEnv *env, + jobject thiz, + jobject java_key, + jbyteArray java_payload, + jlong now_epoch_seconds, + jbyteArray java_pro_backend_pub_key) { + + session::DecryptEnvelopeKey key; + + std::vector> privateKeysStorage; + + struct RegularStorage { + JavaLocalRef ed25519PrivKeyRef; + JavaByteArrayRef ed25519PrivKeyBytesRef; + }; + + struct GroupData { + JavaLocalRef groupPubKeyRef; + JavaByteArrayRef groupPubKeyBytesRef; + + std::vector, JavaByteArrayRef>> groupKeysRef; + }; + + std::optional regularStorage; + std::optional groupStorage; + + JavaLocalRef regularClazz(env, env->FindClass("network/loki/messenger/libsession_util/protocol/DecryptEnvelopeKey$Regular")); + if (env->IsInstanceOf(java_key, regularClazz.get())) { + auto bytes = reinterpret_cast(env->CallObjectMethod(java_key, env->GetMethodID(regularClazz.get(), "getEd25519PrivKey", "()[B"))); + regularStorage.emplace(RegularStorage { + .ed25519PrivKeyRef = JavaLocalRef(env, bytes), + .ed25519PrivKeyBytesRef = JavaByteArrayRef(env, bytes) + }); + + privateKeysStorage.push_back(regularStorage->ed25519PrivKeyBytesRef.get()); + } + + JavaLocalRef groupClazz(env, env->FindClass("network/loki/messenger/libsession_util/protocol/DecryptEnvelopeKey$Group")); + if (env->IsInstanceOf(java_key, groupClazz.get())) { + auto pubKeyBytes = reinterpret_cast(env->CallObjectMethod(java_key, env->GetMethodID(groupClazz.get(), "getGroupEd25519PubKey", "()[B"))); + groupStorage.emplace(GroupData { + .groupPubKeyRef = JavaLocalRef(env, pubKeyBytes), + .groupPubKeyBytesRef = JavaByteArrayRef(env, pubKeyBytes) + }); + + key.group_ed25519_pubkey.emplace(groupStorage->groupPubKeyBytesRef.get()); + + JavaLocalRef privKeyArrays(env, reinterpret_cast(env->CallObjectMethod(java_key, env->GetMethodID(groupClazz.get(), "getGroupKeys", "()[[B")))); + for (int i = 0, size = env->GetArrayLength(privKeyArrays.get()); i < size; i++) { + auto bytes = reinterpret_cast(env->GetObjectArrayElement(privKeyArrays.get(), i)); + const auto &last = groupStorage->groupKeysRef.emplace_back(JavaLocalRef(env, bytes), JavaByteArrayRef(env, bytes)); + privateKeysStorage.emplace_back(last.second.get()); + } + } + + key.ed25519_privkeys = { privateKeysStorage.data(), privateKeysStorage.size() }; + + return run_catching_cxx_exception_or_throws(env, [&] { + auto envelop = session::decrypt_envelope(key, JavaByteArrayRef(env, java_payload).get(), + std::chrono::sys_seconds { std::chrono::seconds { now_epoch_seconds } }, + *java_to_cpp_array<32>(env, java_pro_backend_pub_key)); + + JavaLocalRef sender_ed25519(env, util::bytes_from_span(env, envelop.sender_ed25519_pubkey)); + JavaLocalRef sender_x25519(env, util::bytes_from_span(env, envelop.sender_x25519_pubkey)); + JavaLocalRef content(env, util::bytes_from_vector(env, envelop.content_plaintext)); + + JavaLocalRef envelopClass(env, env->FindClass("network/loki/messenger/libsession_util/protocol/DecryptedEnvelope")); + jmethodID init = env->GetMethodID( + envelopClass.get(), + "", + "(Lnetwork/loki/messenger/libsession_util/protocol/ProStatus;[B[B[BJ)V" + ); + + return env->NewObject(envelopClass.get(), init, + serializeProStatus(env, envelop).get(), + content.get(), + sender_ed25519.get(), + sender_x25519.get(), + static_cast(envelop.envelope.timestamp.count())); + }); +} + + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_encryptFor1o1(JNIEnv *env, + jobject thiz, + jbyteArray plaintext, + jbyteArray my_ed25519_priv_key, + jlong timestamp_ms, + jbyteArray recipient_pub_key, + jbyteArray pro_signature) { + return run_catching_cxx_exception_or_throws(env, [=] { + return util::bytes_from_vector( + env, + session::encrypt_for_1o1( + JavaByteArrayRef(env, plaintext).get(), + JavaByteArrayRef(env, my_ed25519_priv_key).get(), + std::chrono::milliseconds { timestamp_ms }, + *java_to_cpp_array<33>(env, recipient_pub_key), + java_to_cpp_array<64>(env, pro_signature) + )); + }); +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_encryptForCommunityInbox( + JNIEnv *env, jobject thiz, jbyteArray plaintext, jbyteArray my_ed25519_priv_key, + jlong timestamp_ms, jbyteArray recipient_pub_key, jbyteArray community_server_pub_key, + jbyteArray pro_signature) { + return run_catching_cxx_exception_or_throws(env, [=] { + return util::bytes_from_vector( + env, + session::encrypt_for_community_inbox( + JavaByteArrayRef(env, plaintext).get(), + JavaByteArrayRef(env, my_ed25519_priv_key).get(), + std::chrono::milliseconds { timestamp_ms }, + *java_to_cpp_array<33>(env, recipient_pub_key), + *java_to_cpp_array<32>(env, community_server_pub_key), + java_to_cpp_array<64>(env, pro_signature) + )); + }); +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_encryptForGroup(JNIEnv *env, + jobject thiz, + jbyteArray plaintext, + jbyteArray my_ed25519_priv_key, + jlong timestamp_ms, + jbyteArray group_ed25519_public_key, + jbyteArray group_ed25519_private_key, + jbyteArray pro_signature) { + return run_catching_cxx_exception_or_throws(env, [=] { + session::cleared_uc32 group_private_key; + + auto array = *java_to_cpp_array<32>(env, group_ed25519_private_key); + std::copy(array.begin(), array.end(), group_private_key.begin()); + + return util::bytes_from_vector( + env, + session::encrypt_for_group( + JavaByteArrayRef(env, plaintext).get(), + JavaByteArrayRef(env, my_ed25519_priv_key).get(), + std::chrono::milliseconds { timestamp_ms }, + *java_to_cpp_array<33>(env, group_ed25519_public_key), + group_private_key, + java_to_cpp_array<64>(env, pro_signature) + )); + }); +} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt index d177bf5..053ae71 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -221,6 +221,7 @@ sealed class ConfigSig(pointer: Long) : Config(pointer) interface ReadableGroupKeysConfig { fun groupKeys(): List + fun groupEncKey(): ByteArray fun needsDump(): Boolean fun dump(): ByteArray fun needsRekey(): Boolean diff --git a/library/src/main/java/network/loki/messenger/libsession_util/GroupKeysConfig.kt b/library/src/main/java/network/loki/messenger/libsession_util/GroupKeysConfig.kt index 4f7fff9..2970257 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/GroupKeysConfig.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/GroupKeysConfig.kt @@ -39,6 +39,7 @@ class GroupKeysConfig private constructor( override fun namespace() = Namespace.GROUP_KEYS() external override fun groupKeys(): List + external override fun groupEncKey(): ByteArray external override fun needsDump(): Boolean external override fun dump(): ByteArray external fun loadKey(message: ByteArray, diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptEnvelopeKey.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptEnvelopeKey.kt new file mode 100644 index 0000000..58de8fd --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptEnvelopeKey.kt @@ -0,0 +1,22 @@ +package network.loki.messenger.libsession_util.protocol + + +sealed interface DecryptEnvelopeKey { + + class Group( + val groupEd25519PubKey: ByteArray, + val groupKeys: Array + ) : DecryptEnvelopeKey { + constructor( + groupEd25519PubKey: ByteArray, + groupKeys: Collection + ) : this( + groupEd25519PubKey, + groupKeys.toTypedArray() + ) + } + + class Regular( + val ed25519PrivKey: ByteArray + ) : DecryptEnvelopeKey +} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptedEnvelope.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptedEnvelope.kt new file mode 100644 index 0000000..637af9a --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/DecryptedEnvelope.kt @@ -0,0 +1,26 @@ +package network.loki.messenger.libsession_util.protocol + +import network.loki.messenger.libsession_util.util.Bytes +import java.time.Instant + +data class DecryptedEnvelope( + val proStatus: ProStatus, + val contentPlainText: Bytes, + val senderEd25519PubKey: Bytes, + val senderX25519PubKey: Bytes, + val timestamp: Instant +) { + constructor( + proStatus: ProStatus, + contentPlainText: ByteArray, + senderEd25519PubKey: ByteArray, + senderX25519PubKey: ByteArray, + timestampEpochMills: Long + ): this( + proStatus = proStatus, + contentPlainText = Bytes(contentPlainText), + senderEd25519PubKey = Bytes(senderEd25519PubKey), + senderX25519PubKey = Bytes(senderX25519PubKey), + timestamp = Instant.ofEpochMilli(timestampEpochMills) + ) +} diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProFeatures.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProFeatures.kt new file mode 100644 index 0000000..843be42 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProFeatures.kt @@ -0,0 +1,24 @@ +package network.loki.messenger.libsession_util.protocol + + +enum class ProFeature(internal val bitIndex: Int) { + HIGHER_CHARACTER_LIMIT(0), + PRO_BADGE(1), + ANIMATED_AVATAR(2), +} + +internal fun Long.toFeatures(): Set { + return buildSet(ProFeature.entries.size) { + for (entry in ProFeature.entries) { + if (this@toFeatures and (1L shl entry.bitIndex) != 0L) { + add(entry) + } + } + } +} + +internal fun Collection.toLong(): Long { + return fold(0L) { acc, entry -> + acc or (1L shl entry.bitIndex) + } +} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProStatus.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProStatus.kt new file mode 100644 index 0000000..bf53bb8 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/ProStatus.kt @@ -0,0 +1,22 @@ +package network.loki.messenger.libsession_util.protocol + +import java.time.Instant + + +sealed interface ProStatus { + data object None : ProStatus + data object Invalid : ProStatus + + data class Valid( + val expiresAt: Instant, + val proFeatures: Set + ) { + constructor( + expiresAtEpochSeconds: Long, + proFeatures: Long + ): this( + expiresAt = Instant.ofEpochSecond(expiresAtEpochSeconds), + proFeatures = proFeatures.toFeatures() + ) + } +} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt new file mode 100644 index 0000000..8de9a48 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt @@ -0,0 +1,38 @@ +package network.loki.messenger.libsession_util.protocol + +import network.loki.messenger.libsession_util.LibSessionUtilCApi + +object SessionProtocol : LibSessionUtilCApi() { + external fun encryptFor1o1( + plaintext: ByteArray, + myEd25519PrivKey: ByteArray, + timestampMs: Long, + recipientPubKey: ByteArray, // 33 bytes prefixed key + proSignature: ByteArray?, // 64 bytes + ): ByteArray + + external fun encryptForCommunityInbox( + plaintext: ByteArray, + myEd25519PrivKey: ByteArray, + timestampMs: Long, + recipientPubKey: ByteArray, // 33 bytes prefixed key + communityServerPubKey: ByteArray, // 32 bytes key + proSignature: ByteArray?, // 64 bytes + ): ByteArray + + external fun encryptForGroup( + plaintext: ByteArray, + myEd25519PrivKey: ByteArray, + timestampMs: Long, + groupEd25519PublicKey: ByteArray, // 33 bytes 03 prefixed key + groupEd25519PrivateKey: ByteArray, // 32 bytes group "encryption" key + proSignature: ByteArray?, // 64 bytes + ): ByteArray + + external fun decryptEnvelope( + key: DecryptEnvelopeKey, + payload: ByteArray, + nowEpochSeconds: Long, + proBackendPubKey: ByteArray, // 32 bytes backend key + ): DecryptedEnvelope +} \ No newline at end of file diff --git a/libsession-util b/libsession-util index 3ee705f..e0ec4c1 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 3ee705f3248337c71c159a88f6baafe85c02f1c3 +Subproject commit e0ec4c198ba003acf95f96e29e2e855b8ae3fad5