nostr
package: vastly simplify the API (#412)
* vastly simplify the api * add missing await * add eose to emitter * add eose to conn * add eose to the client * eose test * improve test suite, add dm tests * demonstrate that nostr-rs-relay auth options don't work * readme files * cleanup * fetch relay info * test readyState * export fetchRelayInfo * cleanup * better async/await linting * use strictEqual in tests * additional eslint rules * allow arbitrary extensions * saner error handling * update README * implement nip-02 --------- Co-authored-by: Kieran <kieran@harkin.me>
This commit is contained in:
@ -1,105 +1,97 @@
|
||||
import * as secp from "@noble/secp256k1"
|
||||
import { ProtocolError } from "./error"
|
||||
import base64 from "base64-js"
|
||||
import { bech32 } from "bech32"
|
||||
import { NostrError } from "./common"
|
||||
|
||||
// TODO Use toHex as well as toString? Might be more explicit
|
||||
// Or maybe replace toString with toHex
|
||||
// TODO Or maybe always store Uint8Array and properly use the format parameter passed into toString
|
||||
|
||||
/**
|
||||
* A 32-byte secp256k1 public key.
|
||||
* A lowercase hex string.
|
||||
*/
|
||||
export class PublicKey {
|
||||
#hex: Hex
|
||||
export type Hex = string
|
||||
|
||||
/**
|
||||
* Expects the key encoded as an npub-prefixed bech32 string, lowercase hex string, or byte buffer.
|
||||
*/
|
||||
constructor(key: string | Uint8Array) {
|
||||
this.#hex = parseKey(key, "npub1")
|
||||
if (this.#hex.toString().length !== 64) {
|
||||
throw new ProtocolError(`invalid pubkey: ${key}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A public key encoded as hex.
|
||||
*/
|
||||
export type PublicKey = string
|
||||
|
||||
toHex(): string {
|
||||
return this.#hex.toString()
|
||||
}
|
||||
/**
|
||||
* A private key encoded as hex or bech32 with the "nsec" prefix.
|
||||
*/
|
||||
export type HexOrBechPublicKey = string
|
||||
|
||||
toString(): string {
|
||||
return this.toHex()
|
||||
}
|
||||
/**
|
||||
* A private key encoded as hex.
|
||||
*/
|
||||
export type PrivateKey = string
|
||||
|
||||
/**
|
||||
* A private key encoded as hex or bech32 with the "nsec" prefix.
|
||||
*/
|
||||
export type HexOrBechPrivateKey = string
|
||||
|
||||
/**
|
||||
* Get a public key corresponding to a private key.
|
||||
*/
|
||||
export function getPublicKey(priv: HexOrBechPrivateKey): PublicKey {
|
||||
priv = parsePrivateKey(priv)
|
||||
return toHex(secp.schnorr.getPublicKey(priv))
|
||||
}
|
||||
|
||||
/**
|
||||
* A 32-byte secp256k1 private key.
|
||||
* Convert the data to lowercase hex.
|
||||
*/
|
||||
export class PrivateKey {
|
||||
#hex: Hex
|
||||
|
||||
/**
|
||||
* Expects the key encoded as an nsec-prefixed bech32 string, lowercase hex string, or byte buffer.
|
||||
*/
|
||||
constructor(key: string | Uint8Array) {
|
||||
this.#hex = parseKey(key, "nsec1")
|
||||
if (this.#hex.toString().length !== 64) {
|
||||
throw new ProtocolError(`invalid private key: ${this.#hex}`)
|
||||
}
|
||||
}
|
||||
|
||||
get pubkey(): PublicKey {
|
||||
return new PublicKey(secp.schnorr.getPublicKey(this.#hex.toString()))
|
||||
}
|
||||
|
||||
/**
|
||||
* The hex representation of the private key. Use with caution!
|
||||
*/
|
||||
toHexDangerous(): string {
|
||||
return this.#hex.toString()
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "PrivateKey"
|
||||
}
|
||||
function toHex(data: Uint8Array): Hex {
|
||||
return secp.utils.bytesToHex(data).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a public or private key into its hex representation.
|
||||
* Convert the public key to hex. Accepts a hex or bech32 string with the "npub" prefix.
|
||||
*/
|
||||
function parseKey(key: string | Uint8Array, bechPrefix: string): Hex {
|
||||
if (typeof key === "string") {
|
||||
// If the key is bech32-encoded, decode it.
|
||||
if (key.startsWith(bechPrefix)) {
|
||||
const { words } = bech32.decode(key)
|
||||
const bytes = Uint8Array.from(bech32.fromWords(words))
|
||||
return new Hex(bytes)
|
||||
}
|
||||
}
|
||||
return new Hex(key)
|
||||
export function parsePublicKey(key: HexOrBechPublicKey): PublicKey {
|
||||
return parseKey(key, "npub")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SHA256 hash of the data.
|
||||
* Convert the private key to hex. Accepts a hex or bech32 string with the "nsec" prefix.
|
||||
*/
|
||||
export async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
||||
return await secp.utils.sha256(data)
|
||||
export function parsePrivateKey(key: HexOrBechPrivateKey): PrivateKey {
|
||||
return parseKey(key, "nsec")
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a public or private key into its hex representation.
|
||||
*/
|
||||
function parseKey(key: string, bechPrefix: string): Hex {
|
||||
// If the key is bech32-encoded, decode it.
|
||||
if (key.startsWith(bechPrefix)) {
|
||||
const { words } = bech32.decode(key)
|
||||
const bytes = Uint8Array.from(bech32.fromWords(words))
|
||||
return toHex(bytes)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SHA256 hash of the data, in hex format.
|
||||
*/
|
||||
export async function sha256(data: Uint8Array): Promise<Hex> {
|
||||
return toHex(await secp.utils.sha256(data))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the data using elliptic curve cryptography.
|
||||
*/
|
||||
export async function schnorrSign(
|
||||
data: Hex,
|
||||
key: PrivateKey
|
||||
): Promise<Uint8Array> {
|
||||
return secp.schnorr.sign(data.toString(), key.toHexDangerous())
|
||||
export async function schnorrSign(data: Hex, priv: PrivateKey): Promise<Hex> {
|
||||
return toHex(await secp.schnorr.sign(data, priv))
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the elliptic curve signature is correct.
|
||||
*/
|
||||
export async function schnorrVerify(
|
||||
export function schnorrVerify(
|
||||
sig: Hex,
|
||||
data: Hex,
|
||||
key: PublicKey
|
||||
@ -107,21 +99,13 @@ export async function schnorrVerify(
|
||||
return secp.schnorr.verify(sig.toString(), data.toString(), key.toString())
|
||||
}
|
||||
|
||||
interface AesEncryptedBase64 {
|
||||
data: string
|
||||
iv: string
|
||||
}
|
||||
|
||||
export async function aesEncryptBase64(
|
||||
sender: PrivateKey,
|
||||
recipient: PublicKey,
|
||||
plaintext: string
|
||||
): Promise<AesEncryptedBase64> {
|
||||
const sharedPoint = secp.getSharedSecret(
|
||||
sender.toHexDangerous(),
|
||||
"02" + recipient.toHex()
|
||||
)
|
||||
const sharedKey = sharedPoint.slice(2, 33)
|
||||
const sharedPoint = secp.getSharedSecret(sender, "02" + recipient)
|
||||
const sharedKey = sharedPoint.slice(1, 33)
|
||||
if (typeof window === "object") {
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
@ -158,7 +142,7 @@ export async function aesEncryptBase64(
|
||||
)
|
||||
let encrypted = cipher.update(plaintext, "utf8", "base64")
|
||||
// TODO Could save an allocation here by avoiding the +=
|
||||
encrypted += cipher.final()
|
||||
encrypted += cipher.final("base64")
|
||||
return {
|
||||
data: encrypted,
|
||||
iv: Buffer.from(iv.buffer).toString("base64"),
|
||||
@ -166,20 +150,16 @@ export async function aesEncryptBase64(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
export async function aesDecryptBase64(
|
||||
sender: PublicKey,
|
||||
recipient: PrivateKey,
|
||||
{ data, iv }: AesEncryptedBase64
|
||||
): Promise<string> {
|
||||
const sharedPoint = secp.getSharedSecret(
|
||||
recipient.toHexDangerous(),
|
||||
"02" + sender.toHex()
|
||||
)
|
||||
const sharedKey = sharedPoint.slice(2, 33)
|
||||
const sharedPoint = secp.getSharedSecret(recipient, "02" + sender)
|
||||
const sharedKey = sharedPoint.slice(1, 33)
|
||||
if (typeof window === "object") {
|
||||
// TODO Can copy this from the legacy code
|
||||
throw new Error("todo")
|
||||
throw new NostrError("todo")
|
||||
} else {
|
||||
const crypto = await import("crypto")
|
||||
const decipher = crypto.createDecipheriv(
|
||||
@ -192,33 +172,7 @@ export async function aesDecryptBase64(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string in lowercase hex. This type is not available to the users of the library.
|
||||
*/
|
||||
export class Hex {
|
||||
#value: string
|
||||
|
||||
/**
|
||||
* Passing a non-lowercase or non-hex string to the constructor
|
||||
* results in an error being thrown.
|
||||
*/
|
||||
constructor(value: string | Uint8Array) {
|
||||
if (value instanceof Uint8Array) {
|
||||
value = secp.utils.bytesToHex(value).toLowerCase()
|
||||
}
|
||||
if (value.length % 2 != 0) {
|
||||
throw new ProtocolError(`invalid lowercase hex string: ${value}`)
|
||||
}
|
||||
const valid = "0123456789abcdef"
|
||||
for (const c of value) {
|
||||
if (!valid.includes(c)) {
|
||||
throw new ProtocolError(`invalid lowercase hex string: ${value}`)
|
||||
}
|
||||
}
|
||||
this.#value = value
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.#value
|
||||
}
|
||||
interface AesEncryptedBase64 {
|
||||
data: string
|
||||
iv: string
|
||||
}
|
||||
|
Reference in New Issue
Block a user