290 lines
9.4 KiB
TypeScript
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
|
|
}
|