Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
// Generated, do not edit. sha256:48d09dbb5cf2116c2b2c3990d8d1b662451d072fe40458fddcaa483e74bb3100
import { bech32, hexToBytes, bytesToHex, schnorr } from '../vendor/crypto/noble-scure.js';
const HEX64 = /^[0-9a-f]{64}$/;
export function npubFromHex(hex) {
const v = String(hex ?? '').toLowerCase();
if (!HEX64.test(v)) return null;
return bech32.encode('npub', bech32.toWords(hexToBytes(v)));
}
export function shortKey(key, keep = 8) {
const k = String(key ?? '');
return k.length > keep * 2 + 1 ? `${k.slice(0, keep)}…${k.slice(-keep)}` : k;
}
function validSecret(bytes) {
try { schnorr.getPublicKey(bytes); return true; } catch { return false; }
}
export function secretFromText(text) {
const v = String(text ?? '').trim();
if (v.startsWith('npub1')) return { error: 'npub' };
let bytes = null;
if (HEX64.test(v.toLowerCase())) bytes = hexToBytes(v.toLowerCase());
else if (v.startsWith('nsec1')) {
try {
const { prefix, words } = bech32.decode(v);
if (prefix === 'nsec') bytes = Uint8Array.from(bech32.fromWords(words));
} catch { bytes = null; }
}
return bytes && bytes.length === 32 && validSecret(bytes) ? { secret: bytes } : { error: 'invalid' };
}
export const publicKeyOf = (secret) => bytesToHex(schnorr.getPublicKey(secret));
export const nsecFromSecret = (secret) => bech32.encode('nsec', bech32.toWords(secret));
export function newSecret(random = (n) => crypto.getRandomValues(new Uint8Array(n))) {
for (;;) {
const bytes = random(32);
if (validSecret(bytes)) return bytes;
}
}
+74
View File
@@ -0,0 +1,74 @@
// Generated, do not edit. sha256:c9e44c618374bc6218029ec68095d9d8a62c6e5839ffe1dd3084b46600ccb118
import {
secp256k1, sha256, chacha20, hkdfExtract, hkdfExpand, hmac, base64,
hexToBytes, utf8ToBytes, concatBytes,
} from '../vendor/crypto/noble-scure.js';
const VERSION = 2;
const SALT = utf8ToBytes('nip44-v2');
const MIN = 1;
const MAX = 65535;
const asBytes = (x) => (typeof x === 'string' ? hexToBytes(x) : x);
export function conversationKey(secret, pubkey) {
const shared = secp256k1.getSharedSecret(asBytes(secret), concatBytes(Uint8Array.of(2), asBytes(pubkey)));
return hkdfExtract(sha256, shared.subarray(1, 33), SALT);
}
export function messageKeys(convKey, nonce) {
const keys = hkdfExpand(sha256, asBytes(convKey), asBytes(nonce), 76);
return { chachaKey: keys.subarray(0, 32), chachaNonce: keys.subarray(32, 44), hmacKey: keys.subarray(44, 76) };
}
export function paddedLength(len) {
if (len <= 32) return 32;
const next = 1 << (Math.floor(Math.log2(len - 1)) + 1);
const chunk = next <= 256 ? 32 : next / 8;
return chunk * (Math.floor((len - 1) / chunk) + 1);
}
function pad(text) {
const bytes = utf8ToBytes(text);
if (bytes.length < MIN || bytes.length > MAX) throw new Error(`A NIP-44 message is ${MIN} to ${MAX} bytes.`);
const out = new Uint8Array(2 + paddedLength(bytes.length));
out[0] = bytes.length >> 8;
out[1] = bytes.length & 0xff;
out.set(bytes, 2);
return out;
}
function unpad(padded) {
const len = (padded[0] << 8) | padded[1];
if (len < MIN || padded.length !== 2 + paddedLength(len)) throw new Error('Invalid padding.');
return new TextDecoder('utf-8', { fatal: true }).decode(padded.subarray(2, 2 + len));
}
function sameBytes(a, b) {
if (a.length !== b.length) return false;
let d = 0;
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
return d === 0;
}
export function encrypt(text, convKey, nonce = crypto.getRandomValues(new Uint8Array(32))) {
const n = asBytes(nonce);
const { chachaKey, chachaNonce, hmacKey } = messageKeys(convKey, n);
const ciphertext = chacha20(chachaKey, chachaNonce, pad(text));
const mac = hmac(sha256, hmacKey, concatBytes(n, ciphertext));
return base64.encode(concatBytes(Uint8Array.of(VERSION), n, ciphertext, mac));
}
export function decrypt(payload, convKey) {
const p = String(payload ?? '');
if (!p || p[0] === '#') throw new Error('Unknown NIP-44 version.');
if (p.length < 132 || p.length > 87472) throw new Error('Invalid NIP-44 payload length.');
const data = base64.decode(p);
if (data[0] !== VERSION) throw new Error(`Unknown NIP-44 version ${data[0]}.`);
const nonce = data.subarray(1, 33);
const ciphertext = data.subarray(33, data.length - 32);
const mac = data.subarray(data.length - 32);
const { chachaKey, chachaNonce, hmacKey } = messageKeys(convKey, nonce);
if (!sameBytes(mac, hmac(sha256, hmacKey, concatBytes(nonce, ciphertext)))) throw new Error('Invalid MAC.');
return unpad(chacha20(chachaKey, chachaNonce, ciphertext));
}
+58
View File
@@ -0,0 +1,58 @@
// Generated, do not edit. sha256:f29f3a90c87550fd7890bda37eec12ad08ada3b62d0dc1ae18995512314d74ce
import { schnorr, sha256, bytesToHex, hexToBytes, utf8ToBytes } from '../vendor/crypto/noble-scure.js';
import { publicKeyOf } from './keys.js';
import { encrypt as nip44, decrypt as nip44Open, conversationKey } from './nip44.js';
const KIND = 27235;
export const eventId = (e) => bytesToHex(sha256(utf8ToBytes(JSON.stringify([0, e.pubkey, e.created_at, e.kind, e.tags, e.content]))));
export function nip98Event({ pubkey, url, method, body = '', now = Date.now() }) {
const tags = [['u', url], ['method', method]];
if (body) tags.push(['payload', bytesToHex(sha256(utf8ToBytes(body)))]);
return { pubkey, created_at: Math.floor(now / 1000), kind: KIND, tags, content: '' };
}
export function keySigner(secret) {
const pubkey = publicKeyOf(secret);
return {
kind: 'key',
pubkey,
async sign(event) {
const id = eventId(event);
return { ...event, id, sig: bytesToHex(schnorr.sign(hexToBytes(id), secret)) };
},
async nip44Encrypt(pubkey, text) {
return nip44(text, conversationKey(secret, pubkey));
},
async nip44Decrypt(pubkey, payload) {
return nip44Open(payload, conversationKey(secret, pubkey));
},
};
}
export async function extensionSigner(nostr = globalThis.nostr) {
if (!nostr || typeof nostr.getPublicKey !== 'function' || typeof nostr.signEvent !== 'function') {
throw new Error('No nostr extension (NIP-07) in this browser.');
}
const pubkey = String(await nostr.getPublicKey()).toLowerCase();
return {
kind: 'extension',
pubkey,
sign: (event) => nostr.signEvent(event),
nip44Encrypt: (to, text) => {
if (typeof nostr.nip44?.encrypt !== 'function') throw new Error('This nostr extension cannot encrypt with NIP-44.');
return nostr.nip44.encrypt(to, text);
},
nip44Decrypt: (from, payload) => {
if (typeof nostr.nip44?.decrypt !== 'function') throw new Error('This nostr extension cannot decrypt with NIP-44.');
return nostr.nip44.decrypt(from, payload);
},
};
}
export async function nip98Header(signer, { url, method, body = '', now = Date.now() }) {
const signed = await signer.sign(nip98Event({ pubkey: signer.pubkey, url, method, body, now }));
if (signed.pubkey !== signer.pubkey) throw new Error('The signer signed with another key than it gave.');
return `Nostr ${btoa(JSON.stringify(signed))}`;
}