jedisct1 / libsodium.js

libsodium compiled to Webassembly and pure JavaScript, with convenient wrappers.
Other
968 stars 138 forks source link

Serialize state_out and state_in #333

Open Mikescops opened 6 months ago

Mikescops commented 6 months ago

Hello,

I'm wondering if there is a way to serialize the state_in and the state_out from crypto_secretstream_xchacha20poly1305_init_push and crypto_secretstream_xchacha20poly1305_init_pull in order to store them in a persistent memory.

My first use case is a client / server exchanges where the server is stateless and cannot keep in RAM the states.

My second use case is a client / server exchanges where the server is a set of multiple machines behind a load balancer where the client has no guarantee to hit the same machine in a row.

Thanks!

jedisct1 commented 6 months ago

Yes, that should totally be doable. The state is just the address of a 52 byte array, that can be safely moved to different hosts.

Mikescops commented 6 months ago

@jedisct1 thanks, where that change should happen? in the parent libsodium library?

jedisct1 commented 6 months ago

No, just in the JavaScript code. I think it can be done even without changing libsodium-wrappers.

Mikescops commented 6 months ago

First, I simply printed the the state_in and and state_out value. They seems to be numbers, which is not exactly what the @types/libsodium-wrappers is expecting.

What's weird too is that even if I rebuild the project and run multiple times i always get the same numbers.

state_in =  102240 
state_out = 102144 

So I tried to read through the wrapper code to understand what's going on.

I took a look at where the crypto_secretstream_xchacha20poly1305_init_pull is used:

      var h = new u(52).address;
            if (0 == (0 | a._crypto_secretstream_xchacha20poly1305_init_pull(h, n, c))) {
                var p = h;
                return g(_),
                p
            }
            b(_, "invalid usage")

In this code:

and then where crypto_secretstream_xchacha20poly1305_init_push is used:

            var s = new u(52).address,
            c = new u(0 | a._crypto_secretstream_xchacha20poly1305_headerbytes()),
            o = c.address;
            if (t.push(o), 0 == (0 | a._crypto_secretstream_xchacha20poly1305_init_push(s, o, _))) {
                var h = {
                    state: s,
                    header: y(c, r)
                };
                return g(t),
                h
            }
            b(t, "invalid usage")

In this code:

Now taking a look at the function u() that seems to allocate some memory.

        function u(e) {
            this.length = e,
            this.address = v(e)
        }
        function d(e) {
            var r = v(e.length);
            return a.HEAPU8.set(e, r),
            r
        }
        function v(e) {
            var r = a._malloc(e);
            if (0 === r) throw {
                message: "_malloc() failed",
                length: e
            };
            return r
        }

Sounds like it's a binding to a malloc (probably to the C code?).

What I get from this point is that the state and the header are constructed the same way with the u() function, and from the output of the function we get the address in a Uint8Array which is easily convertible to any other format.

The call to y(c, r) seems to do the trick, so let's look at the code:

function y(e, r) {
            var a = r || t;
            if (!i(a)) throw new Error(a + " output format is not available");
            if (e instanceof u) {
                if ("uint8array" === a) return e.to_Uint8Array();
                if ("text" === a) return s(e.to_Uint8Array());
                if ("hex" === a) return c(e.to_Uint8Array());
                if ("base64" === a) return p(e.to_Uint8Array(), o.URLSAFE_NO_PADDING);
                throw new Error('What is output format "' + a + '"?')
            }
            if ("object" == typeof e) {
                for (var _ = Object.keys(e), n = {},
                h = 0; h < _.length; h++) n[_[h]] = y(e[_[h]], a);
                return n
            }
            if ("string" == typeof e) return e;
            throw new TypeError("Cannot format output")
        }

So the .to_Uint8Array() method from the prototype is converting this address to the needed Uint8Array, which look like this in the code:

 return u.prototype.to_Uint8Array = function() {
            var e = new Uint8Array(this.length);
            return e.set(a.HEAPU8.subarray(this.address, this.address + this.length)),
            e
        },

In the end the issue relies on the fact that for the state we return only the address var s = new u(52).address while for the header we convert the entire buffer to a to_Uint8Array().

Two options:

What do you think?