import axios from 'axios' import { Event, EventTemplate, finalizeEvent, generateSecretKey, getPublicKey, nip04, verifyEvent } from 'nostr-tools' import { toast } from 'react-toastify' import { NostrController } from '../controllers' import { AuthState } from '../store/auth/types' import store from '../store/store' import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types' import { hexToNpub, now } from './nostr' import { parseJson } from './string' import { hexToBytes } from '@noble/hashes/utils' import { Mark } from '../types/mark.ts' /** * Uploads a file to a file storage service. * @param blob The Blob object representing the file to upload. * @param nostrController The NostrController instance for handling authentication. * @returns The URL of the uploaded file. */ export const uploadToFileStorage = async (file: File) => { // Define event metadata for authorization const event: EventTemplate = { kind: 24242, content: 'Authorize Upload', created_at: Math.floor(Date.now() / 1000), tags: [ ['t', 'upload'], ['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now ['name', file.name], ['size', String(file.size)] ] } const key = store.getState().userAppData?.keyPair?.private if (!key) { throw new Error( 'Key to interact with blossom server is not defined in user app data' ) } // Sign the authorization event using the dedicated key stored in user app data const authEvent = finalizeEvent(event, hexToBytes(key)) // URL of the file storage service const FILE_STORAGE_URL = 'https://blossom.sigit.io' // REFACTOR: should be an env // Upload the file to the file storage service using Axios const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, { headers: { Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header 'Content-Type': 'application/sigit' // Set content type header } }) // Return the URL of the uploaded file return response.data.url as string } /** * Signs an event for a meta.json file. * @param content contains content for event. * @param nostrController The NostrController instance for signing the event. * @param setIsLoading Function to set loading state in the component. * @returns A Promise resolving to the signed event, or null if signing fails. */ export const signEventForMetaFile = async ( content: string, nostrController: NostrController, setIsLoading: (value: React.SetStateAction) => void ) => { // Construct the event metadata for the meta file const event: EventTemplate = { kind: 27235, // Event type for meta file content: content, // content for event created_at: Math.floor(Date.now() / 1000), // Current timestamp tags: [['-']] // For understanding why "-" tag is used here see: https://github.com/nostr-protocol/nips/blob/protected-events-tag/70.md } // Sign the event const signedEvent = await nostrController.signEvent(event).catch((err) => { console.error(err) toast.error(err.message || 'Error occurred in signing nostr event') setIsLoading(false) // Set loading state to false return null }) return signedEvent // Return the signed event } /** * Generates the content for keys.json file. * * @param users - An array of public keys. * @param key - The key that will be encrypted for each user. * @returns A promise that resolves to a JSON string containing the sender's public key and encrypted keys, or null if an error occurs. */ export const generateKeysFile = async ( users: string[], key: string ): Promise => { // Generate a random private key to act as the sender const privateKey = generateSecretKey() // Calculate the required length to be a multiple of 10 const requiredLength = Math.ceil(users.length / 10) * 10 const additionalKeysCount = requiredLength - users.length if (additionalKeysCount > 0) { // generate random public keys to make the keys array multiple of 10 const additionalPubkeys = Array.from({ length: additionalKeysCount }, () => getPublicKey(generateSecretKey()) ) users.push(...additionalPubkeys) } // Encrypt the key for each user's public key const promises = users.map((pubkey) => nip04.encrypt(privateKey, pubkey, key)) // Wait for all encryption promises to resolve const keys = await Promise.all(promises).catch((err) => { console.log('Error while generating keys :>> ', err) toast.error(err.message || 'An error occurred while generating key') return null }) // If any encryption promise failed, return null if (!keys) return null try { // Return a JSON string containing the sender's public key and encrypted keys return JSON.stringify({ sender: getPublicKey(privateKey), keys }) } catch (error) { // Return null if an error occurs during JSON stringification return null } } /** * Encrypt decryption key for each users pubkey. * * @param pubkeys - An array of public keys. * @param key - The key that will be encrypted for each user. * @returns A promise that resolves to a JSON object containing a random pubkey as sender and encrypted keys for each user, or null if an error occurs. */ export const generateKeys = async ( pubkeys: string[], key: string ): Promise<{ sender: string keys: { [user: `npub1${string}`]: string } } | null> => { // Generate a random private key to act as the sender const privateKey = generateSecretKey() const keys: { [user: `npub1${string}`]: string } = {} // Encrypt the key for each user's public key for (const pubkey of pubkeys) { const npub = hexToNpub(pubkey) const encryptedKey = await nip04 .encrypt(privateKey, pubkey, key) .catch((err) => { console.log(`An error occurred in encrypting key for ${npub}`, err) toast.error('An error occurred in key encryption') return null }) if (!encryptedKey) return null keys[npub] = encryptedKey } return { sender: getPublicKey(privateKey), keys } } /** * Function to extract the ZIP URL and encryption key from the provided metadata. * @param meta - The metadata object containing the create signature and encryption keys. * @returns A promise that resolves to an object containing the create signature event, * create signature content, ZIP URL, and decrypted encryption key. */ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => { // Parse the create signature event from the metadata const createSignatureEvent = await parseJson( meta.createSignature ).catch((err) => { // Log and display an error message if parsing fails console.log('err in parsing the createSignature event:>> ', err) toast.error( err.message || 'error occurred in parsing the create signature event' ) return null }) // Return null if the create signature event could not be parsed if (!createSignatureEvent) return null // Verify the validity of the create signature event const isValidCreateSignature = verifyEvent(createSignatureEvent) if (!isValidCreateSignature) { toast.error('Create signature is invalid') return null } // Parse the content of the create signature event const createSignatureContent = await parseJson( createSignatureEvent.content ).catch((err) => { // Log and display an error message if parsing fails console.log(`err in parsing the createSignature event's content :>> `, err) toast.error( `error occurred in parsing the create signature event's content` ) return null }) // Return null if the create signature content could not be parsed if (!createSignatureContent) return null // Extract the ZIP URL from the create signature content const zipUrl = createSignatureContent.zipUrl // Retrieve the user's public key from the state const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersNpub = hexToNpub(usersPubkey) // Return null if the metadata does not contain keys if (!meta.keys) return null const { sender, keys } = meta.keys // Check if the user's public key is in the keys object if (usersNpub in keys) { // Instantiate the NostrController to decrypt the encryption key const nostrController = NostrController.getInstance() const decrypted = await nostrController .nip04Decrypt(sender, keys[usersNpub]) .catch((err) => { // Log and display an error message if decryption fails console.log('An error occurred in decrypting encryption key', err) toast.error('An error occurred in decrypting encryption key') return null }) // Return null if the encryption key could not be decrypted if (!decrypted) return null // Return the parsed and decrypted data return { createSignatureEvent, createSignatureContent, zipUrl, encryptionKey: decrypted } } return null } export const extractMarksFromSignedMeta = (meta: Meta): Mark[] => { return Object.values(meta.docSignatures) .map((val: string) => JSON.parse(val as string)) .map((val: Event) => JSON.parse(val.content)) .flatMap((val: SignedEventContent) => val.marks); }