cyphertap
Version:
Nostr, Lightning and Ecash on a single Button component
90 lines (89 loc) • 3.55 kB
JavaScript
// src/lib/utils/nip49.ts
import { scrypt } from '@noble/hashes/scrypt.js';
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
import { concatBytes, randomBytes, hexToBytes, bytesToHex } from '@noble/hashes/utils.js';
import { bech32 } from '@scure/base';
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
// Maximum size for bech32 encoding
const Bech32MaxSize = 5000;
/**
* Encode bytes using bech32 encoding with a specific prefix
*/
function encodeBech32(prefix, data) {
const words = bech32.toWords(data);
return bech32.encode(prefix, words, Bech32MaxSize);
}
/**
* Encrypt a private key using a password
*
* @param privateKey - The private key to encrypt as Uint8Array
* @param password - The password to use for encryption
* @param logn - Scrypt difficulty parameter (default: 16)
* @param ksb - Key security byte (default: 0x02)
* @returns - Encrypted key as a bech32-encoded string with 'ncryptsec' prefix
*/
export function encrypt(privateKey, password, logn = 16, ksb = 0x02) {
const salt = randomBytes(16);
const n = 2 ** logn;
const key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 });
const nonce = randomBytes(24);
const aad = Uint8Array.from([ksb]);
const cipher = xchacha20poly1305(key, nonce, aad);
const ciphertext = cipher.encrypt(privateKey);
// Combine all components into a single byte array
const combinedBytes = concatBytes(Uint8Array.from([0x02]), // Version
Uint8Array.from([logn]), // Difficulty parameter
salt, nonce, aad, ciphertext);
// Encode as bech32 with 'ncryptsec' prefix
return encodeBech32('ncryptsec', combinedBytes);
}
/**
* Decrypt an encrypted private key
*
* @param encryptedKey - The encrypted key as a bech32-encoded string
* @param password - The password used for encryption
* @returns - Decrypted private key as Uint8Array
*/
export function decrypt(encryptedKey, password) {
// Decode the bech32 string
const { prefix, words } = bech32.decode(encryptedKey, Bech32MaxSize);
if (prefix !== 'ncryptsec') {
throw new Error(`Invalid prefix ${prefix}, expected 'ncryptsec'`);
}
const bytes = new Uint8Array(bech32.fromWords(words));
// Check version
const version = bytes[0];
if (version !== 0x02) {
throw new Error(`Invalid version ${version}, expected 0x02`);
}
// Extract components
const logn = bytes[1];
const n = 2 ** logn;
const salt = bytes.slice(2, 2 + 16);
const nonce = bytes.slice(2 + 16, 2 + 16 + 24);
const ksb = bytes[2 + 16 + 24];
const aad = Uint8Array.from([ksb]);
const ciphertext = bytes.slice(2 + 16 + 24 + 1);
// Derive key and decrypt
const key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 });
const cipher = xchacha20poly1305(key, nonce, aad);
return cipher.decrypt(ciphertext);
}
/**
* Encrypt an NDKPrivateKeySigner with a password, returning a NIP-49 encrypted string
*/
export function encryptSigner(signer, password) {
if (!signer.privateKey) {
throw new Error('Signer does not have a private key');
}
const privateKeyBytes = hexToBytes(signer.privateKey);
return encrypt(privateKeyBytes, password);
}
/**
* Create an NDKPrivateKeySigner from a NIP-49 encrypted key
*/
export function decryptToSigner(encryptedKey, password) {
const privateKeyBytes = decrypt(encryptedKey, password);
const privateKeyHex = bytesToHex(privateKeyBytes);
return new NDKPrivateKeySigner(privateKeyHex);
}