In offline mode create a wrapper zip file #110

Merged
s merged 8 commits from issue-109 into staging 2024-06-13 10:01:31 +00:00
4 changed files with 364 additions and 130 deletions
Showing only changes of commit ded8304c66 - Show all commits

View File

@ -378,6 +378,54 @@ export class NostrController extends EventEmitter {
throw new Error('Login method is undefined') throw new Error('Login method is undefined')
} }
nip04Decrypt = async (sender: string, content: string) => {
const loginMethod = (store.getState().auth as AuthState).loginMethod
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
if (!nostr.nip04) {
throw new Error(
`Your nostr extension does not support nip04 encryption & decryption`
)
}
const decrypted = await nostr.nip04.decrypt(sender, content)
return decrypted
}
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const decrypted = await nip04.decrypt(privateKey, sender, content)
return decrypted
}
if (loginMethod === LoginMethods.nsecBunker) {
const user = new NDKUser({ pubkey: sender })
this.remoteSigner?.on('authUrl', (authUrl) => {
this.emit('nsecbunker-auth', authUrl)
})
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
const decrypted = await this.remoteSigner.decrypt(user, content)
return decrypted
}
throw new Error('Login method is undefined')
}
/** /**
* Function will capture the public key from the nostr extension or if no extension present * Function will capture the public key from the nostr extension or if no extension present
* function wil capture the public key from the local storage * function wil capture the public key from the local storage

View File

@ -33,6 +33,7 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types'
import { import {
encryptArrayBuffer, encryptArrayBuffer,
generateEncryptionKey, generateEncryptionKey,
generateKeysFile,
getHash, getHash,
hexToNpub, hexToNpub,
isOnline, isOnline,
@ -49,15 +50,12 @@ import { HTML5Backend } from 'react-dnd-html5-backend'
import type { Identifier, XYCoord } from 'dnd-core' import type { Identifier, XYCoord } from 'dnd-core'
import { useDrag, useDrop } from 'react-dnd' import { useDrag, useDrop } from 'react-dnd'
import saveAs from 'file-saver' import saveAs from 'file-saver'
import CopyModal from '../../components/copyModal'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [openCopyModal, setOpenCopyModel] = useState(false)
const [textToCopy, setTextToCopy] = useState('')
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
@ -374,26 +372,68 @@ export const CreatePage = () => {
encryptionKey: string encryptionKey: string
): Promise<ArrayBuffer> => { ): Promise<ArrayBuffer> => {
setLoadingSpinnerDesc('Encrypting zip file') setLoadingSpinnerDesc('Encrypting zip file')
return encryptArrayBuffer(arraybuffer, encryptionKey).finally(() => return encryptArrayBuffer(arraybuffer, encryptionKey)
setIsLoading(false) }
// create final zip file
const createFinalZipFile = async (
encryptedArrayBuffer: ArrayBuffer,
encryptionKey: string
): Promise<File | null> => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
const blob = new Blob([encryptedArrayBuffer])
// Create a File object with the Blob data
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
const keysFileContent = await generateKeysFile(
[firstSigner.pubkey],
encryptionKey
) )
if (!keysFileContent) return null
const zip = new JSZip()
zip.file(`compressed.sigit`, file)
zip.file('keys.json', keysFileContent)
const arraybuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
.catch(handleZipError)
if (!arraybuffer) return null
const finalZipFile = new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
type: 'application/zip'
}
)
return finalZipFile
} }
// Handle file upload and further actions based on online/offline status // Handle file upload and further actions based on online/offline status
const handleFileUpload = async (blob: Blob, encryptionKey: string) => { const handleFileUpload = async (file: File, arrayBuffer: ArrayBuffer) => {
if (await isOnline()) { if (await isOnline()) {
const fileUrl = await uploadFile(blob) const fileUrl = await uploadFile(file)
if (!fileUrl) return if (!fileUrl) return
await sendDMs(fileUrl, encryptionKey) await sendDMs(fileUrl)
setIsLoading(false) setIsLoading(false)
navigate( navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
`${appPrivateRoutes.sign}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}`
)
} else { } else {
handleOffline(blob, encryptionKey) handleOffline(file, arrayBuffer)
} }
} }
@ -406,11 +446,11 @@ export const CreatePage = () => {
} }
// Upload the file to the storage and send DMs to signers/viewers // Upload the file to the storage and send DMs to signers/viewers
const uploadFile = async (blob: Blob): Promise<string | null> => { const uploadFile = async (file: File): Promise<string | null> => {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Uploading zip file to file storage.') setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController) const fileUrl = await uploadToFileStorage(file, nostrController)
.then((url) => { .then((url) => {
toast.success('zip file uploaded to file storage') toast.success('zip file uploaded to file storage')
return url return url
@ -420,8 +460,8 @@ export const CreatePage = () => {
return fileUrl return fileUrl
} }
// Send DMs to signers and viewers with the file URL and encryption key // Send DMs to signers and viewers with the file URL
const sendDMs = async (fileUrl: string, encryptionKey: string) => { const sendDMs = async (fileUrl: string) => {
setLoadingSpinnerDesc('Sending DM to signers/viewers') setLoadingSpinnerDesc('Sending DM to signers/viewers')
const signers = users.filter((user) => user.role === UserRole.signer) const signers = users.filter((user) => user.role === UserRole.signer)
@ -430,7 +470,6 @@ export const CreatePage = () => {
if (signers.length > 0) { if (signers.length > 0) {
await sendDM( await sendDM(
fileUrl, fileUrl,
encryptionKey,
signers[0].pubkey, signers[0].pubkey,
nostrController, nostrController,
true, true,
@ -438,34 +477,15 @@ export const CreatePage = () => {
) )
} else { } else {
for (const viewer of viewers) { for (const viewer of viewers) {
await sendDM( await sendDM(fileUrl, viewer.pubkey, nostrController, false, setAuthUrl)
fileUrl,
encryptionKey,
viewer.pubkey,
nostrController,
false,
setAuthUrl
)
} }
} }
} }
// Manage offline scenarios for signing or viewing the file // Manage offline scenarios for signing or viewing the file
const handleOffline = (blob: Blob, encryptionKey: string) => { const handleOffline = (file: File, arrayBuffer: ArrayBuffer) => {
const signers = users.filter((user) => user.role === UserRole.signer) saveAs(file, 'request.sigit.zip')
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
if (signers[0] && signers[0].pubkey === usersPubkey) {
// Create a File object with the Blob data for offline signing
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } })
} else {
// Save the file and show encryption key for offline viewing
saveAs(blob, 'request.sigit')
setTextToCopy(encryptionKey)
setOpenCopyModel(true)
}
} }
const handleCreate = async () => { const handleCreate = async () => {
@ -497,9 +517,15 @@ export const CreatePage = () => {
arraybuffer, arraybuffer,
encryptionKey encryptionKey
) )
const blob = new Blob([encryptedArrayBuffer])
return await handleFileUpload(blob, encryptionKey) const finalZipFile = await createFinalZipFile(
encryptedArrayBuffer,
encryptionKey
)
if (!finalZipFile) return
return await handleFileUpload(finalZipFile, arraybuffer)
} }
if (authUrl) { if (authUrl) {
@ -596,15 +622,6 @@ export const CreatePage = () => {
</Button> </Button>
</Box> </Box>
</Box> </Box>
<CopyModal
open={openCopyModal}
handleClose={() => {
setOpenCopyModel(false)
navigate(appPrivateRoutes.sign)
}}
title="Decryption key for Sigit file"
textToCopy={textToCopy}
/>
</> </>
) )
} }

View File

@ -10,7 +10,6 @@ import {
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
TextField,
Tooltip, Tooltip,
Typography, Typography,
useTheme useTheme
@ -52,7 +51,8 @@ import {
shorten, shorten,
signEventForMetaFile, signEventForMetaFile,
uploadToFileStorage, uploadToFileStorage,
isOnline isOnline,
generateKeysFile
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { import {
@ -71,14 +71,13 @@ enum SignedStatus {
export const SignPage = () => { export const SignPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { file, encryptionKey: encKey } = location.state || {} const { arrayBuffer: decryptedArrayBuffer } = location.state || {}
const [searchParams, setSearchParams] = useSearchParams() const [searchParams] = useSearchParams()
const [displayInput, setDisplayInput] = useState(false) const [displayInput, setDisplayInput] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [encryptionKey, setEncryptionKey] = useState('')
const [zip, setZip] = useState<JSZip>() const [zip, setZip] = useState<JSZip>()
@ -194,9 +193,8 @@ export const SignPage = () => {
useEffect(() => { useEffect(() => {
const fileUrl = searchParams.get('file') const fileUrl = searchParams.get('file')
const key = searchParams.get('key')
if (fileUrl && key) { if (fileUrl) {
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Fetching file from file server') setLoadingSpinnerDesc('Fetching file from file server')
@ -208,7 +206,7 @@ export const SignPage = () => {
const fileName = fileUrl.split('/').pop() const fileName = fileUrl.split('/').pop()
const file = new File([res.data], fileName!) const file = new File([res.data], fileName!)
decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => { decrypt(file).then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
}) })
}) })
@ -221,40 +219,109 @@ export const SignPage = () => {
.finally(() => { .finally(() => {
setIsLoading(false) setIsLoading(false)
}) })
} else if (file && encKey) { } else if (decryptedArrayBuffer) {
decrypt(file, decodeURIComponent(encKey)) handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
.then((arrayBuffer) => {
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
})
.catch((err) => {
console.error(`error occurred in decryption`, err)
toast.error(err.message || `error occurred in decryption`)
})
.finally(() => {
setIsLoading(false) setIsLoading(false)
}) )
} else { } else {
setIsLoading(false) setIsLoading(false)
setDisplayInput(true) setDisplayInput(true)
} }
}, [searchParams, file, encKey]) }, [searchParams, decryptedArrayBuffer])
const decrypt = async (file: File, key: string) => { const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
'keys.json',
'string'
)
if (!keysFileContent) return null
const parsedJSON = await parseJson<{ sender: string; keys: string[] }>(
keysFileContent
).catch((err) => {
console.log(`Error parsing content of keys.json:`, err)
toast.error(err.message || `Error parsing content of keys.json`)
return null
})
return parsedJSON
}
const decrypt = async (file: File) => {
setLoadingSpinnerDesc('Decrypting file') setLoadingSpinnerDesc('Decrypting file')
const encryptedArrayBuffer = await file.arrayBuffer() const zip = await JSZip.loadAsync(file).catch((err) => {
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return
const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key) const parsedKeysJson = await parseKeysJson(zip)
if (!parsedKeysJson) return
const encryptedArrayBuffer = await readContentOfZipEntry(
zip,
'compressed.sigit',
'arraybuffer'
)
if (!encryptedArrayBuffer) return
const { keys, sender } = parsedKeysJson
for (const key of keys) {
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
// Set up timeout promise to handle encryption timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout occurred'))
}, 60000) // Timeout duration = 60 seconds
})
// decrypt the encryptionKey, with timeout
const encryptionKey = await Promise.race([
nostrController.nip04Decrypt(sender, key),
timeoutPromise
])
.then((res) => {
return res
})
.catch((err) => {
console.log('err :>> ', err)
return null
})
.finally(() => {
setAuthUrl(undefined) // Clear authentication URL
})
console.log('encryptionKey :>> ', encryptionKey)
// Return if encryption failed
if (!encryptionKey) continue
const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer,
encryptionKey
)
.catch((err) => { .catch((err) => {
console.log('err in decryption:>> ', err) console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
return null return null
}) })
.finally(() => { .finally(() => {
setIsLoading(false) setIsLoading(false)
}) })
return arrayBuffer if (arrayBuffer) return arrayBuffer
}
return null
} }
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
@ -348,13 +415,10 @@ export const SignPage = () => {
} }
const handleDecrypt = async () => { const handleDecrypt = async () => {
if (!selectedFile || !encryptionKey) return if (!selectedFile) return
setIsLoading(true) setIsLoading(true)
const arrayBuffer = await decrypt( const arrayBuffer = await decrypt(selectedFile)
selectedFile,
decodeURIComponent(encryptionKey)
)
if (!arrayBuffer) return if (!arrayBuffer) return
@ -407,13 +471,15 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Encrypting zip file') setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
const blob = new Blob([encryptedArrayBuffer]) const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
if (!finalZipFile) return
if (await isOnline()) { if (await isOnline()) {
await handleOnlineFlow(blob, key) await handleOnlineFlow(finalZipFile)
} else {
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
} }
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
} }
// Read the content of the hashes.json file // Read the content of the hashes.json file
@ -491,32 +557,101 @@ export const SignPage = () => {
}) })
} }
// create final zip file
const createFinalZipFile = async (
encryptedArrayBuffer: ArrayBuffer,
encryptionKey: string
): Promise<File | null> => {
// Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000)
const blob = new Blob([encryptedArrayBuffer])
// Create a File object with the Blob data
const file = new File([blob], `compressed.sigit`, {
type: 'application/sigit'
})
const isLastSigner = checkIsLastSigner(signers)
const userSet = new Set<string>()
if (isLastSigner) {
if (submittedBy) {
userSet.add(submittedBy)
}
signers.forEach((signer) => {
userSet.add(npubToHex(signer)!)
})
viewers.forEach((viewer) => {
userSet.add(npubToHex(viewer)!)
})
} else {
const usersNpub = hexToNpub(usersPubkey!)
const signerIndex = signers.indexOf(usersNpub)
const nextSigner = signers[signerIndex + 1]
userSet.add(npubToHex(nextSigner)!)
}
const keysFileContent = await generateKeysFile(
Array.from(userSet),
encryptionKey
)
if (!keysFileContent) return null
const zip = new JSZip()
zip.file(`compressed.sigit`, file)
zip.file('keys.json', keysFileContent)
const arraybuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
})
.catch(handleZipError)
if (!arraybuffer) return null
const finalZipFile = new File(
[new Blob([arraybuffer])],
`${unixNow}.sigit.zip`,
{
type: 'application/zip'
}
)
return finalZipFile
}
// Handle errors during zip file generation
const handleZipError = (err: any) => {
console.log('Error in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
}
// Handle the online flow: upload file and send DMs // Handle the online flow: upload file and send DMs
const handleOnlineFlow = async (blob: Blob, key: string) => { const handleOnlineFlow = async (file: File) => {
const fileUrl = await uploadZipFile(blob) const fileUrl = await uploadZipFile(file)
if (!fileUrl) return if (!fileUrl) return
const isLastSigner = checkIsLastSigner(signers) const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) { if (isLastSigner) {
await sendDMToAllUsers(fileUrl, key) await sendDMToAllUsers(fileUrl)
} else { } else {
await sendDMToNextSigner(fileUrl, key) await sendDMToNextSigner(fileUrl)
} }
setIsLoading(false) setIsLoading(false)
// Update search params with updated file URL and encryption key
setSearchParams({
file: fileUrl,
key: key
})
} }
// Upload the zip file to file storage // Upload the zip file to file storage
const uploadZipFile = async (blob: Blob): Promise<string | null> => { const uploadZipFile = async (file: File): Promise<string | null> => {
setLoadingSpinnerDesc('Uploading zip file to file storage.') setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController) const fileUrl = await uploadToFileStorage(file, nostrController)
.then((url) => { .then((url) => {
toast.success('Zip file uploaded to file storage') toast.success('Zip file uploaded to file storage')
return url return url
@ -540,7 +675,7 @@ export const SignPage = () => {
} }
// Send DM to all users (signers and viewers) // Send DM to all users (signers and viewers)
const sendDMToAllUsers = async (fileUrl: string, key: string) => { const sendDMToAllUsers = async (fileUrl: string) => {
const userSet = new Set<`npub1${string}`>() const userSet = new Set<`npub1${string}`>()
if (submittedBy) { if (submittedBy) {
@ -560,7 +695,6 @@ export const SignPage = () => {
for (const user of users) { for (const user of users) {
await sendDM( await sendDM(
fileUrl, fileUrl,
key,
npubToHex(user)!, npubToHex(user)!,
nostrController, nostrController,
false, false,
@ -570,13 +704,12 @@ export const SignPage = () => {
} }
// Send DM to the next signer // Send DM to the next signer
const sendDMToNextSigner = async (fileUrl: string, key: string) => { const sendDMToNextSigner = async (fileUrl: string) => {
const usersNpub = hexToNpub(usersPubkey!) const usersNpub = hexToNpub(usersPubkey!)
const signerIndex = signers.indexOf(usersNpub) const signerIndex = signers.indexOf(usersNpub)
const nextSigner = signers[signerIndex + 1] const nextSigner = signers[signerIndex + 1]
await sendDM( await sendDM(
fileUrl, fileUrl,
key,
npubToHex(nextSigner)!, npubToHex(nextSigner)!,
nostrController, nostrController,
true, true,
@ -771,18 +904,9 @@ export const SignPage = () => {
value={selectedFile} value={selectedFile}
onChange={(value) => setSelectedFile(value)} onChange={(value) => setSelectedFile(value)}
/> />
{selectedFile && (
<TextField
label="Encryption Key"
variant="outlined"
value={encryptionKey}
onChange={(e) => setEncryptionKey(e.target.value)}
/>
)}
</Box> </Box>
{selectedFile && encryptionKey && ( {selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}> <Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained"> <Button onClick={handleDecrypt} variant="contained">
Decrypt Decrypt

View File

@ -1,5 +1,10 @@
import axios from 'axios' import axios from 'axios'
import { EventTemplate } from 'nostr-tools' import {
EventTemplate,
generateSecretKey,
getPublicKey,
nip04
} from 'nostr-tools'
import { MetadataController, NostrController } from '../controllers' import { MetadataController, NostrController } from '../controllers'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { appPrivateRoutes } from '../routes' import { appPrivateRoutes } from '../routes'
@ -11,17 +16,12 @@ import { appPrivateRoutes } from '../routes'
* @returns The URL of the uploaded file. * @returns The URL of the uploaded file.
*/ */
export const uploadToFileStorage = async ( export const uploadToFileStorage = async (
blob: Blob, file: File,
nostrController: NostrController nostrController: NostrController
) => { ) => {
// Get the current timestamp in seconds // Get the current timestamp in seconds
const unixNow = Math.floor(Date.now() / 1000) const unixNow = Math.floor(Date.now() / 1000)
// Create a File object with the Blob data
const file = new File([blob], `compressed-${unixNow}.sigit`, {
type: 'application/sigit'
})
// Define event metadata for authorization // Define event metadata for authorization
const event: EventTemplate = { const event: EventTemplate = {
kind: 24242, kind: 24242,
@ -56,7 +56,6 @@ export const uploadToFileStorage = async (
/** /**
* Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication. * Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
* @param fileUrl The URL of the encrypted zip file to be included in the DM. * @param fileUrl The URL of the encrypted zip file to be included in the DM.
* @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
* @param pubkey The public key of the recipient. * @param pubkey The public key of the recipient.
* @param nostrController The NostrController instance for handling authentication and encryption. * @param nostrController The NostrController instance for handling authentication and encryption.
* @param isSigner Boolean indicating whether the recipient is a signer or viewer. * @param isSigner Boolean indicating whether the recipient is a signer or viewer.
@ -64,7 +63,6 @@ export const uploadToFileStorage = async (
*/ */
export const sendDM = async ( export const sendDM = async (
fileUrl: string, fileUrl: string,
encryptionKey: string,
pubkey: string, pubkey: string,
nostrController: NostrController, nostrController: NostrController,
isSigner: boolean, isSigner: boolean,
@ -77,9 +75,7 @@ export const sendDM = async (
const decryptionUrl = `${window.location.origin}/#${ const decryptionUrl = `${window.location.origin}/#${
appPrivateRoutes.sign appPrivateRoutes.sign
}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent( }?file=${encodeURIComponent(fileUrl)}`
encryptionKey
)}`
const content = `${initialLine}\n\n${decryptionUrl}` const content = `${initialLine}\n\n${decryptionUrl}`
@ -205,3 +201,52 @@ export const signEventForMetaFile = async (
return signedEvent // Return the signed event 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
}
}