feat: add prev signer's signature in the content of next signer's signed event #74
@ -34,6 +34,7 @@ import {
|
||||
CreateSignatureEventContent,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
SignedEventContent,
|
||||
User,
|
||||
UserRole
|
||||
} from '../../types'
|
||||
@ -52,7 +53,12 @@ import {
|
||||
uploadToFileStorage
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { Download } from '@mui/icons-material'
|
||||
import {
|
||||
Cancel,
|
||||
CheckCircle,
|
||||
Download,
|
||||
HourglassTop
|
||||
} from '@mui/icons-material'
|
||||
|
||||
enum SignedStatus {
|
||||
Fully_Signed,
|
||||
@ -350,9 +356,13 @@ export const SignPage = () => {
|
||||
setLoadingSpinnerDesc('Generating hashes for files')
|
||||
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
|
||||
if (!prevSig) return
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
fileHashes: currentFileHashes
|
||||
prevSig
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
@ -461,7 +471,7 @@ export const SignPage = () => {
|
||||
key,
|
||||
npubToHex(nextSigner)!,
|
||||
nostrController,
|
||||
false,
|
||||
true,
|
||||
setAuthUrl
|
||||
)
|
||||
}
|
||||
@ -488,9 +498,13 @@ export const SignPage = () => {
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const prevSig = await getLastSignersSig()
|
||||
if (!prevSig) return
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
fileHashes: currentFileHashes
|
||||
prevSig
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
@ -535,6 +549,70 @@ export const SignPage = () => {
|
||||
navigate(appPrivateRoutes.verify)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts an npub of a signer and return the signature of its previous signer.
|
||||
* This prevSig will be used in the content of the provided signer's signedEvent
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns the signature of last signer
|
||||
* It will be used in the content of export signature's signedEvent
|
||||
*/
|
||||
const getLastSignersSig = () => {
|
||||
if (!meta) return null
|
||||
|
||||
// if there're no signers then use creator's signature
|
||||
if (signers.length === 0) {
|
||||
try {
|
||||
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
||||
return createSignatureEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// get last signer
|
||||
const lastSigner = signers[signers.length - 1]
|
||||
|
||||
// get the signature of last signer
|
||||
try {
|
||||
const lastSignatureEvent: Event = JSON.parse(
|
||||
meta.docSignatures[lastSigner]
|
||||
)
|
||||
return lastSignatureEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
@ -583,9 +661,10 @@ export const SignPage = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{submittedBy && zip && (
|
||||
{submittedBy && zip && meta && (
|
||||
<>
|
||||
<DisplayMeta
|
||||
meta={meta}
|
||||
zip={zip}
|
||||
submittedBy={submittedBy}
|
||||
signers={signers}
|
||||
@ -594,6 +673,7 @@ export const SignPage = () => {
|
||||
currentFileHashes={currentFileHashes}
|
||||
signedBy={signedBy}
|
||||
nextSigner={nextSinger}
|
||||
getPrevSignersSig={getPrevSignersSig}
|
||||
/>
|
||||
{signedStatus === SignedStatus.Fully_Signed && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
@ -618,6 +698,7 @@ export const SignPage = () => {
|
||||
}
|
||||
|
||||
type DisplayMetaProps = {
|
||||
meta: Meta
|
||||
zip: JSZip
|
||||
submittedBy: string
|
||||
signers: `npub1${string}`[]
|
||||
@ -626,9 +707,11 @@ type DisplayMetaProps = {
|
||||
currentFileHashes: { [key: string]: string | null }
|
||||
signedBy: `npub1${string}`[]
|
||||
nextSigner?: string
|
||||
getPrevSignersSig: (usersNpub: string) => string | null
|
||||
}
|
||||
|
||||
const DisplayMeta = ({
|
||||
meta,
|
||||
zip,
|
||||
submittedBy,
|
||||
signers,
|
||||
@ -636,7 +719,8 @@ const DisplayMeta = ({
|
||||
creatorFileHashes,
|
||||
currentFileHashes,
|
||||
signedBy,
|
||||
nextSigner
|
||||
nextSigner,
|
||||
getPrevSignersSig
|
||||
}: DisplayMetaProps) => {
|
||||
const theme = useTheme()
|
||||
|
||||
@ -784,16 +868,16 @@ const DisplayMeta = ({
|
||||
>
|
||||
{filename}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="label"
|
||||
sx={{
|
||||
color: isValidHash
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.main
|
||||
}}
|
||||
>
|
||||
{isValidHash ? 'Valid' : 'Invalid'} hash
|
||||
</Typography>
|
||||
{isValidHash && (
|
||||
<Tooltip title="File integrity check passed" arrow>
|
||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isValidHash && (
|
||||
<Tooltip title="File integrity check failed" arrow>
|
||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
@ -809,46 +893,177 @@ const DisplayMeta = ({
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user, index) => {
|
||||
const userMeta = metadata[user.pubkey]
|
||||
|
||||
let signedStatus = '-'
|
||||
|
||||
if (user.role === UserRole.signer) {
|
||||
// check if user has signed the document
|
||||
const usersNpub = hexToNpub(user.pubkey)
|
||||
if (signedBy.includes(usersNpub)) {
|
||||
signedStatus = 'Signed'
|
||||
}
|
||||
// check if user is the next signer
|
||||
else if (user.pubkey === nextSigner) {
|
||||
signedStatus = 'Awaiting Signature'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className={styles.tableCell}>
|
||||
<UserComponent
|
||||
pubkey={user.pubkey}
|
||||
name={
|
||||
userMeta?.display_name ||
|
||||
userMeta?.name ||
|
||||
shorten(hexToNpub(user.pubkey))
|
||||
}
|
||||
image={userMeta?.picture}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={styles.tableCell}>
|
||||
{user.role}
|
||||
</TableCell>
|
||||
<TableCell>{signedStatus}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
{users.map((user) => (
|
||||
<DisplayUser
|
||||
key={user.pubkey}
|
||||
meta={meta}
|
||||
user={user}
|
||||
metadata={metadata}
|
||||
signedBy={signedBy}
|
||||
nextSigner={nextSigner}
|
||||
getPrevSignersSig={getPrevSignersSig}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ListItem>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
enum PrevSignatureValidationEnum {
|
||||
Pending,
|
||||
Valid,
|
||||
Invalid
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
Viewer = 'Viewer',
|
||||
Awaiting = 'Awaiting Signature',
|
||||
Signed = 'Signed',
|
||||
Pending = 'Pending'
|
||||
}
|
||||
|
||||
type DisplayUserProps = {
|
||||
meta: Meta
|
||||
user: User
|
||||
metadata: { [key: string]: ProfileMetadata }
|
||||
signedBy: `npub1${string}`[]
|
||||
nextSigner?: string
|
||||
getPrevSignersSig: (usersNpub: string) => string | null
|
||||
}
|
||||
|
||||
const DisplayUser = ({
|
||||
meta,
|
||||
user,
|
||||
metadata,
|
||||
signedBy,
|
||||
nextSigner,
|
||||
getPrevSignersSig
|
||||
}: DisplayUserProps) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const userMeta = metadata[user.pubkey]
|
||||
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
||||
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
||||
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
||||
|
||||
useEffect(() => {
|
||||
if (user.role === UserRole.viewer) {
|
||||
setUserStatus(UserStatus.Viewer)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user has signed the document
|
||||
const usersNpub = hexToNpub(user.pubkey)
|
||||
if (signedBy.includes(usersNpub)) {
|
||||
setUserStatus(UserStatus.Signed)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user is the next signer
|
||||
if (user.pubkey === nextSigner) {
|
||||
setUserStatus(UserStatus.Awaiting)
|
||||
return
|
||||
}
|
||||
}, [user, nextSigner, signedBy])
|
||||
|
||||
useEffect(() => {
|
||||
const validatePrevSignature = async () => {
|
||||
const handleNullCase = () => {
|
||||
setPreviousSignatureStatus(PrevSignatureValidationEnum.Invalid)
|
||||
return
|
||||
}
|
||||
|
||||
// get previous signers sig from the content of current signers signed event
|
||||
const npub = hexToNpub(user.pubkey)
|
||||
const signedEvent = await parseJson<Event>(
|
||||
meta.docSignatures[npub]
|
||||
).catch((err) => {
|
||||
console.log(`err in parsing the singed event for ${npub}:>> `, err)
|
||||
toast.error(
|
||||
err.message ||
|
||||
'error occurred in parsing the signed event signature event'
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return handleNullCase()
|
||||
|
||||
// now that we have signed event of current signer, we'll extract prevSig from its content
|
||||
const parsedContent = await parseJson<SignedEventContent>(
|
||||
signedEvent.content
|
||||
).catch((err) => {
|
||||
console.log(
|
||||
`an error occurred in parsing the content of signedEvent of ${npub}`,
|
||||
err
|
||||
)
|
||||
toast.error(
|
||||
err.message ||
|
||||
`an error occurred in parsing the content of signedEvent of ${npub}`
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!parsedContent) return handleNullCase()
|
||||
|
||||
const prevSignersSignature = getPrevSignersSig(npub)
|
||||
|
||||
if (!prevSignersSignature) return handleNullCase()
|
||||
|
||||
setPreviousSignatureStatus(
|
||||
parsedContent.prevSig === prevSignersSignature
|
||||
? PrevSignatureValidationEnum.Valid
|
||||
: PrevSignatureValidationEnum.Invalid
|
||||
)
|
||||
}
|
||||
|
||||
if (userStatus === UserStatus.Signed) {
|
||||
validatePrevSignature()
|
||||
}
|
||||
}, [userStatus, meta.docSignatures, user.pubkey, getPrevSignersSig])
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className={styles.tableCell}>
|
||||
<UserComponent
|
||||
pubkey={user.pubkey}
|
||||
name={
|
||||
userMeta?.display_name ||
|
||||
userMeta?.name ||
|
||||
shorten(hexToNpub(user.pubkey))
|
||||
}
|
||||
image={userMeta?.picture}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Typography component="label">{userStatus}</Typography>
|
||||
{userStatus === UserStatus.Signed && (
|
||||
<>
|
||||
{prevSignatureStatus === PrevSignatureValidationEnum.Valid && (
|
||||
<Tooltip title="Contains valid signature of prev signer" arrow>
|
||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{prevSignatureStatus === PrevSignatureValidationEnum.Invalid && (
|
||||
<Tooltip
|
||||
title="Contains invalid signature of prev signer"
|
||||
arrow
|
||||
>
|
||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{userStatus === UserStatus.Awaiting && (
|
||||
<Tooltip title="Waiting for user's sign" arrow>
|
||||
<HourglassTop />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
@ -15,7 +16,12 @@ import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { UserComponent } from '../../components/username'
|
||||
import { MetadataController } from '../../controllers'
|
||||
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
SignedEventContent
|
||||
} from '../../types'
|
||||
import {
|
||||
getHash,
|
||||
hexToNpub,
|
||||
@ -25,6 +31,7 @@ import {
|
||||
shorten
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
||||
|
||||
export const VerifyPage = () => {
|
||||
const theme = useTheme()
|
||||
@ -208,6 +215,35 @@ export const VerifyPage = () => {
|
||||
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 displayUser = (pubkey: string, verifySignature = false) => {
|
||||
const profile = metadata[pubkey]
|
||||
|
||||
@ -219,7 +255,27 @@ export const VerifyPage = () => {
|
||||
if (signedEventString) {
|
||||
try {
|
||||
const signedEvent = JSON.parse(signedEventString)
|
||||
isValidSignature = verifyEvent(signedEvent)
|
||||
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}`,
|
||||
@ -240,16 +296,19 @@ export const VerifyPage = () => {
|
||||
/>
|
||||
|
||||
{verifySignature && (
|
||||
<Typography
|
||||
component="label"
|
||||
sx={{
|
||||
color: isValidSignature
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.main
|
||||
}}
|
||||
>
|
||||
{isValidSignature ? 'Valid' : 'Invalid'} Signature
|
||||
</Typography>
|
||||
<>
|
||||
{isValidSignature && (
|
||||
<Tooltip title="Valid signature">
|
||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isValidSignature && (
|
||||
<Tooltip title="Invalid signature">
|
||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@ -425,16 +484,20 @@ export const VerifyPage = () => {
|
||||
>
|
||||
{filename}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="label"
|
||||
sx={{
|
||||
color: isValidHash
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.main
|
||||
}}
|
||||
>
|
||||
{isValidHash ? 'Valid' : 'Invalid'} hash
|
||||
</Typography>
|
||||
{isValidHash && (
|
||||
<Tooltip title="File integrity check passed" arrow>
|
||||
<CheckCircle
|
||||
sx={{ color: theme.palette.success.light }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isValidHash && (
|
||||
<Tooltip title="File integrity check failed" arrow>
|
||||
<Cancel
|
||||
sx={{ color: theme.palette.error.main }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -19,3 +19,7 @@ export interface CreateSignatureEventContent {
|
||||
viewers: `npub1${string}`[]
|
||||
fileHashes: { [key: string]: string }
|
||||
}
|
||||
|
||||
export interface SignedEventContent {
|
||||
prevSig: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user