ForcePilot/backend/package/yuxi/channels/adapters/nostr/crypto.py
Kris a1f8288d20 feat(nostr): 实现完整的 Nostr 协议适配器模块
该提交新增了完整的 Nostr 去中心化社交网络适配器实现,包含以下核心功能:
1. 基础加密与密钥处理:支持 nsec/npub/hex 格式密钥转换,实现 NIP-04/NIP-17 加解密
2. Relay 管理与健康监控:支持多 Relay 连接、自动重连、健康评分与自动选优
3. 事件与消息处理:实现事件校验、去重、速率限制、消息缓存与状态持久化
4. 个人资料管理:支持发布/导入/合并 Nostr Kind 0 元数据事件
5. 配对机制:实现安全的双向配对通信流程
6. Zap 功能:支持 NIP-57 打赏请求与收据解析
7. NIP-05 验证:实现域名身份验证
8. 配置系统:完整的配置校验与多账户支持
9. 监控与指标:提供连接监控、事件统计与健康快照
10. HTTP API:提供个人资料管理的 RESTful 接口
11. 安装向导:命令行配置向导与初始化流程

所有模块均遵循 Nostr 协议规范,支持多账户、多 Relay 部署,内置安全防护与流量控制机制。
2026-05-12 00:47:41 +08:00

140 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import time
from nostr_sdk import (
Event,
EventBuilder,
Keys,
Kind,
PublicKey,
Tag,
UnwrappedGift,
nip04_decrypt,
nip04_encrypt,
)
from yuxi.utils.logging_config import logger
class NostrCryptoError(Exception):
pass
def normalize_pubkey(raw: str) -> str:
if not raw or not raw.strip():
return ""
raw = raw.strip()
if raw.startswith("nostr:"):
raw = raw[6:]
if raw.startswith("npub1"):
try:
return PublicKey.from_bech32(raw).to_hex()
except Exception:
return ""
if raw.startswith("nprofile1"):
try:
from nostr_sdk import Profile
profile = Profile.from_bech32(raw)
return profile.public_key().to_hex()
except Exception:
return ""
if len(raw) == 64 and all(c in "0123456789abcdef" for c in raw):
return raw
if raw.startswith("nsec1"):
try:
keys = Keys.parse(raw)
return keys.public_key().to_hex()
except Exception:
return ""
return ""
class NostrCrypto:
def __init__(self, nsec_or_hex: str | None = None):
if nsec_or_hex:
try:
self._keys = Keys.parse(nsec_or_hex)
except Exception as e:
msg = f"Nostr 私钥解析失败: {e}。请检查配置中的 private_key (nsec/hex) 是否正确。"
logger.error(msg)
raise NostrCryptoError(msg) from e
else:
self._keys = Keys.generate()
logger.warning(
"未配置 Nostr 私钥已自动生成临时密钥。npub: %s。生产环境请配置正确的 private_key",
self._keys.public_key().to_bech32(),
)
self._public_key = self._keys.public_key()
self._secret_key = self._keys.secret_key()
@property
def npub(self) -> str:
return self._public_key.to_bech32()
def pubkey_hex(self) -> str:
return self._public_key.to_hex()
def nsec(self) -> str:
return self._secret_key.to_bech32()
def encrypt_nip04(self, plaintext: str, receiver_pubkey_hex: str) -> str:
receiver_pk = PublicKey.from_hex(receiver_pubkey_hex)
return nip04_encrypt(self._secret_key, receiver_pk, plaintext)
def decrypt_nip04(self, ciphertext: str, sender_pubkey_hex: str) -> str:
sender_pk = PublicKey.from_hex(sender_pubkey_hex)
return nip04_decrypt(self._secret_key, sender_pk, ciphertext)
async def encrypt_nip17(self, plaintext: str, receiver_pubkey_hex: str) -> str:
receiver_pk = PublicKey.from_hex(receiver_pubkey_hex)
rumor = EventBuilder.private_msg_rumor(receiver_pk, plaintext).build(self._public_key)
sealed = await EventBuilder.seal(self._keys, receiver_pk, rumor)
return sealed.sign_with_keys(self._keys).as_json()
async def decrypt_nip17(self, wrapped_json: str) -> str:
event = Event.from_json(wrapped_json)
unwrapped = UnwrappedGift.from_gift_wrap(event)
if unwrapped is None:
raise NostrCryptoError("NIP-17 解密失败: Gift Wrap 解封返回空")
rumor = unwrapped.rumor()
if rumor is None:
raise NostrCryptoError("NIP-17 解密失败: Rumor 为空")
return rumor.content()
def build_and_sign_event(self, kind: int, content: str, tags: list[list[str]]) -> dict:
builder = EventBuilder(Kind(kind), content)
parsed_tags = []
for tag in tags:
try:
parsed_tags.append(Tag.parse(tag))
except Exception as e:
raise NostrCryptoError(f"Tag 解析失败: {tag}, 错误: {e}") from e
if parsed_tags:
builder = builder.tags(parsed_tags)
builder = builder.custom_created_at(int(time.time()))
event = builder.sign_with_keys(self._keys)
return json.loads(event.as_json())
def verify_event(self, raw_event: dict) -> bool:
try:
event = Event.from_json(json.dumps(raw_event))
event.verify()
return True
except Exception:
return False
def verify_event_strict(self, raw_event: dict) -> bool:
try:
event = Event.from_json(json.dumps(raw_event))
event_id = event.id()
expected_id = raw_event.get("id", "")
if event_id.to_hex() != expected_id and event_id.to_bech32() != expected_id:
return False
event.verify()
return True
except Exception:
return False