In offline mode create a wrapper zip file #110
@ -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) => {
|
||||||
s marked this conversation as resolved
Outdated
|
|||||||
|
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
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
setIsLoading(false)
|
||||||
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
|
)
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(`error occurred in decryption`, err)
|
|
||||||
toast.error(err.message || `error occurred in decryption`)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
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)
|
||||||
.catch((err) => {
|
if (!parsedKeysJson) return
|
||||||
console.log('err in decryption:>> ', err)
|
|
||||||
toast.error(err.message || 'An error occurred in decrypting file.')
|
const encryptedArrayBuffer = await readContentOfZipEntry(
|
||||||
return null
|
zip,
|
||||||
})
|
'compressed.sigit',
|
||||||
.finally(() => {
|
'arraybuffer'
|
||||||
setIsLoading(false)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
return arrayBuffer
|
// 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) => {
|
||||||
|
console.log('err in decryption:>> ', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user
pls add comments