NIP-59:三层封装实现元数据隐藏
NIP-59 定义了一种封装 nostr 事件的协议,通过 rumor(未签名事件)、seal(kind 13,加密的 rumor)和 gift wrap(kind 1059,加密的 seal)三层结构实现元数据隐藏。加密基于 NIP-44,seal 使用发送者私钥加密,gift wrap 使用一次性随机密钥加密,并包含收件人 p 标签。协议支持可否认性、协作签名,并建议调整时间戳防止时序分析,以及基于 AUTH 保护收件人元数据。文章包含完整示例和 JavaScript 代码。
礼物包装
可选 中继
本 NIP 定义了一种封装任意 nostr 事件的协议。它可以隐藏给定事件的大部分元数据,支持协作签名等。
本 NIP 不定义任何消息协议。本 NIP 的应用应单独定义。
本 NIP 依赖于 NIP-44 的版本化加密算法。
概述
该协议使用三个主要概念来保护目标事件的传输:谣言、封印 和 礼物包装。
谣言是一个常规的 nostr 事件,但未签名。这意味着一旦泄露,无法验证其真实性。谣言被序列化为 JSON,加密后放入封印的content字段。然后封印由笔记作者签名。封印上公开的唯一信息是谁签署了它,而不是说了什么。封印被序列化为 JSON,加密后放入礼物包装的content字段。
这实现了跨层的关注点隔离:
- 谣言携带内容但未签名,这意味着如果泄露,会被中继和客户端拒绝,且无法被认证。这提供了一定程度的可否认性。
- 封印标识了作者,但不透露内容或接收者。
- 礼物包装可以添加元数据(接收者、标签、不同的作者),而不暴露真正的作者。
协议描述
1. 谣言事件种类
谣言 与未签名事件相同。任何事件种类都可以通过移除签名变成 谣言。
2. 封印事件种类
封印 是一个 kind:13 事件,它使用发送者的常规密钥包裹一个 谣言。封印 始终加密到接收者的公钥,但没有指向接收者的 p 标签。如果没有接收者或发送者的私钥,就无法知道谣言是给谁的。此事件中唯一公开的信息是谁在签名。
{
"id": "<id>",
"pubkey": "<真实作者的公钥>",
"content": "<加密的谣言>",
"kind": 13,
"created_at": 1686840217,
"tags": [],
"sig": "<真实作者公钥的签名>"
}
kind:13 中的标签必须始终为空。内部事件必须始终未签名。
3. 礼物包装事件种类
礼物包装 事件是一个 kind:1059 事件,它包裹任何其他事件。tags 应包含路由事件到预期接收者所需的任何信息,包括接收者的 p 标签或 NIP-13 工作量证明。
{
"id": "<id>",
"pubkey": "<随机的、一次性使用的公钥>",
"content": "<加密的 kind 13>",
"kind": 1059,
"created_at": 1686840217,
"tags": [["p", "<接收者公钥>"]],
"sig": "<随机的、一次性使用公钥的签名>"
}
加密有效载荷
加密按照 NIP-44 对 JSON 编码的事件进行。将加密有效载荷放入包装事件(封印 或 礼物包装)的 .content 中。
其他考虑
如果 谣言 是给多方准备的,或者作者希望保留一份加密副本,则可以为每个接收者单独包装和寻址一个 谣言。
规范的 created_at 时间属于 谣言。所有其他时间戳应被调整以挫败时间分析攻击。注意,一些中继不提供未来的事件,因此所有时间戳应为过去。
中继可能会选择不存储礼物包装的事件,因为它们在公开用途上不大。客户端可选择根据 NIP-13 向包装事件附加一定量的工作量证明,以证明该事件不是垃圾邮件或拒绝服务攻击。
为了保护接收者元数据,中继应根据用户 AUTH 限制对 kind 1059 事件的访问。在可能的情况下,客户端应仅向提供此保护的中继发送包装事件。
为了保护接收者元数据,中继应仅提供针对标记接收者的 kind 1059 事件。在可能的情况下,客户端应仅将包装事件发送给接收者的 读取 中继,这些中继实现了 AUTH,并拒绝向非接收者提供包装事件。
在 封印 和 礼物包装 层添加过期标签时,实现应为每层使用独立的随机时间戳。使用不同的 created_at 值可以增加时间方差,有助于防止元数据关联攻击。
由于签名密钥是随机的,中继应删除那些其 p 标签与 NIP-09 删除或 NIP-62 消失请求的签名者匹配的 kind 1059 事件。
示例
让我们在两方之间发送一个包裹的 kind 1 消息,内容为“你今晚去参加派对吗?”
- 作者私钥:
0beebd062ec8735f4243466049d7747ef5d6594ee838de147f8aab842b15e273 - 接收者私钥:
e108399bd8424357a710b606ae0c13166d853d327e47a6e5e038197346bdbf45 - 临时包装密钥:
4f02eac59266002db5801adc5270700ca69d5b8f761d8732fab2fbf233c90cbd
请注意,此消息协议不应在实际中使用,这仅是一个示例。请参考其他依赖于礼物包装的具体消息协议的 NIP。
1. 创建事件
创建一个带有消息、接收者以及任何其他标签的 kind 1 事件,并由作者签名。不要对事件进行签名。
{
"created_at": 1691518405,
"content": "Are you going to the party tonight?",
"tags": [],
"kind": 1,
"pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
"id": "9dd003c6d3b73b74a85a9ab099469ce251653a7af76f523671ab828acd2a0ef9"
}
2. 封印谣言
使用作者的私钥和接收者的公钥派生的对话密钥加密 JSON 编码的 谣言。将结果放入 kind 13 封印 事件的 content 字段。用作者的密钥签名。
{
"content": "AqBCdwoS7/tPK+QGkPCadJTn8FxGkd24iApo3BR9/M0uw6n4RFAFSPAKKMgkzVMoRyR3ZS/aqATDFvoZJOkE9cPG/TAzmyZvr/WUIS8kLmuI1dCA+itFF6+ULZqbkWS0YcVU0j6UDvMBvVlGTzHz+UHzWYJLUq2LnlynJtFap5k8560+tBGtxi9Gx2NIycKgbOUv0gEqhfVzAwvg1IhTltfSwOeZXvDvd40rozONRxwq8hjKy+4DbfrO0iRtlT7G/eVEO9aJJnqagomFSkqCscttf/o6VeT2+A9JhcSxLmjcKFG3FEK3Try/WkarJa1jM3lMRQqVOZrzHAaLFW/5sXano6DqqC5ERD6CcVVsrny0tYN4iHHB8BHJ9zvjff0NjLGG/v5Wsy31+BwZA8cUlfAZ0f5EYRo9/vKSd8TV0wRb9DQ=",
"kind": 13,
"created_at": 1703015180,
"pubkey": "611df01bfcf85c26ae65453b772d8f1dfd25c264621c0277e1fc1518686faef9",
"tags": [],
"id": "28a87d7c074d94a58e9e89bb3e9e4e813e2189f285d797b1c56069d36f59eaa7",
"sig": "02fc3facf6621196c32912b1ef53bac8f8bfe9db51c0e7102c073103586b0d29c3f39bdaa1e62856c20e90b6c7cc5dc34ca8bb6a528872cf6e65e6284519ad73"
}
3. 包装封印
使用你的临时、一次性随机密钥加密 JSON 编码的 kind 13 事件。将结果放入 kind 1059 的 content 字段。添加一个包含接收者公钥的 p 标签。使用上一步生成的随机密钥签名 礼物包装。
{
"content": "AhC3Qj/QsKJFWuf6xroiYip+2yK95qPwJjVvFujhzSguJWb/6TlPpBW0CGFwfufCs2Zyb0JeuLmZhNlnqecAAalC4ZCugB+I9ViA5pxLyFfQjs1lcE6KdX3euCHBLAnE9GL/+IzdV9vZnfJH6atVjvBkNPNzxU+OLCHO/DAPmzmMVx0SR63frRTCz6Cuth40D+VzluKu1/Fg2Q1LSst65DE7o2efTtZ4Z9j15rQAOZfE9jwMCQZt27rBBK3yVwqVEriFpg2mHXc1DDwHhDADO8eiyOTWF1ghDds/DxhMcjkIi/o+FS3gG1dG7gJHu3KkGK5UXpmgyFKt+421m5o++RMD/BylS3iazS1S93IzTLeGfMCk+7IKxuSCO06k1+DaasJJe8RE4/rmismUvwrHu/HDutZWkvOAhd4z4khZo7bJLtiCzZCZ74lZcjOB4CYtuAX2ZGpc4I1iOKkvwTuQy9BWYpkzGg3ZoSWRD6ty7U+KN+fTTmIS4CelhBTT15QVqD02JxfLF7nA6sg3UlYgtiGw61oH68lSbx16P3vwSeQQpEB5JbhofW7t9TLZIbIW/ODnI4hpwj8didtk7IMBI3Ra3uUP7ya6vptkd9TwQkd/7cOFaSJmU+BIsLpOXbirJACMn+URoDXhuEtiO6xirNtrPN8jYqpwvMUm5lMMVzGT3kMMVNBqgbj8Ln8VmqouK0DR+gRyNb8fHT0BFPwsHxDskFk5yhe5c/2VUUoKCGe0kfCcX/EsHbJLUUtlHXmTqaOJpmQnW1tZ/siPwKRl6oEsIJWTUYxPQmrM2fUpYZCuAo/29lTLHiHMlTbarFOd6J/ybIbICy2gRRH/LFSryty3Cnf6aae+A9uizFBUdCwTwffc3vCBae802+R92OL78bbqHKPbSZOXNC+6ybqziezwG+OPWHx1Qk39RYaF0aFsM4uZWrFic97WwVrH5i+/Nsf/OtwWiuH0gV/SqvN1hnkxCTF/+XNn/laWKmS3e7wFzBsG8+qwqwmO9aVbDVMhOmeUXRMkxcj4QreQkHxLkCx97euZpC7xhvYnCHarHTDeD6nVK+xzbPNtzeGzNpYoiMqxZ9bBJwMaHnEoI944Vxoodf51cMIIwpTmmRvAzI1QgrfnOLOUS7uUjQ/IZ1Qa3lY08Nqm9MAGxZ2Ou6R0/Z5z30ha/Q71q6meAs3uHQcpSuRaQeV29IASmye2A2Nif+lmbhV7w8hjFYoaLCRsdchiVyNjOEM4VmxUhX4VEvw6KoCAZ/XvO2eBF/SyNU3Of4SO",
"kind": 1059,
"created_at": 1703021488,
"pubkey": "18b1a75918f1f2c90c23da616bce317d36e348bcf5f7ba55e75949319210c87c",
"id": "5c005f3ccf01950aa8d131203248544fb1e41a0d698e846bd419cec3890903ac",
"sig": "35fabdae4634eb630880a1896a886e40fd6ea8a60958e30b89b33a93e6235df750097b04f9e13053764251b8bc5dd7e8e0794a3426a90b6bcc7e5ff660f54259",
"tags": [["p", "166bf3765ebd1fc55decfe395beff2ea3b2a4e0a8946e7eb578512b555737c99"]],
}
4. 选择性广播
仅向接收者的中继广播 kind 1059 事件。删除所有其他事件。
代码示例
JavaScript
import {bytesToHex} from "@noble/hashes/utils"
import type {EventTemplate, UnsignedEvent, Event} from "nostr-tools"
import {getPublicKey, getEventHash, nip19, nip44, finalizeEvent, generateSecretKey} from "nostr-tools"
type Rumor = UnsignedEvent & {id: string}
const TWO_DAYS = 2 * 24 * 60 * 60
const now = () => Math.round(Date.now() / 1000)
const randomNow = () => Math.round(now() - (Math.random() * TWO_DAYS))
const nip44ConversationKey = (privateKey: Uint8Array, publicKey: string) =>
nip44.v2.utils.getConversationKey(bytesToHex(privateKey), publicKey)
const nip44Encrypt = (data: EventTemplate, privateKey: Uint8Array, publicKey: string) =>
nip44.v2.encrypt(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey))
const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
JSON.parse(nip44.v2.decrypt(data.content, nip44ConversationKey(privateKey, data.pubkey)))
const createRumor = (event: Partial<UnsignedEvent>, privateKey: Uint8Array) => {
const rumor = {
created_at: now(),
content: "",
tags: [],
...event,
pubkey: getPublicKey(privateKey),
} as any
rumor.id = getEventHash(rumor)
return rumor as Rumor
}
const createSeal = (rumor: Rumor, privateKey: Uint8Array, recipientPublicKey: string) => {
return finalizeEvent(
{
kind: 13,
content: nip44Encrypt(rumor, privateKey, recipientPublicKey),
created_at: randomNow(),
tags: [],
},
privateKey
) as Event
}
const createWrap = (event: Event, recipientPublicKey: string) => {
const randomKey = generateSecretKey()
return finalizeEvent(
{
kind: 1059,
content: nip44Encrypt(event, randomKey, recipientPublicKey),
created_at: randomNow(),
tags: [["p", recipientPublicKey]],
},
randomKey
) as Event
}
// 使用上述示例的测试用例
const senderPrivateKey = nip19.decode(`nsec1p0ht6p3wepe47sjrgesyn4m50m6avk2waqudu9rl324cg2c4ufesyp6rdg`).data
const recipientPrivateKey = nip19.decode(`nsec1uyyrnx7cgfp40fcskcr2urqnzekc20fj0er6de0q8qvhx34ahazsvs9p36`).data
const recipientPublicKey = getPublicKey(recipientPrivateKey)
const rumor = createRumor(
{
kind: 1,
content: "Are you going to the party tonight?",
},
senderPrivateKey
)
const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey)
const wrap = createWrap(seal, recipientPublicKey)
// 接收者使用他们的私钥解包
const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey)
const unsealedRumor = nip44Decrypt(unwrappedSeal, recipientPrivateKey)
- 原文链接: github.com/nostr-protoco...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~