javascripthashcryptography

Using javascript `crypto.subtle` in synchronous function


In javascript, is it possible to use the browser built-in sha256 hash (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string) inside a synchronous function?

Ideally, I'd like to do something like

String.prototype.sha256 = function() {
    // ...
    return hash
}

I already tried things like (async() => {hash = await digestMessage(message); return hash})(), but I can only get back the promise object.

It seems to me that it might not be possible to achieve what I want, but I thought I'll ask here before giving up. Thanks!


Solution

  • TL;DR

    No, it is not possible to wrap an asynchronous function in a synchronous one in JavaScript and propagate the results. Please see this excellent blog post on sync vs. async functions in various languages. The takeaway is that JavaScript is one (of many languages) in which async functions are infectious due to the nature of how the language runs.

    Async built-in functions are a savior in JS

    JavaScript runs on one thread. More concretely, all JavaScript related to a particular webpage runs on the same thread to keep the guarantee that only one line of JS will be running in any particular moment. This spares us neanderthal web programmers the responsibility of writing synchronization code like mutexes and atomic operations lest multiple threads write to the same memory simultaneously and cause data corruption or even crashes.

    But then, it kind of sucks that we only have one thread to manipulate the visual elements on the webpage and run all sorts of business logic like encryption/decryption and data management. That could all get kinda slow and hurt the user experience. But how do async functions fix that? Take this function:

    function syncGenRSAKey() {
        // fancy math stuff...
    
        return generatedKey;
    }
    

    Let's make it async (promise-based):

    function asyncGenRSAKey() {
        return new Promise((resolve, reject) => {
            resolve(syncGenRSAKey());
        });
    }
    

    Hopefully your intuition doesn't tell you the promise-based function is faster here. All that happens is this:

    1. Some code calls asyncGenRSAKey()
    2. Browser runs Promise constructor
    3. Promise constructor immediately/synchronously calls the (resolve, reject) => { ... } callback function passed to it
    4. Browser runs the syncGenRSAKey() function
    5. The promise fulfills synchronously

    Our code is still completely synchronous. We gained nothing. Remember, only one line of our JavaScript will ever run at a time. As long as our underlying key generation code (syncGenRSAKey()) is written in JavaScript, it will always eat up time on the main thread no matter where it is called from. That means it will prevent the browser from jumping around to other JavaScript, meaning, event handlers. The browser also renders the page on the main thread so it will freeze almost everything (some CSS animations get rendered specially) on the page while genRSAKey() is running. The user can hover a button and neither the button background nor the mouse cursor will update.

    Now, please refer back to the subheader for this section of my answer. The key words are built-in. Built-in functions, like those provided under crypto.subtle are written in whatever language the browser implementers choose: C++, Rust, etc. Those functions are not being run by the JavaScript engine, they are a part of it. They can spawn up as many OS threads as they want to run on as many (or as few) CPU cores as your computer can spare in a given moment. This means the key generation code could and often will run completely in parallel with a bunch of your JavaScript code and page rendering options, and then the browser will just call back into your JavaScript when the key is ready and any currently running JavaScript is done running, triggering the promise to resolve (or reject if there was an error generating the key), which can then kick off the code in any promises chained onto the key generating one.

    Now, is this really necessary for SHA-256 checksums in particular? No. In fact I myself still have a GitHub PR I've been putting off because I got tired of promisifying everything (which included some very complex Angular components) because I compute one f**king hash when the user opens a modal. This aside is for you, Suzanne.

    Below are two excellent videos that anyone reading this StackOverflow post should make time to watch. Unless you understand the synchronous/asynchronous nature of JavaScript well enough to picture exactly how your code is going to run, you don't really know JavaScript and you will run into bugs eventually that you won't understand.

    The Node.js Event Loop: Not So Single Threaded

    Jake Archibald: In The Loop - JSConf.Asia

    Clarification of async/await in JavaScript

    The async and await keywords are pure syntax sugar. They don't enable you to do anything you previously couldn't using old-fashioned promise chaining, much as promises don't enable you to do anything you couldn't with good ole nested callback functions. async/await just make your code 10x cleaner. Lastly, promises actually incurred a small amount of runtime overhead vs. using nested callbacks since promises have all sorts of state to facilitate chaining them nicely and are heap-allocated; async/await, I have heard, may undo that little step-back by making it much easier for the JS engines to see the overall context of the asynchronous code and where variables are used, etc., and make optimizations.

    Here are some common examples of async/await used properly. They are written in TypeScript for clarity of return types but if you just take off the : Whatevers it becomes JavaScript.

    Wrap a synchronous function in promise-based API

    This is rarely actually necessary but sometimes you need your code to fit an interface required by 3rd party code like a library.

    function withoutAsyncAwait(): Promise<number> {
        // Note that the reject callback provided to us by the Promise
        // constructor is rarely useful because the promise will
        // automatically be rejected if our callback throws an error,
        // e.g., if the Math.random() throws an error.
        return new Promise((resolve, reject) => resolve(Math.random()));
    
        // Could be (ignore the reject callback):
        // return new Promise(resolve => resolve(Math.random()));
    }
    
    async function withAsyncAwait(): Promise<number> {
        // If any synchronous code inside an async function throws an
        // error, a promise will still be returned by the async function,
        // but it will be rejected (by far the only desirable behavior).
        // The same is true if an await'ed promise rejects.
        return Math.random();
    }
    

    You cannot (and why would you) avoid the Promise constructor if you are wrapping traditional callback-based async functions as promises.

    function timeout(milliseconds: number): Promise<void> {
        return new Promise(resolve => window.setTimeout(resolve, milliseconds));
    }
    

    Conditional async step

    Sometimes you want to conditionally perform an asynchronous action before a bunch of synchronous code. Before async/await this meant you had to duplicate the synchronous code or just wrap it all up in a promise chain where the initial promise would be a no-op if the condition wasn't true.

    function doStuffWithoutAsyncAwait1(needToMakeAsyncRequest: boolean): Promise<void> {
        // Might be a no-op promise if we don't need to make a request before sync code
        const promise = needToMakeAsyncRequest ? makeAsyncRequest() : Promise.resolve();
    
        return promise.then(() => {
            // tons of code omitted here, imagine like 30 lines...
        });
    }
    
    function doStuffWithoutAsyncAwait2(needToMakeAsyncRequest: boolean): Promise<void> {
        // Or we can just write the sync code twice, wrapping it in a promise in the branch
        // where we make an async request first. This sucks because our 30 lines of sync
        // code is written twice AND one of the times it is nested/indented inside of both
        // an if-statement and a .then() call
        if (needToMakeAsyncRequest) {
            return makeAsyncRequest().then(() => {
                // tons of code omitted here, imagine like 30 lines...
            });
        }
        
        // tons of code omitted here, imagine like 30 lines...
    }
    
    async function cmereAsyncAwaitYouSexyBoiYou(needToMakeAsyncRequest: boolean): Promise<void> {
        if (needToMakeAsyncRequest) {
            // Brings tears to my eyes 🥲
            await makeAsyncRequest();
        }
    
        // tons of code omitted here, imagine like 30 lines...
    }
    

    Combining async/await and existing promise machinery

    async/await is not a silver bullet. It makes writing a sequence of async steps very clean but sometimes we don't just want a sequence: we want multiple async steps to run at the same time.

    async function takes12SecondsTotal(): Promise<[string, string]> {
        const result1 = await takes7Seconds();
        const result2 = await takes5Seconds(); // will not get here till 1st result is done
    
        return [result1, result2];
    }
    
    async function takes7SecondsTotal(): Promise<[string, string]> {
        // Both inner functions start doing stuff immediately and we just wait for them
        // both to finish
        const [result1, result2] = await Promise.all([
            takes7Seconds(),
            takes5Seconds()
        ]);
    
        return [result1, result2];
    }
    
    function nottttttActuallyyyyyTheSammeeeeIKnowIKnowScrewErrorHandling(): Promise<[string, string]> {
        // We are almost there! However, we just introduced a potential sh!tstorm by reducing down our
        // code and getting rid of async/await: we now have the assumption that both the takes7Seconds()
        // and takes5Seconds() calls DO return promises... but they might have synchronous code and the
        // beginning of them that could throw an error because the author screwed up and then they will
        // blow up SYNCHRONOUSLY in our face and this function will also blow up SYNCHRONOUSLY and it
        // will continue up the call stack until it hits a try-catch or it reaches all the way out and
        // the JS engine stops it and logs it in the dev tools
        return Promise.all([
            takes7Seconds(),
            takes5Seconds()
        ]);
    
        // Let me illustrate:
        function takes5Seconds(): Promise<string> {
            const now = new Date; // Trivia: you don't need constructor parenthesis if no parameters
    
            if (now.getDay() === 6 && now.getHours() === 21) { // 9pm on a Saturday
                // Synchronous error
                throw Error("I ain't workin' right now, ok?")
            }
    
            // Returns a promise, whose rejection will be handled by the promise chain, so an
            // "asynchronous" error (but this function could also throw a synchronous error, you
            // never know)
            return doSomeWork();
        }
    }
    
    function thisIsFunctionallyTheSame(): Promise<[string, string]> {
        try {
            return Promise.all([
                takes7Seconds(),
                takes5Seconds()
            ]);
        } catch (err) {
            // catch any synchronous error and gift-wrap it in a promise to protect whoever calls
            // us from a synchronous error explosion
            return Promise.reject(err);
        }
    }
    
    async function justBeSmartAndUseAsync(): Promise<[string, string]> {
        // Even though we don't use await at all, async functions act as a stalwart line of defense,
        // stopping any synchronous errors thrown from continuing up the callstack, implicitly
        // catching them and making sure we return a promise NO MATTER WHAT (implicitly does what
        // I did above but the browser probably does it better since async functions are part of the
        // language spec and lots of work has been and will be put into optimizing them)
        return Promise.all([
            takes7Seconds(),
            takes5Seconds()
        ]);
    }
    

    We might even want multiple sequences of async steps to run at the same time.

    async function youCouldBeForgivenForDoingThis(): Promise<void> {
        // Please edit this answer if I'm wrong, but last time I checked, an await keyword holds up
        // the entire expression it's part of--in our case, that means the entire Promise.all(...)
        // expression. The doSomethingUnrelated() will not even start running until writeCode()
        // finishes
        await Promise.all([
            pushCodeToGitHub(await writeCode()),
            doSomethingUnrelated()
        ]);
    }
    
    async function armedWithEsotericJSKnowledge(): Promise<void> {
        // Also please note I just await the Promise.all to discard the array of undefined's and
        // return void from our async function
        await Promise.all([
            writeCode().then(code => pushCodeToGitHub(code)),
            doSomethingUnrelated()
        ]);
    }
    

    Never be afraid to store promises in variables, or mix an async arrow function into a traditional .then() promise chain as necessary to get the smartest code.

    The esoteric bullsh*t with returns in async functions

    If you use TypeScript or are generally well-acquainted with JS promises, you may already know that inside of a .then() callback, you can return a type T or a Promise<T> and the promise mechanism internally does the work to make sure just a plain T gets passed to the next .then() on the chain. T could be number or any other type for that matter. async functions do the same thing. Error handling is not as simple.

    function getNumber(): number {
        return 420;
    }
    
    async function getNumberAsync(): Promise<number> {
        return getNumber(); // auto-wrap it in a promise cuz we're an async function
    }
    
    async function idkJavaScriptButIWantToMakeSureIGetThatNumber(): Promise<number> {
        return await getNumberAsync(); // this IS fine, really
    }
    
    async function iKNOWJavaScript(): Promise<number> {
        return getNumberAsync(); // this will NOT return Promise<Promise<number>> because async unwraps it
    }
    
    function iLikeToBlowUpRandomly(): Promise<number> {
        if (Math.random() > 0.5) {
            // This is not an async function so this throw clause will NOT get wrapped in a rejected promise
            // and returned pleasantly to the caller
            throw new Error("boom");
        }
    
        return getNumberAsync();
    }
    
    async function iHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
        try {
            return iLikeToBlowUpRandomly();
        } catch (err) {
            // This will always catch the "boom" explosions, BUT, if iLikeToBlowUpRandomly() returns a
            // rejected promise, it will sneakily slip through our try-catch because try-catches only
            // catch THROWN errors, and whoever called us will get a bad promise even though we
            // promised (haha) we would only ever return fulfilled promises containing numbers
            return -1;
        }
    }
    
    async function iActuallyHandleMyProblemsAndAlwaysFulfillMyPromises(): Promise<number> {
        try {
            // Bam! The normally extraneous await here brings this promise into our pseudo-synchronous
            // async/await code so if it was rejected, it will also trigger our catch branch just like
            // a synchronous error would
            return await iLikeToBlowUpRandomly();
        } catch (err) {
            return 3522047650; // call me if you have job offers 😉 but I'm kinda busy rn and spent way too much time on this
        }
    }