Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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))}`;
|
||||
}
|
||||
Reference in new issue
Block a user