import { exists } from "jsr:@std/fs"; import * as nostrTools from "@nostr/tools"; import * as nip06 from "@nostr/tools/nip06"; import { decodeBase64, encodeBase64 } from "jsr:@std/encoding@0.224/base64"; import { getEveFilePath } from "./utils/files.ts"; import { decryptUint8Array, encryptionKey, encryptUint8Array, } from "./utils/encryption.ts"; import { NSec } from "@nostr/tools/nip19"; export function isLocalhost(req: Request): boolean { const url = new URL(req.url); const hostname = url.hostname; return ( hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost" ); } export function isValidJSON(str: string) { try { JSON.parse(str); } catch { return false; } return true; } export function isArray(obj: unknown): obj is T[] { return Array.isArray(obj); } export function randomTimeUpTo2DaysInThePast() { const now = Date.now(); const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew return Math.floor( (Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000, ); } export async function getCCNPubkey(): Promise { const ccnPubPath = await getEveFilePath("ccn.pub"); const seedPath = await getEveFilePath("ccn.seed"); const doWeHaveKey = await exists(ccnPubPath); if (doWeHaveKey) return Deno.readTextFileSync(ccnPubPath); const ccnSeed = Deno.env.get("CCN_SEED") || ((await exists(seedPath)) ? Deno.readTextFileSync(seedPath) : nip06.generateSeedWords()); const ccnPrivateKey = nip06.privateKeyFromSeedWords(ccnSeed); const ccnPublicKey = nostrTools.getPublicKey(ccnPrivateKey); const encryptedPrivateKey = encryptUint8Array(ccnPrivateKey, encryptionKey); Deno.writeTextFileSync(ccnPubPath, ccnPublicKey); Deno.writeTextFileSync( await getEveFilePath("ccn.priv"), encodeBase64(encryptedPrivateKey), ); Deno.writeTextFileSync(seedPath, ccnSeed); return ccnPublicKey; } export async function getMLSPrivateKey(): Promise { const mlsPrivPath = await getEveFilePath("mls.priv"); const doWeHaveKey = await exists(mlsPrivPath); if (doWeHaveKey) { const encryptedPrivateKey = Deno.readTextFileSync(mlsPrivPath); const decryptedPrivateKey = decryptUint8Array( decodeBase64(encryptedPrivateKey), encryptionKey, ); return nostrTools.nip19.nsecEncode(decryptedPrivateKey); } const mlsPrivateKey = nostrTools.generateSecretKey(); const encryptedPrivateKey = encryptUint8Array(mlsPrivateKey, encryptionKey); Deno.writeTextFileSync(mlsPrivPath, encodeBase64(encryptedPrivateKey)); return nostrTools.nip19.nsecEncode(mlsPrivateKey); } export async function getCCNPrivateKey(): Promise { const encryptedPrivateKey = Deno.readTextFileSync( await getEveFilePath("ccn.priv"), ); return decryptUint8Array(decodeBase64(encryptedPrivateKey), encryptionKey); } /** * Compares two byte-like objects in a constant-time manner to prevent timing attacks. * * @param a - First byte-like object to compare * @param b - Second byte-like object to compare * @returns boolean indicating whether the inputs contain identical bytes */ export function bytesEqual< T extends Uint8Array | number[] | string, >(a: T, b: T): boolean { const aLength = a.length; const bLength = b.length; let result = aLength !== bLength ? 1 : 0; const maxLength = Math.max(aLength, bLength); for (let i = 0; i < maxLength; i++) { const aVal = i < aLength ? (typeof a === "string" ? a.charCodeAt(i) : a[i]) : 0; const bVal = i < bLength ? (typeof b === "string" ? b.charCodeAt(i) : b[i]) : 0; result |= aVal ^ bVal; } return result === 0; }