# 04. NCrypt

## Encrypted Messaging

NCrypt ("NC" or "nc") provides end-to-end encrypted messaging with blockchain-anchored message delivery and optional read receipts.

### Overview

**Purpose**: Send and receive encrypted messages with verifiable delivery and read status.

**Key Features**:

* End-to-end encryption using ChaCha20-Poly1305
* X25519 key exchange
* On-chain message delivery for verifiability
* Read receipts with cryptographic proof
* File attachments
* Thread-based conversation view

**Location**: src/packages/ncrypt/

**Encryption Details**:

* **Algorithm**: ChaCha20-Poly1305 (AEAD - Authenticated Encryption with Associated Data)
* **Key Exchange**: X25519 (Curve25519 for Diffie-Hellman)
* **Message ID**: 8-byte random or SHA-256 hash
* **Key Storage**: LocalStorage (format: `ncrypt_private_key_{encodedAddress}`)

***

### Encryption Setup

#### Generate Encryption Keys

Before sending/receiving encrypted messages, users must generate and publish their X25519 key pair.

**Implementation**:

```typescript
import { x25519 } from '@noble/curves/ed25519'
import { encodeAddress } from '@polkadot/util-crypto'
import { u8aToHex } from '@polkadot/util'

export function useGenerateKeys() {
  const { api, currentAccount } = useGen6()
  const { getSigner } = useSigner()

  const generateKeys = async () => {
    // 1. Generate X25519 key pair using @noble/curves
    const privateKey = x25519.utils.randomPrivateKey()
    const publicKey = x25519.getPublicKey(privateKey)

    // 2. Store private key in localStorage
    const encodedAddress = encodeAddress(currentAccount.address, 355)
    const storageKey = `ncrypt_private_key_${encodedAddress}`
    localStorage.setItem(storageKey, u8aToHex(privateKey))

    // 3. Publish public key on blockchain
    await publishPublicKey(u8aToHex(publicKey))

    return { publicKey, privateKey }
  }

  return { generateKeys }
}
```

**Blockchain Extrinsic**: `api.tx.postman.publishKey()`

**Parameters**:

* `keyPayload`: X25519 public key (32 bytes)

**Implementation**:

```typescript
async function publishPublicKey(publicKey: Uint8Array) {
  const signerResult = await getSigner()

  // Format payload as expected by the chain (MultiAddress/Enum)
  const keyPayload = {
    X25519: u8aToHex(publicKey)
  }

  const extrinsic = api.tx.postman.publishKey(keyPayload)

  return new Promise<void>((resolve, reject) => {
    extrinsic
      .signAndSend(
        currentAccount.address,
        { signer: signerResult.signer },
        ({ status, dispatchError }) => {
          if (status.isFinalized) {
            if (dispatchError) {
              reject(new Error('Failed to publish key'))
            } else {
              resolve()
            }
          }
        }
      )
      .catch(reject)
  })
}
```

**Storage Location**:

* **Private Key**: `localStorage.ncrypt_private_key_{address}`
* **Public Key**: On-chain via `postman.keys()` query

***

#### Retrieve Public Key

Fetch another user's published X25519 public key from the blockchain.

**Blockchain Query**: `api.query.postman.keys()`

**Parameters**:

* `encodedAddress`: SS58-355 encoded Gen6 address

**Implementation**:

```typescript
import { encodeAddress } from '@polkadot/util-crypto'

export function useBlockchainKey() {
  const { api } = useGen6()

  const getPublicKey = async (address: string) => {
    const encodedAddress = encodeAddress(address, 355)
    const result = await api.query.postman.keys(encodedAddress)

    if (result.isEmpty) {
      throw new Error('User has not published encryption key')
    }

    return result.toString()
  }

  return { getPublicKey }
}
```

**Usage**:

```typescript
import { useQuery } from '@tanstack/react-query'

function RecipientKeyCheck({ recipientAddress }: { recipientAddress: string }) {
  const { getPublicKey } = useBlockchainKey()

  const { data: publicKey, error } = useQuery({
    queryKey: ['blockchainKey', recipientAddress],
    queryFn: () => getPublicKey(recipientAddress)
  })

  if (error) {
    return <Alert>User has not enabled encryption</Alert>
  }

  return <div>User has encryption enabled</div>
}
```

***

#### Query Read Receipts

Check if messages have been read by querying the blockchain inbox.

**Blockchain Query**: `api.query.postman.inbox()`

**Parameters**:

* `recipientAddress`: SS58-355 encoded recipient address
* `senderAddress`: SS58-355 encoded sender address

**Implementation**:

```typescript
import { encodeAddress } from '@polkadot/util-crypto'

export function useThreadInbox(
  senderAddress: string | undefined,
  recipientAddress: string | undefined
) {
  const { api, currentAccount } = useGen6()

  return useQuery({
    queryKey: ['threadInbox', senderAddress, recipientAddress],
    queryFn: async () => {
      if (!api || !currentAccount || !senderAddress || !recipientAddress) {
        return []
      }

      const encodedRecipient = encodeAddress(recipientAddress, 355)
      const encodedSender = encodeAddress(senderAddress, 355)

      const inboxData = await api.query.postman.inbox(
        encodedRecipient,
        encodedSender
      )
      return (inboxData.toJSON() as Array<any>) || []
    },
    enabled: !!api && !!currentAccount && !!senderAddress && !!recipientAddress,
    staleTime: 1000,
    refetchInterval: 3000
  })
}
```

**Response**: Array of read receipt data

**Purpose**: Provides cryptographic proof that messages have been read by the recipient, enabling read receipts functionality.

***

### Messaging API

#### Send Message

Create and send an encrypted message.

**Endpoint**: `POST /ncrypt/messages`

**Authentication**: Required (JWT)

**Request**:

```typescript
interface CreateMessageParams {
  recipient_g6_address: string
  encrypted_content_for_recipient: string
  encrypted_content_for_sender: string
  subject?: string
  requires_read_signature?: boolean
  on_chain_message_id?: string
  attachment_ids?: string[]
}

export const createMessage = async (params: CreateMessageParams) => {
  const response = await axiosInstance.post<Message>('/ncrypt/messages', params)
  return response.data
}
```

**Request Body**:

```json
{
  "recipient_g6_address": "g6x123...",
  "encrypted_content_for_recipient": "base64_encrypted_data",
  "encrypted_content_for_sender": "base64_encrypted_data",
  "subject": "Meeting Notes",
  "requires_read_signature": true,
  "on_chain_message_id": "0xabc123...",
  "attachment_ids": ["uuid-1", "uuid-2"]
}
```

**Response**:

```typescript
interface Message {
  id: number
  sender_g6_address: string
  recipient_g6_address: string
  encrypted_content: string | null
  encrypted_content_for_sender: string | null
  encrypted_content_for_recipient: string | null
  created_at: string
  read_at: string | null
  requires_read_signature: boolean
  is_unlocked: boolean
  has_attachments: boolean
  attachments: Attachment[]
  on_chain_message_id?: string
}

interface Attachment {
  id: string
  filename: string
  url: string
  mime_type: string
  size: number
}
```

**Encryption Process**:

```typescript
import { x25519 } from '@noble/curves/ed25519'
import { chacha20poly1305 } from '@noble/ciphers/chacha'
import { randomBytes } from '@noble/ciphers/webcrypto'

async function encryptMessage(
  content: string,
  recipientPublicKey: Uint8Array,
  senderPrivateKey: Uint8Array
) {
  // 1. Calculate shared secret
  const sharedSecret = x25519.getSharedSecret(
    senderPrivateKey,
    recipientPublicKey
  )

  // 2. Generate nonce (12 bytes for ChaCha20-Poly1305)
  const nonce = randomBytes(12)

  // 3. Encrypt content
  const messageBytes = new TextEncoder().encode(content)
  const aead = chacha20poly1305(sharedSecret, nonce)
  const encrypted = aead.encrypt(messageBytes)

  // 4. Combine nonce + encrypted data and encode
  // Implementation specific combination logic
  // ...
}
```

**Important**: Messages are encrypted twice:

1. `encrypted_content_for_recipient` - Recipient can decrypt
2. `encrypted_content_for_sender` - Sender can decrypt (for message history)

***

#### Blockchain Message Delivery

For verifiable delivery, messages can be sent on-chain.

**Blockchain Extrinsic**: `api.tx.postman.sendMessage()`

**Parameters**:

* `recipientAddress`: SS58-355 encoded address
* `messagePayload`: Encrypted message payload
* `trackingHash` (optional): SHA-256 hash for tracking

**Implementation**:

```typescript
export function useSendMessage() {
  const { api, currentAccount } = useGen6()
  const { getSigner } = useSigner()

  const sendMessageOnChain = async (
    recipientAddress: string,
    encryptedContent: string,
    trackingHash?: string
  ) => {
    const signerResult = await getSigner()

    const extrinsic = api.tx.postman.sendMessage(
      recipientAddress,
      encryptedContent,
      trackingHash || null
    )

    return new Promise<string>((resolve, reject) => {
      extrinsic
        .signAndSend(
          currentAccount.address,
          { signer: signerResult.signer },
          ({ status, events }) => {
            if (status.isFinalized) {
              // Extract message ID from events
              const messageId = events
                .find(({ event }) => event.section === 'postman')
                ?.event.data[0]?.toString()

              resolve(messageId || '')
            }
          }
        )
        .catch(reject)
    })
  }

  return { sendMessageOnChain }
}
```

**Complete Send Flow**:

```typescript
async function sendEncryptedMessage(
  recipientAddress: string,
  content: string,
  requiresReadSignature: boolean
) {
  // 1. Get recipient's public key
  const recipientPublicKey = await getPublicKey(recipientAddress)

  // 2. Get sender's private key
  const senderPrivateKey = getPrivateKeyFromStorage(currentAccount.address)

  // 3. Encrypt for recipient
  const encryptedForRecipient = await encryptMessage(
    content,
    recipientPublicKey,
    senderPrivateKey
  )

  // 4. Encrypt for sender (using own public key)
  const senderPublicKey = await getPublicKey(currentAccount.address)
  const encryptedForSender = await encryptMessage(
    content,
    senderPublicKey,
    senderPrivateKey
  )

  // 5. Send on-chain (optional)
  let onChainMessageId
  if (requiresReadSignature) {
    onChainMessageId = await sendMessageOnChain(
      recipientAddress,
      encryptedForRecipient
    )
  }

  // 6. Store in database
  await createMessage({
    recipient_g6_address: recipientAddress,
    encrypted_content_for_recipient: encryptedForRecipient,
    encrypted_content_for_sender: encryptedForSender,
    requires_read_signature: requiresReadSignature,
    on_chain_message_id: onChainMessageId
  })

  toast.success('Message sent!')
}
```

***

#### Get Message Threads

Retrieve a paginated list of message threads (conversations).

**Endpoint**: `GET /ncrypt/threads`

**Authentication**: Required (JWT)

**Query Parameters**:

* `page` (optional): Page number (default: 1)
* `page_size` (optional): Items per page (default: 20)

**Implementation**:

```typescript
interface GetThreadsParams {
  page?: number
  page_size?: number
}

export const getThreads = async (params?: GetThreadsParams) => {
  const response = await axiosInstance.get<PaginatedThreadsResponse>(
    '/ncrypt/threads',
    { params }
  )
  return response.data
}
```

**Response**:

```typescript
interface PaginatedThreadsResponse {
  threads: Thread[]
  total_count: number
  page: number
  page_size: number
}
```

**Example Response**:

```json
{
  "threads": [
    {
      "peer_address": "g6x123...",
      "last_message": {
        "id": 42,
        "sender_g6_address": "g6x123...",
        "encrypted_content": "...",
        "created_at": "2025-01-15T10:30:00Z"
      },
      "unread_count": 3,
      "total_messages": 15
    }
  ],
  "total_count": 5,
  "page": 1,
  "page_size": 20
}
```

**Usage**:

```typescript
function MessageThreads() {
  const [page, setPage] = useState(1)

  const { data, isLoading } = useQuery({
    queryKey: ['threads', page],
    queryFn: () => getThreads({ page, page_size: 20 })
  })

  if (isLoading) return <div>Loading...</div>

  return (
    <div>
      {data.threads.map(thread => (
        <ThreadCard key={thread.peer_address} thread={thread} />
      ))}
      <Pagination
        currentPage={page}
        totalPages={data.total_pages}
        onPageChange={setPage}
      />
    </div>
  )
}
```

***

#### Get Messages in Thread

Retrieve messages in a specific conversation.

**Endpoint**: `GET /ncrypt/threads/{peer_address}`

**Authentication**: Required (JWT)

**Parameters**:

* `peer_address` (path): Gen6 address of conversation partner
* `page` (query): Page number
* `page_size` (query): Items per page

**Implementation**:

```typescript
export const getMessagesInThread = async (
  peerAddress: string,
  params?: { page?: number; page_size?: number }
) => {
  const response = await axiosInstance.get<PaginatedMessagesResponse>(
    `/ncrypt/threads/${peerAddress}`,
    { params }
  )
  return response.data
}
```

**Response**:

```typescript
interface PaginatedMessagesResponse {
  messages: Message[]
  total_count: number
  total_pages: number
  page: number
  page_size: number
  has_next: boolean
  has_previous: boolean
}
```

**Decryption**:

```typescript
function DecryptedMessage({ message }: { message: Message }) {
  const [decrypted, setDecrypted] = useState<string>('')
  const { currentAccount } = useGen6()

  useEffect(() => {
    const decrypt = async () => {
      // Determine which encrypted content to use
      const isReceived = message.recipient_g6_address === currentAccount.address
      const encryptedContent = isReceived
        ? message.encrypted_content_for_recipient
        : message.encrypted_content_for_sender

      // Get private key from localStorage
      const privateKey = getPrivateKeyFromStorage(currentAccount.address)

      // Get peer's public key
      const peerAddress = isReceived
        ? message.sender_g6_address
        : message.recipient_g6_address
      const peerPublicKey = await getPublicKey(peerAddress)

      // Decrypt message
      const decryptedContent = await decryptMessage(
        encryptedContent,
        peerPublicKey,
        privateKey
      )

      setDecrypted(decryptedContent)
    }

    decrypt()
  }, [message])

  return <div>{decrypted || 'Decrypting...'}</div>
}
```

**Decryption Implementation**:

```typescript
import { x25519 } from '@noble/curves/ed25519'
import { chacha20poly1305 } from '@noble/ciphers/chacha'
import { hexToU8a } from '@polkadot/util'

async function decryptMessage(
  encryptedBase64: string,
  peerPublicKey: string,
  privateKey: string
) {
  // 1. Decode base64
  const combined = Buffer.from(encryptedBase64, 'base64')

  // 2. Extract nonce and encrypted data
  const nonce = combined.slice(0, 12)
  const encrypted = combined.slice(12)

  // 3. Calculate shared secret
  const sharedSecret = x25519.getSharedSecret(
    hexToU8a(privateKey),
    hexToU8a(peerPublicKey)
  )

  // 4. Decrypt
  const aead = chacha20poly1305(sharedSecret, nonce)
  const decrypted = aead.decrypt(encrypted)

  if (!decrypted) {
    throw new Error('Failed to decrypt message')
  }

  // 5. Convert to string
  return new TextDecoder().decode(decrypted)
}
```

***

#### Mark Thread as Read

Mark all messages in a thread as read.

**Endpoint**: `PATCH /ncrypt/threads/{peer_address}/read`

**Authentication**: Required (JWT)

**Parameters**:

* `peer_address` (path): Peer Gen6 address

**Implementation**:

```typescript
export const markThreadAsRead = async (peerAddress: string) => {
  const response = await axiosInstance.patch(
    `/ncrypt/threads/${peerAddress}/read`
  )
  return response.data
}
```

**Response**: Updated thread with `unread_count: 0`

**Usage**:

```typescript
function MessageThread({ peerAddress }: { peerAddress: string }) {
  const markReadMutation = useMutation({
    mutationFn: () => markThreadAsRead(peerAddress),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['threads'] })
    }
  })

  useEffect(() => {
    // Mark as read when user opens thread
    markReadMutation.mutate()
  }, [peerAddress])

  return <div>...</div>
}
```

***

#### Delete Thread

Delete an entire conversation thread.

**Endpoint**: `DELETE /ncrypt/threads/{peer_address}`

**Authentication**: Required (JWT)

**Parameters**:

* `peer_address` (path): Peer Gen6 address

**Implementation**:

```typescript
export const deleteThread = async (peerAddress: string) => {
  await axiosInstance.delete(`/ncrypt/threads/${peerAddress}`)
}
```

**Response**: 204 No Content

**Warning**: This permanently deletes all messages in the thread.

***

### Attachments

#### Upload Attachment

Upload a file to attach to messages.

**Endpoint**: `POST /ncrypt/attachments`

**Authentication**: Required (JWT)

**Request**:

```typescript
export const uploadAttachment = async (file: File) => {
  const formData = new FormData()
  formData.append('file', file)

  const response = await axiosInstance.post<Attachment>(
    '/ncrypt/attachments',
    formData
  )
  return response.data
}
```

**Response**:

```typescript
interface Attachment {
  id: string
  filename: string
  url: string
  mime_type: string
  size: number
}
```

**Usage Flow**:

```typescript
function SendMessageWithAttachment() {
  const [attachmentIds, setAttachmentIds] = useState<string[]>([])

  const uploadMutation = useMutation({
    mutationFn: uploadAttachment,
    onSuccess: (data) => {
      setAttachmentIds(prev => [...prev, data.id])
      toast.success('File uploaded!')
    }
  })

  const sendMutation = useMutation({
    mutationFn: (params: CreateMessageParams) => createMessage(params)
  })

  const handleSend = async (content: string, recipientAddress: string) => {
    // Encrypt message (see previous section)
    const encrypted = await encryptMessage(content, ...)

    // Send with attachments
    await sendMutation.mutateAsync({
      recipient_g6_address: recipientAddress,
      encrypted_content_for_recipient: encrypted.forRecipient,
      encrypted_content_for_sender: encrypted.forSender,
      attachment_ids: attachmentIds
    })
  }

  return (
    <div>
      <input
        type="file"
        onChange={(e) => {
          const file = e.target.files?.[0]
          if (file) uploadMutation.mutate(file)
        }}
      />
      <button onClick={() => handleSend(content, recipient)}>
        Send
      </button>
    </div>
  )
}
```

***

### Read Receipts

#### Request Read Proof

Request cryptographic proof that a message was read.

**Blockchain Extrinsic**: `api.tx.postman.requestPassphrase()`

**Parameters**:

* `senderAddress`: Message sender address
* `messagePayload`: Original encrypted message

**Implementation**:

```typescript
export function useRequestReadProof() {
  const { api, currentAccount } = useGen6()
  const { getSigner } = useSigner()

  const requestReadProof = async (
    senderAddress: string,
    messagePayload: string
  ) => {
    const signerResult = await getSigner()

    const extrinsic = api.tx.postman.requestPassphrase(
      senderAddress,
      messagePayload
    )

    return new Promise<void>((resolve, reject) => {
      extrinsic
        .signAndSend(
          currentAccount.address,
          { signer: signerResult.signer },
          ({ status, dispatchError }) => {
            if (status.isFinalized) {
              if (dispatchError) {
                reject(new Error('Failed to request read proof'))
              } else {
                resolve()
              }
            }
          }
        )
        .catch(reject)
    })
  }

  return { requestReadProof }
}
```

**Usage**:

```typescript
function MessageWithReadReceipt({ message }: { message: Message }) {
  const { requestReadProof } = useRequestReadProof()

  const handleRequestProof = async () => {
    if (!message.on_chain_message_id) {
      toast.error('Message not sent on-chain')
      return
    }

    await requestReadProof(
      message.sender_g6_address,
      message.encrypted_content_for_recipient!
    )

    toast.success('Read proof requested')
  }

  return (
    <div>
      <p>{message.encrypted_content}</p>
      {message.requires_read_signature && !message.read_at && (
        <Button onClick={handleRequestProof}>
          Request Read Proof
        </Button>
      )}
      {message.read_at && (
        <Badge>Read at {message.read_at}</Badge>
      )}
    </div>
  )
}
```

**Note**: Read receipts only work for messages sent on-chain (`on_chain_message_id` must be set).

***

### Query Keys

**TanStack Query Keys**:

```typescript
// Message threads
;['threads'][('threads', page)][
  // Messages in thread
  ('messages', peerAddress)
][('messages', peerAddress, page)][
  // Blockchain keys
  ('blockchainKey', address)
][
  // Attachments
  ('attachment', attachmentId)
]
```

***

### Best Practices

1. **Generate keys on first use** - Prompt users to enable encryption
2. **Check recipient has keys** - Verify before allowing message send
3. **Backup private keys** - Provide export/import functionality

***

### Next Steps

* Parking - Stake tokens for validator rewards
* Finance - Transfer tokens


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.gen6.life/developer-resources/sdk-and-tooling/g6-mw-and-chain-api-guide/04.-ncrypt.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
