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

/**
* 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;
}