nips/73.md

7.4 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, 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 3 modes of operation.

Author-only permission

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

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.

The viewing private key is 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's 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(textToEncrypt, sender.privatekey, receiver.publickey).

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)

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.

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 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)

Special Case: No Viewing Keys

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.

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.

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.