sigit.io/src/utils/misc.ts

275 lines
9.1 KiB
TypeScript
Raw Normal View History

import axios from 'axios'
import {
2024-06-28 09:24:14 +00:00
Event,
EventTemplate,
finalizeEvent,
generateSecretKey,
getPublicKey,
2024-06-28 09:24:14 +00:00
nip04,
verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
2024-07-05 09:17:12 +00:00
import { NostrController } from '../controllers'
import { AuthState } from '../store/auth/types'
import store from '../store/store'
import { CreateSignatureEventContent, Meta, SignedEventContent } from '../types'
2024-07-05 08:38:04 +00:00
import { hexToNpub, now } from './nostr'
2024-06-28 09:24:14 +00:00
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'],
2024-07-05 08:38:04 +00:00
['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
2024-05-16 06:23:26 +00:00
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
2024-05-16 06:23:26 +00:00
'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.
2024-05-22 06:19:40 +00:00
* @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 (
2024-05-22 06:19:40 +00:00
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
2024-05-22 06:19:40 +00:00
content: content, // content for event
2024-05-14 09:27:05 +00:00
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
}
}
2024-06-28 09:24:14 +00:00
/**
* 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 }
}
2024-07-08 20:16:47 +00:00
/**
* 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.
*/
2024-07-05 08:38:04 +00:00
export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
2024-07-08 20:16:47 +00:00
// Parse the create signature event from the metadata
2024-07-05 08:38:04 +00:00
const createSignatureEvent = await parseJson<Event>(
meta.createSignature
).catch((err) => {
2024-07-08 20:16:47 +00:00
// Log and display an error message if parsing fails
2024-07-05 08:38:04 +00:00
console.log('err in parsing the createSignature event:>> ', err)
toast.error(
err.message || 'error occurred in parsing the create signature event'
)
return null
})
2024-06-28 09:24:14 +00:00
2024-07-08 20:16:47 +00:00
// Return null if the create signature event could not be parsed
2024-06-28 09:24:14 +00:00
if (!createSignatureEvent) return null
2024-07-08 20:16:47 +00:00
// Verify the validity of the create signature event
2024-06-28 09:24:14 +00:00
const isValidCreateSignature = verifyEvent(createSignatureEvent)
if (!isValidCreateSignature) {
toast.error('Create signature is invalid')
return null
}
2024-07-08 20:16:47 +00:00
// Parse the content of the create signature event
2024-06-28 09:24:14 +00:00
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content
).catch((err) => {
2024-07-08 20:16:47 +00:00
// Log and display an error message if parsing fails
2024-06-28 09:24:14 +00:00
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
})
2024-07-08 20:16:47 +00:00
// Return null if the create signature content could not be parsed
2024-06-28 09:24:14 +00:00
if (!createSignatureContent) return null
2024-07-08 20:16:47 +00:00
// Extract the ZIP URL from the create signature content
2024-07-05 08:38:04 +00:00
const zipUrl = createSignatureContent.zipUrl
2024-07-08 20:16:47 +00:00
// Retrieve the user's public key from the state
2024-06-28 09:24:14 +00:00
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
const usersNpub = hexToNpub(usersPubkey)
2024-07-08 20:16:47 +00:00
// Return null if the metadata does not contain keys
2024-07-05 08:38:04 +00:00
if (!meta.keys) return null
const { sender, keys } = meta.keys
2024-06-28 09:24:14 +00:00
2024-07-08 20:16:47 +00:00
// Check if the user's public key is in the keys object
2024-06-28 09:24:14 +00:00
if (usersNpub in keys) {
2024-07-08 20:16:47 +00:00
// Instantiate the NostrController to decrypt the encryption key
2024-07-05 08:38:04 +00:00
const nostrController = NostrController.getInstance()
const decrypted = await nostrController
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
2024-07-08 20:16:47 +00:00
// Log and display an error message if decryption fails
2024-07-05 08:38:04 +00:00
console.log('An error occurred in decrypting encryption key', err)
toast.error('An error occurred in decrypting encryption key')
return null
})
2024-07-08 20:16:47 +00:00
// Return null if the encryption key could not be decrypted
2024-07-05 08:38:04 +00:00
if (!decrypted) return null
2024-07-08 20:16:47 +00:00
// Return the parsed and decrypted data
2024-06-28 09:24:14 +00:00
return {
2024-07-05 08:38:04 +00:00
createSignatureEvent,
createSignatureContent,
zipUrl,
encryptionKey: decrypted
2024-06-28 09:24:14 +00:00
}
}
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);
}