/*
 * Creates a CAPTCHA token based upon Proof of Work (PoW), used to invoke the server.
 *
 * The idea is based upon PoW, requiring the client to create a SHA256 hash, with
 * at least n number of trailing zeros, implying it will need to create a SHA256 hash
 * a lot of times before it succeeds in creating a hash with enough trailing zeros
 * to be considered valid.
 * 
 * The server again will reject the token and throw an exception if the hash is not valid,
 * was not created less than n seconds ago, or does not have enough trailing zeros to be
 * considered valid.
 * 
 * The token is generated by creating a string as follows; sha256('[[public-key]];unix-timestamp-milliseconds;seed').
 * 
 * The seed is a random number, and starts at 0, and is incremented in a loop where we're generating SHA256
 * values, until we've got a hash with enough trailing zeros to be considered a valid token.
 *
 * Your callback function will be invoked when a valid token has been created, and workload defines how
 * much CPU time the client is required to spend to generate a valid token.
 * 
 * The higher the workload, the more CPU time is required to spend to generate
 * a valid token. Workload is basically number of trailing zeros the hash requires
 * before it's considered a valid hash. Since the end resulting hash is a hex number,
 * this implies a workload of 3 is 16x16x16, implying on average of 4,096 iterations
 * before a valid hash has been found. Workload iterations are as follows.
 * 
 * - 1 in workload becomes 16 iterations on average
 * - 2 in workload becomes 256 iterations on average
 * - 3 in workload becomes 4,096 iterations on average
 * - 4 in workload becomes 65,536 iterations on average
 * 
 * For highly important endpoints, you might want to consider increasing the workload
 * from its default value of 3, to maybe 4. However, with a workload
 * of more than 3, it could in theory take several seconds, or even minutes to generate a valid
 * token, which is especially true on less expensive hardware.
 * 
 * On the server you will need to verify that the token is not
 * older than some n amount of seconds, implying if the token cannot be older than 5 seconds,
 * and it takes 6 seconds to generate the token, the token will always be rejected by the server.
 * 
 * The server again needs to verify the token has enough trailing zeros to be considered
 * valid, and also obviously reproduce the hash and verify it's a match.
 * 
 * The hash algorithm is as follows; SHA256(secret + ';' + unix_time + ';' + seed)
 * 
 * The secret should be a string unique to your server. In Magic we're using a double hash of
 * our auth secret. Double hashing to avoid predictable hash values, resulting in something equivalent
 * to an HMAC.
 * 
 * On my computer, which is a MacBook Pro M3, I can create roughly 50 hash values for every 1 millisecond.
 * Implying for workload 3 on my computer it requires on average 80 milliseconds to generate a valid
 * token.
 * 
 * To avoid having the thing become too expensive on inexpensive hardware, you would probably
 * want to create some "room" also for less expensive hardware. I would assume a workload of 2/3 is a good
 * start, with workloads of 2 for less crucial endpoints, and a workload of 3 only for
 * important endpoints, where you know the machine invoking the endpoint is also modern having a fairly
 * fast CPU/GPU.
 * 
 * Concerns; The algorithm assumes the client and the server are synchronised on their BIOS time, since
 * the client generates a Unix timespan it passes to the client, that must be within a 10 second window.
 * 
 * This text was written the 19th of April 2024. If you're reading this 5 years into the future, higher
 * workloads might be considred due to Moore's law for obvious reasons.
 */

(function() {

// Global object through which we can interact with blowfish / CAPTCHA.
window.mcaptcha = {};

/*
 * Function to be invoked whenever a CAPTCHA token is required.
 */
mcaptcha.token = async function(callback, workload = 3) {
  const now = Date.now();
  const toHash = '[[public-key]];' + now;
  let seed = 0;
  const trailing = '0'.repeat(workload);
  while (true) {
    const uIntArray = new TextEncoder('utf-8').encode(toHash + ';' + seed);
    const array = await crypto.subtle.digest('SHA-256', uIntArray);
    const hashArray = Array.from(new Uint8Array(array));
    const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('');
    if (hashHex.endsWith(trailing)) {
      const token = hashHex + ';' + now + ';' + seed;

      // We've got a valid token fulfilling the workload requirements.
      const finished = Date.now();
      console.log(hashHex + ' generated in; ' + (finished - now) + ' milliseconds with ' + seed + ' iterations');
      callback(token);
      return;
    }

    // Incrementing the seed value and trying again.
    seed += 1;
  }
}
})();