该提交新增了完整的 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 部署,内置安全防护与流量控制机制。
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
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
|