Open vinliao opened 2 years ago
I like this idea, but for a different NIP instead of NIP04. And one can use random keys to sign instead of a blank key.
Maybe instead of using a shared key parties could instead send an initial message through NIP04 containing a random encryption key and then proceed using this new protocol using that?
Or, instead of a blank key (inbox address), both parties use MuSig2 as the event's pubkey.
Ideally, no one can even surveil which pubkey talks to which pubkey. The problem here seems to be privately announcing sender's pubkey before both can talk privately using this new protocol.
Introducing "encryption key" to Nostr means future devs have to juggle between RSA and Schnorr, no?
I think loquaz
and nostr.chat
are suited to implement this idea, to experiment and see whether this new protocol actually works.
By encryption key I meant the key that would be used to encrypt the messages using the same AES-CBC that NIP04 was using already, not RSA.
Revision to the idea:
Result:
Below is the JS implementation of this revised idea. I've published an event to a relay, and it's actually accepted as a valid NIP-04 event.
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)
// not sure how safe this is, but sharedX is 32 bytes
// which can be used as a privatekey, and which
// a schnorr pubkey can be derived from
const schnorrMutualKey = secp.schnorr.getPublicKey(sharedX)
const schnorrMutualHex = toHexString(schnorrMutualKey)
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
)
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, schnorrMutualHex, unixTime, 4, [], 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);
// sharedX is used as a private key here
const signatureRaw = await secp.schnorr.sign(eventId, sharedX);
const signature = toHexString(signatureRaw);
return {
id: eventId,
pubkey: schnorrMutualHex,
created_at: unixTime,
kind: 4,
tags: [],
content: encryptedMessage,
sig: signature
}
// example output
/*
{
id: '24d6b6ba8b938c393ed13ee8fb1cfd639fc841f936dcbe28a039338340acda03',
pubkey: '6646a4323f0e7234488579975d2dcaabfe87e8b086a91ddc3f08f4c9e978e82d',
created_at: 1643932277,
kind: 4,
tags: [],
content: 'lXDprQheAyTQ10AyKmYXcg==?iv=15FpmW00Nk+Fe+zdM+KCXA==',
sig: '728b8dec8719cef760afd5a6b46ee02054441e9547c1c3af5c6fdd834b2c4ea7aeecb0b1c1584220c92278a1b987c1c8243586c5319fe5d76794e5c6341fc1c4'
}
*/
}
async function decrypt(priv, pub) {
const sharedPointBytes = secp.getSharedSecret(priv, "02" + pub);
const sharedPoint = toHexString(sharedPointBytes);
const sharedX = sharedPoint.substr(2, 64)
const schnorrMutualKey = secp.schnorr.getPublicKey(sharedX)
const schnorrMutualHex = toHexString(schnorrMutualKey)
// request to relay: ["REQ", "foobar", [{"authors": schnorrMutualHex}]]
// let's say we get back the data, then parse the encrypted message
const encryptedMessage = "lXDprQheAyTQ10AyKmYXcg=="
const iv = Buffer.from("15FpmW00Nk+Fe+zdM+KCXA==", "base64")
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(sharedX, 'hex'), iv)
const message = Buffer.concat([decipher.update(encryptedMessage, 'base64'), decipher.final()]);
console.log(message.toString()) // supersecret
}
I've sent a private message to @fiatjaf's known pubkey on Branle (3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d) with a dummy pubkey (ab1a33b0cf3d8f8896c433e6996744e48f1401e6fbc94aea6f84291074fb1b75) through wss://relayer.fiatjaf.com
. Here's what the event looks like:
{
id: '66337672d578940cf97ec84a8d82e547564ec527a26ac275b039d52c7a7cfe4b',
pubkey: 'c00a84bd0c368c32a339e7200995c419ebd4a9fdc0b4230f82271eb9ea13d394',
created_at: 1643934337,
kind: 4,
tags: [],
content: 'KO8Cqvw5RIuynVkXjJze5g==?iv=n4YGW9p0L/RoFbs0t3vBYA==',
sig: 'f0e0b1f54920c6fa636b6151c4268e807e7f8f5457042a2e318d46171edc89b1956697d1e7cda011ad074252675c95db734104f19a243351b63c9275062bee86'
}
Neither pubkeys are displayed on the event. With this improvement, NIP-04 only leaks timestamp.
I like this idea, but for a different NIP instead of NIP04.
If this idea actually works in improving privacy, I agree that this should be a new NIP and a new kind
.
I've implemented a half-baked POC to test this idea, and it's pretty... bad - it's a super-mega-complex stuff added on top of NIP-04 with marginal benefit. Not worth the headache.
Hahaha, thank you for your honesty and for actually trying to implement. I was indeed afraid it would be too complex, but I thought that was just me being lazy. Since you, the creator of the idea, say it's bad super-mega-complex, then I'll follow you.
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.
Wouldn't it be easier, on the client side, for the sender to create two disposable profiles, and NIP-04 a message to the receiver from the first profile with the second profile (and other information if desired, such as a private relay) as encrypted payload.
Receiver then creates a disposable profile and begins the conversation with the second profile from the sender (aka initiator).
It appears to have the advantage of being able to be implemented (🤞) using existing primitives, but could be formalised in a new NIP with a new kind.
(I see similarities to the concept you describe here https://github.com/fiatjaf/nostr/issues/70#issue-1126915577)
It seems to me that using two disposable profiles makes impersonation easy. I think the alias system described on #70 is good enough to obfuscate NIP-04 pubkeys, and it's also extensible enough for other purposes.
It seems to me that using two disposable profiles makes impersonation easy.
Three disposable profiles are involved, and how do you see any third party gaining access to the private keys of any of them (required for impersonation), or linking them to either of the main profiles?
I can create two pubkeys, send one to you (encrypted), claiming to be some popular guy, telling you to trust me that I'm that popular guy, then tell you to chat with me through the pubkey I've encrypted to you. That counts as impersonation to me.
telling you to trust me that I'm that popular guy
This popular guy's public key is not well known?
Telling me to do something, particularly to trust, isn't the same as me doing it.
This may be an attempt at impersonation but it isn't easy to succeed with this approach.
Thanks to the comment above by @fiatjaf and @HamishMacEwan - I took some inspiration from their ideas. The ideas thrown around in this issue have merit. I've built a working POC: https://github.com/vinliao/clust. NIP-04 is more private now.
It's still clunky as hell, but it works. I'm gonna iron out a few UX details before sharing it to the Telegram group. Let me know if the explanation in the readme is lacking, or if there's a bug in the CLI app.
So what is the state of this? Please update the original description as we all agree that events with publicly known private keys are not cool. Heck, random others could even delete them as they appeared.
So do I understand right the last iteration was:
I find this protocol quite straight-forward and if I understood it correctly, could implement and try it in NostrPostrLib, too.
I don't know the state of this, we're free to experiment with many things. So let me just throw some random thoughts:
NIP-04 was never supposed to be the perfect solution, just the simplest possible protocol until someone else made a good thing later (I was actually opposed to NIP-04 since the beginning).
My personal preferences are, in order:
Another unrelated thing I think is that we should try to address other use cases before DMs. It's very hard to gain users for DMs, the market is too crowded, and Nostr doesn't excel in things that are needed in DMs -- for example, phone notifications. I think group chats (which are public by design) could benefit more from Nostr (and benefit Nostr more).
@fiatjaf you are derailing this issue a bit.
I would hope to see the above to be pushed forward to replace nip-4 over some lets-use-telegram or lets-trust-relays-pinky-swear. The metadata leak of what I last described above is minimal and way better than nip-4 which sadly now is the default in all clients.
Not all group chats are public but for public group chats you could use kind a new "kind" but apply most of kind-1 to it. Clients would not show those in the feed but only show those groups that you subscribed to. A group is simply tagged like a hashtag. Most importantly for this would be to make abundantly clear that these chats are public.
What I was saying is that NIP-04 should not be replaced at all. These ideas to make DMs better belong to different NIPs.
Also since there are multiple different approaches being talked about by different people and none of them seems to be obviously the best possible way to do anything (they all look hacky and ugly to me), my suggestion was that interested parties experiment with them.
So I think I came up with something simple enough to gain traction. I just updated the referenced nips PR to a completely different approach which just removes the leaky data from the messages. A summary is here.
The upside over nip-4:
Downside:
[](e/<event id>)
. Of course such event linking could lead to clients leaking interest in kind-18 events again.The linked explanation:
All mentioned issues are addressed by a massive simplification of the protocol. The new protocol is:
- Pretend it's nip-04
- Put recipient only on first messages or not if they are following you already. False recipients are encouraged on subsequent messages.
- Query all your follows' kind-18 events regardless of the advertised recipient.
- Query all kind-18 events that advertised you as recipient.
- Query all events from pubkeys that previously sent you events using some privacy tools such as TOR.
- Or ... query all kind-18 events as long as it's low volume.
Alternatively, would there be any interest in allowing relays to require REQ
messages for kind 4 events to include a pubkey and signature? So that relays only deliver events if the REQ
signature is valid and pubkey matches the recipient for the kind 4 events. I know it doesn't help if you do not trust the relays, but could be a simple change towards improving the situation.
@tclementdev that's what NIP-42 is doing, and is being implemented in multiple relays and clients already.
@fiatjaf ah thank you, I missed that. Now one would need to make sure to only use relays that restrict kind 4 access for DMs.
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.
Idea:
Example of such an event:
Request to relay to get such an event:
["REQ", "foobar", {"#shared", sha256(shared_key)}]
Cons of this idea:
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.
Edit: This issue has been edited to clarify.