-
Notifications
You must be signed in to change notification settings - Fork 761
NIP-33 Personal notes #2271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
NIP-33 Personal notes #2271
Changes from all commits
2810906
5b54ae8
63b8379
7da2813
bf379a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,165 @@ | ||
| NIP-33 | ||
| ====== | ||
|
|
||
| Parameterized Replaceable Events | ||
| -------------------------------- | ||
| Personal Notes | ||
| -------------- | ||
|
|
||
| `final` `mandatory` | ||
| `draft` `optional` | ||
|
|
||
| Renamed to "Addressable events" and moved to [NIP-01](01.md). | ||
| This NIP defines a standard for storing personal, self-addressed encrypted notes on Nostr relays. The primary goal is privacy: all content is encrypted before leaving the client, event tags carry no plaintext metadata, and the author's pubkey is the only identity involved. Encryption uses [NIP-44](44.md) for relay payloads and AES-256-GCM for file storage. | ||
|
|
||
| Two event kinds are defined: | ||
|
|
||
| | Kind | Description | | ||
| |-------|-------------------| | ||
| | 33301 | Plain text note | | ||
| | 33302 | File attachment | | ||
|
|
||
| `All kinds are **addressable events** (30000–39999 range per [NIP-01](01.md)), identified by a `d` tag set to a client-generated note ID. This enables in-place replacement (edits) and deduplication across relays. | ||
|
|
||
| ## Kind 33301 — Plain note | ||
|
|
||
| A plain text note. The `content` field holds a **NIP-44 ciphertext** produced by encrypting a JSON payload using the author's own keypair on both sides of the ECDH derivation(self-encryption). No other tags carry meaningful content. | ||
|
|
||
| **Event fields:** | ||
|
|
||
| | Field | Value | | ||
| |-----------|-----------------------------------------------| | ||
| | `kind` | `33301` | | ||
| | `tags` | `[["d", "<client-generated note ID>"]]` | | ||
| | `content` | NIP-44 self-encrypted JSON `{"text": "..."}` | | ||
|
|
||
| **Payload 'content' fields:** | ||
|
|
||
| | Field | Required | Description | | ||
| |-------------|----------|-------------------------------------------| | ||
| | `text` | yes | Plain text content | | ||
| | `sensitive` | no | It should be handled discreetly by the UI | | ||
|
|
||
| **Payload example (before encryption):** | ||
|
|
||
| ```json | ||
| { | ||
| "text": "<note content>", | ||
| "sensitive": <true|false> | ||
| } | ||
| ``` | ||
|
|
||
|
|
||
| **Full event example:** | ||
|
|
||
| ```jsonc | ||
| { | ||
| "kind": 33301, | ||
| "pubkey": "<author-pubkey>", | ||
| "created_at": 1700000000, | ||
| "tags": [ | ||
| ["d", "0193a4b2e8f1c3d7"] | ||
| ], | ||
| "content": "<nip44-ciphertext>", | ||
| "id": "<event-id>", | ||
| "sig": "<signature>" | ||
| } | ||
| ``` | ||
|
|
||
| The decrypted `content` for the above would be: | ||
|
|
||
| ```json | ||
| { | ||
| "text": "Remember to pick up groceries", | ||
| "sensitive": true | ||
| } | ||
| ``` | ||
|
|
||
| ## Kind 33302 — Attachment | ||
|
|
||
| A file attachment note. The `content` field holds a NIP-44 self-encrypted JSON object describing the file. The file bytes themselves are encrypted separately with AES-256-GCM (wire format: `nonce[12] || ciphertext || mac[16]`) and uploaded to a [Blossom](https://github.com/hzrd149/blossom) server. Small files (e.g. config files, credentials backup, SSH keys) MAY be stored inline in the `data` field as base64-encoded AES-256-GCM ciphertext, removing the need for a separate file server. | ||
|
|
||
| **Threshold:** files 32 KB or larger SHOULD be uploaded to a Blossom server and referenced via `url`; files smaller than 32 KB MAY be stored inline in the `data` field as base64-encoded AES-256-GCM ciphertext. | ||
|
|
||
| **Encryption:** AES-256-GCM encryption is required for Blossom-hosted files since they are publicly accessible by URL. A fresh key MUST be randomly generated for every event, so each upload is independently encrypted. Inline files apply the same encryption uniformly, even if it's technically redundant since the payload is NIP-44 encrypted, to follow the same code path regardless of storage destination. | ||
|
|
||
| **Event fields:** | ||
|
|
||
| | Field | Value | | ||
| |-----------|-----------------------------------------------|` | ||
| | `kind` | `33302` | | ||
| | `tags` | `[["d", "<client-generated note ID>"]]` | | ||
| | `content` | NIP-44 self-encrypted attachment JSON | | ||
|
|
||
| **Payload fields:** | ||
|
|
||
| | Field | Required | Description | | ||
| |------------------|----------|-------------------------------------------------------------------| | ||
| | `filename` | yes | Original filename including extension | | ||
| | `file-type` | yes | MIME type of the file before encryption | | ||
| | `size` | yes | Original unencrypted file size in bytes | | ||
| | `dim` | no | Size of image in pixels in the form <width>x<height> | | ||
| | `x` | yes | Hex SHA-256 of the **encrypted** file bytes | | ||
| | `decryption-key` | yes | Hex-encoded 32-byte AES-256-GCM key, MUST be randomly generated per event | | ||
| | `url` | no* | Blossom URL of the encrypted file (required if `data` is absent) | | ||
| | `data` | no* | Base64-encoded encrypted file bytes (required if `url` is absent) | | ||
| | `thumbhash` | no | Base64-encoded [thumbhash](https://github.com/evanw/thumbhash) for images, shown as placeholder while loading | | ||
| | `caption` | no | Optional text caption | | ||
| | `sensitive` | no | Should be blurred in the UI, default false | | ||
|
|
||
| *Exactly one of `url` or `data` MUST be present. | ||
|
|
||
| **Payload example (before encryption):** | ||
|
|
||
| ```json | ||
| { | ||
| "filename": "<original filename>", | ||
| "file-type": "<MIME type>", | ||
| "size": "<original unencrypted size in bytes>", | ||
| "dim": "<width>x<height>", | ||
| "x": "<hex SHA-256 of the encrypted file bytes>", | ||
| "decryption-key": "<hex-encoded 32-byte AES-256-GCM key>", | ||
| "url": "<Blossom URL>", | ||
| "data": "<base64-encoded encrypted bytes>", | ||
| "thumbhash": "<base64-encoded thumbhash>", | ||
| "caption": "<optional text caption>", | ||
| "sensitive": <true|false> | ||
| } | ||
| ``` | ||
|
|
||
| **Full event example:** | ||
|
|
||
| ```jsonc | ||
| { | ||
| "kind": 33302, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this replaceable if blossom is not?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the caption can be updated (and any future metadata, e.g. tags). |
||
| "pubkey": "<author-pubkey>", | ||
| "created_at": 1700000000, | ||
| "tags": [ | ||
| ["d", "0193a4b2e8f1c3d8"] | ||
| ], | ||
| "content": "<nip44-ciphertext>", | ||
| "id": "<event-id>", | ||
| "sig": "<signature>" | ||
| } | ||
| ``` | ||
|
|
||
| The decrypted `content` for the above would be: | ||
|
|
||
| ```jsonc | ||
| { | ||
| "filename": "photo.jpg", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filename is just confusing. There is no need for this. The name of the file is the hash.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filename is absolutely necessary. If I upload a document I WANT to know its exact original filename, not an useless hash. We don't just deal with images (where I might still be interested in the file name, btw).
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nobody chooses filenames in modern computing architectures... this is a 90s idea. Especially in this case where people will want the metadata info, not the filename as a reference.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not only a matter of users naming files (I do it, and many other people do it), but also softwares name files with useful bits of information (dates, usernames, etc) that are useful to quickly understand the content, especially when you cannot open/parse the file.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are using Blossom, the filename is the hash. There is no way around it. Otherwise, the mirroring system fails, the API fails. It's fine to add a name or description to the file as extra information, but as long as you are using Blossom, it will never be an actual filename. That's why it is confusing (and to me completely unnecessary).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I cannot understand the problem. The filename is a metadata and I want to save it. When the user save back the file locally the original filename is proposed. What's the issue? I only see advantages.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just confusing... are we supposed to save files with that filename in disk when the user asks to save or to download? That breaks Blossom and makes things difficult to sync with other Blossom servers. Is the user going to see a readable filename in your app, but a hash on his own Blossom server? Why? That seems confusing too. How are they supposed to find things if the names keep changing? To me, this is an unnecessary interoperability mess.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fwiw, I think filenames can have a lot of value; it doesn't matter if in blossom its served as the hash -- when you save it as whatever filename its been given
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't break Blossom in any way. And it doesn't create any confusion for the user, that for sure doesn't know (and doesn't have to know) how the blob is stored remotely. Try Manent's UX, it's simply good and meets users' basic expectations of uploading and downloading a file with the same name, or simply check it, if needed. |
||
| "file-type": "image/jpeg", | ||
| "size": 204800, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dimensions are more important than size because clients can pre-compute screen layout and then just transition to the real image when it's available. Without it, everything moves in the UI. Image dimensions should be a MUST everywhere.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that, but here we are talking about private notes that are displayed in a really simple structure. Anyway, adding the dimensions doesn't hurt, it can be useful to infer the orientation of an image. |
||
| "dim": "800x600", | ||
| "x": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | ||
| "decryption-key": "a2b4c6d8e0f1a3b5c7d9...", | ||
| "url": "https://blossom.example.com/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", // AES-256-GCM encrypted file | ||
| "thumbhash": "YQkGDYSId3h3d4d3aIeHeA==", | ||
| "caption": "Sunset at the coast", | ||
| "sensitive": true | ||
| } | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| To add an additional layer of privacy, clients SHOULD publish these events to personal or authenticated relays (e.g. via [NIP-42](42.md)) that only serve events back to their owner. Publishing to public relays is not forbidden, but it increases metadata exposure (everyone can observe pubkey activity and event timestamps). | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using the same relay list (kind: 10013) that NIP-37 Drafts use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having a dedicated list is good, but I'm unsure about NIP-37: drafts are supposed to be temporary and so a relay, maybe a free public one, could clean them with a temporal logic; it is, however, essential that personal notes never be deleted.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not supposed to be temporary... It's just a relay list that is for private information that only the user has access to them. We save a lot of settings and other event kinds in there too. It because the main list for any private event that other users don't need to know you have. |
||
|
|
||
| ## Reference implementation | ||
|
|
||
| [Manent](https://github.com/dtonon/manent) is the reference client implementing this NIP. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you storing image/video data in relays as base64?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, drop this and make Blossom only. Optionality always sucks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Files smaller than 32 KB MAY be stored inline, as stated in the NIP.
Again, we are not just dealing with media.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optionality sucks. 100% -- just store them in blossom, regardless of size -- blossom is for blobs; doesn't matter if its media or not
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not about optionality, it's a kind of progressive enhancement. You can use just your relays for light (< 32KB) stuff without having any other dependencies, and add a blossom server (that is actually a spec outside Nostr) if you need to save other stuff.