doauthor
Version:
A client for DoAuth: a fast, lean and reliable authentication server based on verifiable credentials standard
388 lines (330 loc) • 13.3 kB
JavaScript
export const require = async () => {
// Idempotence for poor
if (window.hasOwnProperty('__doauthorHasLoaded__')) {
return 'Welcome to DoAuth';
}
const hasSodiumBeenLoaded = () => window.hasOwnProperty('sodium')
&& window.sodium.hasOwnProperty('SODIUM_LIBRARY_VERSION_MAJOR');
await observeMany(() => {
if (!hasSodiumBeenLoaded()) {
return [undefined];
}
return [true];
}, 5000);
import_doauth_into_global_namespace();
window.__doauthorHasLoaded__ = true;
return 'Sodium has loaded!';
}
const import_doauth_into_global_namespace = () => {
const maybePort = () => {
if (window.location.port) {
return ':' + window.location.port
} else {
return ''
}
};
window.doauthor = {
/* server: window.location.protocol + '//' + window.location.hostname + maybePort(), */
// server: "https://maja.doma.dev",
saltSize: 16,
hashSize: 32,
keySize: 32,
defaultParams: {
opsLimit: sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
memLimit: 5 * sodium.crypto_pwhash_MEMLIMIT_MIN
},
};
window.doauthor.crypto = {};
window.doauthor.crypto.show = (bs) => {
return sodium.to_base64(bs, sodium.base64_variants["URLSAFE"]);
}
window.doauthor.crypto.read = (s) => {
return sodium.from_base64(s, sodium.base64_variants["URLSAFE"]);
}
window.doauthor.crypto.slipConfig = () => {
return {
ops: doauthor.defaultParams.opsLimit,
mem: doauthor.defaultParams.memLimit,
saltSize: doauthor.saltSize
};
}
window.doauthor.crypto.mainKey = (pass) => {
const slipMaybe = localStorage.getItem("slip");
if (slipMaybe) {
return doauthor.crypto.mainKeyReproduce2(pass, JSON.parse(slipMaybe));
} else {
let [mkey, slip] = doauthor.crypto.mainKeyInit2(pass, doauthor.crypto.slipConfig());
localStorage.setItem("slip", JSON.stringify(slip));
return mkey;
}
}
window.doauthor.crypto.mainKeyInit2 = (pass, slipConfig) => {
const slip1 = { ...slipConfig, salt: doauthor.crypto.show(sodium.randombytes_buf(slipConfig.saltSize)) };
const mkey = doauthor.crypto.mainKeyReproduce2(pass, slip1);
return [mkey, slip1];
}
window.doauthor.crypto.mainKeyReproduce2 = (pass, slip) => {
let { ops, mem, salt } = slip;
const mkey = sodium.crypto_pwhash(
doauthor.hashSize, // kinda hardcoded but ok
pass,
doauthor.crypto.read(salt),
ops,
mem,
sodium.crypto_pwhash_ALG_DEFAULT
);
return mkey;
}
window.doauthor.crypto.deriveSigningKeypair = (mkey, n) => {
const mkd = sodium.crypto_kdf_derive_from_key(doauthor.keySize, n, "signsign", mkey);
let { publicKey, privateKey } = sodium.crypto_sign_seed_keypair(mkd);
doauthor.did.memorisePublicKey(publicKey);
return { public: publicKey, secret: privateKey };
}
window.doauthor.crypto.sign = (msg, kp) => {
return { public: kp.public, signature: sodium.crypto_sign_detached(msg, kp.secret) };
}
window.doauthor.crypto.verify = (msg, detached) => {
return sodium.crypto_sign_verify_detached(detached.signature, msg, detached.public);
}
window.doauthor.crypto.bland_hash = (msg) => {
return doauthor.crypto.show(sodium.crypto_generichash(doauthor.hashSize, msg));
}
window.doauthor.crypto.sign_map = (kp, the_map, overrides) => {
if (typeof (overrides) === 'undefined') {
overrides = {};
}
const opts0 = {
"proofField": "proof",
"signatureField": "signature",
"keyField": "verificationMethod",
"keyFieldConstructor": (pk) => {
const pk64 = doauthor.crypto.show(pk);
/* const hash = doauthor.crypto.bland_hash(pk64);
return "did:doma:" + hash; */
return pk64;
},
"ignore": ["id"],
};
const opts = Object.assign({}, opts0, overrides);
var mut_the_map = { ...the_map };
opts["ignore"].reduce((acc, x) => {
delete mut_the_map[x];
})
const to_prove = { ...mut_the_map };
const canonical_claim = doauthor.crypto.canonicalise(to_prove);
const detached_signature = doauthor.crypto.sign(JSON.stringify(canonical_claim), kp);
const did = opts["keyFieldConstructor"](kp["public"]);
const issuer = did;
const proof_map = doauthor.proof.from_signature(issuer, detached_signature["signature"]);
var res = { ...the_map };
res[opts["proofField"]] = proof_map;
return res;
}
window.doauthor.crypto.verify_map = (verifiable_map, overrides) => {
if (typeof (overrides) === 'undefined') {
overrides = {};
}
const opts0 = {
"proofField": "proof",
"signatureField": "signature",
//"keyExtractor": (proof) => doauthor.did.fetchPublicKey(proof["verificationMethod"]),
"keyExtractor": (proof) => proof["verificationMethod"],
"ignore": ["id"]
};
const opts = Object.assign({}, opts0, overrides);
var mut_verifiable_map = { ...verifiable_map };
const verifiable_canonical = doauthor.crypto.canonicalise(
(() => {
opts["ignore"].concat(opts["proofField"]).reduce((_acc, x) => { delete mut_verifiable_map[x]; })
return { ...mut_verifiable_map };
})()
);
var mut_proofs = [];
var zoom_proofs = verifiable_map[opts["proofField"]];
if (Array.isArray(zoom_proofs)) {
mut_proofs = [...zoom_proofs];
} else { // In this case, by standard, we have a single proof.
mut_proofs = [zoom_proofs];
}
const proofs = [...mut_proofs];
return proofs.reduce(async (acc, proof) => {
const pk = await opts["keyExtractor"](proof);
const sig = proof[opts["signatureField"]];
const reconstructed_detached_sig = {
"public": doauthor.crypto.read(pk),
"signature": doauthor.crypto.read(proof[opts["signatureField"]]),
};
const is_valid = doauthor.crypto.verify(JSON.stringify(verifiable_canonical), reconstructed_detached_sig);
return is_valid && acc;
}, true);
}
/*
* TODO:
* console.log's are left in to show what the author
* checked. They are NOT A SUFFICIENT EVIDENCE that this
* function does what it's supposed to do and proper
* invariant testing and audit are absolutely necessary to
* run it in production.
*
* UPDATE (Aug 18th, 2021):
* With more test coverage for simple cases, we have more evidence
* that canonicalise works as intended, but we still need to attack
* this function with as messed up test cases as possible and
* compare it with reference implementation.
*/
function _canonicalise(x) {
// console.log("Canonicalising ", x)
if (typeof (x) === "string" || typeof (x) === "number" || typeof (x) === "bigint") {
// console.log("It's just a value", x)
return x;
} else if (typeof (x) === "object") {
if (Array.isArray(x) === true) {
return x.map(x => _canonicalise(x));
} else {
var ks = Object.keys(x);
const x1 = { ...x };
ks.sort();
var y = new Array();
for (let i = 0; i < ks.length; i++) {
// console.log("Got object, working on adding **", ks[i], "**, the", i, "th element of", ks)
y.push([ks[i], _canonicalise(x1[ks[i]], y)]);
// console.log("Accumulator so far:", [...y])
}
return y;
}
}
};
window.doauthor.crypto.canonicalise = _canonicalise;
window.doauthor.proof = {};
window.doauthor.proof.from_signature64 = (issuer, sig64) => {
return { "verificationMethod": issuer, "signature": sig64, "timestamp": doauthor.util.isoUtcNow() };
}
window.doauthor.proof.from_signature = (issuer, sig) => {
return doauthor.proof.from_signature64(issuer, doauthor.crypto.show(sig));
}
window.doauthor.credential = {};
window.doauthor.credential.from_claim = (kp, claim, misc) => {
const tau0 = doauthor.util.isoUtcNow();
const did = doauthor.did.from_pk(kp["public"]);
const issuer = did;
var cred_so_far = {
"@context": [],
"type": [],
"issuer": issuer,
"issuanceDate": tau0,
"credentialSubject": claim,
}
if (typeof (misc) === 'object') {
["effectiveDate", "validFrom", "validUntil"].map((x) => {
if (!(x in cred_so_far) && (x in misc)) {
cred_so_far[x] = misc[x];
}
});
["issuanceDate"].map((x) => {
if (x in misc) {
cred_so_far[x] = misc[x];
}
})
}
return doauthor.crypto.sign_map(kp, cred_so_far);
}
window.doauthor.credential.present_credential = (kp, cred, misc) => {
var presentation_claim_so_far = {
"verifiableCredential": cred,
"issuer": doauthor.did.from_pk(kp["public"])
};
if (typeof misc === 'object') {
["id", "holder", "credentialSubject"].map((x) => {
if (!(x in presentation_claim_so_far) && (x in misc)) {
presentation_claim_so_far[x] = misc[x];
}
});
["issuanceDate"].map((x) => {
if (x in misc) {
presentation_claim_so_far[x] = misc[x];
}
});
}
return doauthor.crypto.sign_map(kp, presentation_claim_so_far);
}
window.doauthor.credential.proofless = (cred) => {
ctxs = cred['@context'];
let { type, issuer, issuanceDate, credentialSubject } = cred;
return { '@context': ctxs, type: type, issuer: issuer, issuanceDate: issuanceDate, credentialSubject: credentialSubject };
};
window.doauthor.credential.prooflessJSON = (cred) => {
return JSON.stringify(
doauthor.crypto.canonicalise(
doauthor.credential.proofless(cred)
)
);
};
window.doauthor.credential.verify = (cred, pk) => {
return doauthor.crypto.verify(
doauthor.credential.prooflessJSON(cred),
{
public: pk,
signature: doauthor.crypto.read(cred.proof.signature)
}
);
};
window.doauthor.credential.verify64 = (cred, pk) => {
return doauthor.credential.verify(cred, doauthor.crypto.read(pk));
};
window.doauthor.did = {};
window.doauthor.did.from_pk = (pk) => {
return doauthor.did.from_pk64(doauthor.crypto.show(pk));
}
window.doauthor.did.from_pk64 = (pk64) => {
/* return "did:doma:" + doauthor.crypto.bland_hash(pk64); */
return pk64;
}
window.doauthor.did.recallPublicKey = (did_str) => {
return localStorage.getItem("pk|" + did_str);
}
// window.doauthor.did.fetchPublicKey = async (did_str) => {
// var pk_by_did = doauthor.did.recallPublicKey(did_str);
// if (pk_by_did === null) {
// const did_public_resp = await fetch(doauthor.server + "/did/public/" + did_str).then(resp => resp.json);
// pk_by_did = did_public_resp["public"];
// doauthor.did.memorisePublicKey64(pk_by_did);
// }
// return pk_by_did;
// }
window.doauthor.did.memorisePublicKey64 = (pk64) => {
return localStorage.setItem("pk|" + doauthor.did.from_pk64(pk64), pk64);
}
window.doauthor.did.memorisePublicKey = (pk) => {
return doauthor.did.memorisePublicKey64(doauthor.crypto.show(pk));
}
window.doauthor.util = {};
window.doauthor.util.prettyPrint = (x) => JSON.stringify(x, null, 2);
window.doauthor.util.isoUtcNowOld = () => {
var date = new Date();
var isoDate = date.toISOString().slice(0, -5);
return isoDate + "Z";
}
window.doauthor.util.isoUtcNow = () => {
return (new Date()).toISOString();
}
window.__doauthorHasLoaded__ = true;
}
export const observePeriodMsec = 30;
export const observeMany = async (varsF, timeLeft) => {
var timeLeft1 = undefined;
if (varsF().reduce((acc, v) => acc && v, true)) {
return varsF();
}
if (typeof timeLeft !== 'undefined') {
if (timeLeft < 0) {
throw new Error("Observer timed out");
} else {
timeLeft1 = timeLeft - observePeriodMsec;
}
}
await new Promise(_f => {
setTimeout(_f, observePeriodMsec);
});
return await observeMany(varsF, timeLeft1);
}