Skip to content
davay edited this page Mar 16, 2021 · 37 revisions

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 made 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.

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, and a BLACKLIST in case you want to revoke access that you gave to someone.

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!

Blacklist

  • 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.

SOME EXAMPLES

var Alice = await SEA.pair()
var AliceHusband = 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), blacklist: 'blacklist'})

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

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

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

Personal paths in a room

With SEA.certify you can use a dedicated keypair space as a place to store any structured app data. Let's assume we want our users to be able to participate in different rooms, like chat rooms or something. Even thought each user can have a main profile in the private user space, we may want to let any user to store some room specific profile info in the room graph itself.

  1. Room initialization
// generate a new crypto pair
const room = await SEA.pair() 

// issue a certificate for all to write personal items to the 'profile'
const cert = await SEA.certify( '*', { '*':'profile', '+': '*' }, room, null, { blacklist: 'ban' } ) 

// authenticate with the pair, and run the callback
gun.user().auth(room, () => { 

  // put the certificate into the room graph for ease of later use
  gun.user()
    .get('certs')
    .get('profile')
    .put(cert) 
})
  1. User editing personal data
// generate a fresh user pair
const user = await SEA.pair() 

// load the room 'profile' certificate
const certificate = await gun.get('~'+room.pub).get('certs').get('profile').then() 

// use the certificate to write to his personal route at the room profile
gun
  .get('~'+room.pub)
  .get('profile')
  .get(user.pub)
  .put({name: 'New user'}, null, {opt: {cert: certificate }} )

// you can also null the data later, if needed
gun
  .get('~'+room.pub)
  .get('profile')
  .get(user.pub)
  .put(null, null, {opt: {cert: certificate }} 

Personalised content-addressed items list

You may want to create a list of items, filled only by some verified users. We use content-addressing as a way to make urls immutable and to show some more complex use of SEA primitives. If you want your items to be mutable, just omit the SEA.work part and use Gun.text.random() instead of the hash.

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

  1. Room initialization
// let's generate the users
const Alice = await SEA.pair() // Alice will be the host of the room
const Bob = await SEA.pair()
const Chuck = await SEA.pair() // Chuck will show himself as a rude person later
const Dave = await SEA.pair() // Dave will be a little late

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

// we generate a new crypto pair for the room
const room = await SEA.pair() 

// we authenticate with the room pair to set everything up
gun.user().auth(room, async () => { 

  // Alice is the room host, so we may store the encrypted room pair here in the room for her later use (i.e. to issue new certificates)
  let enc = await SEA.encrypt(room, Alice)
  gun.user().get('host').get(Alice.pub).put(enc)

  // Alice will be able manage the banlist with her personal ban certificate
  let banCert = await SEA.certify(Alice.pub, { '*':'ban' }, room)
  gun.user().get('certs').get('ban').get(Alice.pub).put(banCert)

  // we iterate over the verified users list. 
  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, null, { blacklist: 'ban' } ) 

    // put the user certificate to a 'certs/links' path for ease of later use (make sure not to use `#` hash symbol as it will impose content-addressing and the put will fail)
    gun.user().get('certs').get('links').get(pub).put(cert) 
  })
})
  1. Users add personal data to the public list
// Bob logs in
gun.user().auth(Bob)

// loads his certificate
const certificate = await gun.get('~'+room.pub).get('certs').get('links').get(Bob.pub).then() 

// checks if he has the certificate to use
if(certificate) {

  //Bob add creates a new url object to be added to the list
  let url = {url: 'https://gun.eco'}

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

  // uses the certificate to write to his personal profile in the room
  gun
    .get('~'+room.pub)
    .get('#links')
    .get(`${Bob.pub}@${hash}`) 
    .put(text, null, {opt: {cert: certificate }})

  // the link item will have a key of `user.pub @ hash`, something like`7TbFCpq79fs_ZZlFEzwMmSBnn8xeoQTpDnq0xrB7sxE.1FsktZDUllWLi8wIOC9tiqpKA4Eiqgx3fIaYCn1I4c8@RkbhM2E/Co5l2z8mr6WuWVy1HWi+XCOFwoO1ulzc1Ag=`
}
  1. Alice manages the room
  // Alice logs in and wants to ban Chuck and add Dave
  gun.user().auth(Alice)

  // she gets her banlist certificate
  gun
    .get('~'+room.pub)
    .get('certs')
    .get('ban')
    .get(Alice.pub)
    .once(banCert=> {
      if( banCert ) { //if it's in place
        // Alice puts Chuck into the banlist. From now Chuck will be unable to add his links to the list
        gun
        .get('~'+room.pub)
        .get('ban')
        .get(Chuck.pub)
        .put(true, null, {opt: { cert: banCert }})
      }
    })

  // to add Dave as a new user she gets the encoded room pair
  let enc = await gun.get('~'+room.pub).get('host').get(Alice.pub).then()
  
  // decodes it with her pair 
  let room = await SEA.decrypt(enc, Alice)

  //issues a certificate for Dave
  let daveCert = await SEA.certify(Dave.pub, { '*':'#links', '+': '*' }, room, null, { blacklist: 'ban' })
  
  //Alice adds Dave's certificate to the room
  // NOTICE: you may want to use a second gun instance to login into the room while authed with the user
  gun2.user().auth(room, ()=> {
    gun2.user().get('certs').get(Dave.pub).put(daveCert)
  })
  1. We render the list of links with verified authorship
```js
const links = {}
gun
  .get('~'+room.pub)
  .get('#links')
  .map()
  .on((data,key)=> {

    // extract author pub from the key
    let author = key.slice(0,87) 

    // and the unique hash of the data
    let hash = key.slice(-44) 

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

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

//we still can get links of the particular user
const bobLinks = {}

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

//or just filter the incoming data by key
gun.get('~'+room.pub).get('#links').map().once((d,k)=> {
  if (!k.includes(Bob.pub)) return
  bobList[k.slice(-44)] = JSON.parse(d)
})

This wiki is where all the GUN website documentation comes from.

You can read it here or on the website, but the website has some special features like rendering some markdown extensions to create interactive coding tutorials.

Please feel free to improve the docs itself, we need contributions!

Clone this wiki locally