> For the complete documentation index, see [llms.txt](https://wiki.gen6.life/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.gen6.life/technology-core/developer-resources/sdk-and-tooling/gen6-mw-and-chain-api-guide/04.-ncrypt.md).

# 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
