Skip to content

Latest commit

 

History

History
132 lines (102 loc) · 11.7 KB

76.md

File metadata and controls

132 lines (102 loc) · 11.7 KB

NIP-76

Private Channels

draft optional author:d-krause

Given the privacy concerns detailed in A Cypherpunk's Manifesto, additional measures can be taken in the NOSTR protocol to further maintain the privacy of NOSTR users when they desire it. This NIP defines a protocol that can be implemented by conforming clients to maximize the privacy of a user's posts.

Goals

  1. The first goal of NIP-76 is to provide a way for users to share private data while also making it very difficult for governments or other third parties to scan content to identify posters and subscribers of the content. The third party "needing" this information must start with identifying the targeted entity first, and only then can they begin to work on obtaining access to their NOSTR private channel events. Without the channel keys, third party readers of NIP-76 events will have no idea who is saying what to whom.
  2. No private key is ever shared. All channel indexes can be read with public keys. A profile private key is needed to create new events on a channel, as with all nostr events.
  3. Each event in a private channel is a replaceable kind 17761, which means a unique public/private key pair is derived for every channel event.
  4. Except for content size, all 17761 Private channel nostr events look the same.

A. Event Kind 17761

Below is a typical 17761 Kind NOSTR Event. All 17761 events have exactly 1 "e" tag and content which is AES-256-GCM encrypted and base64 encoded.

{
  "content": "faZUUH5w8kHJM5ODWz8m4fxdStfwWMN6ex/aGWkvKEgEKBCtTpo5SXGp9TZcQbL8G1BklVRVkZa2+eehbzlAmCe2VocwAAhTGeBzuTAmePoSWXHsLwzCHcTYZLazgrHiAy/+aSdu0bHs5sJaY2nzlz20/YFe75f5/g6oerNinrpkgKuaCWLH1jGza7y1+tmbIuB4y/w10mamxHlNLixg/mg=",
  "created_at": 1680621353,
  "id": "5f6f38768cee83ac6d2511511ae206a2e66b0349011fc381f1afadbcaae8b53d",
  "kind": 17761,
  "pubkey": "e7dce79883d14ee1f8a417293c430e8fe28f040780e14730ed2b1cb1258cfa69",
  "sig": "9293fce32643906191342f32dd920e6396a28adbc41f3d76f7822f0b79d665573adc30a9cc10eb38905e4829eb17e59f6b75b8ef39b05fe7a63423af0a497f9a",
  "tags": [["e","c72bccceb80484a28c2fd86c0dcf9ba672de168ec8d39bb684efc33b8f8c1db9"]]
}

At first this may seem overly restrictive, but the reasons for it are as follows:

  1. Inside the content is further tag data and kind specific information providing us all the flexibility of a normal nostr Event, while at the same time ...
  2. Keeps our goal to have ALL nostr Events of kind 17761 to be indistinguishable from other 17761 events without the channel keys, therefore providing no information to unwelcome third parties what the content contains.

B. BIP-32 Wallet

A master private key should be generated by the conforming NIP-76 client. It can be generated and stored by whatever means the client implementer deems adequate. It need not, and we recommend should not, be derived from the profile key, nor be used for any other blockchain transaction. In short, it is a master private key that will be used solely for NIP76 Private Channel communication.

For NIP-76, developers implemented new Bip32Network values as follows:

nip76API1: {
        name: 'nip76API1',
        bip32: {
            public: 0x0befd9f6,
            private: 0x0befda38, 
        },
        networkId: 0,
        cloaked: true
    }

This results in extended key strings that look different than their bitcoinMain counterparts. A NIP-76 extended key pair example:

public:
n76pVwqeW219XjiTjfsnGn7Dx8qG8SGuuFsFYyBEfnpiJCBziLLdDwCXiTdjokbMy8jHA1PQw5SHFBNmatovgwBTc4ME2aifB2c
private:
n76sQARyjUAr96E83Etm5mYx1hmrbVBfWZ1pKmro7ZsP7M4qBzhfHk3UbhZHBwBoPbFSD9F8DPowF3WM6LEGJTMQQwSaVbN17cp

The cloaked: true simply means that the depth, parentFingerprint, and derivation index value are removed from the serialization. We saw no reason to provide this information in the serialization value.

C. Deterministic Key Indexes

NIP-76 will use Heirarchical Deterministic Key Index (DKX) to save and encrypt its 17761 kind nostr Events. Each DKX requires two HD Keys - a signingParent and an encryptParent.

The signingParent is used to derive which pubkey will be used to author the event - the user's pub key is used in the content, not the pubkey property of the nostr event. The encryptParent will be used to derive which key is used to encrypt or decrypt the event content.

There are two main types of DKX - Sequential and Time Based.

C.1. Sequential Private Index

Sequential indexes always use the the next unused child offset as the derivation index number of the above two keys. We reserve child 0 (zero) to keep any additional metadata about the index, so the dirst document would be derived with the number 1. Therefore, the first document on a Sequential index would be use the public key derived from signingParent/1', signed with that key's private key, and encrypted with encryptParent/1'.

Because Sequential indexes need the private key of the *signingParent" to sign the event, Sequential indexes are always Private, meaning while we can share the public keys to let others read the contents, others will not be able to create documents on this index.

The "e" tag on a sequentially indexed event is there only for NIP-76 compliance and provides no useful information at this time. Events can be subscribed to by author pubkey.

C.2. Time Based Public Index

TimeBased indexes determine the child derivation index number from the nostrEvent created_at value. The pubkey (and therefore private signing key) is derived from the content creator, by creating a new HDKey, made from the creator's privateKey, and signingParent.chainCode, deriving a child index at the event created_at.

For Example, take user's privateKey='AB12' and the signingParent.chainCode='CC01', we make a new HDKey newSigner = new HDKey('AB12','CC01').deriveChild(event.created_at)'. The reader of the content can then verify the content was created by the creator because his profile pubkey is in the decrypted content of the event, and the event pubkey should match the derived child which signed the event.

The "e" tag on a time base indexed event is shared across all events on that index. Events on a public Time Based index can be easily subscribed to via the "e" tag.

D.1. Key Exchange - Invitations

NIP-76 offers 2 means by which channel creators can provide keys to other nostr users - Managed Invitations, and Unmanaged Invitations. Both of these types of key exchange techniques allow the channel creator to make the invitation either for a single pubkey only, or an open invitation protcted by a password.

Invitations targeted for a single pubkey are encrypted with a computed secret between the channel's private key and the targeted user's pubkey, and therefore should only be readable to the user with the private key of the targeted public key. Invitations encrypted with a password can be read by anyone that can enter the correct password.

Both Managed Invitations and Unmanaged Invitations have strengths and weaknesses. Unmanaged Invitations give the channel creator less exposure as the keys to the channel are not stored in nostr events and there is no record of the the creator having invited the recipient. Managed Invitations do keep a record of the invitation as nostr events, but allow the channel creator to Suspend the invitation at any time.

D.2. Key Exchange - Managed Invitations

Managed Invitations are stored as a nostr event, kind 17761, the content of which contains the full keyset to the private channel.

The pointer to the Managed Invitation is a bech-32 encoded string that when decrypted points the user to load the invitation record that contains the channel key set. Here is one that is password encrypted:

nprivatechan1z5kkcrv4kjv3asn233rp0n907nkyxu6s3prwkdaxjxtrpmxhs4tty07qfuunsq7gvve6c2cah86u5u244xw2mvyxtyzesv4qlhzjyap3zqnmy2nqqkja0u7sfz0kgpel8x8e9cu6qkak5wzjnq0s9fs7kwt2yk679m3ljr

D.3. Key Exchange - Unmanaged Invitations

Unmanaged Invitations are not stored, but are simply copied from the UI and the channel owner can send them wherever they please.

The pointer to the Unmanaged Invitation is a bech-32 encoded string that when decrypted contains the channel key set. Here is one that is password encrypted:

nprivatechan18knmx39nevjew4yfsdl40aunpxrz3njvcw00hwfnvvcw2ugjygt3dvev3nrzkjfktj9j6ww4tnxl39z845v3lk450n7eax9dwmar90zs4axydc204700avpqsqpz47jj63yf2c5e6a4rt6f0pwms4rz5ma6s0unzg34z873eld6wxf8dnkdjx56e74atnhvvp0n2a7femphmaxjazhmxyd2tzcp9xhveflmp7qkukupavljry7pwesqu85w9qd3u79uv3tsynt30v

D.3. Key Exchange - RSVPs

For all intents and purposes, an RSVP is essentially a "follow". But we wanted a word with less baggage, one that impllies politeness, but not necessarily "agreement", which the term "follow" seems to imply.

Each NIP-76 RSVP consists of 2 (two) nostr events. The first nostr event saves the key invitation information on the invitation recipients private documents. The second nostr event is void of any key information and is sent back to the channel's own rsvp index so that it can keep track of who has its keys and can view its channel notes.

Saving the 1st RSVP without the 2nd is tantamount to "lurking", so we decided as a matter of politeness that all conforming clients should either create both RSVPs, or neither. The invitee can always reload the invitation if they really want to lurk without RSVP'ing. This rule could come up for debate later and we can cross that bridge when we get to it.

E. Key Derivation (Optional)

We made this section optional only because there are so many ways any of us could achieve the same intended result. But conformance to this section would mean that a user could derive the same set of channels and posts no matter which client is used. It also ensures that though the wallet is not derived from the profile, it is married to the profile, so that no other profile could load the wallet and find the creators channels and keys easily.

The process is a bit complex, but the accompanying nip76-tools reduces this complexity down to a single line of code.

From the master private key, we derive m/44'/1237'/0'/1776' as our "wallet-root". We take a sha512 of the private key from the NOSTR profile to produce 64 bytes of data read as 16 32-bit integers, which we will refer to as a "wordset".

We derive a wallet documentsIndex as a Private Sequential index with keys as follows:

documentsIndex.signingParent = wallet-root/wordset(0)'/wordset(1)'/wordset(2)'/wordset(3)'
documentsIndex.encryptParent = wallet-root/wordset(4)'/wordset(5)'/wordset(6)'/wordset(7)'

We then pass the remaining 8 words of wordset to the documentsIndex so that it can define additional indexes.

Since the wallet documents index is private and contains RSVPs and Channels, and channels contain more indexes, we get more separation between our document keys as follows:

docN.signingParent = documentsIndex.signingParent/N*wordset(8)'/N*wordset(9)'/N*wordset(10)'/N*wordset(11)'
docN.encryptParent = documentsIndex.encryptParent/N*wordset(12)'/N*wordset(13)'/N*wordset(14)'/N*wordset(15)'
  • N starts with 1, and when N*i > 0x800000000, then the modulus of 0x80000000 is used to keep the child index number within the BIP-32 boundary.

Additional info