6.9 KiB
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.
{
"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.
],
"content": "", // reserved for private content below
// ...
}
Sheets MUST be rendered in the order of their tags.
As an example:
{
"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
Access Controls to Private Spreadsheets
Private spreadsheets NIP-44 encrypt the tag array and place it on the .content
of the event and use p
tags to pass rights to encrypt and decrypt to other users.
This section explores the 4 modes of operation.
Author-only design
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.
{
"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),
// ...
}
Viewing permissions
Ready-only sharing is achieved by adding a p
tag to each receiver with an encrypted private key that should be used to decrypt the .content
.
The viewing private key is a new Nostr Private Key in hex, NIP-44-encrypted to each p
tag and placed as a 4th value in each tag.
The .content
is then encrypted by a conversation key between the new author's private key and the viewing public key.
val viewingKeyPair = nostr.generateKeyPair()
{
"pubkey": author.pubkey
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
["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
], author.privateKey, viewingKeyPair.publicKey),
"sig": signWith(author.privateKey)
// ...
}
with nip44Encrypt("text to encrypt", sender.privatekey, receiver.publickey)
.
To decrypt, receivers SHOULD find the ciphertext in the p
-tag for their key, decrypt that to get the viewing private key and use the viewing private key to decrypt the .content
: nip44Decrypt(event.content, viewing.privatekey, event.pubkey)
Clients SHOULD include the author as a p
tag to make sure the author can retieve the viewing key on updates of this event.
Editing permissions
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.
The editting keyPair
val edittingKeyPair = nostr.generateKeyPair()
val viewingKeyPair = nostr.generateKeyPair()
{
"pubkey": edittingKeyPair.publicKey
"kind": 35337,
"tags": [
["d", "<unique identifier>"]
["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
], edittingKeyPair.privateKey, viewingKeyPair.publicKey),
"sig": signWith(edittingKeyPair.privateKey)
// ...
}
Receivers SHOULD find the ciphertext for their key and decrypt that to get the shared private key. If the corresponding public key of the shared key is the pubkey
of the event, this is the editing key and the receiving user has edit permissions. That permission also allows the receiving user to decrypt everybody's key and find the viewing key it would need to decrypt the .content
.
Special Case: No Viewing Group
When no user has view permissions only, the .content
MUST be encrypted to it's own public key.
val edittingKeyPair = nostr.generateKeyPair()
{
"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)
// ...
}
Final Considerations
Spreadsheets SHOULD NOT have private and public parts at the same time.