store-sigits and update working flow #116

Merged
b merged 18 commits from store-sigits into staging 2024-07-11 11:42:19 +00:00
7 changed files with 139 additions and 23 deletions
Showing only changes of commit 0aaa20092e - Show all commits

View File

@ -67,6 +67,7 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation()
// navigate to user's profile
navigate(getProfileRoute(pubkey))
}

View File

@ -304,95 +304,135 @@ export class NostrController extends EventEmitter {
return event
}
/**
* Encrypts the given content for the specified receiver using NIP-44 encryption.
*
* @param receiver The public key of the receiver.
* @param content The content to be encrypted.
* @returns The encrypted content as a string.
* @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported.
*/
nip44Encrypt = async (receiver: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod
// Handle encryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 encryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Encrypt the content using NIP-44 provided by the nostr extension.
const encrypted = await nostr.nip44.encrypt(receiver, content)
return encrypted as string
}
// Handle encryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
receiver
)
// Encrypt the content using the generated conversation key.
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
return encrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
}
/**
* Decrypts the given content from the specified sender using NIP-44 decryption.
*
* @param sender The public key of the sender.
* @param content The encrypted content to be decrypted.
* @returns The decrypted content as a string.
* @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported.
*/
nip44Decrypt = async (sender: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod
// Handle decryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 decryption.
if (!nostr.nip44) {
throw new Error(
`Your nostr extension does not support nip44 encryption & decryption`
)
}
// Decrypt the content using NIP-44 provided by the nostr extension.
const decrypted = await nostr.nip44.decrypt(sender, content)
return decrypted as string
}
// Handle decryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) {
throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
)
}
// Decode the private key.
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey,
sender
)
// Decrypt the content using the generated conversation key.
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
return decrypted
}
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) {
throw new Error(
`nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
)
}
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined')
}

View File

@ -32,6 +32,7 @@ import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserComponent } from '../../components/username'
import { MetadataController, NostrController } from '../../controllers'
import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
import {
CreateSignatureEventContent,
@ -42,6 +43,7 @@ import {
} from '../../types'
import {
encryptArrayBuffer,
formatTimestamp,
generateEncryptionKey,
generateKeys,
generateKeysFile,
@ -58,24 +60,6 @@ import {
uploadToFileStorage
} from '../../utils'
import styles from './style.module.scss'
import { appPrivateRoutes } from '../../routes'
/**
* Helper function to get the current timestamp in YYMMDD:HHMMSS format
*/
const getFormattedTimestamp = () => {
const now = new Date()
const padZero = (num: number) => (num < 10 ? '0' + num : num)
const year = now.getFullYear()
const month = padZero(now.getMonth() + 1) // Months are zero-indexed
const day = padZero(now.getDate())
const hours = padZero(now.getHours())
const minutes = padZero(now.getMinutes())
const seconds = padZero(now.getSeconds())
return `${year}-${month}-${day}_${hours}:${minutes}:${seconds}`
}
export const CreatePage = () => {
const navigate = useNavigate()
@ -87,7 +71,7 @@ export const CreatePage = () => {
const [authUrl, setAuthUrl] = useState<string>()
const [title, setTitle] = useState(`sigit_${getFormattedTimestamp()}`)
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [userInput, setUserInput] = useState('')

View File

@ -42,6 +42,12 @@ enum SignedStatus {
export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
/**
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
* arrayBuffer will be received in navigation from create page in offline mode
* meta will be received in navigation from create & home page in online mode
*/
const {
meta: metaInNavState,
arrayBuffer: decryptedArrayBuffer,

View File

@ -44,6 +44,10 @@ export const VerifyPage = () => {
)
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)

View File

@ -181,10 +181,18 @@ export const generateKeys = async (
return { sender: getPublicKey(privateKey), keys }
}
/**
* 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.
*/
export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
// Parse the create signature event from the metadata
const createSignatureEvent = await parseJson<Event>(
meta.createSignature
).catch((err) => {
// Log and display an error message if parsing fails
console.log('err in parsing the createSignature event:>> ', err)
toast.error(
err.message || 'error occurred in parsing the create signature event'
@ -192,17 +200,21 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
return null
})
// Return null if the create signature event could not be parsed
if (!createSignatureEvent) return null
// Verify the validity of the create signature event
const isValidCreateSignature = verifyEvent(createSignatureEvent)
if (!isValidCreateSignature) {
toast.error('Create signature is invalid')
return null
}
// Parse the content of the create signature event
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content
).catch((err) => {
// Log and display an error message if parsing fails
console.log(`err in parsing the createSignature event's content :>> `, err)
toast.error(
`error occurred in parsing the create signature event's content`
@ -210,29 +222,38 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
return null
})
// Return null if the create signature content could not be parsed
if (!createSignatureContent) return null
// Extract the ZIP URL from the create signature content
const zipUrl = createSignatureContent.zipUrl
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
const usersNpub = hexToNpub(usersPubkey)
// Return null if the metadata does not contain keys
if (!meta.keys) return null
const { sender, keys } = meta.keys
// Check if the user's public key is in the keys object
if (usersNpub in keys) {
// Instantiate the NostrController to decrypt the encryption key
const nostrController = NostrController.getInstance()
const decrypted = await nostrController
.nip04Decrypt(sender, keys[usersNpub])
.catch((err) => {
// Log and display an error message if decryption fails
console.log('An error occurred in decrypting encryption key', err)
toast.error('An error occurred in decrypting encryption key')
return null
})
// Return null if the encryption key could not be decrypted
if (!decrypted) return null
// Return the parsed and decrypted data
return {
createSignatureEvent,
createSignatureContent,

View File

@ -493,8 +493,10 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!newBlossomUrl) return null
// insert new blossom url at the start of the array
blossomUrls.unshift(newBlossomUrl)
// only keep last 10 blossom urls, delete older ones
if (blossomUrls.length > 10) {
const filesToDelete = blossomUrls.splice(10)
filesToDelete.forEach((url) => {
@ -509,6 +511,7 @@ export const updateUsersAppData = async (meta: Meta) => {
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
// encrypt content for storing in kind 30078 event
const nostrController = NostrController.getInstance()
const encryptedContent = await nostrController
.nip04Encrypt(
@ -624,24 +627,37 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
console.log('response.data :>> ', response.data)
}
/**
* Function to upload user application data to the Blossom server.
* @param sigits - An object containing metadata for the user application data.
* @param processedGiftWraps - An array of processed gift wrap IDs.
* @param privateKey - The private key used for encryption.
* @returns A promise that resolves to the URL of the uploaded file.
*/
const uploadUserAppDataToBlossom = async (
sigits: { [key: string]: Meta },
processedGiftWraps: string[],
privateKey: string
) => {
// Create an object containing the sigits and processed gift wraps
const obj = {
sigits,
processedGiftWraps
}
// Convert the object to a JSON string
const stringified = JSON.stringify(obj)
// Convert the private key from hex to bytes
const secretKey = hexToBytes(privateKey)
// Encrypt the JSON string using the secret key
const encrypted = nip44.v2.encrypt(
stringified,
nip44ConversationKey(secretKey, getPublicKey(secretKey))
)
// Create a blob from the encrypted data
const blob = new Blob([encrypted], { type: 'application/octet-stream' })
// Create a file from the blob
const file = new File([blob], 'encrypted.txt', {
type: 'application/octet-stream'
})
@ -659,8 +675,10 @@ const uploadUserAppDataToBlossom = async (
]
}
// Finalize the event with the private key
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
// URL of the file storage service
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
// Upload the file to the file storage service using Axios
@ -674,22 +692,34 @@ const uploadUserAppDataToBlossom = async (
return response.data.url as string
}
/**
* Function to retrieve and decrypt user application data from Blossom server.
* @param url - The URL to fetch the encrypted data from.
* @param privateKey - The private key used for decryption.
* @returns A promise that resolves to the decrypted and parsed user application data.
*/
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
// Initialize errorCode to track HTTP error codes
let errorCode = 0
// Fetch the encrypted data from the provided URL
const encrypted = await axios
.get(url, {
responseType: 'blob'
responseType: 'blob' // Expect a blob response
})
.then(async (res) => {
// Convert the blob response to a File object
const file = new File([res.data], 'encrypted.txt')
// Read the text content from the file
const text = await file.text()
return text
})
.catch((err) => {
// Log and display an error message if the request fails
console.error(`error occurred in getting file from ${url}`, err)
toast.error(err.message || `error occurred in getting file from ${url}`)
// Set errorCode to the HTTP status code if available
if (err.request) {
const { status } = err.request
errorCode = status
@ -698,6 +728,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
return null
})
// Return a default value if the requested resource is not found (404)
if (errorCode === 404) {
return {
sigits: {},
@ -705,20 +736,26 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
}
}
// Return null if the encrypted data could not be retrieved
if (!encrypted) return null
// Convert the private key from hex to bytes
const secret = hexToBytes(privateKey)
// Get the public key corresponding to the private key
const pubkey = getPublicKey(secret)
// Decrypt the encrypted data using the secret and public key
const decrypted = nip44.v2.decrypt(
encrypted,
nip44ConversationKey(secret, pubkey)
)
// Parse the decrypted JSON content
const parsedContent = await parseJson<{
sigits: { [key: string]: Meta }
processedGiftWraps: string[]
}>(decrypted).catch((err) => {
// Log and display an error message if parsing fails
console.log(
'An error occurred in parsing the user app data content from blossom server',
err
@ -729,15 +766,22 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
return null
})
// Return the parsed content
return parsedContent
}
/**
* Function to subscribe to sigits notifications for a specified public key.
* @param pubkey - The public key to subscribe to.
* @returns A promise that resolves when the subscription is successful.
*/
export const subscribeForSigits = async (pubkey: string) => {
// Get relay list metadata
// Instantiate the MetadataController to retrieve relay list metadata
const metadataController = new MetadataController()
const relaySet = await metadataController
.findRelayListMetadata(pubkey)
.catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
err
@ -751,15 +795,20 @@ export const subscribeForSigits = async (pubkey: string) => {
// Ensure relay list is not empty
if (relaySet.read.length === 0) return
// Define the filter for the subscription
const filter: Filter = {
kinds: [1059],
'#p': [pubkey]
}
// Instantiate a new SimplePool for the subscription
const pool = new SimplePool()
// Subscribe to the specified relays with the defined filter
return pool.subscribeMany(relaySet.read, [filter], {
// Define a callback function to handle received events
onevent: (event) => {
processReceivedEvent(event)
processReceivedEvent(event) // Process the received event
}
})
}
@ -810,9 +859,16 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
updateUsersAppData(meta)
}
/**
* Function to send a notification to a specified receiver.
* @param receiver - The recipient's public key.
* @param meta - Metadata associated with the notification.
*/
export const sendNotification = async (receiver: string, meta: Meta) => {
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
// Create an unsigned event object with the provided metadata
const unsignedEvent: UnsignedEvent = {
kind: 938,
pubkey: usersPubkey,
@ -821,13 +877,15 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
created_at: now()
}
// Wrap the unsigned event with the receiver's information
const wrappedEvent = createWrap(unsignedEvent, receiver)
// Get relay list metadata
// Instantiate the MetadataController to retrieve relay list metadata
const metadataController = new MetadataController()
const relaySet = await metadataController
.findRelayListMetadata(receiver)
.catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err
@ -845,10 +903,12 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
// Publish the notification event to the recipient's read relays
const nostrController = NostrController.getInstance()
// Attempt to publish the event to the relays, with a timeout of 2 minutes
await Promise.race([
nostrController.publishEvent(wrappedEvent, relaySet.read),
timeout(1000 * 60 * 2)
]).catch((err) => {
// Log an error if publishing the notification event fails
console.log(
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
err