WIP: Maybe integrated libsodium + JNI

This commit is contained in:
2026-08-25 13:11:29 +09:00
parent 9f0114e963
commit bbac50a8fe
12 changed files with 295 additions and 10 deletions
+3
View File
@@ -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
+13
View File
@@ -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 {
+120
View File
@@ -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
)
+83
View File
@@ -0,0 +1,83 @@
#include <jni.h>
#include <sodium.h>
#include <string.h>
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;
}
@@ -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
+8
View File
@@ -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
@@ -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) {
@@ -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);
}
}
}
@@ -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
}
+1 -1
Submodule gm-java updated: 20e462ec03...954d8760ac
+1 -1
View File
@@ -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"