Skip to content

Commit 8126e1d

Browse files
committed
Add Nostr profile management and encrypted NWC secret storage
1 parent 12b549e commit 8126e1d

6 files changed

Lines changed: 402 additions & 52 deletions

File tree

src/main/db.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,14 @@ function init() {
7373
);
7474
7575
CREATE TABLE IF NOT EXISTS nwc_connections (
76-
id INTEGER PRIMARY KEY AUTOINCREMENT,
77-
name TEXT NOT NULL,
78-
relay_url TEXT NOT NULL,
79-
wallet_pubkey TEXT NOT NULL,
80-
secret TEXT NOT NULL,
81-
active INTEGER NOT NULL DEFAULT 1,
82-
created_at INTEGER NOT NULL
76+
id INTEGER PRIMARY KEY AUTOINCREMENT,
77+
name TEXT NOT NULL,
78+
relay_url TEXT NOT NULL,
79+
wallet_pubkey TEXT NOT NULL,
80+
secret TEXT NOT NULL DEFAULT '',
81+
encrypted_secret TEXT,
82+
active INTEGER NOT NULL DEFAULT 1,
83+
created_at INTEGER NOT NULL
8384
);
8485
8586
CREATE TABLE IF NOT EXISTS privacy_settings (
@@ -93,8 +94,20 @@ function init() {
9394
);
9495
INSERT OR IGNORE INTO privacy_settings (id) VALUES (1);
9596
`)
97+
migrateNwcConnectionsSchema()
9698
}
99+
function migrateNwcConnectionsSchema() {
100+
const columns = db
101+
.prepare('PRAGMA table_info(nwc_connections)')
102+
.all()
103+
.map(c => c.name)
97104

105+
if (!columns.includes('encrypted_secret')) {
106+
db.prepare(
107+
'ALTER TABLE nwc_connections ADD COLUMN encrypted_secret TEXT'
108+
).run()
109+
}
110+
}
98111
const now = () => Math.floor(Date.now() / 1000)
99112

100113
// ── Settings ──────────────────────────────────────────────────────────────────

src/main/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ ipcMain.handle('nostr-create-profile', (_, args) => nostr.createProfile(DB, args
357357
ipcMain.handle('nostr-import-nsec', (_, args) => nostr.importNsec(DB, args))
358358
ipcMain.handle('nostr-skip', () => DB.setSetting('nostr_skipped', '1'))
359359
ipcMain.handle('nostr-get-profile', () => nostr.getProfile(DB))
360+
ipcMain.handle('nostr-remove-profile', () => nostr.removeProfile(DB))
360361
ipcMain.handle('nostr-get-relays', () => nostr.getRelays())
361362
ipcMain.handle('nostr-sign-event', (_, { event }) => nostr.signEvent(DB, event))
362363
ipcMain.handle('nostr-get-pubkey', () => nostr.getPubkey(DB))

src/main/nostr.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,22 @@ function getRelays() {
122122
'wss://nostr.wine': { read: true, write: false },
123123
}
124124
}
125+
function removeProfile(DB) {
126+
DB._db()
127+
.prepare('DELETE FROM nostr_profile WHERE id=1')
128+
.run()
129+
130+
DB.setSetting('nostr_skipped', '0')
131+
132+
return { ok: true }
133+
}
125134

126-
module.exports = { createProfile, importNsec, getProfile, getPubkey, signEvent, getRelays }
135+
module.exports = {
136+
createProfile,
137+
importNsec,
138+
getProfile,
139+
getPubkey,
140+
signEvent,
141+
getRelays,
142+
removeProfile,
143+
}

src/main/nwc.js

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
const WebSocket = require('ws')
44
const { getPublicKey, getSignature, getEventHash, nip04 } = require('nostr-tools')
55
const { hexToBytes } = require('@noble/hashes/utils')
6-
6+
const keychain = require('./keychain')
77
let activeWs = null
88
let activeConn = null
99
let pendingCalls = new Map()
10+
async function decryptStoredSecret(row) {
11+
if (row.encrypted_secret) {
12+
const key = await keychain.getOrCreateKey()
13+
return keychain.decrypt(row.encrypted_secret, key)
14+
}
1015

16+
// Backward compatibility with older databases
17+
return row.secret
18+
}
1119
function parseNwcUri(uri) {
1220
const normalised = uri.trim()
1321
.replace('nostr+walletconnect://', 'nwc://')
@@ -91,30 +99,97 @@ async function nwcRequest(method, params = {}) {
9199
})
92100
}
93101

102+
94103
async function connect(DB, { nwcUri, name }) {
95104
const parsed = parseNwcUri(nwcUri)
105+
96106
await openWs(parsed.relay, parsed.pubkey, parsed.secret)
107+
108+
const key = await keychain.getOrCreateKey()
109+
const encryptedSecret = keychain.encrypt(parsed.secret, key)
110+
97111
const db = DB._db()
112+
98113
db.prepare('UPDATE nwc_connections SET active=0').run()
114+
99115
const r = db
100-
.prepare('INSERT INTO nwc_connections(name,relay_url,wallet_pubkey,secret,active,created_at) VALUES(?,?,?,?,1,?)')
101-
.run(name || 'My node', parsed.relay, parsed.pubkey, parsed.secret, Math.floor(Date.now() / 1000))
102-
activeConn = { relay_url: parsed.relay, wallet_pubkey: parsed.pubkey, secret: parsed.secret }
103-
return { id: r.lastInsertRowid, name, relay: parsed.relay, pubkey: parsed.pubkey, active: true }
116+
.prepare(`
117+
INSERT INTO nwc_connections
118+
(name, relay_url, wallet_pubkey, secret, encrypted_secret, active, created_at)
119+
VALUES (?, ?, ?, ?, ?, 1, ?)
120+
`)
121+
.run(
122+
name || 'My node',
123+
parsed.relay,
124+
parsed.pubkey,
125+
'', // legacy field intentionally blank
126+
encryptedSecret,
127+
Math.floor(Date.now() / 1000)
128+
)
129+
130+
activeConn = {
131+
relay_url: parsed.relay,
132+
wallet_pubkey: parsed.pubkey,
133+
secret: parsed.secret,
134+
}
135+
136+
return {
137+
id: r.lastInsertRowid,
138+
name,
139+
relay: parsed.relay,
140+
pubkey: parsed.pubkey,
141+
active: true,
142+
}
104143
}
105144

106145
async function reconnectFromDB(DB) {
107-
const row = DB._db().prepare('SELECT * FROM nwc_connections WHERE active=1').get()
146+
const row = DB._db()
147+
.prepare('SELECT * FROM nwc_connections WHERE active=1')
148+
.get()
149+
108150
if (!row) return false
151+
109152
try {
110-
await openWs(row.relay_url, row.wallet_pubkey, row.secret)
111-
activeConn = row
153+
const secret = await decryptStoredSecret(row)
154+
155+
await openWs(
156+
row.relay_url,
157+
row.wallet_pubkey,
158+
secret
159+
)
160+
161+
// Auto-migrate legacy plaintext entries
162+
if (!row.encrypted_secret && row.secret) {
163+
const key = await keychain.getOrCreateKey()
164+
const encryptedSecret = keychain.encrypt(row.secret, key)
165+
166+
DB._db()
167+
.prepare(`
168+
UPDATE nwc_connections
169+
SET encrypted_secret = ?, secret = ''
170+
WHERE id = ?
171+
`)
172+
.run(encryptedSecret, row.id)
173+
}
174+
175+
activeConn = {
176+
...row,
177+
secret,
178+
}
179+
112180
return true
113-
} catch (_) {
181+
} catch (err) {
182+
console.error('Failed to reconnect NWC:', err.message)
114183
return false
115184
}
116185
}
117186

187+
188+
189+
190+
191+
192+
118193
function disconnect(DB) {
119194
if (activeWs) { try { activeWs.close() } catch (_) {} activeWs = null }
120195
activeConn = null

src/preload/shell.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ contextBridge.exposeInMainWorld('zap', {
4545
nostrImportNsec: (a) => ipcRenderer.invoke('nostr-import-nsec', a),
4646
nostrSkip: () => ipcRenderer.invoke('nostr-skip'),
4747
nostrGetProfile: () => ipcRenderer.invoke('nostr-get-profile'),
48+
nostrRemoveProfile: () => ipcRenderer.invoke('nostr-remove-profile'),
4849
nostrGetRelays: () => ipcRenderer.invoke('nostr-get-relays'),
4950
nostrSignEvent: (a) => ipcRenderer.invoke('nostr-sign-event', a),
5051
nostrGetPubkey: () => ipcRenderer.invoke('nostr-get-pubkey'),

0 commit comments

Comments
 (0)