import { Box, Button, List, ListItem, ListSubheader, Tooltip, Typography, useTheme } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, kinds, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserAvatar } from '../../components/UserAvatar' import { MetadataController, NostrController } from '../../controllers' import { CreateSignatureEventContent, Meta, ProfileMetadata, SignedEventContent } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, getHash, hexToNpub, now, npubToHex, parseJson, readContentOfZipEntry, shorten, signEventForMetaFile } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' import { useLocation } from 'react-router-dom' import axios from 'axios' import { PdfFile } from '../../types/drawing.ts' import { addMarks, convertToPdfBlob, convertToPdfFile, groupMarksByPage } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' import { useSelector } from 'react-redux' import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' export const VerifyPage = () => { const theme = useTheme() const textColor = theme.palette.getContrastText( theme.palette.background.paper ) const location = useLocation() /** * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * meta will be received in navigation from create & home page in online mode */ const { uploadedZip, meta: metaInNavState } = location.state || {} const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [selectedFile, setSelectedFile] = useState(null) const [meta, setMeta] = useState(null) const [submittedBy, setSubmittedBy] = useState() const [signers, setSigners] = useState<`npub1${string}`[]>([]) const [viewers, setViewers] = useState<`npub1${string}`[]>([]) const [creatorFileHashes, setCreatorFileHashes] = useState<{ [key: string]: string }>({}) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>({}) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( {} ) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() useEffect(() => { if (uploadedZip) { setSelectedFile(uploadedZip) } else if (metaInNavState) { const processSigit = async () => { setIsLoading(true) setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta') const res = await extractZipUrlAndEncryptionKey(metaInNavState) if (!res) { setIsLoading(false) return } const { zipUrl, encryptionKey, createSignatureEvent, createSignatureContent } = res setLoadingSpinnerDesc('Fetching file from file server') axios .get(zipUrl, { responseType: 'arraybuffer' }) .then(async (res) => { const fileName = zipUrl.split('/').pop() const file = new File([res.data], fileName!) const encryptedArrayBuffer = await file.arrayBuffer() const arrayBuffer = await decryptArrayBuffer( encryptedArrayBuffer, encryptionKey ).catch((err) => { console.log('err in decryption:>> ', err) toast.error( err.message || 'An error occurred in decrypting file.' ) return null }) if (arrayBuffer) { const zip = await JSZip.loadAsync(arrayBuffer).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 files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map( (entry) => entry.name ) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) if (arrayBuffer) { files[fileName] = await convertToPdfFile( arrayBuffer, fileName! ) const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName.replace(/^files\//, '')] = hash } } else { fileHashes[fileName.replace(/^files\//, '')] = null } } setCurrentFileHashes(fileHashes) setFiles(files) setSigners(createSignatureContent.signers) setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) setMeta(metaInNavState) setIsLoading(false) } }) .catch((err) => { console.error(`error occurred in getting file from ${zipUrl}`, err) toast.error( err.message || `error occurred in getting file from ${zipUrl}` ) }) .finally(() => { setIsLoading(false) }) } processSigit() } }, [uploadedZip, metaInNavState]) useEffect(() => { if (submittedBy) { const metadataController = new MetadataController() const users = [submittedBy, ...signers, ...viewers] users.forEach((user) => { const pubkey = npubToHex(user)! if (!(pubkey in metadata)) { const handleMetadataEvent = (event: Event) => { const metadataContent = metadataController.extractProfileMetadataContent(event) if (metadataContent) setMetadata((prev) => ({ ...prev, [pubkey]: metadataContent })) } metadataController.on(pubkey, (kind: number, event: Event) => { if (kind === kinds.Metadata) { handleMetadataEvent(event) } }) metadataController .findMetadata(pubkey) .then((metadataEvent) => { if (metadataEvent) handleMetadataEvent(metadataEvent) }) .catch((err) => { console.error( `error occurred in finding metadata for: ${user}`, err ) }) } }) } }, [submittedBy, signers, viewers]) const handleVerify = async () => { if (!selectedFile) return setIsLoading(true) const zip = await JSZip.loadAsync(selectedFile).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 fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) .map((entry) => entry.name) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) if (arrayBuffer) { const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName.replace(/^files\//, '')] = hash } } else { fileHashes[fileName.replace(/^files\//, '')] = null } } console.log('fileHashes :>> ', fileHashes) setCurrentFileHashes(fileHashes) setLoadingSpinnerDesc('Parsing meta.json') const metaFileContent = await readContentOfZipEntry( zip, 'meta.json', 'string' ) if (!metaFileContent) { setIsLoading(false) return } const parsedMetaJson = await parseJson(metaFileContent).catch( (err) => { console.log('err in parsing the content of meta.json :>> ', err) toast.error( err.message || 'error occurred in parsing the content of meta.json' ) setIsLoading(false) return null } ) if (!parsedMetaJson) return const createSignatureEvent = await parseJson( parsedMetaJson.createSignature ).catch((err) => { console.log('err in parsing the createSignature event:>> ', err) toast.error( err.message || 'error occurred in parsing the create signature event' ) setIsLoading(false) return null }) if (!createSignatureEvent) return const isValidCreateSignature = verifyEvent(createSignatureEvent) if (!isValidCreateSignature) { toast.error('Create signature is invalid') setIsLoading(false) return } const createSignatureContent = await parseJson( createSignatureEvent.content ).catch((err) => { console.log( `err in parsing the createSignature event's content :>> `, err ) toast.error( err.message || `error occurred in parsing the create signature event's content` ) setIsLoading(false) return null }) if (!createSignatureContent) return setSigners(createSignatureContent.signers) setViewers(createSignatureContent.viewers) setCreatorFileHashes(createSignatureContent.fileHashes) setSubmittedBy(createSignatureEvent.pubkey) setMeta(parsedMetaJson) setIsLoading(false) } const getPrevSignersSig = (npub: string) => { if (!meta) return null // if user is first signer then use creator's signature if (signers[0] === npub) { try { const createSignatureEvent: Event = JSON.parse(meta.createSignature) return createSignatureEvent.sig } catch (error) { return null } } // find the index of signer const currentSignerIndex = signers.findIndex((signer) => signer === npub) // return null if could not found user in signer's list if (currentSignerIndex === -1) return null // find prev signer const prevSigner = signers[currentSignerIndex - 1] // get the signature of prev signer try { const prevSignersEvent: Event = JSON.parse(meta.docSignatures[prevSigner]) return prevSignersEvent.sig } catch (error) { return null } } const handleExport = async () => { if (Object.entries(files).length === 0 || !meta || !usersPubkey) return const usersNpub = hexToNpub(usersPubkey) if ( !signers.includes(usersNpub) && !viewers.includes(usersNpub) && submittedBy !== usersNpub ) { return } setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') if (!meta) return const prevSig = getLastSignersSig(meta, signers) if (!prevSig) return const signedEvent = await signEventForMetaFile( JSON.stringify({ prevSig }), nostrController, setIsLoading ) if (!signedEvent) return const exportSignature = JSON.stringify(signedEvent, null, 2) const updatedMeta = { ...meta, exportSignature } const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) const zip = new JSZip() zip.file('meta.json', stringifiedMeta) const marks = extractMarksFromSignedMeta(updatedMeta) const marksByPage = groupMarksByPage(marks) for (const [fileName, pdf] of Object.entries(files)) { const pages = await addMarks(pdf.file, marksByPage) const blob = await convertToPdfBlob(pages) zip.file(`/files/${fileName}`, blob) } const arrayBuffer = await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch((err) => { console.log('err in zip:>> ', err) setIsLoading(false) toast.error(err.message || 'Error occurred in generating zip file') return null }) if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) saveAs(blob, `exported-${now()}.sigit.zip`) setIsLoading(false) } const displayUser = (pubkey: string, verifySignature = false) => { const profile = metadata[pubkey] let isValidSignature = false if (verifySignature) { const npub = hexToNpub(pubkey) const signedEventString = meta ? meta.docSignatures[npub] : null if (signedEventString) { try { const signedEvent = JSON.parse(signedEventString) const isVerifiedEvent = verifyEvent(signedEvent) if (isVerifiedEvent) { // get the actual signature of prev signer const prevSignersSig = getPrevSignersSig(npub) // get the signature of prev signer from the content of current signers signedEvent try { const obj: SignedEventContent = JSON.parse(signedEvent.content) if ( obj.prevSig && prevSignersSig && obj.prevSig === prevSignersSig ) { isValidSignature = true } } catch (error) { isValidSignature = false } } } catch (error) { console.error( `An error occurred in parsing and verifying the signature event for ${pubkey}`, error ) } } } return ( <> {verifySignature && ( <> {isValidSignature && ( )} {!isValidSignature && ( )} )} ) } const displayExportedBy = () => { if (!meta || !meta.exportSignature) return null const exportSignatureString = meta.exportSignature try { const exportSignatureEvent = JSON.parse(exportSignatureString) as Event if (verifyEvent(exportSignatureEvent)) { return displayUser(exportSignatureEvent.pubkey) } else { toast.error(`Invalid export signature!`) return ( Invalid export signature ) } } catch (error) { console.error(`An error occurred wile parsing exportSignature`, error) return null } } return ( <> {isLoading && } {!meta && ( <> Select exported zip file setSelectedFile(value)} InputProps={{ inputProps: { accept: '.sigit.zip' } }} /> {selectedFile && ( )} )} {meta && ( <> Meta Info } > {submittedBy && ( Submitted By {displayUser(submittedBy)} )} Exported By {displayExportedBy()} {signers.length > 0 && ( Signers
    {signers.map((signer) => (
  • {displayUser(npubToHex(signer)!, true)}
  • ))}
)} {viewers.length > 0 && ( Viewers
    {viewers.map((viewer) => (
  • {displayUser(npubToHex(viewer)!)}
  • ))}
)} Files {Object.entries(currentFileHashes).map( ([filename, hash], index) => { const isValidHash = creatorFileHashes[filename] === hash return ( {filename} {isValidHash && ( )} {!isValidHash && ( )} ) } )}
)}
) }