You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
3.9 KiB
131 lines
3.9 KiB
/** |
|
* Nsec key storage (NIP-49 encrypted) |
|
* Stores encrypted nsec keys in IndexedDB |
|
*/ |
|
|
|
import { getDB } from './indexeddb-store.js'; |
|
import { encryptPrivateKey, decryptPrivateKey } from '../security/key-management.js'; |
|
|
|
export interface StoredNsecKey { |
|
id: string; // pubkey |
|
ncryptsec: string; // NIP-49 encrypted key |
|
pubkey: string; // Public key for identification |
|
created_at: number; |
|
keyType?: 'nsec' | 'anonymous'; // Distinguish between nsec and anonymous keys |
|
} |
|
|
|
/** |
|
* Store an nsec key (encrypted) |
|
* NEVER log the nsec or password - they are sensitive |
|
*/ |
|
export async function storeNsecKey( |
|
nsec: string, |
|
password: string, |
|
pubkey: string |
|
): Promise<void> { |
|
// Encrypt the private key - never store plaintext |
|
const ncryptsec = await encryptPrivateKey(nsec, password); |
|
|
|
const db = await getDB(); |
|
|
|
// Check if there's an existing key with this pubkey |
|
// If so, verify it matches before overwriting |
|
const existing = await db.get('keys', pubkey); |
|
if (existing) { |
|
const existingKey = existing as StoredNsecKey; |
|
// If the existing key is an nsec key, verify it matches |
|
if (existingKey.keyType === 'nsec' && existingKey.pubkey === pubkey) { |
|
// Key exists and matches - we'll overwrite it |
|
// This is fine, we're updating with the same pubkey |
|
} |
|
} |
|
|
|
const stored: StoredNsecKey = { |
|
id: pubkey, |
|
ncryptsec, |
|
pubkey, |
|
created_at: Date.now(), |
|
keyType: 'nsec' |
|
}; |
|
|
|
// Store encrypted key - never log the nsec or password |
|
await db.put('keys', stored); |
|
|
|
// Note: Verification is done in authenticateWithNsec after storage |
|
// to ensure the key is committed to IndexedDB and can be retrieved |
|
} |
|
|
|
/** |
|
* Retrieve and decrypt an nsec key |
|
* NEVER log the password or decrypted nsec |
|
*/ |
|
export async function getNsecKey( |
|
pubkey: string, |
|
password: string |
|
): Promise<string | null> { |
|
const db = await getDB(); |
|
const stored = await db.get('keys', pubkey); |
|
if (!stored) return null; |
|
|
|
const key = stored as StoredNsecKey; |
|
if (!key.ncryptsec || typeof key.ncryptsec !== 'string') { |
|
throw new Error('Stored nsec key has invalid ncryptsec format - key may be corrupted'); |
|
} |
|
|
|
// Validate ncryptsec format before attempting decryption |
|
if (!key.ncryptsec.startsWith('ncryptsec1')) { |
|
throw new Error('Stored nsec key has invalid encryption format - key may need to be re-saved'); |
|
} |
|
|
|
// Decrypt and return - never log the result |
|
try { |
|
return await decryptPrivateKey(key.ncryptsec, password); |
|
} catch (error) { |
|
// Provide helpful error without exposing sensitive data |
|
if (error instanceof Error && error.message.includes('Invalid password')) { |
|
throw error; // Re-throw password errors |
|
} |
|
throw new Error('Failed to decrypt stored nsec key - the key may be corrupted or the password is incorrect'); |
|
} |
|
} |
|
|
|
/** |
|
* Check if an nsec key exists for a pubkey |
|
*/ |
|
export async function hasNsecKey(pubkey: string): Promise<boolean> { |
|
const db = await getDB(); |
|
const stored = await db.get('keys', pubkey); |
|
return stored !== undefined; |
|
} |
|
|
|
/** |
|
* Delete an nsec key |
|
*/ |
|
export async function deleteNsecKey(pubkey: string): Promise<void> { |
|
const db = await getDB(); |
|
await db.delete('keys', pubkey); |
|
} |
|
|
|
/** |
|
* List all stored nsec keys (pubkeys only) |
|
*/ |
|
export async function listNsecKeys(): Promise<Array<{ pubkey: string; created_at: number; keyType?: 'nsec' | 'anonymous' }>> { |
|
const db = await getDB(); |
|
const keys: Array<{ pubkey: string; created_at: number; keyType?: 'nsec' | 'anonymous' }> = []; |
|
const tx = db.transaction('keys', 'readonly'); |
|
|
|
for await (const cursor of tx.store.iterate()) { |
|
const key = cursor.value as StoredNsecKey; |
|
// Only include nsec keys (not anonymous, which are handled separately) |
|
if (!key.keyType || key.keyType === 'nsec') { |
|
keys.push({ |
|
pubkey: key.pubkey, |
|
created_at: key.created_at, |
|
keyType: key.keyType || 'nsec' |
|
}); |
|
} |
|
} |
|
|
|
await tx.done; |
|
return keys; |
|
}
|
|
|