Files
sovereign_browser/src/key_store.c
T

215 lines
7.1 KiB
C

/*
* key_store.c — Nostr identity (in-memory only) for sovereign_browser
*
* Private keys are NEVER persisted to disk. They live only in RAM for
* the duration of the session and are gone when the browser quits.
*
* This module provides:
* - key_store_create_signer() — create a nostr_signer_t from an
* in-memory identity struct
* - key_store_delete_legacy_file() — defensively delete any
* identity.json left by a previous version that persisted keys
* - key_store_save_profile_identity() — save public identity info
* (method, pubkey_hex) to the per-profile identity.json
* - key_store_load_profile_identity() — load public identity info
* from the per-profile identity.json
*/
#include "key_store.h"
#include "profile.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "nostr_core/nostr_core.h"
#include "nostr_core/nip006.h"
#include "nostr_core/nip019.h"
/* ── Signer creation ──────────────────────────────────────────────── */
nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity) {
if (identity == NULL) {
return NULL;
}
switch (identity->method) {
case KEY_STORE_METHOD_LOCAL:
case KEY_STORE_METHOD_SEED: {
unsigned char privkey[32];
if (nostr_hex_to_bytes(identity->privkey_hex, privkey, 32) != 0) {
return NULL;
}
return nostr_signer_local(privkey);
}
case KEY_STORE_METHOD_READONLY:
/* No signer for read-only mode. */
return NULL;
case KEY_STORE_METHOD_NIP46:
/* NIP-46 requires a live WebSocket session — handled in login_dialog
* and the bridge, not here. For now, return NULL; the session
* will be established separately. */
return NULL;
case KEY_STORE_METHOD_NSIGNER: {
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
nostr_signer_t *signer = NULL;
if (strcmp(identity->nsigner_transport, "serial") == 0) {
signer = nostr_signer_nsigner_serial(identity->nsigner_device, NULL, 15000);
} else if (strcmp(identity->nsigner_transport, "unix") == 0) {
signer = nostr_signer_nsigner_unix(identity->nsigner_device, NULL, 15000);
} else if (strcmp(identity->nsigner_transport, "tcp") == 0) {
/* nsigner_device is "host:port" */
char host[256];
int port = 0;
const char *sep = strrchr(identity->nsigner_device, ':');
if (sep && sep != identity->nsigner_device) {
size_t hlen = (size_t)(sep - identity->nsigner_device);
if (hlen < sizeof(host)) {
memcpy(host, identity->nsigner_device, hlen);
host[hlen] = '\0';
port = atoi(sep + 1);
signer = nostr_signer_nsigner_tcp(host, port, NULL, 15000);
}
}
} else if (strcmp(identity->nsigner_transport, "qrexec") == 0) {
signer = nostr_signer_nsigner_qrexec(identity->nsigner_device, "qubes.NsignerRpc", NULL, 30000);
}
if (signer && identity->nsigner_index >= 0) {
nostr_signer_nsigner_set_nostr_index(signer, identity->nsigner_index);
}
return signer;
#else
return NULL;
#endif
}
default:
return NULL;
}
}
/* ── Legacy file cleanup ──────────────────────────────────────────── */
int key_store_delete_legacy_file(void) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
return -1;
}
char path[512];
int n = snprintf(path, sizeof(path), "%s/.sovereign_browser/identity.json",
home);
if (n < 0 || (size_t)n >= sizeof(path)) {
return -1;
}
/* Delete the file if it exists. ENOENT is not an error. */
if (unlink(path) != 0 && errno != ENOENT) {
return -1;
}
return 0;
}
/* ── Per-profile identity persistence ──────────────────────────────── *
* The per-profile identity.json stores public identity information
* (method, pubkey_hex) so the browser can display profile info without
* requiring the user to log in first. Private keys are NEVER stored.
*/
int key_store_save_profile_identity(const char *pubkey_hex,
key_store_method_t method) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "pubkey_hex", pubkey_hex);
cJSON_AddNumberToObject(root, "method", (double)method);
/* Method name for human readability. */
const char *method_name = "none";
switch (method) {
case KEY_STORE_METHOD_NONE: method_name = "none"; break;
case KEY_STORE_METHOD_LOCAL: method_name = "local"; break;
case KEY_STORE_METHOD_SEED: method_name = "seed"; break;
case KEY_STORE_METHOD_READONLY: method_name = "readonly"; break;
case KEY_STORE_METHOD_NIP46: method_name = "nip46"; break;
case KEY_STORE_METHOD_NSIGNER: method_name = "nsigner"; break;
}
cJSON_AddStringToObject(root, "method_name", method_name);
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (json == NULL) {
return -1;
}
FILE *f = fopen(path, "w");
if (f == NULL) {
free(json);
return -1;
}
fputs(json, f);
fputc('\n', f);
fclose(f);
free(json);
return 0;
}
int key_store_load_profile_identity(const char *pubkey_hex,
key_store_method_t *method_out) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
return -1;
}
int rc = -1;
cJSON *pk = cJSON_GetObjectItemCaseSensitive(root, "pubkey_hex");
if (cJSON_IsString(pk) && strcmp(pk->valuestring, pubkey_hex) == 0) {
if (method_out) {
cJSON *m = cJSON_GetObjectItemCaseSensitive(root, "method");
if (cJSON_IsNumber(m)) {
*method_out = (key_store_method_t)m->valuedouble;
}
}
rc = 0;
}
cJSON_Delete(root);
return rc;
}