NIP-44 加密訊息 v2
深入了解 Nostr 的改進加密標準,XChaCha20-Poly1305 和更安全的密鑰派生。
什麼是 NIP-44?
NIP-44 定義了 Nostr 的改進加密標準,解決了 NIP-04 的多個安全問題。 它使用 XChaCha20-Poly1305 認證加密、HKDF 密鑰派生,並提供版本控制和固定長度填充。
推薦使用: NIP-44 是 NIP-04 的安全替代方案。新的客戶端應該優先實現 NIP-44, 並逐步淘汰對 NIP-04 的支持。
NIP-44 vs NIP-04
| 特性 | NIP-04 | NIP-44 |
|---|---|---|
| 加密算法 | AES-256-CBC | XChaCha20-Poly1305 |
| 認證加密 (AEAD) | 無 | 有 |
| 密鑰派生 | 直接使用 ECDH 共享點 | HKDF-SHA256 |
| 填充方式 | PKCS7(洩露長度) | 固定長度填充 |
| 版本控制 | 無 | 有 |
| Nonce 長度 | 16 bytes (IV) | 24 bytes |
加密流程
ECDH 密鑰交換
shared_point = secp256k1_ecdh(私鑰_A, 公鑰_B)
HKDF 密鑰派生
conversation_key = hkdf_sha256(shared_point, "nip44-v2")
生成 Nonce
生成 24 bytes 隨機 nonce
填充訊息
將訊息填充到預定義長度(隱藏實際長度)
XChaCha20-Poly1305
使用 conversation_key 和 nonce 加密,生成密文和認證標籤
組合輸出
version(1) || nonce(24) || ciphertext || tag(16)
密鑰派生 (HKDF)
NIP-44 使用 HKDF-SHA256 從 ECDH 共享點派生對話密鑰:
// ECDH 計算共享點
const sharedX = secp256k1.getSharedSecret(privateKey, publicKey).slice(1, 33)
// HKDF 密鑰派生
// 1. Extract: 從共享點提取偽隨機密鑰
const prk = hkdf.extract(sha256, sharedX, salt)
// 2. Expand: 擴展為對話密鑰
const conversationKey = hkdf.expand(sha256, prk, "nip44-v2", 32)
// conversationKey 用於所有該對話的加密/解密 填充機制
NIP-44 使用固定長度填充來隱藏訊息的實際長度:
// 填充長度表(預定義的固定長度)
const PADDING_SIZES = [
32, 64, 128, 256, 512, 1024, 2048, 4096,
8192, 16384, 32768, 65536
]
function calcPaddedLen(unpaddedLen) {
// 找到大於訊息長度的最小填充長度
for (const size of PADDING_SIZES) {
if (unpaddedLen <= size) return size
}
throw new Error('Message too long')
}
// 填充格式:[2 bytes 長度][訊息][零填充]
function pad(message) {
const len = message.length
const paddedLen = calcPaddedLen(len + 2) // +2 for length prefix
const padded = new Uint8Array(paddedLen)
// 寫入長度(big-endian)
padded[0] = (len >> 8) & 0xff
padded[1] = len & 0xff
// 寫入訊息
padded.set(message, 2)
return padded
} XChaCha20-Poly1305
NIP-44 使用 XChaCha20-Poly1305 進行認證加密:
| 元件 | 說明 |
|---|---|
| XChaCha20 | 串流加密,24-byte nonce,防止 nonce 重用 |
| Poly1305 | 訊息認證碼(MAC),確保完整性 |
| AEAD | 認證加密,同時提供機密性和完整性 |
程式碼範例
加密
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import { hkdf } from '@noble/hashes/hkdf'
import { sha256 } from '@noble/hashes/sha256'
import * as secp256k1 from '@noble/secp256k1'
function encrypt(privateKey, recipientPubkey, message) {
// 1. ECDH 計算共享點
const sharedX = secp256k1.getSharedSecret(
privateKey,
recipientPubkey
).slice(1, 33)
// 2. HKDF 派生對話密鑰
const conversationKey = hkdf(
sha256,
sharedX,
new Uint8Array(32), // salt
'nip44-v2',
32
)
// 3. 生成隨機 nonce
const nonce = crypto.getRandomValues(new Uint8Array(24))
// 4. 填充訊息
const padded = pad(new TextEncoder().encode(message))
// 5. XChaCha20-Poly1305 加密
const cipher = xchacha20poly1305(conversationKey, nonce)
const ciphertext = cipher.encrypt(padded)
// 6. 組合輸出:version || nonce || ciphertext
const result = new Uint8Array(1 + 24 + ciphertext.length)
result[0] = 2 // version 2
result.set(nonce, 1)
result.set(ciphertext, 25)
return base64.encode(result)
} 解密
function decrypt(privateKey, senderPubkey, payload) {
const data = base64.decode(payload)
// 1. 解析版本
const version = data[0]
if (version !== 2) throw new Error('Unsupported version')
// 2. 提取 nonce 和密文
const nonce = data.slice(1, 25)
const ciphertext = data.slice(25)
// 3. ECDH 計算共享點
const sharedX = secp256k1.getSharedSecret(
privateKey,
senderPubkey
).slice(1, 33)
// 4. HKDF 派生對話密鑰
const conversationKey = hkdf(
sha256,
sharedX,
new Uint8Array(32),
'nip44-v2',
32
)
// 5. XChaCha20-Poly1305 解密
const cipher = xchacha20poly1305(conversationKey, nonce)
const padded = cipher.decrypt(ciphertext)
// 6. 移除填充
return unpad(padded)
} 安全改進
認證加密
Poly1305 MAC 確保密文未被竄改,防止 padding oracle 和 bit-flipping 攻擊。
密鑰隔離
HKDF 密鑰派生將 ECDH 共享點轉換為專用密鑰,提供更好的密鑰分離。
長度隱藏
固定長度填充隱藏訊息的實際長度,防止基於長度的流量分析。
版本控制
內建版本欄位允許未來升級加密方案而保持向後兼容。
使用場景
// NIP-44 加密可用於多種事件類型
// Kind 4: 私訊(建議使用 NIP-44 而非 NIP-04)
// Kind 1059: Gift Wrap(用於隱藏元數據)
// 其他需要加密的自定義事件類型
// 範例:使用 NIP-44 加密的私訊
{
"kind": 4,
"content": "<NIP-44 加密的內容>",
"tags": [
["p", "<接收者公鑰>"]
],
...
} 實作建議: 使用經過審計的加密庫如 @noble/ciphers 和 @noble/hashes。 不要自己實現加密算法。測試向量可在 NIP-44 規範中找到。
注意: NIP-44 仍然沒有前向保密(Forward Secrecy)。如果長期私鑰洩露, 過去的所有訊息都可能被解密。對於高度敏感的通訊, 考慮使用支持 PFS 的協議。