2026-08-31 13:29:36 +01:00
|
|
|
//! JNI surface for `com.archipelago.app.fips.FipsNative` and
|
|
|
|
|
//! `com.archipelago.app.NativeCore` — JSON over strings, no codegen (the
|
|
|
|
|
//! myco / nostr-vpn embedding pattern). Errors come back as
|
2026-08-12 10:55:50 +00:00
|
|
|
//! `{"error": "…"}` so Kotlin never sees a raw exception from native code.
|
|
|
|
|
|
|
|
|
|
use std::sync::Once;
|
|
|
|
|
|
|
|
|
|
use jni::objects::{JClass, JString};
|
|
|
|
|
use jni::sys::{jboolean, jint, jstring};
|
|
|
|
|
use jni::JNIEnv;
|
|
|
|
|
|
|
|
|
|
use crate::mesh;
|
|
|
|
|
|
|
|
|
|
static LOG_INIT: Once = Once::new();
|
|
|
|
|
|
|
|
|
|
fn init_logging() {
|
|
|
|
|
LOG_INIT.call_once(|| {
|
|
|
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
|
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
|
let _ = tracing_subscriber::registry()
|
|
|
|
|
.with(tracing_subscriber::EnvFilter::new("info"))
|
|
|
|
|
.with(paranoid_android::layer("archy-fips"))
|
|
|
|
|
.try_init();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn jstr(env: &mut JNIEnv, s: &JString) -> String {
|
|
|
|
|
env.get_string(s).map(|s| s.into()).unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn out(env: &JNIEnv, s: String) -> jstring {
|
|
|
|
|
env.new_string(s)
|
|
|
|
|
.map(|s| s.into_raw())
|
|
|
|
|
.unwrap_or(std::ptr::null_mut())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn err_json(e: impl std::fmt::Display) -> String {
|
|
|
|
|
serde_json::json!({ "error": e.to_string() }).to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn identity_json(info: &mesh::IdentityInfo) -> String {
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"secret": info.secret_hex,
|
|
|
|
|
"npub": info.npub,
|
|
|
|
|
"address": info.address,
|
|
|
|
|
})
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun generateIdentity(): String`
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_generateIdentity(
|
|
|
|
|
env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let json = match mesh::generate_identity() {
|
|
|
|
|
Ok(info) => identity_json(&info),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun deriveIdentity(secret: String): String`
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_deriveIdentity(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
secret: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let secret = jstr(&mut env, &secret);
|
|
|
|
|
let json = match mesh::derive_identity(&secret) {
|
|
|
|
|
Ok(info) => identity_json(&info),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun start(secret: String, peersJson: String, tunFd: Int, listenPort: Int): String`
|
|
|
|
|
/// Returns `{"npub": "...", "address": "..."}` or `{"error": "..."}`.
|
|
|
|
|
/// `listenPort` 0 = outbound-only; non-zero = fixed UDP bind (party mode).
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_start(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
secret: JString,
|
|
|
|
|
peers_json: JString,
|
|
|
|
|
tun_fd: jint,
|
|
|
|
|
listen_port: jint,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let secret = jstr(&mut env, &secret);
|
|
|
|
|
let peers = jstr(&mut env, &peers_json);
|
|
|
|
|
let listen_port = u16::try_from(listen_port).unwrap_or(0);
|
|
|
|
|
let json = match mesh::start(&secret, &peers, tun_fd, listen_port) {
|
|
|
|
|
Ok((npub, address)) => {
|
|
|
|
|
serde_json::json!({ "npub": npub, "address": address }).to_string()
|
|
|
|
|
}
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun stop()`
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_stop(
|
|
|
|
|
_env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
) {
|
|
|
|
|
mesh::stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun isRunning(): Boolean`
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_isRunning(
|
|
|
|
|
_env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
) -> jboolean {
|
|
|
|
|
mesh::is_running() as jboolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun statusJson(): String`
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_fips_FipsNative_statusJson(
|
|
|
|
|
env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
out(&env, mesh::status_json())
|
|
|
|
|
}
|
2026-08-31 13:29:36 +01:00
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// com.archipelago.app.NativeCore — companion backup (#128) and NIP-46 remote
|
|
|
|
|
// signer crypto (#139). Same library, JSON-over-strings contract.
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun backupEncrypt(payload: String, passphrase: String): String`
|
|
|
|
|
/// Returns the ADR-005 envelope JSON or `{"error": …}`.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_backupEncrypt(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
payload: JString,
|
|
|
|
|
passphrase: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let payload = jstr(&mut env, &payload);
|
|
|
|
|
let passphrase = jstr(&mut env, &passphrase);
|
|
|
|
|
let json = match crate::backup::encrypt(&payload, &passphrase) {
|
|
|
|
|
Ok(envelope) => envelope,
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun backupDecrypt(envelope: String, passphrase: String): String`
|
|
|
|
|
/// Returns the decrypted payload JSON or `{"error": …}`.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_backupDecrypt(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
envelope: JString,
|
|
|
|
|
passphrase: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let envelope = jstr(&mut env, &envelope);
|
|
|
|
|
let passphrase = jstr(&mut env, &passphrase);
|
|
|
|
|
let json = match crate::backup::decrypt(&envelope, &passphrase) {
|
|
|
|
|
Ok(payload) => payload,
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun nostrGenerateSecret(): String`
|
|
|
|
|
/// Returns `{"secret": hex, "pubkey": hex, "npub": …, "nsec": …}` or `{"error": …}`.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_nostrGenerateSecret(
|
|
|
|
|
env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let json = match crate::nostr::generate_secret() {
|
|
|
|
|
Ok(secret) => nostr_key_info_json(&secret),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun nostrSecretFromAny(secret: String): String`
|
|
|
|
|
/// Accepts hex or `nsec…`; returns key-info JSON or `{"error": …}`.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_nostrSecretFromAny(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
secret: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let secret = jstr(&mut env, &secret);
|
|
|
|
|
let json = match crate::nostr::secret_from_any(&secret) {
|
|
|
|
|
Ok(hex) => nostr_key_info_json(&hex),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nostr_key_info_json(secret_hex: &str) -> String {
|
|
|
|
|
match (
|
|
|
|
|
crate::nostr::pubkey_hex(secret_hex),
|
|
|
|
|
crate::nostr::npub_from_pubkey(&crate::nostr::pubkey_hex(secret_hex).unwrap_or_default()),
|
|
|
|
|
crate::nostr::nsec_from_secret(secret_hex),
|
|
|
|
|
) {
|
|
|
|
|
(Ok(pubkey), Ok(npub), Ok(nsec)) => serde_json::json!({
|
|
|
|
|
"secret": secret_hex,
|
|
|
|
|
"pubkey": pubkey,
|
|
|
|
|
"npub": npub,
|
|
|
|
|
"nsec": nsec,
|
|
|
|
|
})
|
|
|
|
|
.to_string(),
|
|
|
|
|
(e, _, _) => err_json(e.unwrap_err()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun nostrParseConnectUri(uri: String): String`
|
|
|
|
|
/// Returns the parsed URI fields or `{"error": …}`.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_nostrParseConnectUri(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
uri: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let uri = jstr(&mut env, &uri);
|
|
|
|
|
let json = match crate::nostr::parse_connect_uri(&uri) {
|
|
|
|
|
Ok(info) => info.to_json().to_string(),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kotlin: `external fun nostrSignEvent(secretHex: String, eventJson: String): String`
|
|
|
|
|
/// Returns the signed event JSON or `{"error": …}`. The approve/deny decision
|
|
|
|
|
/// is made in Kotlin BEFORE this is called — native code never signs unasked.
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn Java_com_archipelago_app_NativeCore_nostrSignEvent(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
secret_hex: JString,
|
|
|
|
|
event_json: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let secret = jstr(&mut env, &secret_hex);
|
|
|
|
|
let event = jstr(&mut env, &event_json);
|
|
|
|
|
let json = match crate::nostr::sign_event(&secret, &event) {
|
|
|
|
|
Ok(signed) => signed,
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! nostr_cipher {
|
|
|
|
|
($name:ident, $doc:literal, $fn:path) => {
|
|
|
|
|
#[doc = $doc]
|
|
|
|
|
#[no_mangle]
|
|
|
|
|
pub extern "system" fn $name(
|
|
|
|
|
mut env: JNIEnv,
|
|
|
|
|
_class: JClass,
|
|
|
|
|
secret_hex: JString,
|
|
|
|
|
peer_pub: JString,
|
|
|
|
|
text: JString,
|
|
|
|
|
) -> jstring {
|
|
|
|
|
init_logging();
|
|
|
|
|
let secret = jstr(&mut env, &secret_hex);
|
|
|
|
|
let peer = jstr(&mut env, &peer_pub);
|
|
|
|
|
let text = jstr(&mut env, &text);
|
|
|
|
|
let json = match $fn(&secret, &peer, &text) {
|
|
|
|
|
Ok(out) => serde_json::json!({ "result": out }).to_string(),
|
|
|
|
|
Err(e) => err_json(e),
|
|
|
|
|
};
|
|
|
|
|
out(&env, json)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nostr_cipher!(
|
|
|
|
|
Java_com_archipelago_app_NativeCore_nostrNip44Encrypt,
|
|
|
|
|
"Kotlin: `external fun nostrNip44Encrypt(secretHex: String, peerPub: String, plaintext: String): String` — returns `{\"result\": payload}` or `{\"error\": …}`.",
|
|
|
|
|
crate::nostr::nip44_encrypt
|
|
|
|
|
);
|
|
|
|
|
nostr_cipher!(
|
|
|
|
|
Java_com_archipelago_app_NativeCore_nostrNip44Decrypt,
|
|
|
|
|
"Kotlin: `external fun nostrNip44Decrypt(secretHex: String, peerPub: String, payload: String): String`",
|
|
|
|
|
crate::nostr::nip44_decrypt
|
|
|
|
|
);
|
|
|
|
|
nostr_cipher!(
|
|
|
|
|
Java_com_archipelago_app_NativeCore_nostrNip04Encrypt,
|
|
|
|
|
"Kotlin: `external fun nostrNip04Encrypt(secretHex: String, peerPub: String, plaintext: String): String`",
|
|
|
|
|
crate::nostr::nip04_encrypt
|
|
|
|
|
);
|
|
|
|
|
nostr_cipher!(
|
|
|
|
|
Java_com_archipelago_app_NativeCore_nostrNip04Decrypt,
|
|
|
|
|
"Kotlin: `external fun nostrNip04Decrypt(secretHex: String, peerPub: String, payload: String): String`",
|
|
|
|
|
crate::nostr::nip04_decrypt
|
|
|
|
|
);
|