Files
sovereign_browser/src/agent_login.c
T

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');
}