coze
Version:
Coze - A cryptographic JSON messaging specification designed for human readability.
259 lines (224 loc) • 7.42 kB
JavaScript
;
import * as CTK from './cryptokey.js';
import * as Can from './canon.js';
import * as Coze from './coze.js';
import * as Alg from './alg.js';
import {
isEmpty
} from './coze.js';
export {
NewKey,
Correct,
Valid,
Thumbprint,
Revoke,
IsRevoked,
// RecalcX,
TmbCanon,
}
/**
@typedef {import('./typedef.js').Tmb} Tmb
@typedef {import('./typedef.js').Alg} Alg
@typedef {import('./typedef.js').Use} Use
@typedef {import('./typedef.js').Sig} Sig
@typedef {import('./typedef.js').Key} Key
*/
// Coze key Thumbprint Canons.
const TmbCanon = ["alg", "x"];
/**
NewKey returns a new Coze key.
If no alg is given, the returned key will be an 'ES256' key.
@param {Alg} [alg=ES256] - Alg of the key to generate. (e.g. "ES256")
@returns {Key}
*/
async function NewKey(alg) {
if (isEmpty(alg)) {
alg = Alg.Algs.ES256;
}
if (Alg.Genus(alg) == Alg.GenAlgs.ECDSA) {
var keyPair = await CTK.CryptoKey.New(alg);
} else {
throw new Error("Coze.NewKey: only ECDSA algs are currently supported.");
}
let k = await CTK.CryptoKey.ToCozeKey(keyPair.privateKey);
k.iat = Math.floor(Date.now() / 1000); // To get Unix from js, divide by 1000.
k.tmb = await Thumbprint(k);
k.kid = "My Cyphr.me Key.";
return k;
}
/**
Thumbprint calculates and returns a B64 Coze key thumbprint. Fails on empty
'alg' or 'x'.
@param {Key} cozeKey
@returns {Tmb}
@throws {error}
*/
async function Thumbprint(cozeKey) {
if (isEmpty(cozeKey.alg) || isEmpty(cozeKey.x)) {
throw new Error("Coze.Thumbprint: alg or x is empty.");
}
return Can.CanonicalHash64(cozeKey, await Alg.HashAlg(cozeKey.alg), TmbCanon);
};
/**
Valid returns true only for a valid private Coze key.
@param {Key} privateCozeKey Private Coze key.
@returns {boolean}
*/
async function Valid(privateCozeKey) {
if (isEmpty(privateCozeKey.d)) {
console.error("Coze key missing `d`");
return false;
}
try {
let msg = `7AtyaCHO2BAG06z0W1tOQlZFWbhxGgqej4k9-HWP3DE-zshRbrE-69DIfgY704_FDYez7h_rEI1WQVKhv5Hd5Q`;
let sig = await Coze.SignPay(msg, privateCozeKey);
return Coze.VerifyPay(msg, privateCozeKey, sig);
} catch (e) {
//console.debug("Valid error: " + e);
return false;
}
}
/**
Correct checks for the correct construction of a Coze key, but may return
true on cryptographically invalid public keys. Key must have `alg` and at
least one of `tmb`, `x`, and `d`. Using input information, if it is possible
to definitively know the given key is incorrect, Correct returns false, but
if it's plausible it's correct, Correct returns true. Correct answers the
question: "Is the given Coze key reasonable using the information provided?".
Correct is useful for sanity checking public keys without signed messages,
sanity checking `tmb` only keys, and validating private keys. Use function
"Verify" instead for verifying public keys when a signed message is
available. Correct is considered an advanced function. Please understand it
thoroughly before use.
Correct:
1. Checks the length of `x` and/or `tmb` against `alg`.
2. If `x` and `tmb` are present, verifies correct `tmb`.
3. If `d` is present, verifies correct `tmb` and `x` if present, and verifies
the key by verifying a generated signature.
@param {Key} ck
@returns {boolean}
*/
async function Correct(ck) {
if (typeof ck !== "object") {
console.error("Correct: CozeKey must be passed in as an object.");
return false;
}
if (isEmpty(ck.alg)) {
console.error("Correct: Alg must be set");
return false;
}
let p = Alg.Params(ck.alg);
let isTmbEmpty = isEmpty(ck.tmb);
let isXEmpty = isEmpty(ck.x);
let isDEmpty = isEmpty(ck.d);
if (isTmbEmpty && isXEmpty && isDEmpty) {
console.error("Correct: At least one of [x, tmb, d] must be set");
return false;
}
// tmb only key
if (isXEmpty && isDEmpty) {
if (isTmbEmpty || ck.tmb.length !== p.HashSizeB64) {
console.error("Correct: Incorrect `tmb` size: ", ck.tmb.length);
return false;
}
return true;
}
// d is not set
if (!isXEmpty && ck.x.length !== p.XSizeB64) {
console.error("Correct: Incorrect x size: ", ck.x.length);
return false;
}
// We currently do not support recalculating `x`, as subtle does not provide
// the necessary API for computing the points from the private component.
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle
//
// See RecalcX docs below
//
// If d and (x and/or tmb) is given, recompute from d and compare:
// let x = RecalcX(ck);
// If tmb is set, recompute and compare.
if (!isTmbEmpty && !isXEmpty) {
let t = await Thumbprint(ck);
if (ck.tmb !== t) {
console.error("Correct: Incorrect given `tmb`: ", ck.tmb);
return false;
}
}
// If private key, validate by signing and verifying.
// `x` must also be populated, for cryptokey, since we do not have RecalcX().
if (!isDEmpty && !isXEmpty) {
let cryptoKey = await CTK.CryptoKey.FromCozeKey(ck);
let mldBuffer = await Coze.SToArrayBuffer("Test Signing")
let sig = await CTK.CryptoKey.SignBuffer(cryptoKey, mldBuffer);
let pubKey = await CTK.CryptoKey.FromCozeKey(ck, true);
let result = await CTK.CryptoKey.VerifyArrayBuffer(ck.alg, pubKey, mldBuffer, sig);
if (!result) {
console.error("Correct: private key invalid.");
return false;
}
}
return true;
};
// TODO Support RecalcX if crypto.subtle provides necessary API for computing
// https://stackoverflow.com/questions/72151096/how-to-derive-public-key-from-private-key-using-webcryptoapi/72153942#72153942
//
// scalar/jacobian/affinity from private component.
// Alternatively, use noble.
// function RecalcX(ck) {
// let x;
// switch (ck.alg) {
// case "ES256":
// case "ES384":
// case "ES512":
// break;
// default:
// x = null;
// }
// return x;
// }
/**
Revoke generates a self revoke message and sets the input key as revoked.
'rvk' will be set on given cozeKey.
@param {Key} cozeKey Private Coze key.
@param {string} [msg] Optional, human readable non programmatic reason for revoking the key.
@returns {Coze} Signed revoke Coze.
@throws {error} Fails if cryptoKeyPrivate is nil or invalid.
*/
async function Revoke(cozeKey, msg) {
if (isEmpty(cozeKey)) {
throw new Error("CozeKey.Revoke: Private key not set. Cannot sign message");
}
var coze = {};
coze.pay = {};
if (!isEmpty(msg)) { // Optional revoke message.
coze.pay.msg = msg;
}
coze.pay.rvk = Math.round((Date.now() / 1000)); // Javascript's Date converted to Unix time.
// SignCoze does not allow revoked keys to sign messages. Temporarily remove
// key.revoke and then set back afterward, otherwise set key with new revoke.
let prevRvk = cozeKey.rvk;
delete cozeKey.rvk;
coze = await Coze.Sign(coze, cozeKey);
if (prevRvk !== undefined) {
cozeKey.rvk = prevRvk;
} else {
cozeKey.rvk = coze.pay.rvk;
}
return coze
};
/**
IsRevoked returns true if a key or a coze is marked as revoked. `rvk` should
be an integer Unix timestamp, however this function also checks for the
string "true" as well as the bool `true`.
Messages self-revoking keys must have `rvk` with an integer value greater
than 0.
@param {Key|Coze} cozeKey Coze key or coze object.
@param {string} [msg] Optional reason for revoking the key.
@returns {boolean}
*/
function IsRevoked(cozeKey) {
if (isEmpty(cozeKey.rvk) || !(parseInt(cozeKey.rvk) > 0)) {
return false;
}
return true;
};