From 44af48327f65c4dd61c85a6da13db084510c4d27 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 14 Dec 2024 13:18:54 -0300 Subject: [PATCH 1/3] nip4e: decoupling encryption from identity --- 4e.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 4e.md diff --git a/4e.md b/4e.md new file mode 100644 index 00000000..1700fb34 --- /dev/null +++ b/4e.md @@ -0,0 +1,82 @@ +NIP-4e +====== + +Decoupling encryption from identity +----------------------------------- + +`optional` `draft` + +This NIP describes a system for users to share private data between their own devices that doesn't rely on all devices holding the user account private key. + +### The problem + +Currently many NIPs rely on encrypting data from the user to themselves -- such that the data can be accessed later on a different device -- using NIP-04 or NIP-44 and the users as both the sender and the receiver, e.g. [NIP-51](51.md) and [NIP-60](60.md). This works fine, but it assumes all devices have direct or indirect access to the same secret key. This assumption cannot be fulfilled in the case of approaches where the key isn't known, such as when using FROST or MuSig2 signers. + +Also, in some use cases having the encryption key be on device can drastically increase performance of encrypting and decrypting stuff, and such a thing is not possible to do while also using [NIP-46](46.md) for keeping the user's main Nostr key safer. It's also not possible to perform any encryption while offline if the encryption keys live in a remote bunker. + +There are probably other advantages to not tying the user's identity to the keys used for more mundane things such as encryption, which we can write here later. + +### The solution + +1. Every client on every device can generate a new _device key_ and store it locally, while making its public key public in a Nostr event. +2. The first device to come into the world will generate a random _encryption key_. +3. When another device's _device key_ is spotted, the device that knows the original encryption key encrypts that key to the target device's _device key_ using [NIP-44](44.md) and sends it out. +4. Encryption and decryption are performed using the _encryption key_ using the [NIP-44](44.md) algorithm, skipping the first step and proceeding with _conversation key_ set to the shared _encryption key_. + +### Specifics + +All events should be signed by the user's pubkey. + +- encryption key announcement (`kind:4330`) + +The purpose of this event is for a client to tell other clients that a global encryption key exists (and what is the latest). Each _encryption key_ has an ID given by the first 16 chars of its hex-encoded sha256 hash output. + +```jsonc +{ + "kind": 4330, + "pubkey": "", + "tags": [ + ["p", ""], + ["latest", ""] + ] + // other fields... +} +``` + +- device key announcement event (`kind:4331`) + +```jsonc +{ + "kind": 4331, + "pubkey": "", + "tags": [ + ["p", ""] + ], + "content": "device key for client X on platform Y" + // other fields... +} +``` + +- encryption key sharing event (`kind:4332`) + +These should be deleted after they're read and used. + +```jsonc +{ + "kind": 4332, + "pubkey": "", + "tags": [ + ["p", ""] + ], + "content": "" + // other fields... +} +``` + +- Usage + + - clients can rotate the latest _encryption key_ for a user anytime -- any time they do it they should publish a new `kind:4330` event then a new `kind:4332` event for every other client they know of. + - clients can rotate their _device key_, which is equivalent to them being a totally new client from the point of view of the other clients. + - clients may exclude _device keys_ from other clients known to be compromised by just blacklisting them and then rotating the _encryption key_. + - since every new iteration of the _encryption key_ has a natural ID, whenever it is used to encrypt something (for example, a NIP-51 list), the ID should be added to the event as a tag `["ekey", ""]`. + - clients should keep track of past _encryption keys_ until they're confident that all the possible events that may have used the previous _encryption key_ were properly decrypted and updated to the latest key. From db85e13a58594a66d9084ddeb24e196263d24744 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 16 Feb 2025 23:36:34 -0300 Subject: [PATCH 2/3] update flow and add images. --- 4e.md | 70 ++++++++++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/4e.md b/4e.md index 1700fb34..548d9a62 100644 --- a/4e.md +++ b/4e.md @@ -10,7 +10,7 @@ This NIP describes a system for users to share private data between their own de ### The problem -Currently many NIPs rely on encrypting data from the user to themselves -- such that the data can be accessed later on a different device -- using NIP-04 or NIP-44 and the users as both the sender and the receiver, e.g. [NIP-51](51.md) and [NIP-60](60.md). This works fine, but it assumes all devices have direct or indirect access to the same secret key. This assumption cannot be fulfilled in the case of approaches where the key isn't known, such as when using FROST or MuSig2 signers. +Currently many NIPs rely on encrypting data from the user to themselves -- such that the data can be accessed later on a different device -- using NIP-04 or NIP-44 and the users as both the sender and the receiver, e.g. [NIP-51](51.md) and [NIP-60](60.md). This works fine, but it assumes all devices have direct or indirect access to the same secret key. This assumption cannot be fulfilled in the case of approaches where the key isn't known, such as when using bunkers powered by FROST, MuSig2 or hosted secure enclaves. Also, in some use cases having the encryption key be on device can drastically increase performance of encrypting and decrypting stuff, and such a thing is not possible to do while also using [NIP-46](46.md) for keeping the user's main Nostr key safer. It's also not possible to perform any encryption while offline if the encryption keys live in a remote bunker. @@ -18,65 +18,61 @@ There are probably other advantages to not tying the user's identity to the keys ### The solution -1. Every client on every device can generate a new _device key_ and store it locally, while making its public key public in a Nostr event. -2. The first device to come into the world will generate a random _encryption key_. -3. When another device's _device key_ is spotted, the device that knows the original encryption key encrypts that key to the target device's _device key_ using [NIP-44](44.md) and sends it out. -4. Encryption and decryption are performed using the _encryption key_ using the [NIP-44](44.md) algorithm, skipping the first step and proceeding with _conversation key_ set to the shared _encryption key_. +1. Every client can generate a new _client key_ and store it locally, while making its public key public in a Nostr event. +2. The first client to come into the world will generate a random _encryption key_. +3. When another client's _client key_ is spotted, the client that knows the original encryption key encrypts that key to the target client's _client key_ using [NIP-44](44.md) and sends it out. +4. Encryption and decryption are performed using the _encryption key_, not the user's _identity key_. -### Specifics +### The protocol flow -All events should be signed by the user's pubkey. - -- encryption key announcement (`kind:4330`) - -The purpose of this event is for a client to tell other clients that a global encryption key exists (and what is the latest). Each _encryption key_ has an ID given by the first 16 chars of its hex-encoded sha256 hash output. +1. **Alice** creates a keypair `(a, A)` (`a` is the secret key, `A` is the public key) on some onboarding website, say **jump.nostrstart.com**. +2. `A` is Alice's main identity on Nostr, her npub will be, say, `npub1A`; +3. Alice installs a client called **Cope**, **Cope** somehow realizes Alice can't use her `a` secret key for encryption because it's behind a FROST bunker, so **Cope** creates an encryption keypair `(e, E)`. This doesn't change Alice's identity, it will only be used for encryption. +4. **Cope** publishes an event (`kind:10044`) to announce this to the world: ```jsonc { - "kind": 4330, - "pubkey": "", + "kind": 10044, + "pubkey": "", "tags": [ - ["p", ""], - ["latest", ""] + ["n", ""] // `n` is for "encryption", doesn't matter ] - // other fields... } ``` -- device key announcement event (`kind:4331`) +5. Now **Bob** (keypairs `(b, B)`) will send a DM to **Alice**. Because Bob's client fetched Alice's `kind:10044` event, instead of computing the conversation key with `ecdh(b, A)` he does `ecdh(b, E) = S` +6. Because Alice knows `e` Alice can decrypt Bob's message doing `ecdh(e, B) = S` and all is good +7. Now the fun part starts: Alice has decided to use a client called **Tortilla** to chat on her phone, and **Tortilla* wants to do encryption stuff. +8. **Tortilla** sees that Alice has a `kind:10044` published, which means **Tortilla** won't create a new key, **Tortilla** will have to ask for **Cope** to share that key securely. So **Tortilla** generates a local keypair `(t, T)` that won't be shown or leave the device ever, and **Tortilla** publishes an announcement (`kind:4454`) for that local key (signed by Alice): ```jsonc { - "kind": 4331, - "pubkey": "", + "kind": 4454, + "pubkey": "", "tags": [ - ["p", ""] - ], - "content": "device key for client X on platform Y" - // other fields... + ["client", "Tortilla on Android"], + ["pubkey", ""] + ] } ``` -- encryption key sharing event (`kind:4332`) - -These should be deleted after they're read and used. +9. **Tortilla** cannot proceed without known the secret key `e`, so it has to tell the user to turn **Cope** on. +10. Alice opens up **Cope** and **Cope** immediately looks for all `kind:4454` events from Alice, and sees that there is this app called "Tortilla on Android" signed by Alice herself, so **Cope** publishes the secret key `e` nip44-encrypted to `ecdh(c, T)` -- in which `c` is the secret key of a keypair that **Cope** has just generated locally. **Cope** does that using a new event, `kind:4455`: ```jsonc { - "kind": 4332, - "pubkey": "", + "kind": 4455, + "pubkey": "" "tags": [ - ["p", ""] + ["P", ""] + ["p", ""] ], - "content": "" - // other fields... + "content": "" } ``` -- Usage +12. Immediately **Tortilla** wakes up and sees the `kind:4455` that had just been published by **Cope**, decrypts the content using `ecdh(t, C)` and now **Tortilla** also knows the secret key `e`. **Tortilla** can now decrypt and encrypt the same things **Cope** could before. - - clients can rotate the latest _encryption key_ for a user anytime -- any time they do it they should publish a new `kind:4330` event then a new `kind:4332` event for every other client they know of. - - clients can rotate their _device key_, which is equivalent to them being a totally new client from the point of view of the other clients. - - clients may exclude _device keys_ from other clients known to be compromised by just blacklisting them and then rotating the _encryption key_. - - since every new iteration of the _encryption key_ has a natural ID, whenever it is used to encrypt something (for example, a NIP-51 list), the ID should be added to the event as a tag `["ekey", ""]`. - - clients should keep track of past _encryption keys_ until they're confident that all the possible events that may have used the previous _encryption key_ were properly decrypted and updated to the latest key. +### The protocol flow again, now in a colorful infographic + +![](https://cdn.azzamo.net/89c543d261ad0d665c1dea78f91e527c2e39e7fe503b440265a3c47e63c9139f.png) From 5de76542c345475a05d31aca40d608e3070cb6ad Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Oct 2025 09:52:21 +0000 Subject: [PATCH 3/3] grammar. --- 4e.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/4e.md b/4e.md index 548d9a62..67f2021d 100644 --- a/4e.md +++ b/4e.md @@ -40,9 +40,9 @@ There are probably other advantages to not tying the user's identity to the keys } ``` -5. Now **Bob** (keypairs `(b, B)`) will send a DM to **Alice**. Because Bob's client fetched Alice's `kind:10044` event, instead of computing the conversation key with `ecdh(b, A)` he does `ecdh(b, E) = S` -6. Because Alice knows `e` Alice can decrypt Bob's message doing `ecdh(e, B) = S` and all is good -7. Now the fun part starts: Alice has decided to use a client called **Tortilla** to chat on her phone, and **Tortilla* wants to do encryption stuff. +5. Now **Bob** (keypair `(b, B)`) will send a DM to **Alice**. Because Bob's client fetched Alice's `kind:10044` event, instead of computing the conversation key with `ecdh(b, A)` he does `ecdh(b, E) = S` +6. Because Alice knows `e`, she can decrypt Bob's message doing `ecdh(e, B) = S` and all is good +7. Now the fun part starts: Alice has decided to use a client called **Tortilla** to chat on her phone, and **Tortilla** wants to do encryption stuff. 8. **Tortilla** sees that Alice has a `kind:10044` published, which means **Tortilla** won't create a new key, **Tortilla** will have to ask for **Cope** to share that key securely. So **Tortilla** generates a local keypair `(t, T)` that won't be shown or leave the device ever, and **Tortilla** publishes an announcement (`kind:4454`) for that local key (signed by Alice): ```jsonc @@ -56,22 +56,22 @@ There are probably other advantages to not tying the user's identity to the keys } ``` -9. **Tortilla** cannot proceed without known the secret key `e`, so it has to tell the user to turn **Cope** on. +9. **Tortilla** cannot proceed without knowing the secret key `e`, so it has to tell the user to turn **Cope** on. 10. Alice opens up **Cope** and **Cope** immediately looks for all `kind:4454` events from Alice, and sees that there is this app called "Tortilla on Android" signed by Alice herself, so **Cope** publishes the secret key `e` nip44-encrypted to `ecdh(c, T)` -- in which `c` is the secret key of a keypair that **Cope** has just generated locally. **Cope** does that using a new event, `kind:4455`: ```jsonc { "kind": 4455, - "pubkey": "" + "pubkey": "", "tags": [ - ["P", ""] + ["P", ""], ["p", ""] ], "content": "" } ``` -12. Immediately **Tortilla** wakes up and sees the `kind:4455` that had just been published by **Cope**, decrypts the content using `ecdh(t, C)` and now **Tortilla** also knows the secret key `e`. **Tortilla** can now decrypt and encrypt the same things **Cope** could before. +11. Immediately **Tortilla** wakes up and sees the `kind:4455` that has just been published by **Cope**, decrypts the content using `ecdh(t, C)` and now **Tortilla** also knows the secret key `e`. **Tortilla** can now decrypt and encrypt the same things **Cope** could before. ### The protocol flow again, now in a colorful infographic