E2EE Quick Reference - nself-org/nchat GitHub Wiki
Version: v0.4.0 Last Updated: January 30, 2026
A quick reference for developers working with End-to-End Encryption in nChat.
// src/config/app-config.ts
{
features: {
endToEndEncryption: true
},
encryption: {
enabled: true,
enforceForDirectMessages: true
}
}import { useE2EEContext } from '@/contexts/e2ee-context';
function MyComponent() {
const { initialize, getRecoveryCode } = useE2EEContext();
const setupE2EE = async (password: string) => {
await initialize(password);
const code = getRecoveryCode();
alert(`Save this: ${code}`);
};
return <button onClick={() => setupE2EE('mypassword')}>Setup E2EE</button>;
}import { encryptMessageForSending } from '@/lib/e2ee/message-encryption'
import { useApolloClient } from '@apollo/client'
const apolloClient = useApolloClient()
const payload = await encryptMessageForSending(
'Hello, World!',
{
recipientUserId: 'user-123',
recipientDeviceId: 'device-456',
isDirectMessage: true,
},
apolloClient
)import { extractMessageContent } from '@/lib/e2ee/message-encryption'
const plaintext = await extractMessageContent(message, apolloClient)const { status, isInitialized, isEnabled } = useE2EEContext()
console.log('Initialized:', isInitialized)
console.log('Device ID:', status.deviceId)const { generateSafetyNumber, formatSafetyNumber } = useE2EEContext()
const safetyNumber = await generateSafetyNumber(peerUserId, peerIdentityKey)
const formatted = formatSafetyNumber(safetyNumber)
// Output: "12345 67890 12345 67890 ..." (60 digits)const { rotateSignedPreKey, replenishOneTimePreKeys } = useE2EEContext()
// Rotate signed prekey (weekly)
await rotateSignedPreKey()
// Replenish one-time prekeys (when low)
await replenishOneTimePreKeys(50)const { recover } = useE2EEContext()
await recover('alpha-bravo-charlie-delta-...')query GetPreKeyBundle($userId: uuid!, $deviceId: String!) {
nchat_prekey_bundles(
where: { user_id: { _eq: $userId }, device_id: { _eq: $deviceId } }
limit: 1
) {
identity_key_public
signed_prekey_public
one_time_prekey_public
}
}query GetSession($deviceId: String!, $peerUserId: uuid!, $peerDeviceId: String!) {
nchat_signal_sessions(
where: {
device_id: { _eq: $deviceId }
peer_user_id: { _eq: $peerUserId }
peer_device_id: { _eq: $peerDeviceId }
is_active: { _eq: true }
}
) {
session_state_encrypted
is_initiator
}
}mutation SendMessage(
$channelId: uuid!
$content: String!
$isEncrypted: Boolean!
$encryptedPayload: [Int!]
) {
insert_nchat_messages_one(
object: {
channel_id: $channelId
content: $content
is_encrypted: $isEncrypted
encrypted_payload: $encryptedPayload
}
) {
id
}
}import { E2EESetup } from '@/components/e2ee/E2EESetup';
<E2EESetup
onComplete={() => console.log('Setup complete')}
onCancel={() => console.log('Setup cancelled')}
/>import { E2EEStatus } from '@/components/e2ee/E2EEStatus';
<E2EEStatus
isEncrypted={true}
isVerified={false}
/>import { SafetyNumberDisplay } from '@/components/e2ee/SafetyNumberDisplay';
<SafetyNumberDisplay
peerUserId="user-123"
peerName="Alice"
peerIdentityKey={identityKey}
/>try {
await initialize(password)
} catch (error) {
if (error.message === 'Invalid password') {
alert('Wrong password')
} else if (error.message === 'E2EE not initialized') {
alert('Please set up E2EE first')
} else if (error.message === 'No prekey bundle available') {
alert('Recipient has not enabled E2EE')
}
}// If decryption fails, try re-establishing session
const { hasSession } = useE2EEContext()
if (!(await hasSession(peerUserId, peerDeviceId))) {
// Send a new PreKey message to establish session
await sendNewMessage()
}interface EncryptionConfig {
enabled: boolean // Enable/disable E2EE globally
enforceForPrivateChannels: boolean // Force private channels to be encrypted
enforceForDirectMessages: boolean // Force DMs to be encrypted
allowUnencryptedPublicChannels: boolean // Allow public channels unencrypted
enableSafetyNumbers: boolean // Show safety number UI
requireDeviceVerification: boolean // Require device verification
automaticKeyRotation: boolean // Auto-rotate signed prekeys
keyRotationDays: number // Days between rotations (default: 7)
}// Sessions are automatically cached in memory
// No need to fetch from database every time// Generate 100 one-time prekeys at once
await manager.replenishOneTimePreKeys(100)// Only initialize E2EE when user needs it
// Don't initialize on app startup- Store recovery code offline (paper, password manager)
- Verify safety numbers out-of-band (phone call, in person)
- Use strong password (12+ characters)
- Rotate keys regularly (automatic by default)
- Clear recovery code after saving (automatic)
- Share recovery code via chat or email
- Skip safety number verification
- Use weak password (< 8 characters)
- Store master key in plaintext
- Disable key rotation without reason
# .env.local
NEXT_PUBLIC_E2EE_DEBUG=true// In browser console
const manager = getE2EEManager(apolloClient)
console.log(manager.getStatus())-- In database
SELECT
content,
is_encrypted,
array_length(encrypted_payload, 1) as payload_size
FROM nchat_messages
WHERE is_encrypted = true
ORDER BY created_at DESC
LIMIT 10;-- One-time prekeys available
SELECT
device_id,
COUNT(*) as available_keys
FROM nchat_one_time_prekeys
WHERE is_consumed = false
GROUP BY device_id;POST /api/e2ee/initialize
Content-Type: application/json
{
"password": "mysecretpassword"
}
Response:
{
"success": true,
"deviceId": "abc123",
"recoveryCode": "alpha-bravo-charlie-..."
}POST /api/e2ee/recover
Content-Type: application/json
{
"recoveryCode": "alpha-bravo-charlie-..."
}
Response:
{
"success": true,
"deviceId": "abc123"
}GET /api/e2ee/safety-number?peerUserId=user-123
Authorization: Bearer <token>
Response:
{
"safetyNumber": "12345678901234567890...",
"formatted": "12345 67890 12345 67890 ..."
}// E2EE Status
interface E2EEStatus {
initialized: boolean
masterKeyInitialized: boolean
deviceKeysGenerated: boolean
deviceId?: string
}
// Encrypted Message Payload
interface EncryptedMessagePayload {
isEncrypted: boolean
encryptedContent?: Uint8Array
messageType?: 'PreKey' | 'Normal'
senderDeviceId?: string
plainContent?: string
}
// Message Encryption Options
interface MessageEncryptionOptions {
recipientUserId: string
recipientDeviceId?: string
channelId?: string
isDirectMessage: boolean
}
// Safety Number Display Props
interface SafetyNumberDisplayProps {
peerUserId: string
peerName: string
peerIdentityKey: Uint8Array
onVerify?: () => void
}// Crypto
const PBKDF2_ITERATIONS = 100000
const SALT_LENGTH = 32
const KEY_LENGTH = 32
const IV_LENGTH = 12
const AUTH_TAG_LENGTH = 16
// Key Management
const DEFAULT_ONE_TIME_PREKEYS = 100
const MIN_ONE_TIME_PREKEYS = 20
const SIGNED_PREKEY_ROTATION_DAYS = 7
// Safety Number
const SAFETY_NUMBER_LENGTH = 60
const SAFETY_NUMBER_VERSION = 1-
Full Documentation:
/docs/features/E2EE-Complete.md -
Integration Summary:
/docs/E2EE-Integration-Summary.md - Signal Protocol: https://signal.org/docs/
- libsignal GitHub: https://github.com/signalapp/libsignal
| Problem | Solution |
|---|---|
| "E2EE not initialized" | Run initialize(password)
|
| "Failed to decrypt" | Re-establish session |
| "No prekey bundle" | Ask recipient to enable E2EE |
| "Master key not found" | Use recovery code |
| "Out of prekeys" | Auto-replenished (wait) |
For detailed information, see: /docs/features/E2EE-Complete.md