chore(nostr-client): move dev scripts to dev/ folder
Move 9 development/testing .mjs scripts out of the package root into dev/ to keep the published package clean. Update relative imports and dev.sh reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
01bac04cdf
commit
5448a620a9
10 changed files with 3 additions and 3 deletions
111
packages/nostr-client/dev/nip44v1.mjs
Normal file
111
packages/nostr-client/dev/nip44v1.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* NIP-44 v1 Implementation
|
||||
*
|
||||
* This is the XChaCha20-based encryption used by Lightning.Pub for Kind 21000 RPC events.
|
||||
* It differs from standard NIP-44 v2 (used in nostr-tools) which uses ChaCha20-Poly1305.
|
||||
*/
|
||||
|
||||
import { base64 } from '@scure/base'
|
||||
import { randomBytes } from '@noble/hashes/utils.js'
|
||||
import { streamXOR as xchacha20 } from '@stablelib/xchacha20'
|
||||
import { secp256k1 } from '@noble/curves/secp256k1.js'
|
||||
import { sha256 } from '@noble/hashes/sha2.js'
|
||||
|
||||
const XCHACHA20_VERSION = 1
|
||||
|
||||
/**
|
||||
* Convert hex string to Uint8Array
|
||||
* @param {string} hex - Hex string
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
function hexToBytes(hex) {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shared secret for NIP-44 v1 encryption
|
||||
* @param {Uint8Array|string} privateKey - Private key (32 bytes or hex string)
|
||||
* @param {string} publicKey - Public key (32 bytes hex string, no prefix)
|
||||
* @returns {Uint8Array} - 32-byte shared secret
|
||||
*/
|
||||
export function getConversationKey(privateKey, publicKey) {
|
||||
// Convert private key to Uint8Array if it's a hex string
|
||||
const privKeyBytes = typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey
|
||||
|
||||
// Convert public key (with 02 prefix) to Uint8Array
|
||||
const pubKeyBytes = hexToBytes('02' + publicKey)
|
||||
|
||||
// Get ECDH shared point
|
||||
const sharedPoint = secp256k1.getSharedSecret(privKeyBytes, pubKeyBytes)
|
||||
|
||||
// Hash the x-coordinate of the shared point
|
||||
return sha256(sharedPoint.slice(1, 33))
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt content using NIP-44 v1 (XChaCha20)
|
||||
* @param {string} content - Plaintext content to encrypt
|
||||
* @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey
|
||||
* @returns {string} - Base64-encoded encrypted payload
|
||||
*/
|
||||
export function encrypt(content, conversationKey) {
|
||||
const nonce = randomBytes(24)
|
||||
const plaintext = new TextEncoder().encode(content)
|
||||
|
||||
// XChaCha20 stream cipher - encrypts in place
|
||||
const ciphertext = new Uint8Array(plaintext.length)
|
||||
xchacha20(conversationKey, nonce, plaintext, ciphertext)
|
||||
|
||||
// Encode: version byte + nonce + ciphertext
|
||||
return base64.encode(new Uint8Array([XCHACHA20_VERSION, ...nonce, ...ciphertext]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt content using NIP-44 v1 (XChaCha20)
|
||||
* @param {string} content - Base64-encoded encrypted payload
|
||||
* @param {Uint8Array} conversationKey - 32-byte conversation key from getConversationKey
|
||||
* @returns {string} - Decrypted plaintext
|
||||
*/
|
||||
export function decrypt(content, conversationKey) {
|
||||
const payload = decodePayload(content)
|
||||
|
||||
// XChaCha20 stream cipher - decrypts in place
|
||||
const plaintext = new Uint8Array(payload.ciphertext.length)
|
||||
xchacha20(conversationKey, payload.nonce, payload.ciphertext, plaintext)
|
||||
|
||||
return new TextDecoder().decode(plaintext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode encrypted payload (supports both formats)
|
||||
* @param {string} content - Base64-encoded or JSON-encoded payload
|
||||
* @returns {{nonce: Uint8Array, ciphertext: Uint8Array}}
|
||||
*/
|
||||
function decodePayload(content) {
|
||||
// Check for JSON format
|
||||
if (content.startsWith('{') && content.endsWith('}')) {
|
||||
const parsed = JSON.parse(content)
|
||||
if (parsed.v !== XCHACHA20_VERSION) {
|
||||
throw new Error(`Unsupported encryption version: ${parsed.v}`)
|
||||
}
|
||||
return {
|
||||
nonce: base64.decode(parsed.nonce),
|
||||
ciphertext: base64.decode(parsed.ciphertext),
|
||||
}
|
||||
}
|
||||
|
||||
// Binary format: version byte + nonce (24 bytes) + ciphertext
|
||||
const buf = base64.decode(content)
|
||||
if (buf[0] !== XCHACHA20_VERSION) {
|
||||
throw new Error(`Unsupported encryption version: ${buf[0]}`)
|
||||
}
|
||||
|
||||
return {
|
||||
nonce: buf.subarray(1, 25),
|
||||
ciphertext: buf.subarray(25),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue