275 lines
9.1 KiB
TypeScript
275 lines
9.1 KiB
TypeScript
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<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: 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<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 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);
|
|
}
|