From bbac50a8fe30c8cd87cdfb0a5ee36189b2217384 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 25 Aug 2026 13:11:29 +0900 Subject: [PATCH] WIP: Maybe integrated libsodium + JNI --- .gitmodules | 3 + app/build.gradle.kts | 13 ++ app/src/main/cpp/CMakeLists.txt | 120 ++++++++++++++++++ app/src/main/cpp/gaju-jni.c | 83 ++++++++++++ app/src/main/cpp/libsodium | 1 + .../main/cpp/sodium-config/sodium/version.h | 8 ++ app/src/main/cpp/sodium-config/version.h | 8 ++ .../java/swiss/qpq/gajumobile/MainActivity.kt | 4 +- .../gajumobile/security/AccountAirlock.java | 25 +++- .../qpq/gajumobile/security/GajuNative.kt | 36 ++++++ gm-java | 2 +- gradle/libs.versions.toml | 2 +- 12 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 app/src/main/cpp/CMakeLists.txt create mode 100644 app/src/main/cpp/gaju-jni.c create mode 160000 app/src/main/cpp/libsodium create mode 100644 app/src/main/cpp/sodium-config/sodium/version.h create mode 100644 app/src/main/cpp/sodium-config/version.h create mode 100644 app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt diff --git a/.gitmodules b/.gitmodules index 36a4f21..75bcbf4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "gm-java"] path = gm-java url = ssh://gitea@git.qpq.swiss:21203/QPQ-AG/gm-java.git +[submodule "app/src/main/cpp/libsodium"] + path = app/src/main/cpp/libsodium + url = https://git.qpq.swiss/QPQ-AG/libsodium.git diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7e54a92..e11aaf7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,6 +20,19 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild { + cmake { + cppFlags("") + } + } + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } } buildTypes { diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..1ff9de2 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,120 @@ +cmake_minimum_required(VERSION 3.22.1) + +project("gaju-native") + +set(LIBSODIUM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libsodium) +set(SODIUM_CONFIG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/sodium-config) + +# Define macros for libsodium on Android +add_definitions(-DCONFIG_H -D_GNU_SOURCE -D_POSIX_SOURCE) +add_definitions(-DHAVE_MMAP -DHAVE_MPROTECT -DHAVE_MADVISE) + +# Target-specific optimizations +if(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + add_definitions(-DHAVE_ARMCRYPTO -DHAVE_TI_MODE) +elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") + add_definitions(-DHAVE_TI_MODE) +endif() + +add_library(gaju-native SHARED + gaju-jni.c + + # Core & Runtime + ${LIBSODIUM_DIR}/src/libsodium/sodium/core.c + ${LIBSODIUM_DIR}/src/libsodium/sodium/runtime.c + ${LIBSODIUM_DIR}/src/libsodium/sodium/utils.c + ${LIBSODIUM_DIR}/src/libsodium/sodium/version.c + ${LIBSODIUM_DIR}/src/libsodium/sodium/codecs.c + + # Random + ${LIBSODIUM_DIR}/src/libsodium/randombytes/randombytes.c + ${LIBSODIUM_DIR}/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c + + # Hash (SHA) + ${LIBSODIUM_DIR}/src/libsodium/crypto_hash/crypto_hash.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_hash/sha256/hash_sha256.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_hash/sha512/hash_sha512.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c + + # Generic Hash (Blake2b) + ${LIBSODIUM_DIR}/src/libsodium/crypto_generichash/crypto_generichash.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_generichash/blake2b/generichash_blake2.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c + + # Sign (Ed25519) + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/crypto_sign.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/ed25519/sign_ed25519.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/ed25519/ref10/keypair.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/ed25519/ref10/open.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/ed25519/ref10/sign.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_sign/ed25519/ref10/obsolete.c + + # Scalarmult (Curve25519) + ${LIBSODIUM_DIR}/src/libsodium/crypto_scalarmult/crypto_scalarmult.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c + + # Core Crypto + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/hchacha20/core_hchacha20.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/hsalsa20/core_hsalsa20.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_core/softaes/softaes.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_verify/verify.c + + # OneTimeAuth (Poly1305) + ${LIBSODIUM_DIR}/src/libsodium/crypto_onetimeauth/crypto_onetimeauth.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c + + # Stream (Chacha20/Salsa20) + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/crypto_stream.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/chacha20/stream_chacha20.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/salsa20/stream_salsa20.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c + + # PWHash (Argon2) + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/crypto_pwhash.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/argon2.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/argon2-core.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/argon2-encoding.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/blake2b-long.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/pwhash_argon2i.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/pwhash_argon2id.c + + # AEAD (Aegis) + ${LIBSODIUM_DIR}/src/libsodium/crypto_aead/aegis128l/aead_aegis128l.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_aead/aegis128l/aegis128l_soft.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_aead/aegis256/aead_aegis256.c + ${LIBSODIUM_DIR}/src/libsodium/crypto_aead/aegis256/aegis256_soft.c +) + +# Architecture specific optimizations +if(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + target_sources(gaju-native PRIVATE + ${LIBSODIUM_DIR}/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-neon.c + ) +endif() + +target_include_directories(gaju-native PRIVATE + ${SODIUM_CONFIG_DIR} + ${LIBSODIUM_DIR}/src/libsodium/include + ${LIBSODIUM_DIR}/src/libsodium/include/sodium + ${LIBSODIUM_DIR}/src/libsodium/include/sodium/private +) + +# Link log library for Android +find_library(log-lib log) +target_link_libraries(gaju-native ${log-lib}) + +# Target for IDE discovery (read-only reference) +add_custom_target(libsodium_all_headers SOURCES + ${LIBSODIUM_DIR}/src/libsodium/include/sodium.h +) diff --git a/app/src/main/cpp/gaju-jni.c b/app/src/main/cpp/gaju-jni.c new file mode 100644 index 0000000..d517f85 --- /dev/null +++ b/app/src/main/cpp/gaju-jni.c @@ -0,0 +1,83 @@ +#include +#include +#include + +JNIEXPORT void JNICALL +Java_swiss_qpq_gajumobile_security_GajuNative_memzero(JNIEnv *env, jclass clazz, jbyteArray array) { + if (array == NULL) return; + jsize len = (*env)->GetArrayLength(env, array); + jbyte *data = (*env)->GetByteArrayElements(env, array, NULL); + if (data != NULL) { + sodium_memzero(data, (size_t) len); + (*env)->ReleaseByteArrayElements(env, array, data, 0); + } +} + +JNIEXPORT jbyteArray JNICALL +Java_swiss_qpq_gajumobile_security_GajuNative_cryptoSignSeedKeypair(JNIEnv *env, jclass clazz, jbyteArray seed) { + if (seed == NULL || (*env)->GetArrayLength(env, seed) != crypto_sign_SEEDBYTES) return NULL; + + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + jbyte *seed_data = (*env)->GetByteArrayElements(env, seed, NULL); + + crypto_sign_seed_keypair(pk, sk, (unsigned char *) seed_data); + (*env)->ReleaseByteArrayElements(env, seed, seed_data, JNI_ABORT); + + jbyteArray result = (*env)->NewByteArray(env, crypto_sign_PUBLICKEYBYTES + crypto_sign_SECRETKEYBYTES); + (*env)->SetByteArrayRegion(env, result, 0, crypto_sign_PUBLICKEYBYTES, (jbyte *) pk); + (*env)->SetByteArrayRegion(env, result, crypto_sign_PUBLICKEYBYTES, crypto_sign_SECRETKEYBYTES, (jbyte *) sk); + + sodium_memzero(pk, sizeof pk); + sodium_memzero(sk, sizeof sk); + + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_swiss_qpq_gajumobile_security_GajuNative_cryptoSignDetached(JNIEnv *env, jclass clazz, jbyteArray message, jbyteArray secretKey) { + if (message == NULL || secretKey == NULL || (*env)->GetArrayLength(env, secretKey) != crypto_sign_SECRETKEYBYTES) return NULL; + + jsize msg_len = (*env)->GetArrayLength(env, message); + unsigned char sig[crypto_sign_BYTES]; + jbyte *msg_data = (*env)->GetByteArrayElements(env, message, NULL); + jbyte *sk_data = (*env)->GetByteArrayElements(env, secretKey, NULL); + + crypto_sign_detached(sig, NULL, (unsigned char *) msg_data, (unsigned long long) msg_len, (unsigned char *) sk_data); + + (*env)->ReleaseByteArrayElements(env, message, msg_data, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, secretKey, sk_data, JNI_ABORT); + + jbyteArray result = (*env)->NewByteArray(env, crypto_sign_BYTES); + (*env)->SetByteArrayRegion(env, result, 0, crypto_sign_BYTES, (jbyte *) sig); + + return result; +} + +JNIEXPORT jboolean JNICALL +Java_swiss_qpq_gajumobile_security_GajuNative_cryptoSignVerifyDetached(JNIEnv *env, jclass clazz, jbyteArray signature, jbyteArray message, jbyteArray publicKey) { + if (signature == NULL || message == NULL || publicKey == NULL) return JNI_FALSE; + if ((*env)->GetArrayLength(env, signature) != crypto_sign_BYTES) return JNI_FALSE; + if ((*env)->GetArrayLength(env, publicKey) != crypto_sign_PUBLICKEYBYTES) return JNI_FALSE; + + jsize msg_len = (*env)->GetArrayLength(env, message); + jbyte *sig_data = (*env)->GetByteArrayElements(env, signature, NULL); + jbyte *msg_data = (*env)->GetByteArrayElements(env, message, NULL); + jbyte *pk_data = (*env)->GetByteArrayElements(env, publicKey, NULL); + + int res = crypto_sign_verify_detached((unsigned char *) sig_data, (unsigned char *) msg_data, (unsigned long long) msg_len, (unsigned char *) pk_data); + + (*env)->ReleaseByteArrayElements(env, signature, sig_data, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, message, msg_data, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, publicKey, pk_data, JNI_ABORT); + + return res == 0 ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jint JNICALL +JNI_OnLoad(JavaVM* vm, void* reserved) { + if (sodium_init() == -1) { + return JNI_ERR; + } + return JNI_VERSION_1_6; +} diff --git a/app/src/main/cpp/libsodium b/app/src/main/cpp/libsodium new file mode 160000 index 0000000..77e1ce5 --- /dev/null +++ b/app/src/main/cpp/libsodium @@ -0,0 +1 @@ +Subproject commit 77e1ce5d6dee871c49ef211222ba18ef0c486bda diff --git a/app/src/main/cpp/sodium-config/sodium/version.h b/app/src/main/cpp/sodium-config/sodium/version.h new file mode 100644 index 0000000..371c500 --- /dev/null +++ b/app/src/main/cpp/sodium-config/sodium/version.h @@ -0,0 +1,8 @@ +#ifndef sodium_version_H +#define sodium_version_H + +#define SODIUM_VERSION_STRING "1.0.22" +#define SODIUM_LIBRARY_VERSION_MAJOR 26 +#define SODIUM_LIBRARY_VERSION_MINOR 1 + +#endif diff --git a/app/src/main/cpp/sodium-config/version.h b/app/src/main/cpp/sodium-config/version.h new file mode 100644 index 0000000..371c500 --- /dev/null +++ b/app/src/main/cpp/sodium-config/version.h @@ -0,0 +1,8 @@ +#ifndef sodium_version_H +#define sodium_version_H + +#define SODIUM_VERSION_STRING "1.0.22" +#define SODIUM_LIBRARY_VERSION_MAJOR 26 +#define SODIUM_LIBRARY_VERSION_MINOR 1 + +#endif diff --git a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt index 9fff3b5..d7c4939 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt +++ b/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt @@ -32,6 +32,7 @@ import swiss.qpq.gajumobile.security.BiometricHelper import swiss.qpq.gajumobile.security.SecurityManager import swiss.qpq.gajumobile.security.KeyManager import swiss.qpq.gajumobile.security.AccountAirlock +import swiss.qpq.gajumobile.security.GajuNative import swiss.qpq.gajumobile.ui.screens.AccountNamingScreen import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen import swiss.qpq.gajumobile.ui.screens.DashboardScreen @@ -54,7 +55,6 @@ import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme import swiss.qpq.gajumaru.core.formatting.GajuFormat import swiss.qpq.gajumaru.core.tools.Grids import swiss.qpq.gajumaru.core.tools.NodeClient -import swiss.qpq.gajumaru.core.tools.CryptoUtils import swiss.qpq.gajumaru.core.tools.TransactionService import java.math.BigInteger import java.util.UUID @@ -657,7 +657,7 @@ fun GajuRouter( ) // 4. Wipe seckey - CryptoUtils.wipe(seckey) + GajuNative.memzero(seckey) // 5. Post tx val result = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java index 105277c..06110f3 100644 --- a/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java +++ b/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java @@ -10,7 +10,6 @@ import java.security.SecureRandom; import java.util.UUID; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import swiss.qpq.gajumaru.core.crypto.Ed25519; import swiss.qpq.gajumaru.core.crypto.Vault; import swiss.qpq.gajumaru.core.encoding.Mnemonic; import swiss.qpq.gajumaru.core.tools.CryptoUtils; @@ -69,8 +68,12 @@ public final class AccountAirlock { // 1. Decode mnemonic to seed seed = Mnemonic.decode(phrase); - // 2. Generate Public Key - byte[] publicKey = Ed25519.publicKey(seed); + // 2. Generate Public Key using Libsodium + byte[] keypair = GajuNative.INSTANCE.cryptoSignSeedKeypair(seed); + byte[] publicKey = new byte[32]; + System.arraycopy(keypair, 0, publicKey, 0, 32); + // keypair contains sk too, but we only need pk here for the Account model + GajuNative.INSTANCE.memzero(keypair); // 3. Generate random Data Encryption Key (DEK) SecureRandom random = new SecureRandom(); @@ -156,11 +159,21 @@ public final class AccountAirlock { envelope.getEncryptedData() ); - return Ed25519.sign(plaintextPrivateKey, message); + // Libsodium SK is 64 bytes: seed || public_key + byte[] keypair = GajuNative.INSTANCE.cryptoSignSeedKeypair(plaintextPrivateKey); + byte[] secretKey = new byte[64]; + System.arraycopy(keypair, 32, secretKey, 0, 64); + + byte[] sig = GajuNative.INSTANCE.cryptoSignDetached(message, secretKey); + + GajuNative.INSTANCE.memzero(keypair); + GajuNative.INSTANCE.memzero(secretKey); + + return sig; } finally { if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); - if (plaintextPrivateKey != null) CryptoUtils.wipe(plaintextPrivateKey); + if (plaintextPrivateKey != null) GajuNative.INSTANCE.memzero(plaintextPrivateKey); } } @@ -220,7 +233,7 @@ public final class AccountAirlock { } finally { if (plaintextDek != null) CryptoUtils.wipe(plaintextDek); - if (plaintextPrivateKey != null) CryptoUtils.wipe(plaintextPrivateKey); + if (plaintextPrivateKey != null) GajuNative.INSTANCE.memzero(plaintextPrivateKey); } } } diff --git a/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt b/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt new file mode 100644 index 0000000..e9bf498 --- /dev/null +++ b/app/src/main/java/swiss/qpq/gajumobile/security/GajuNative.kt @@ -0,0 +1,36 @@ +package swiss.qpq.gajumobile.security + +object GajuNative { + init { + System.loadLibrary("gaju-native") + } + + /** + * Zeros out the provided byte array using native memset. + */ + external fun memzero(array: ByteArray) + + /** + * Generates an Ed25519 keypair from a 32-byte seed. + * @param seed 32-byte seed. + * @return 96-byte array containing public key (32 bytes) followed by secret key (64 bytes). + */ + external fun cryptoSignSeedKeypair(seed: ByteArray): ByteArray + + /** + * Signs a message using Ed25519. + * @param message The message to sign. + * @param secretKey 64-byte secret key. + * @return 64-byte detached signature. + */ + external fun cryptoSignDetached(message: ByteArray, secretKey: ByteArray): ByteArray + + /** + * Verifies an Ed25519 detached signature. + * @param signature 64-byte signature. + * @param message The message. + * @param publicKey 32-byte public key. + * @return true if valid, false otherwise. + */ + external fun cryptoSignVerifyDetached(signature: ByteArray, message: ByteArray, publicKey: ByteArray): Boolean +} diff --git a/gm-java b/gm-java index 20e462e..954d876 160000 --- a/gm-java +++ b/gm-java @@ -1 +1 @@ -Subproject commit 20e462ec03c8b45b36bad0c11edd57723a10d2f4 +Subproject commit 954d8760ac9b7147461f8e88ee64857d99b8dea2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index baa26f5..5cc2ed2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.3.1" +agp = "9.3.2" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5"