ForcePilot/backend/package/yuxi/channel/extensions/nostr/nip44.py
Kris 16455cb303 feat(channel): 添加 Nostr 渠道扩展
新增 Nostr 渠道扩展,支持在 Yuxi 平台中集成 Nostr 去中心化社交协议。

包含以下功能模块:
- bus: 事件总线与中继通信
- account: 账户管理
- key_utils: 密钥工具
- gateway: SSE/WebSocket 网关接入
- outbound: 外发消息管理
- gift_wrap: Gift Wrap 加密
- nip44: NIP-44 加密协议
- config_schema: 配置模式
- defaults: 默认配置
- profile_core: 用户资料核心
- profile_publisher: 资料发布
- event_utils: 事件工具
- deletion: 事件删除
- reactions: 表情反应
- metrics: 指标监控
- seen_tracker: 已读追踪
- session_route: 会话路由
- state_store: 状态存储
2026-05-21 11:31:51 +08:00

129 lines
4.1 KiB
Python

import base64
import hashlib
import hmac
import os
import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from coincurve import PublicKey
NIP44_V2_VERSION = 0x02
NIP44_V2_SALT = b"nip44-v2"
MIN_PADDING_SIZE = 32
MAX_PADDING_SIZE = 65535
def _get_shared_x(sk_bytes: bytes, pk_hex: str) -> bytes:
pk = PublicKey(bytes.fromhex("02" + pk_hex))
shared_point = pk.multiply(sk_bytes)
return shared_point.format(compressed=False)[1:33]
def get_conversation_key(sk_bytes: bytes, pk_hex: str) -> bytes:
shared_x = _get_shared_x(sk_bytes, pk_hex)
hkdf = HKDF(
algorithm=SHA256(),
length=32,
salt=NIP44_V2_SALT,
info=b"",
)
return hkdf.derive(shared_x)
def get_message_keys(conversation_key: bytes, nonce: bytes) -> tuple[bytes, bytes, bytes]:
hkdf = HKDF(
algorithm=SHA256(),
length=76,
salt=None,
info=nonce,
)
keys = hkdf.derive(conversation_key)
chacha_key = keys[0:32]
chacha_nonce = keys[32:48]
hmac_key = keys[48:76]
return chacha_key, chacha_nonce, hmac_key
def _calc_padding_len(plaintext_len: int) -> int:
if plaintext_len <= 0:
return MIN_PADDING_SIZE
if plaintext_len > MAX_PADDING_SIZE:
raise ValueError(f"plaintext too long: {plaintext_len}")
if plaintext_len <= 32:
return 32
next_power = 1
while next_power < plaintext_len:
next_power <<= 1
chunk = next_power // 8
if chunk < 32:
chunk = 32
padded = ((plaintext_len - 1) // chunk + 1) * chunk
return padded
def pad(plaintext: str) -> bytes:
data = plaintext.encode("utf-8")
length = len(data)
padded_len = _calc_padding_len(length)
padding = padded_len - length
result = struct.pack("<H", length) + data + b"\x00" * padding
return result
def unpad(padded: bytes) -> str:
if len(padded) < 2:
raise ValueError("padded data too short")
length = struct.unpack("<H", padded[:2])[0]
if length > len(padded) - 2:
raise ValueError(f"invalid padding length: {length}")
data = padded[2:2 + length]
return data.decode("utf-8")
def _chacha20_encrypt(key: bytes, nonce: bytes, plaintext: bytes) -> bytes:
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None)
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
def _chacha20_decrypt(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None)
decryptor = cipher.decryptor()
return decryptor.update(ciphertext) + decryptor.finalize()
def _hmac_sha256(key: bytes, data: bytes) -> bytes:
return hmac.new(key, data, hashlib.sha256).digest()
def nip44_encrypt(sk_bytes: bytes, pk_hex: str, plaintext: str) -> str:
conversation_key = get_conversation_key(sk_bytes, pk_hex)
nonce = bytes([NIP44_V2_VERSION]) + os.urandom(31)
chacha_key, chacha_nonce, hmac_key = get_message_keys(conversation_key, nonce)
padded = pad(plaintext)
ciphertext = _chacha20_encrypt(chacha_key, chacha_nonce, padded)
mac = _hmac_sha256(hmac_key, nonce + ciphertext)
payload = nonce + ciphertext + mac
return base64.b64encode(payload).decode()
def nip44_decrypt(sk_bytes: bytes, pk_hex: str, payload_b64: str) -> str:
payload = base64.b64decode(payload_b64)
if len(payload) < 64:
raise ValueError("payload too short")
version = payload[0]
if version != NIP44_V2_VERSION:
raise ValueError(f"unsupported nip44 version: {version}")
nonce = payload[:32]
ciphertext = payload[32:-32]
mac = payload[-32:]
conversation_key = get_conversation_key(sk_bytes, pk_hex)
chacha_key, chacha_nonce, hmac_key = get_message_keys(conversation_key, nonce)
expected_mac = _hmac_sha256(hmac_key, nonce + ciphertext)
if not hmac.compare_digest(expected_mac, mac):
raise ValueError("invalid mac")
padded = _chacha20_decrypt(chacha_key, chacha_nonce, ciphertext)
return unpad(padded)