Files
archy/Android/rust/archy-fips-core/src/jni_glue.rs
T
Dorian 57e31eb192 feat(companion): backup envelope + NIP-46 signer crypto in the native core
Extends archy-fips-core with the two companion-release features' crypto
(#128, #139), same JNI-over-JSON contract as the mesh surface:

backup.rs — the ADR-005 encrypted-backup envelope, byte-compatible with
the node's backup code (Argon2id default params + ChaCha20-Poly1305,
blob = base64(salt||nonce||ct)); decrypt ignores extra envelope fields
so node backups read here too. Round-trip, tamper, wrong-passphrase and
cross-shape tests included.

nostr.rs — the phone-side remote-signer crypto: nsec/npub bech32 keys,
BIP340 schnorr event signing (NIP-01 id serialization), NIP-44 v2
payloads (HKDF-SHA256 + ChaCha20 + HMAC-SHA256, both padding prefixes),
NIP-04 fallback, nostrconnect:// parsing with repeated relay params.
Verified against the official NIP-44 vectors (conversation keys, message
keys, padded lengths, byte-exact encrypt vectors), the BIP-340 reference
sign vectors, and round-trip/tamper/failure tests. secp256k1 0.29 note:
Keypair::public_key() is the 33-byte compressed key — x-only pubkeys
must go through .x_only_public_key().0 (one real bug the vectors caught).

JNI glue adds com.archipelago.app.NativeCore: backupEncrypt/Decrypt,
nostrGenerateSecret/SecretFromAny/ParseConnectUri/SignEvent and the
NIP-44/NIP-04 cipher pairs. Android arm64 build verified via cargo-ndk
(7.2 MB .so, +0.4 MB for both modules). Host: cargo test 23/23, clippy clean.
2026-08-31 13:29:36 +01:00

305 lines
9.8 KiB
Rust

//! 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
//! `{"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())
}
// ─────────────────────────────────────────────────────────────────────────────
// 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
);