426 lines
16 KiB
C
426 lines
16 KiB
C
/*
|
|
* agent_login.c — agent-driven Nostr login for sovereign_browser
|
|
*
|
|
* Calls nostr_core_lib directly to authenticate, bypassing the GTK
|
|
* login dialog. The agent provides credentials as JSON params and
|
|
* gets back the pubkey/npub on success.
|
|
*/
|
|
|
|
#include "agent_login.h"
|
|
#include "key_store.h"
|
|
#include "nostr_bridge.h"
|
|
#include "nostr_core/nostr_core.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <signal.h>
|
|
|
|
/* ── Global login state (shared with main.c) ──────────────────────── *
|
|
* main.c owns the app_state_t (signer, pubkey, method, readonly).
|
|
* We access it via extern functions that main.c provides.
|
|
*/
|
|
extern void app_set_signer(nostr_signer_t *signer, const char *pubkey_hex,
|
|
key_store_method_t method, gboolean readonly);
|
|
extern void app_clear_signer(void);
|
|
extern nostr_signer_t *app_get_signer(void);
|
|
extern const char *app_get_pubkey_hex(void);
|
|
extern key_store_method_t app_get_method(void);
|
|
extern gboolean app_get_readonly(void);
|
|
|
|
/* ── Helper functions (same logic as login_dialog.c) ──────────────── */
|
|
|
|
static int derive_pubkey_hex(const unsigned char privkey[32], char pubkey_hex[65]) {
|
|
unsigned char pubkey[32];
|
|
if (nostr_ec_public_key_from_private_key(privkey, pubkey) != 0) return -1;
|
|
for (int i = 0; i < 32; i++) {
|
|
snprintf(pubkey_hex + i * 2, 3, "%02x", pubkey[i]);
|
|
}
|
|
pubkey_hex[64] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static int hex_to_bytes32(const char *hex, unsigned char out[32]) {
|
|
if (strlen(hex) != 64) return -1;
|
|
for (int i = 0; i < 32; i++) {
|
|
unsigned int byte;
|
|
if (sscanf(hex + i * 2, "%02x", &byte) != 1) return -1;
|
|
out[i] = (unsigned char)byte;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void pubkey_hex_to_npub(const char pubkey_hex[65], char npub[128]) {
|
|
unsigned char pubkey[32];
|
|
for (int i = 0; i < 32; i++) {
|
|
unsigned int b;
|
|
sscanf(pubkey_hex + i * 2, "%02x", &b);
|
|
pubkey[i] = (unsigned char)b;
|
|
}
|
|
if (nostr_key_to_bech32(pubkey, "npub", npub) != 0) {
|
|
npub[0] = '\0';
|
|
}
|
|
}
|
|
|
|
/* ── JSON response helpers ────────────────────────────────────────── */
|
|
|
|
static cJSON *make_success(cJSON *data) {
|
|
cJSON *resp = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(resp, "success", TRUE);
|
|
if (data) {
|
|
cJSON_AddItemToObject(resp, "data", data);
|
|
}
|
|
return resp;
|
|
}
|
|
|
|
static cJSON *make_error(const char *code, const char *message) {
|
|
cJSON *resp = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(resp, "success", FALSE);
|
|
cJSON *err = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(err, "code", code);
|
|
cJSON_AddStringToObject(err, "message", message);
|
|
cJSON_AddItemToObject(resp, "error", err);
|
|
return resp;
|
|
}
|
|
|
|
static cJSON *make_login_data(const char *method_str, const char *pubkey_hex,
|
|
gboolean readonly) {
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(data, "method", method_str);
|
|
cJSON_AddStringToObject(data, "pubkey", pubkey_hex);
|
|
|
|
char npub[128];
|
|
pubkey_hex_to_npub(pubkey_hex, npub);
|
|
cJSON_AddStringToObject(data, "npub", npub);
|
|
|
|
cJSON_AddBoolToObject(data, "readonly", readonly);
|
|
return data;
|
|
}
|
|
|
|
/* ── Login method implementations ─────────────────────────────────── */
|
|
|
|
static cJSON *login_local(cJSON *params) {
|
|
const char *nsec = cJSON_GetStringValue(cJSON_GetObjectItem(params, "nsec"));
|
|
const char *privkey_hex = cJSON_GetStringValue(cJSON_GetObjectItem(params, "privkey_hex"));
|
|
|
|
unsigned char privkey[32];
|
|
|
|
if (nsec && nsec[0]) {
|
|
if (strncmp(nsec, "nsec1", 5) == 0) {
|
|
if (nostr_decode_nsec(nsec, privkey) != 0) {
|
|
return make_error("INVALID_NSEC", "Failed to decode nsec string");
|
|
}
|
|
} else if (strlen(nsec) == 64) {
|
|
if (hex_to_bytes32(nsec, privkey) != 0) {
|
|
return make_error("INVALID_HEX", "Invalid hex private key");
|
|
}
|
|
} else {
|
|
return make_error("INVALID_INPUT", "Enter an nsec1... or 64-char hex key");
|
|
}
|
|
} else if (privkey_hex && privkey_hex[0]) {
|
|
if (hex_to_bytes32(privkey_hex, privkey) != 0) {
|
|
return make_error("INVALID_HEX", "Invalid hex private key");
|
|
}
|
|
} else {
|
|
return make_error("MISSING_PARAM", "Provide 'nsec' or 'privkey_hex'");
|
|
}
|
|
|
|
char pubkey_hex[65];
|
|
if (derive_pubkey_hex(privkey, pubkey_hex) != 0) {
|
|
return make_error("DERIVE_FAILED", "Failed to derive public key");
|
|
}
|
|
|
|
nostr_signer_t *signer = nostr_signer_local(privkey);
|
|
if (signer == NULL) {
|
|
return make_error("SIGNER_FAILED", "Failed to create signer");
|
|
}
|
|
|
|
app_set_signer(signer, pubkey_hex, KEY_STORE_METHOD_LOCAL, FALSE);
|
|
nostr_bridge_set_signer(signer, pubkey_hex, FALSE);
|
|
|
|
g_print("[agent-login] Local key: pubkey=%s\n", pubkey_hex);
|
|
return make_success(make_login_data("local", pubkey_hex, FALSE));
|
|
}
|
|
|
|
static cJSON *login_seed(cJSON *params) {
|
|
const char *mnemonic = cJSON_GetStringValue(cJSON_GetObjectItem(params, "mnemonic"));
|
|
cJSON *account_json = cJSON_GetObjectItem(params, "account");
|
|
int account = (account_json && cJSON_IsNumber(account_json))
|
|
? account_json->valueint : 0;
|
|
|
|
if (!mnemonic || !mnemonic[0]) {
|
|
return make_error("MISSING_PARAM", "Provide 'mnemonic' (12 or 24 BIP-39 words)");
|
|
}
|
|
|
|
unsigned char privkey[32], pubkey[32];
|
|
if (nostr_derive_keys_from_mnemonic(mnemonic, account, privkey, pubkey) != 0) {
|
|
return make_error("INVALID_MNEMONIC", "Invalid seed phrase. Check the words and try again.");
|
|
}
|
|
|
|
char pubkey_hex[65];
|
|
for (int i = 0; i < 32; i++) {
|
|
snprintf(pubkey_hex + i * 2, 3, "%02x", pubkey[i]);
|
|
}
|
|
pubkey_hex[64] = '\0';
|
|
|
|
nostr_signer_t *signer = nostr_signer_local(privkey);
|
|
if (signer == NULL) {
|
|
return make_error("SIGNER_FAILED", "Failed to create signer");
|
|
}
|
|
|
|
app_set_signer(signer, pubkey_hex, KEY_STORE_METHOD_SEED, FALSE);
|
|
nostr_bridge_set_signer(signer, pubkey_hex, FALSE);
|
|
|
|
g_print("[agent-login] Seed phrase: pubkey=%s\n", pubkey_hex);
|
|
return make_success(make_login_data("seed", pubkey_hex, FALSE));
|
|
}
|
|
|
|
static cJSON *login_readonly(cJSON *params) {
|
|
const char *npub = cJSON_GetStringValue(cJSON_GetObjectItem(params, "npub"));
|
|
const char *pubkey_hex_in = cJSON_GetStringValue(cJSON_GetObjectItem(params, "pubkey_hex"));
|
|
|
|
unsigned char pubkey[32];
|
|
char pubkey_hex[65];
|
|
|
|
if (npub && npub[0]) {
|
|
if (strncmp(npub, "npub1", 5) == 0) {
|
|
if (nostr_decode_npub(npub, pubkey) != 0) {
|
|
return make_error("INVALID_NPUB", "Failed to decode npub string");
|
|
}
|
|
for (int i = 0; i < 32; i++) {
|
|
snprintf(pubkey_hex + i * 2, 3, "%02x", pubkey[i]);
|
|
}
|
|
pubkey_hex[64] = '\0';
|
|
} else if (strlen(npub) == 64) {
|
|
if (hex_to_bytes32(npub, pubkey) != 0) {
|
|
return make_error("INVALID_HEX", "Invalid hex pubkey");
|
|
}
|
|
memcpy(pubkey_hex, npub, 64);
|
|
pubkey_hex[64] = '\0';
|
|
} else {
|
|
return make_error("INVALID_INPUT", "Enter an npub1... or 64-char hex pubkey");
|
|
}
|
|
} else if (pubkey_hex_in && pubkey_hex_in[0]) {
|
|
if (hex_to_bytes32(pubkey_hex_in, pubkey) != 0) {
|
|
return make_error("INVALID_HEX", "Invalid hex pubkey");
|
|
}
|
|
memcpy(pubkey_hex, pubkey_hex_in, 64);
|
|
pubkey_hex[64] = '\0';
|
|
} else {
|
|
return make_error("MISSING_PARAM", "Provide 'npub' or 'pubkey_hex'");
|
|
}
|
|
|
|
/* Read-only: no signer created. */
|
|
app_set_signer(NULL, pubkey_hex, KEY_STORE_METHOD_READONLY, TRUE);
|
|
nostr_bridge_set_signer(NULL, pubkey_hex, TRUE);
|
|
|
|
g_print("[agent-login] Read-only: pubkey=%s\n", pubkey_hex);
|
|
return make_success(make_login_data("readonly", pubkey_hex, TRUE));
|
|
}
|
|
|
|
static cJSON *login_nip46(cJSON *params) {
|
|
const char *bunker_url = cJSON_GetStringValue(cJSON_GetObjectItem(params, "bunker_url"));
|
|
|
|
if (!bunker_url || !bunker_url[0]) {
|
|
return make_error("MISSING_PARAM", "Provide 'bunker_url' (bunker://<pubkey>?relay=...&secret=...)");
|
|
}
|
|
|
|
nostr_nip46_bunker_url_t bunker;
|
|
memset(&bunker, 0, sizeof(bunker));
|
|
if (nostr_nip46_parse_bunker_url(bunker_url, &bunker) != 0) {
|
|
return make_error("INVALID_BUNKER", "Invalid bunker:// URL. Format: bunker://<pubkey>?relay=wss://...&secret=...");
|
|
}
|
|
|
|
/* Generate a client keypair for NIP-46 encryption. */
|
|
unsigned char client_privkey[32], client_pubkey[32];
|
|
if (nostr_generate_keypair(client_privkey, client_pubkey) != 0) {
|
|
return make_error("KEYGEN_FAILED", "Failed to generate client keypair");
|
|
}
|
|
|
|
char pubkey_hex[65];
|
|
strncpy(pubkey_hex, bunker.remote_signer_pubkey, 64);
|
|
pubkey_hex[64] = '\0';
|
|
|
|
nostr_signer_t *signer = nostr_signer_local(client_privkey);
|
|
if (signer == NULL) {
|
|
return make_error("SIGNER_FAILED", "Failed to create client signer");
|
|
}
|
|
|
|
app_set_signer(signer, pubkey_hex, KEY_STORE_METHOD_NIP46, FALSE);
|
|
nostr_bridge_set_signer(signer, pubkey_hex, FALSE);
|
|
|
|
g_print("[agent-login] NIP-46: remote signer pubkey=%s\n", pubkey_hex);
|
|
return make_success(make_login_data("nip46", pubkey_hex, FALSE));
|
|
}
|
|
|
|
static cJSON *login_nsigner(cJSON *params) {
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
const char *transport = cJSON_GetStringValue(cJSON_GetObjectItem(params, "transport"));
|
|
const char *device = cJSON_GetStringValue(cJSON_GetObjectItem(params, "device"));
|
|
const char *service = cJSON_GetStringValue(cJSON_GetObjectItem(params, "service"));
|
|
cJSON *index_json = cJSON_GetObjectItem(params, "index");
|
|
int index = (index_json && cJSON_IsNumber(index_json)) ? index_json->valueint : 0;
|
|
|
|
if (!transport || !transport[0]) {
|
|
return make_error("MISSING_PARAM", "Provide 'transport' (serial/unix/tcp/qrexec)");
|
|
}
|
|
if (!device || !device[0]) {
|
|
return make_error("MISSING_PARAM", "Provide 'device' (path, socket name, host:port, or qube name)");
|
|
}
|
|
|
|
/* Block SIGCHLD during n_signer calls (qrexec spawns children). */
|
|
sigset_t block_set, old_set;
|
|
sigemptyset(&block_set);
|
|
sigaddset(&block_set, SIGCHLD);
|
|
sigprocmask(SIG_BLOCK, &block_set, &old_set);
|
|
|
|
nostr_signer_t *signer = NULL;
|
|
const char *transport_name = "unknown";
|
|
|
|
if (strcmp(transport, "serial") == 0) {
|
|
signer = nostr_signer_nsigner_serial(device, NULL, 15000);
|
|
transport_name = "serial";
|
|
} else if (strcmp(transport, "unix") == 0) {
|
|
signer = nostr_signer_nsigner_unix(device, NULL, 15000);
|
|
transport_name = "unix";
|
|
} else if (strcmp(transport, "tcp") == 0) {
|
|
char host[256] = {0};
|
|
int port = 7777;
|
|
const char *sep = strchr(device, ':');
|
|
if (sep) {
|
|
size_t hlen = sep - device;
|
|
if (hlen < sizeof(host)) {
|
|
memcpy(host, device, hlen);
|
|
host[hlen] = '\0';
|
|
port = atoi(sep + 1);
|
|
}
|
|
} else {
|
|
strncpy(host, device, sizeof(host) - 1);
|
|
}
|
|
signer = nostr_signer_nsigner_tcp(host, port, NULL, 15000);
|
|
transport_name = "tcp";
|
|
} else if (strcmp(transport, "qrexec") == 0) {
|
|
const char *svc = (service && service[0]) ? service : "qubes.NsignerRpc";
|
|
signer = nostr_signer_nsigner_qrexec(device, svc, NULL, 30000);
|
|
transport_name = "qrexec";
|
|
} else {
|
|
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
|
return make_error("INVALID_TRANSPORT", "Unknown transport. Use: serial, unix, tcp, or qrexec");
|
|
}
|
|
|
|
if (signer == NULL) {
|
|
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
|
return make_error("NSIGNER_CONNECT", "Failed to connect to n_signer. Check the device/path/qube.");
|
|
}
|
|
|
|
int rc = nostr_signer_nsigner_set_nostr_index(signer, index);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
nostr_signer_free(signer);
|
|
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
|
return make_error("NSIGNER_INDEX", "Failed to set nostr_index on n_signer.");
|
|
}
|
|
|
|
char pubkey_hex[65];
|
|
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
nostr_signer_free(signer);
|
|
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
|
return make_error("NSIGNER_PUBKEY", "Failed to get pubkey from n_signer.");
|
|
}
|
|
|
|
sigprocmask(SIG_SETMASK, &old_set, NULL);
|
|
|
|
app_set_signer(signer, pubkey_hex, KEY_STORE_METHOD_NSIGNER, FALSE);
|
|
nostr_bridge_set_signer(signer, pubkey_hex, FALSE);
|
|
|
|
g_print("[agent-login] n_signer: transport=%s index=%d pubkey=%s\n",
|
|
transport_name, index, pubkey_hex);
|
|
return make_success(make_login_data("nsigner", pubkey_hex, FALSE));
|
|
#else
|
|
(void)params;
|
|
return make_error("NOT_COMPILED", "n_signer client not compiled in (NOSTR_ENABLE_NSIGNER_CLIENT)");
|
|
#endif
|
|
}
|
|
|
|
/* ── Public API ───────────────────────────────────────────────────── */
|
|
|
|
cJSON *agent_login_status(void) {
|
|
gboolean logged_in = agent_is_logged_in();
|
|
cJSON *data = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(data, "logged_in", logged_in);
|
|
|
|
if (logged_in) {
|
|
const char *method_str = "none";
|
|
switch (app_get_method()) {
|
|
case KEY_STORE_METHOD_LOCAL: method_str = "local"; break;
|
|
case KEY_STORE_METHOD_SEED: method_str = "seed"; break;
|
|
case KEY_STORE_METHOD_READONLY: method_str = "readonly"; break;
|
|
case KEY_STORE_METHOD_NIP46: method_str = "nip46"; break;
|
|
case KEY_STORE_METHOD_NSIGNER: method_str = "nsigner"; break;
|
|
default: break;
|
|
}
|
|
cJSON_AddStringToObject(data, "method", method_str);
|
|
cJSON_AddStringToObject(data, "pubkey", app_get_pubkey_hex());
|
|
cJSON_AddBoolToObject(data, "readonly", app_get_readonly());
|
|
}
|
|
|
|
return make_success(data);
|
|
}
|
|
|
|
cJSON *agent_login(cJSON *params) {
|
|
if (params == NULL) {
|
|
return make_error("MISSING_PARAMS", "No parameters provided");
|
|
}
|
|
|
|
if (agent_is_logged_in()) {
|
|
return make_error("ALREADY_LOGGED_IN", "Already logged in. Use switch_identity to change.");
|
|
}
|
|
|
|
const char *method = cJSON_GetStringValue(cJSON_GetObjectItem(params, "method"));
|
|
if (!method || !method[0]) {
|
|
return make_error("MISSING_METHOD", "Provide 'method' (local/seed/readonly/nip46/nsigner)");
|
|
}
|
|
|
|
/* Initialize nostr_core_lib if not already done. */
|
|
static gboolean nostr_initialized = FALSE;
|
|
if (!nostr_initialized) {
|
|
if (nostr_init() != NOSTR_SUCCESS) {
|
|
return make_error("INIT_FAILED", "Failed to initialize nostr_core_lib");
|
|
}
|
|
nostr_initialized = TRUE;
|
|
}
|
|
|
|
if (strcmp(method, "local") == 0) {
|
|
return login_local(params);
|
|
} else if (strcmp(method, "seed") == 0) {
|
|
return login_seed(params);
|
|
} else if (strcmp(method, "readonly") == 0) {
|
|
return login_readonly(params);
|
|
} else if (strcmp(method, "nip46") == 0) {
|
|
return login_nip46(params);
|
|
} else if (strcmp(method, "nsigner") == 0) {
|
|
return login_nsigner(params);
|
|
} else {
|
|
return make_error("UNKNOWN_METHOD", "Unknown method. Use: local, seed, readonly, nip46, or nsigner");
|
|
}
|
|
}
|
|
|
|
cJSON *agent_logout(void) {
|
|
app_clear_signer();
|
|
nostr_bridge_set_signer(NULL, "", TRUE);
|
|
key_store_clear();
|
|
g_print("[agent-login] Logged out\n");
|
|
return make_success(NULL);
|
|
}
|
|
|
|
cJSON *agent_switch_identity(cJSON *params) {
|
|
/* Free the old signer first. */
|
|
app_clear_signer();
|
|
/* Then login with the new identity. */
|
|
return agent_login(params);
|
|
}
|
|
|
|
gboolean agent_is_logged_in(void) {
|
|
return (app_get_signer() != NULL) ||
|
|
(app_get_method() == KEY_STORE_METHOD_READONLY && app_get_pubkey_hex()[0] != '\0');
|
|
}
|