nips/73.md

190 lines
7.4 KiB
Markdown
Raw Normal View History

2024-04-22 18:48:56 -04:00
NIP-73
======
Spreadsheets
------------
`draft` `optional`
This NIP provides a simple way to save spreedsheets on Nostr.
Event kind `35337` describes a workbook with `data` tags that contain the value of each cell as well as optional styling.
```js
{
"kind": 35337,
"tags": [
["d", "<unique identifier>"],
["title", "Name of this spreadsheet"], // public title
["data", "<sheet name>", "<column letter>", "<row number>", "<value>"],
["data", "<sheet name>", "<column letter>", "<row number>", "<value>", "<style>"],
["style", "<TBD>"], // Need to specify all options here.
2024-04-22 18:48:56 -04:00
],
"content": "", // reserved for private content below
2024-04-22 18:48:56 -04:00
// ...
}
```
2024-04-22 18:51:16 -04:00
Sheets MUST be rendered in the order of their tags.
As an example:
2024-04-22 18:48:56 -04:00
```json
{
"id": "32360b52b11616ea331aacac516494e36bd4079d8908edc8f26ad1e4acab5a53",
"kind": 35337,
"tags": [
[ "d", "SheetStr Demo" ],
[ "data", "Sheet1", "J", "25", "3" ],
[ "data", "Sheet1", "J", "26", "5" ],
[ "data", "Sheet1", "J", "27", "=SUM(J25:J26)" ]
],
"created_at": 1713819120,
"content": "",
"pubkey": "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
"sig": "880eb3d67fc66ca2d4e7819ae9b9ca577df35950fb5d11d24f95f350cfeab0b4532646c52113d5bb629cf9a2e4d8ef646ff434b59f1c894c8f719f65d59ed8f0",
}
```
## Styling
TBD
2024-05-07 18:49:10 -04:00
## Access Controls to Private Spreadsheets
Private spreadsheets [NIP-44](44.md)-encrypt the tag array, place it on the `.content` of the event, and use `p` tags to pass rights to encrypt and decrypt to other users.
2024-05-07 18:49:10 -04:00
This section explores the 3 modes of operation.
2024-05-07 18:49:10 -04:00
### Author-only permission
2024-05-07 18:49:10 -04:00
In this version, the spreadsheet is signed by the main keys of an author and only the author can decrypt. It doesn't not include any `p` tag.
The encryption in `.content` uses a NIP-44 conversation key between the author's private key and the author's public key.
```js
{
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
],
"content": nip44Encrypt([
["title", "Name of this spreadsheet"], // private title
["data", "<sheet name>", "<column letter>", "<row number>", "<value>"], // private data
["data", "<sheet name>", "<column letter>", "<row number>", "<value>", "<style>"],
["style", "<TBD>"], // Need to specify all options here.
// ... other tags
], author.privateKey, author.pubkey),
// ...
}
```
2024-05-07 18:49:10 -04:00
### Viewing permissions
2024-05-07 19:41:14 -04:00
Viewing permissions are shared by creating a new Nostr Private key, passing it to each user via a `p` tag, and encrypting the `.content` to that key.
2024-05-07 19:41:14 -04:00
The viewing private key is NIP-44-encrypted to each `p` tag and placed as a 4th value in each tag.
2024-05-07 19:41:14 -04:00
The `.content` is then encrypted by a conversation key between the new author's private key and the viewing's public key.
```js
2024-05-07 18:52:54 -04:00
val viewingKeyPair = nostr.generateKeyPair()
{
"pubkey": author.pubkey
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
2024-05-07 18:52:54 -04:00
["p", "<pubkey 1>", "<relay url>", nip44Encrypt(viewingKeyPair.privateKeyHex, author.privateKey, "<pubkey 1>") ]
["p", "<pubkey 2>", "<relay url>", nip44Encrypt(viewingKeyPair.privateKeyHex, author.privateKey, "<pubkey 2>") ]
["p", "<pubkey 3>", "<relay url>", nip44Encrypt(viewingKeyPair.privateKeyHex, author.privateKey, "<pubkey 3>") ]
],
"content": nip44Encrypt([
["title", "Name of this spreadsheet"], // private title
["data", "<sheet name>", "<column letter>", "<row number>", "<value>"], // private data
["data", "<sheet name>", "<column letter>", "<row number>", "<value>", "<style>"],
["style", "<TBD>"], // Need to specify all options here.
// ... other tags
2024-05-07 18:52:54 -04:00
], author.privateKey, viewingKeyPair.publicKey),
"sig": signWith(author.privateKey)
// ...
}
```
2024-05-07 19:41:14 -04:00
with `nip44Encrypt(textToEncrypt, sender.privatekey, receiver.publickey)`.
2024-05-02 14:31:43 -04:00
2024-05-07 19:41:14 -04:00
To decrypt, receivers MUST:
1. find the ciphertext in the `p`-tag for their key
2. decrypt the ciphertext with `nip44Decrypt(tag[3], user.privatekey, event.pubkey)` to get the viewing private key
3. use the viewing private key to decrypt the `.content` with `nip44Decrypt(event.content, viewing.privatekey, event.pubkey)`
2024-05-07 18:49:10 -04:00
Clients SHOULD include the author as a `p` tag to make sure the author can retieve the viewing key on updates of this event.
2024-05-07 18:49:10 -04:00
### Editing permissions
2024-05-07 18:49:10 -04:00
To share editing permissions, a new shareable key is needed to encrypt and sign the replaceable event. The editting key MUST be shared with other users through their `p` tags. At the same time, the viewing key can be shared with users to decrypt the event, but it doesn't allow them to change it.
```js
2024-05-07 18:49:10 -04:00
val edittingKeyPair = nostr.generateKeyPair()
val viewingKeyPair = nostr.generateKeyPair()
{
2024-05-07 18:49:10 -04:00
"pubkey": edittingKeyPair.publicKey
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
2024-05-07 18:52:54 -04:00
["p", "<pubkey 1>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 1>") ]
["p", "<pubkey 2>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 2>") ]
["p", "<pubkey 3>", "<relay url>", nip44Encrypt(viewingKeyPair.privateKeyHex, edittingKeyPair.privateKey, "<pubkey 3>") ] // view only
],
"content": nip44Encrypt([
["title", "Name of this spreadsheet"], // private title
["data", "<sheet name>", "<column letter>", "<row number>", "<value>"], // private data
["data", "<sheet name>", "<column letter>", "<row number>", "<value>", "<style>"],
["style", "<TBD>"], // private styles
// ... other tags
2024-05-07 18:49:10 -04:00
], edittingKeyPair.privateKey, viewingKeyPair.publicKey),
"sig": signWith(edittingKeyPair.privateKey)
// ...
}
2024-05-07 18:49:10 -04:00
```
2024-05-07 19:41:14 -04:00
Receivers MUST:
1. find the ciphertext for their key
2. decrypt the ciphertext to get a private key (`nip44Decrypt(tag[3], user.privatekey, event.pubkey)`).
3. If the corresponding public key of the key is the `pubkey` of the event, this is the editing key and the receiving user has edit permissions.
4. Use the editting key to decrypt all the other `p`-tag keys and find the viewing key
5. Once both keys are known, decrypt the `.content` with `nip44Decrypt(event.content, viewingKeyPair.privatekey, event.pubkey)`
2024-05-07 18:49:10 -04:00
2024-05-07 19:41:14 -04:00
### Special Case: No Viewing Keys
2024-05-07 18:49:10 -04:00
2024-05-07 19:41:14 -04:00
When no user has view permissions only, there won't be another key in the event. The `.content` MUST then be encrypted to the signer's own public key.
2024-05-07 18:49:10 -04:00
```js
val edittingKeyPair = nostr.generateKeyPair()
2024-05-07 18:49:10 -04:00
{
"pubkey": edittingKeyPair.publicKey
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
["p", "<pubkey 1>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKeyHex, "<pubkey 1>") ]
["p", "<pubkey 2>", "<relay url>", nip44Encrypt(edittingKeyPair.privateKeyHex, edittingKeyPair.privateKeyHex, "<pubkey 2>") ]
],
"content": nip44Encrypt([
["title", "Name of this spreadsheet"], // private title
["data", "<sheet name>", "<column letter>", "<row number>", "<value>"], // private data
["data", "<sheet name>", "<column letter>", "<row number>", "<value>", "<style>"],
["style", "<TBD>"], // private styles
// ... other tags
], edittingKeyPair.privateKey, edittingKeyPair.publicKey),
"sig": signWith(edittingKeyPair.privateKey)
// ...
}
```
2024-05-07 18:49:10 -04:00
## Final Considerations
Spreadsheets SHOULD NOT have private and public parts at the same time.
When users sign a private spreadsheet with their own keys, they can add viewers but cannot add editors. In order to add editors, the user will have to duplicate the replaceable in a new shareable key and delete the current one.