SEA.certify - amark/gun GitHub Wiki

THIS IS AN EARLY EXPERIMENTAL COMMUNITY SUPPORTED METHOD THAT MAY CHANGE API BEHAVIOR WITHOUT WARNING IN ANY FUTURE VERSION.

SEA is the Security, Encryption, and Authorization system used with GUN.

Do you want to allow others to write to parts of your own organization's graph without sharing your keypair with them? Then this feature is for you! With SEA.certify, you can create a cryptographically signed Certificate that gives other people write permission. A Certificate describes WHO has the right to write to your graph, WHERE they can write, and (pending security review) until WHEN. The Certificate should not be encrypted because it must be plain text so that it is interpretable by any and every peer and machine in the network, so every peer enforces the same security rules, whether it is a browser, phone, IoT device, or relay.

All you have to do is pass the certificate into any save operation in GUN:

gun.put(data, function(ack){}, {opt: {cert: certificate}})

The certificate will be stored in the graph along with the data for everybody to verify it at any time.

So, now how do you create a certificate?

HOW TO USE SEA.certify

SEA.certify(who, policy, authority, cb, opt)

Who

Who the certificate is for. These are the people you allow to write to your own graph. This could be:

  • 'pub' - the public key string of the other user
  • {pub} - an object that has a pub property on it.
  • [Bob.pub, Carl.pub] a list of public keys,
  • [Bob, Carl] or a list of objects that have a pub property on them.
  • "*" wildcard symbol for everyone/anyone.

While it may be easy to set a list of pubs in one certificate, keep in mind that the certificate is added to any record being put with it. So long lists of certificants may have a significant impact on the size of the database. The more precise are the certificates – the more secure is the system.

Certificates work only for authenticated users, which means each time you see 'everyone/anyone' in that section it means 'who' was previously gun.user().auth(...)

Policy

policy - The rules of the Certificate. Policy may be set in a couple of ways:

  • 'inbox' a string,
  • {'*': 'inbox'} a LEX object,
  • [LEX objects || strings] or an Array of any of them.

These rules are used to check against Path and Key using Gun.text.match or String.match. These rules are used to check against path.

Path is the string that stands after the graph owner's Pub when someone tries to put data to your graph. For example, Bob is trying to run a gun.put like this:

gun.user(yourPub).get('private').get('deep').get('deeper').put('secret')

In this case, the Path is private/deep and Key is deeper and Value is secret.

  • policies.read [LEX String || Array of LEXs || LEX Object]: Rules for read permissions, TO BE DEVELOPED.

  • policies.write [LEX String || Array of LEXs || LEX Object]: Rules for write permissions. You can set write rules directly in policies if there is no policies.read. In case you have policies.read, you need to set policies.write.

  • OVERWRITE-PROOF – personal paths: If any LEX object is matched, and that LEX object has Key "+" and its Value contains "*", like this {"*": "something", "+": "*"} , then either Path string or Key string must contain Certificant's Pub string. Certificant's Pub is the pub key of the one who puts. This feature helps fight against data overwrite, but it is flexible because multiple users can still update the same Path, and everyone can still have their own space if required.

    • KEEP IN MIND: The above OVERWRITE-PROOF feature is not part of RAD/LEX feature, although it is injected to a LEX object. RAD/LEX only have 4 operators: =, *, >, <

    • To learn more about RAD/LEX, please check out these docs: https://gun.eco/docs/RAD or https://gun.eco/docs/LEX

Some examples of policies:


{"*": "notifications", "+": "*"} // Path must start with "notifications", then Path or Key must contain Certificant's Pub (it's just (path||key).indexOf(pub)!=-1)

{"#": {"*": "inbox"}} // Path must start with "inbox". "get('inbox').get('Alice').get('secret').put('abc', null, cert)" and "get('inbox').get('Bob').get('sensitive').put('something', null, cert)" ARE ALL OK.

{"#": {"*": "project"}, ".": {"*": "Bob"}, {"+": "*"}} // Path must start with "project" and Key must start with "Bob", then Path or Key must contain Certificant's Pub.

"inbox/Bob" // Path must equal "inbox/Bob", it is a LEX exact match {"=":"inbox/Bob"}

["inbox", {"*":"projects", "+": "*"}, {"*":"employees"}] // an Array of rules. If any matches, continue.

Authority

authority - Certificate Authority or Certificate Issuer. This is your priv, or your key pair.

Callback

cb - A callback function that runs after a Certificate is created.

Options

opt - the options of the Certificate. Opt is an object that describe WHEN the Certificate expires.

Expiry

  • opt.expiry [Integer || Float]: A timestamp (ie. Date.now()+10000 or Gun.state()+10000) to set the Certificate to expire in the future.
    • If opt.expiry IS NOT SET, the Certificate is valid PERMANENTLY, and this is dangerous!

SOME EXAMPLES

var Alice = await SEA.pair()
var Bob = await SEA.pair()
var Dave = await SEA.pair()

// Alice wants to allow Bob and Dave to use write to her "inbox" and "stories" UNTIL TOMORROW
// On Alice's side:
var certificate = await SEA.certify([Bob.pub, Dave.pub], [{"*": "inbox", "+": "*"}, {"*": "stories"}], Alice, null, {expiry: Gun.state()+(60*60*24*1000)})

// Now on Bob/Dave's side, they can write to Alice's graph using gun.put:
gun.get('~'+Alice.pub).get('inbox').get('deeper'+Bob.pub).put('hello world', null, {opt: {cert: certificate}}) // {opt: {cert: certificate}} is how you use Certificate in gun.put

SOME USE CASES

Consider these examples as pseudocode as you'll need to wrap all awaits in async functions and add more checks for data consistency. It's also a good practice to have expiration dates set for all certificates for better security.


Custom personal profiles in a public room

With SEA.certify we can use a dedicated keypair space as a place to store any structured app data. Let's assume our app has rooms for users to participate. It may be an online game, a chatroom or any kind of public space. Even though all users can have their main profile in their private user graph, we may want to store some room specific profile information right in the room graph.

1. Room initialization

// Generate a new key pair
const room = await SEA.pair() 

// Issue the wildcard certificate for all to write personal items to the 'profile'
const cert = await SEA.certify( 
  '*',  // everybody is allowed to write
  { '*':'profile', '+': '*' }, // to the path that starts with 'profile' and along with the key has the user's pub in it
  room, //authority
  null, //no need for callback here
  { expiry: Date.now() + (60*60*24*1000) } // Let's set a one day expiration period
) 

// Authenticate with the room pair
gun.user().auth(room, () => { 

  // Put the certificate into the room graph for ease of later use
  gun.user()
    .get('certs')
    .get('profile')
    .put(cert) 
})

2. A user can edit a room personal profile record

// Generate the user pair
const user = await SEA.pair() 

//Log in with the user keypair
gun.user().auth(user, () => {

  // Load the 'profile' certificate from the room
  const certificate = await gun.user(room.pub).get('certs').get('profile').then() 

  // Use the certificate to write to the personal route at the room profile
  gun
    .user(room.pub)
    .get('profile')
    .get(user.pub)
    .put({name: 'Alice', city: 'New York'}, null, {opt: { cert: certificate }} )

  // This profile may be easily deleted (tombstoned) by the user later
  gun
    .user(room.pub)
    .get('profile')
    .get(user.pub)
    .put(null, null, {opt: {cert: certificate }} )

})

Personal content-addressed items list

We may want to create a collection of useful links, filled only by a group of verified users.

Notice: We use content-addressing here so the stored URLs are immutable. It is used here to show an example of a more complex use of SEA primitives. If you want your items to be mutable by the authors, just omit the SEA.work part and use Gun.text.random() instead of the hash. We call the collection container a room because users can post there only in person. 😊

Now let's help Alice and Bob collaboratively create a collection of useful URLs.

1. Alice initializes the room

// Generate keys for the participants
const Alice = await SEA.pair() // Alice, she decides to host this collection of links
const Bob = await SEA.pair()  // Bob joins her initiative

// Begin with a list of verified users' public keys
const users = [Alice.pub, Bob.pub]

// Generate a new key pair for the room
const room = await SEA.pair() 

// Authenticate with the room key pair to set it up
gun.user().auth(room, async () => { 

 // Alice is the room host. She stores the encrypted room key pair right here in the room itself to be able to manage it later at any time (i.e. to issue new certificates)
 let enc = await SEA.encrypt(room, Alice)
 gun.user()
  .get('host')
  .get(Alice.pub)
  .put(enc)

  // If you want to let Alice save her room keys in her private graph, you'll have to give her a second instance of Gun (connected to the same peers). She will log in with her own key and store the encrypted room keys there

 // Iterate over the list of verified users public keys and...
 users.forEach(async pub => {

   // Issue a certificate for each user to write personal items to the '#links' path. The hash symbol enforces content-addressing for any item put in it
   const cert = await SEA.certify( pub, { '*':'#links', '+': '*' }, room ) 

   // put the user certificate to a 'certs/links' path for ease of later use (make sure not to use `#` hash symbol here as it will impose content-addressing and putting not hashed item will fail)
   gun.user()
    .get('certs')
    .get('links')
    .get(pub)
    .put(cert) 
 })
})

2. Bob adds some personalized content-addressed data to the public list

// Bob logs in
gun.user().auth(Bob)

// Creates a new url record to be added to the list
let url = { url: 'https://gun.eco' }

// Stringifies the object and uses SEA.work to generate a base64 hash for it
let text = JSON.stringify(url)
let hash = await SEA.work(text, null, null, { name: 'SHA-256' })

// Loads his certificate
const certificate = await gun.user(room.pub).get('certs').get('links').get(Bob.pub).then() 

// Adds the link to the room with the certificate
gun
  .user(room.pub)
  .get('#links')
  .get(`${hash}@${Bob.pub}`) 
  .put(text, null, {opt: {cert: certificate }})

// the link item will have a key of `hash @ user.pub`, something like `RkbhM2E/Co5l2z8mr6WuWVy1HWi+XCOFwoO1ulzc1Ag=@7TbFCpq79fs_ZZlFEzwMmSBnn8xeoQTpDnq0xrB7sxE.1FsktZDUllWLi8wIOC9tiqpKA4Eiqgx3fIaYCn1I4c8`

3. Another time Dave shows up and is willing to add his links to the list too

  // Dave is a little late
  const Dave = await SEA.pair() 

 // Alice gets the encoded room pair
 let enc = await gun.user(room.pub).get('host').get(Alice.pub).then()
 
 // Decodes it with her private pair 
 let room = await SEA.decrypt(enc, Alice)

 // Issues a certificate for Dave
 let daveCert = await SEA.certify(Dave.pub, { '*':'#links', '+': '*' }, room)
 
 // Adds Dave's certificate to the room
 // NOTICE: you may want to use a second gun instance to log into the room while authed with the user
 gun2.user().auth(room, ()=> {
   gun2.user().get('certs').get(Dave.pub).put(daveCert)
 })

4. We can render the full list of links with verified authorship

// Create an object to fill up
 const links = {}

// List all the personal hashed links
 gun
   .user(room.pub)
   .get('#links')
   .map()
   .once((data,key)=> {

     // Extract the unique hash of the data from the key
     let hash = key.slice(0,44) 

     // Extract the author pub from the key
     let author = key.slice(-87) 

     // Recover object from the stored string
     let url = JSON.parse(data) 

     // Construct the final record
     links[hash]= {
       ...url,
       author
     }

     // Or you can list links by their author
     links[author] = links[author] || {}
     links[author][hash] = url

   })

5. There are ways to get a list of links added by a particular user


 const bobLinks = {}

 // 1. We may use a LEX query to get all links for a particular user
 gun
  .user(room.pub)
  .get('#links')
  .get({'.': {'*': Bob.pub}})
  .map()
  .once((d,k)=> {
     bobLinks[k]= JSON.parse(d)
 })

 // 2. Or just filter the incoming data by it's key, it should be fast even on large sets
 gun
  .user(room.pub)
  .get('#links')
  .map()
  .once((d,k)=> {
   if (!k.includes(Bob.pub)) return
   bobLinks[k.slice(0,44)] = JSON.parse(d)
 })

THE BLACKLIST FEATURE IS DEPRECATED in GUN v.0.2020.1232 and higher!

Blacklist

Note: The way this feature works might consume network traffic and cause other problems. We (@mimiza & @amark) have a discussion recently and decided that this feature should be deprecated. Sorry @jabis, I know this was your idea. This feature should be implemented somewhere above gun and lower than SEA.

  • opt.blacklist [String || Object]: A blacklist, in case you want to revoke ie. Bob after giving him the Certificate.
    • opt.blacklist.read [String || Object]: path or a Gun ref object {'#': '~ManagerPub/blacklist'} to the READ blacklist.

    • opt.blacklist OR opt.blacklist.write [String || Object]: path or a Gun ref object {'#': '~ManagerPub/blacklist'} to the WRITE blacklist.

    • If it is a string and starts WITH '~', ie. '~AlicePub/blacklist', SEA will look for it at

gun.get('~AlicePub/blacklist').get(Bob.pub).once(value => value === true || value === 1)
* If it starts WITHOUT '~', _ie. 'blacklist'_, SEA will look for it at 
gun.get('~YourPub').get('blacklist').get(Bob.pub).once(value => value === true || value === 1)
  • If SEA finds the value and it equals true or 1, then Bob is blacklisted and SEA won't sync his writes.
// NOTE: The examples below about blacklist/ban/block won't work in the future, because it will be deprecated.
// Now Alice wants to revoke access of Bob. She has TWO OPTIONS. OPTION 1 is to manage the blacklist by herself.
user.get('blacklist').get(Bob.pub).put(true) // OPTION 1: She directly manages her blacklist, in her graph.

// OPTION 2: Alice could point the blacklist to her husband's graph:
user.get('blacklist').put({'#': '~'+AliceHusband.pub+'/blacklist'})

// Now on AliceHusband's side, HE can add Bob to his blacklist:
user.get('blacklist').get(Bob.pub).put(true)

THE BLACKLIST FEATURE IS DEPRECATED!