Edit: I've built a working POC. See the latest comment.
This is an improvement upon NIP-04. It makes private messaging leaks less metadata.
{
id: '9141dc50144cc243acfe78dc7799768e2f3eef2f857d8adc4fb8600dee6e8e9b',
pubkey: '0000000000000000000000000000000000000000000000000000000000000000',
created_at: 1643777968,
kind: 4,
tags: [
[
'shared',
'dbb06024d1ec94e1d0b76b2105410ecf85f01f0ecf6de563b5485288f6eff6c1'
]
],
content: 'ubrKJp8e2XKQ7utVAY66BQ==?iv=sy0vaW9Au0jIRSFqSrCjnA==',
sig: '78fdfffb8bc983af75a250cb13225b75a74e104a82881fa35d7efa36f48dcd5bedfe43f29b50c7c26a5d39b1bee7d4f23b0c3516b1149c6bc047198e5f9acf99'
}
Please poke holes in this idea (if you find any); I'd like to know whether this idea has merit or it's just full of hot air.
The JS code below is an implementation of this idea.
import * as secp from "@noble/secp256k1";
import crypto from "crypto";
const senderPriv = "d5f9b88ae04e7adb2fc075515e39df546df56d88ccdb304a9a779af1563d79ba";
const senderPub = "ab1a33b0cf3d8f8896c433e6996744e48f1401e6fbc94aea6f84291074fb1b75";
const recipientPriv = "c10d9871f37f5d7dae09e93f2a381593b57697e02e15b446fbc99531b4623555";
const recipientPub = "7002538efd7175b2b5fafe4ee5a933242081c067a48ff019deca56eb13ef2186";
const inboxAddress = "0000000000000000000000000000000000000000000000000000000000000000";
function toHexString(byteArray) {
return Array.prototype.map
.call(byteArray, function (byte) {
return ("0" + (byte & 0xff).toString(16)).slice(-2);
})
.join("");
}
async function broadcastKey() {
// the purpose of this is to announce sender's pubkey to recipient
// it's basically a normal kind `4` event
const dummyMessage = "adsfasdf" // doesn't matter
const unixTime = Math.floor(Date.now() / 1000);
const data = [0, senderPub, unixTime, 4, [["p", recipientPub]], dummyMessage];
const eventString = JSON.stringify(data);
const eventByteArray = new TextEncoder().encode(eventString);
const eventIdRaw = await secp.utils.sha256(eventByteArray);
const eventId = toHexString(eventIdRaw);
const signatureRaw = await secp.schnorr.sign(eventId, senderPriv);
const signature = toHexString(signatureRaw);
const sampleBroadcastEvent = {
id: eventId,
pubkey: senderPub,
created_at: unixTime,
kind: 4,
tags: [["p", recipientPub]],
content: dummyMessage,
sig: signature
}
// sampleBroadcastEvent looks like this:
/*
{
id: '714f104515c8980dc9a793cf15b0a8700f8be1e2436165217bfca6976975c1d4',
pubkey: 'ab1a33b0cf3d8f8896c433e6996744e48f1401e6fbc94aea6f84291074fb1b75',
created_at: 1643778625,
kind: 4,
tags: [
[
'p',
'7002538efd7175b2b5fafe4ee5a933242081c067a48ff019deca56eb13ef2186'
]
],
content: 'adsfasdf',
sig: '03e9b5c375188ff912d536cb8e59bc45e643b0eca642838e8fa8f8692c1ce7bb051a22ff83dd2d17aefc0703ea046aaef9ec3859559a01cbf45226957a57e520'
}
*/
// push sampleBroadcastEvent to relay: `["EVENT", sampleBroadcastEvent]`
// recipient can get the event: `["REQ", "foobar", {"#p": [recipientPub]}]`
// when both have each other's pubkey, they can generate the shared key
// when they both have the shared key, they can communicate privately through inbox address
}
async function generatePrivateEvent(priv, pub) {
const unencryptedMessage = "supersecret"
// `secp.getSharedSecret(senderPriv, "02" + recipientPub)`
// and
// `secp.getSharedSecret(recipientPriv, "02" + senderPub)`
// produce the same value
const sharedPointBytes = secp.getSharedSecret(priv, "02" + pub);
const sharedPoint = toHexString(sharedPointBytes);
const sharedX = sharedPoint.substr(2, 64)
const sharedXByteArray = new TextEncoder().encode(sharedX);
const sharedXByte = await secp.utils.sha256(sharedXByteArray);
const sharedXSha = toHexString(sharedXByte);
const iv = crypto.randomFillSync(new Uint8Array(16))
const ivBase64 = Buffer.from(iv.buffer).toString('base64')
const cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(sharedX, 'hex'),
iv
)
// to decrypt later on, use `crypto.createDecipheriv()`
let encryptedMessage = cipher.update(JSON.stringify(unencryptedMessage), 'utf8', 'base64')
encryptedMessage += cipher.final('base64')
encryptedMessage += "?iv=" + ivBase64
const unixTime = Math.floor(Date.now() / 1000);
const data = [0, inboxAddress, unixTime, 4, [["shared", sharedXSha]], encryptedMessage];
// event id is sha256 of data above
// sig is schnorr sig of id
const eventString = JSON.stringify(data);
const eventByteArray = new TextEncoder().encode(eventString);
const eventIdRaw = await secp.utils.sha256(eventByteArray);
const eventId = toHexString(eventIdRaw);
const signatureRaw = await secp.schnorr.sign(eventId, priv);
const signature = toHexString(signatureRaw);
return {
id: eventId,
pubkey: inboxAddress,
created_at: unixTime,
kind: 4,
tags: [["shared", sharedXSha]],
content: encryptedMessage,
sig: signature
}
}
(async () => {
console.log(await generatePrivateEvent(senderPriv, recipientPub))
console.log(await generatePrivateEvent(recipientPriv, senderPub))
})();
Edit: This issue has been edited to clarify.