Skip to content

Instantly share code, notes, and snippets.

@jabis
Last active July 15, 2023 10:48
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jabis/a3b7d5b475ef23fb859d3acabe9325cb to your computer and use it in GitHub Desktop.
Save jabis/a3b7d5b475ef23fb859d3acabe9325cb to your computer and use it in GitHub Desktop.
Gun user-space and public graph get/set with signing and encryption
/**
* get and put encrypted and/or signed material to paths in Gun
* FIXME: Investigate why root level put doesn't work
*
* DONE: Add signing to make objects unwritable by others
* Changes:
* - 25.02.2020
* - added mergedeep to better merge deeper objects between themselves
* - added pair.osign option to only sign not encrypt when passing existing pairs
**/
(async function(){
var Nug = this.Nug = null;
var atoob = this.atoob = function atoob(arr){
var obj = {};
Gun.list.map(arr, function(v,f,t){
if(Gun.list.is(v) || Gun.obj.is(v)){
obj[f] = atoob(v);
return;
}
obj[f] = v;
});
return obj;
};
const getNug = this.getNug = function(){
if(Nug) return Nug.back(-1);
if(Gun) Nug = new Gun(location.protocol+"//"+location.host+"/gun");
return Nug.back(-1);
}
const mrgdeep = this.mrgdeep = function mrgdeep(...objects) {
const isObject = obj => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mrgdeep(pVal, oVal);
}
else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
const decEnc = this.decEnc = async(...args)=> {
console.log(args);
let data = args.length > 0 ? args.shift() : false; // data as first argument
let pair = args.length > 0 ? args.shift() : false; // pair to use in decrypting/encrypting as second argument
let mode = args.length > 0 ? args.shift() : "decrypt"; // mode optional encrypt/decrypt on SEA defaults to decrypt
//TODO return Promise.reject if we don't have all mandatory arguments passed
let secret = await SEA.secret(pair.epub, pair);
let it = await SEA[mode](data, secret);
return it;
};
const createRWEResource = this.createRWEResource = async(...args)=> {
let key = args.shift();
let data = args.shift();
let encIt = args.length>0 ? args.shift() : false;
let addUuid = args.length>0 ? args.shift() : false;
const uuid = gun._.opt.uuid();
let pair = await SEA.pair();
if(typeOf(encIt) == "object" && encIt.priv) {
pair = encIt;
var onlysign = pair.osign? true:false;
if(pair.hasOwnProperty('osign')) delete pair["osign"];
if(onlysign) encIt = false;
}
let id = "~"+pair.pub;
if(addUuid) id = id + "." + uuid;
let nug = getNug();
if(!encIt){
let datax = {"#":id,'.':key,':':data,'>':Gun.state()}
let signed = await SEA.sign(datax,pair);
let putsi = await nug.get(id).get(key).put(signed).then();
console.log("putsi",putsi);
return {id:id,key:key,ref:nug.get(id).get(key),pair:pair};
}
else {
return await decEnc(data,pair,"encrypt").then(async(enc)=>{
let datax = {"#":id,'.':key,':':'a'+enc,'>':Gun.state()}
let signed = await SEA.sign(datax,pair);
console.log(pair,signed);
let putsi = await nug.get(id).get(key).put(signed).then()
console.log("putsi",putsi);
return {id:id,key:key,ref:nug.get(id).get(key),pair:pair};
});
}
};
Gun.chain.putsenc = async function(){
let args = Array.from(arguments);
let self = this;
let gun = this.back(-1);
let data = args.shift();
let pair = args.length > 0 ? args.shift() : (gun.user().is ? gun.user()._.sea : null);
let genuuid = args.length > 0 ? args.shift() : false;
let onlysign = args.length > 0 ? args.shift() : false;
if(!pair) return Promise.reject("No keypair provided or not logged in");
if(onlysign) pair.osign=true;
return self.once(async function(olddata,key) {
return await createRWEResource(key, data, pair, genuuid);
});
};
Gun.chain.putenc = async function() {
let args = Array.from(arguments);
let self = this;
let gun = this.back(-1);
let nug = getNug();
let data = args.shift();
let me = gun.user().is;
let pair = args.length > 0 ? args.shift() : me ? gun.user()._.sea : null;
let onlysign = args.length > 0 ? args.shift() : false;
//TODO: Implement checking with old pair as well as throw if error
if (!pair) return Promise.reject("No keypair provided or not logged in");
if(onlysign) pair.osign = true;
return this.once(async function(olddata, key) {
console.log("old data", olddata);
//TODO: Check the onlysign and skip decrypt and encrypt in that case
if(typeof olddata === "string" && /^(aSEA)/.test(olddata)) olddata = olddata.slice(1)
return await decEnc(olddata, pair)
.then(async old => {
console.log("old", old);
var nd = null;
if (!old) {
nd = data;
} else {
if (typeof old === "object" && data && typeof data ==="object") {
// trying to simulate regular put
nd = mrgdeep(old, data);
} else {
nd = data;
}
}
console.log("new data", nd);
return await decEnc(nd, pair, "encrypt").then(async (enc) => {
console.log("encrypted", enc);
if (!me && pair && pair.priv) {
let id = "~"+pair.pub;
let signed = await SEA.sign(
{
"#": id,
".": key,
":": "a"+enc,
">": Gun.state()
},
pair
);
console.log("signed",signed);
return await nug.get(id).get(key).put(signed).then();
} else {
return await self.put("a"+enc).then();
}
});
})
.catch(err => {
console.log("whoops", err);
return gun;
});
});
};
Gun.chain.getenc = async function(){
let args = Array.from(arguments);
let self = this;
let gun = this.back(-1);
let path = args.length > 0 ? args.shift() : false;
let pair = args.length > 0 ? args.shift() : (gun.user().is ? gun.user()._.sea : null)
if(!pair) return Promise.reject("No keypair provided or not logged in");
let data;
if(!path) {
data = await self.once().then();
} else {
data = await this.get(path).then();
}
//console.log(data,pair,path);
if(typeof data === "string" && /^(aSEA)/.test(data)) data = data.slice(1)
let dec = await decEnc(data, pair);
return dec ? dec : null;
};
const putMyDataEnc = this.putMyDataEnc = async (...args) => {
//let args = arguments; Array.from(arguments);
console.log(args);
let cb = false;
if(args && args.length > 0 && typeof args[args.length-1] === "function"){
cb = args.pop(); // if we have a cb function in last of the list, then pop that
}
let path = args.length > 0 ? args.shift() : false; // path as first argument
let data = args.length > 0 ? args.shift() : false; // data as second argument
let pair = args.length > 0 ? args.shift() : false; // pair optional pair to use in encrypting
if(!path || !data) return Promise.reject("No path or data!");
const me = gun.user();
if (me.is) {
//authenticated
const mypair = me._.sea;
let usePair = mypair;
if(pair && pair.epub && pair.epriv) usePair = pair;
let enc = await decEnc(data, usePair,"encrypt");
if (cb) {
return me
.get(path)
.put(enc,cb)
} else {
return await me
.get(path)
.put(enc)
.then();
}
} else {
return Promise.reject("Not authenticated");
}
};
const getMyDataEnc = this.getMyDataEnc = async (...args)=>{
console.log(args);
let path = args.shift(); // path as first argument
let pair = args.length > 0 ? args.shift() : false; // optional pair to decrypt with
const me = gun.user();
if(me.is) { //authenticated
const mypair = me._.sea;
let usePair = mypair;
if(pair && pair.epub && pair.epriv) usePair = pair;
let data = await me
.get(path)
.then()
//console.log(data);
let dec = await decEnc(data, usePair)
return dec;
} else {
return Promise.reject("Not authenticated")
}
};
// USAGE:
// creating a new resource with generated keypair signature: {keyname} to store to, {data} to house {pair} a pair or boolean, {uuid} whether to append random uuid to pubkey
let testresource1 = await createRWEResource("testing/encryption/321",{'data':'I','want':'to',encrypt:'true'},true,true);
console.log(testresource1); // {id:created id, key: referenced key, ref:gun node, pair: if not your user-pair save this if you ever want to touch this node again}
// creating/overwriting a resource with your user keypair directly to gun.user().get("testing/encryption/321")
let mypair = gun.user()._.sea;
testresource2 = await createRWEResource("testing/encryption/321",{'data':'I','dontwant':'to',encrypt:'true'},mypair);
console.log(testresource2);
//decode with decEnc
let id = testresource2.id;
let key = testresource2.key;
let setti = testresource2.ref;
let pair = testresource2.pair;
let val = await setti.once(Gun.log).then();
let dec = await decEnc(val.slice(1),testresource2.pair)
console.log(dec)
//{data: "I", dontwant: "to", encrypt: "true"}
let example = await gun.get("testing/encryption/123").get("example").putsenc({"encrypted":"stuff"}); // overwrite whole example
console.log(example);
let readexample = await gun.get("testing/encryption/123").getenc("example");
console.log(readexample);
let updateexample = await gun.get("testing/encryption/123").get("example").putenc({"stuff":"encrypted stuff"});
console.log(updateexample);
setTimeout(async()=>{
let x = await gun.get("testing/encryption/123").getenc("example");
console.log("some wait time so we wont resolve from localstorage",x);
},250)
putMyDataEnc("testing/encryption/123",{socks:"wet"}).then((d)=>{ console.log(d); }).catch((err)=>{ console.log(err); })
getMyDataEnc("testing/encryption/123").then((d)=>{ console.log(d); }).catch((err)=>{ console.log(err); })
//testing decryption of the node:
gun.user().get("testing/encryption/123").once(function(data){
decEnc(data,gun.user()._.sea).then((d)=>{console.log(d)}).catch((err)=>{ console.log(err); })
})
})();
@zilveer
Copy link

zilveer commented Dec 15, 2019

is this correct:
.putsenc overwrites whole object where .putenc tries to update the object, if its an object?

.get("order").putsenc({ stuff:"here"})
.get("order").putenc({something:"updated"}})
.getenc("order") // {stuff:"here",something:"updated"}

also did you fix the chaining promptly on this?
thanks for reply,
regards

@jabis
Copy link
Author

jabis commented Dec 15, 2019

@zilveer:

I'm refactoring some aspects of this example script as I'm wanting it to be a bit more generic, so you would have a choice of

  1. encrypt the whole node
  2. encrypt only values inside the object

And to chaining in general, returning without the put...then() calls seems to keep the chaining functional, but I'm working purely with async/await/promises so in my use case I kept the current setting with the .then()ables, for where I want the encryption/decryption I simply want the decrypted value or ack returned in the process

@jabis
Copy link
Author

jabis commented Dec 15, 2019

Also additional note, these objects are encrypted, but not write protected, without signing them and including the pubkey in the public graph, so it's only partial solution so far :)

@zilveer
Copy link

zilveer commented Dec 15, 2019

@jabis that sounds pretty cool !

When do you think it's functionality will be totally finished, including write permissions ?

Best regards Zilveer

@jabis
Copy link
Author

jabis commented Dec 15, 2019

It's on my to-do list, but can't promise exact timeframe, as I'm working on big project, where this functionality is not the biggest priority :)
I'm thinking maybe I'll throw a new revision by christmas - but if sooner I'll update the gist so you'll get notified if you follow it :)

@zilveer
Copy link

zilveer commented Dec 23, 2019

Hi,
Just a friendly reminder about what I am waiting for :)
I am looking forward to use this in my project :))

Best regards Zilveer

@jabis
Copy link
Author

jabis commented Dec 24, 2019

Handling a family pet emergency, so will take a little longer unfortunately

@zilveer
Copy link

zilveer commented Dec 26, 2019

@jabis sorry to hear, hope everything is going good.

Just eager to your helper class released soon so I can use it in my app :)

Best regards Zilveer

@jabis
Copy link
Author

jabis commented Jan 8, 2020

Hi, just to let you know I haven't forgotten you guys, working on this now for the rest of the week :)
@zilveer @anoxxy, updating now with yet incomplete version

@zilveer
Copy link

zilveer commented Jan 8, 2020

@jabis That is great to hear, hope you pet is better now!

Would you mind to add some kind of callback functionaltity to putenc, putsenc as it works in put(obj, cb)?
It would also be awesome if you can add the callback feature to the standalone functionalities!

Best regards Zilveer

@jabis
Copy link
Author

jabis commented Jan 8, 2020

I'm not a fan of callback-hell but I'll see if I can make it somewhat compatible. The thing is I'm plucking this out of the framework and replacing things that I rely on elsewhere with vanilla, so it's a little chore to do :D

@jabis
Copy link
Author

jabis commented Jan 8, 2020

@zilveer https://gist.github.com/jabis/a3b7d5b475ef23fb859d3acabe9325cb#file-myhelper-js-L24-L65 here I'm now trying upon creation to return the ref to the gun chain for further use, well we'll see at the end of the week if I can wrap my head around how to mold the Gun.chain to somewhat sane implementation ^^

@jabis
Copy link
Author

jabis commented Jan 8, 2020

Note: that's now write protected and put(s)enc can't yet touch it - have to solve the signing (Nug to use) there, but gun.get('~createdpubkeystuff').getenc("testing/encryption/321",pair) is indeed working already

@jabis
Copy link
Author

jabis commented Jan 8, 2020

image

@jabis
Copy link
Author

jabis commented Jan 8, 2020

@zilveer, all my methods work with await or .then(function(woot){ console.log(woot); }) instead of adding callbacks

@jabis
Copy link
Author

jabis commented Jan 8, 2020

image

@jabis
Copy link
Author

jabis commented Jan 8, 2020

Now you can manipulate data with
createRWEResource/getenc/put(s)enc like:

testresource = await createRWEResource("testing/encryption/321",{'data':'I','want':'to',encrypt:'true'},gun.user()._.sea);
console.log(testresource); // {id,key,gunref,pair}
//and directly read with getenc (well might need a little setTimeout if like me it takes awhile to push changes to peers)
await gun.user().getenc("testing/encryption/321");
//and directly write with putenc 
await gun.user().get("testing/encryption/321").putenc({some:"more",data:"here"});

Note createRWEResource currenctly acts like .putsenc overwriting the data

tidbit: the method createRWEResource comes from create ReadWriteprotectedEncryptedResource - gonna come up with a better name soon :D

@zilveer
Copy link

zilveer commented Jan 10, 2020

@jabis wow such improvement over the current coding. You should indeed have this as a repo instead for GUN or else it is somehow "hidden" from public.

I really like what you have achieved so far, thumbs up !

Regards Zilveer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment