sigit.io/src/utils/misc.ts

290 lines
9.4 KiB
TypeScript

import axios, { AxiosResponse } from 'axios'
import {
Event,
EventTemplate,
finalizeEvent,
generateSecretKey,
getPublicKey,
nip04,
verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { NostrController } from '../controllers'
import store from '../store/store'
import {
CreateSignatureEventContent,
FileServerPutResponse,
Meta
} from '../types'
import { hexToNpub, unixNow } from './nostr'
import { parseJson } from './string'
import { hexToBytes } from '@noble/hashes/utils'
import { getHash } from './hash.ts'
import { SIGIT_BLOSSOM } from './const.ts'
/**
* Uploads a file to one or more file storage services.
* @param blob The Blob object representing the file to upload.
* @param nostrController The NostrController instance for handling authentication.
* @returns The array of URLs of the uploaded file.
*/
export const uploadToFileStorage = async (file: File): Promise<string[]> => {
// Define event metadata for authorization
const hash = await getHash(await file.arrayBuffer())
if (!hash) {
throw new Error("Can't get file hash.")
}
const preferredServersMap = store.getState().servers.map || {}
const preferredServers = Object.keys(preferredServersMap)
// If no servers found, use SIGIT as fallback
if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)
const event: EventTemplate = {
kind: 24242,
content: 'Authorize Upload',
created_at: unixNow(),
tags: [
['t', 'upload'],
['x', hash],
['expiration', String(unixNow() + 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))
const uploadPromises: Promise<AxiosResponse<FileServerPutResponse>>[] = []
// Upload the file to the file storage services using Axios
for (const preferredServer of preferredServers) {
const uploadPromise = axios.put<FileServerPutResponse>(
`${preferredServer}/upload`,
file,
{
headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
'Content-Type': 'application/sigit' // Set content type header
}
}
)
uploadPromises.push(uploadPromise)
}
const responses = await Promise.all(uploadPromises)
// Return the URLs of the uploaded files
return responses.map((response) => 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<boolean>) => 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: unixNow(), // 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
return 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
})
}
/**
* 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<string | null> => {
// 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<Event>(
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<CreateSignatureEventContent>(
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 zipUrls = createSignatureContent.zipUrls
// Retrieve the user's public key from the state
const usersPubkey = store.getState().auth.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,
zipUrls: zipUrls,
encryptionKey: decrypted
}
}
return null
}