Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crypto.subtle is available only in secure contexts (HTTPS) #1550

Open
Excel1 opened this issue Jun 20, 2024 · 12 comments
Open

Crypto.subtle is available only in secure contexts (HTTPS) #1550

Excel1 opened this issue Jun 20, 2024 · 12 comments
Labels
question Further information is requested

Comments

@Excel1
Copy link

Excel1 commented Jun 20, 2024

I am currently working on a Vue WebApp (+ Capacitor) and would like to develop in the private network, but I always get the following error message: "Crypto.subtle is available only in secure contexts (HTTPS).": It occurs as soon as I am redirected back from e.g. paypal in the web browser or with capacitor as soon as I click on the login button. My redirect_uri is http://:.

The security mechanism specifies that you should be in the protected network, which can be done by certificates etc. but is very time-consuming (especially since this is not necessary for almost all other oidc clients). It would be nice to switch off this feature for the develop operation by e.g. a parameter.

@Badisi
Copy link
Contributor

Badisi commented Jun 21, 2024

While working in a local dev environment, localhost or 127.0.0.1 are usually considered "secured".
So using http://localhost as the redirect should be fine.

You can also activate https with your Vue local dev server:

// vue.config.js
module.exports = {
  devServer: {
    ...
    host: '0.0.0.0',
    https: true,
    ...
  }
}

@Excel1
Copy link
Author

Excel1 commented Jun 21, 2024

@Badisi Correct but if you got multiple server like keycloak oidc running on your local enviroment and mobile device emulator for developing web apps (android studio) you are forced to use hostnames or ips.

Activating https results into mixed-content cause e.g. keycloak isnt running on https.

@pamapa pamapa changed the title Error: Crypto.subtle is available only in secure contexts (HTTPS) Crypto.subtle is available only in secure contexts (HTTPS) Jun 21, 2024
@pamapa pamapa added the question Further information is requested label Jun 21, 2024
@pamapa
Copy link
Member

pamapa commented Jun 21, 2024

There is no way going back. We are using browser built-in modules as much as possible. If you control you network you might can use development only proxy and handle what you need there...

You can still use v2.4.0 of this library, which does not use Crypto.subtle but custom code, which does not enforce localhost or https...

@Excel1
Copy link
Author

Excel1 commented Jun 21, 2024

I can fully understand why system components are favoured. However, it's just interesting that similarly sized/larger ones take a different path

@bropuffshroom
Copy link

@Badisi Correct but if you got multiple server like keycloak oidc running on your local enviroment and mobile device emulator for developing web apps (android studio) you are forced to use hostnames or ips.

Activating https results into mixed-content cause e.g. keycloak isnt running on https.

This is similar to our use case.

To the point 'using http://localhost as the redirect should be fine', this is actually more insecure than the using IP.
If your source code got leaked somehow, anyone with the source code can run a server on the private network and will be able to obtain the token coz everyone has access to their own http://localhost

@chicco785
Copy link

There is no way going back. We are using browser built-in modules as much as possible. If you control you network you might can use development only proxy and handle what you need there...

You can still use v2.4.0 of this library, which does not use Crypto.subtle but custom code, which does not enforce localhost or https...

Wouldn't it be possible to support this via an ENV=dev mode? setting up a proxy infrastructure just for dev testing and similar purposes seems overkilling to me.
I totally understand the "enforcing" security principles, but there is a a difference between production and development environments :)

@tekhedd
Copy link

tekhedd commented Dec 11, 2024

Honestly, for dev use there are a number of workarounds. I am, in my company, the annoying person who constantly pushes for more, stronger security. But we have this thing called "customers." This isn't a purely academic issue for me. No, I don't like it, but it's reality.

Customers, while outwardly very dedicated to "SSO" and any other buzzwords associated with security, will not be capable of things like "IT managing HTTPS certs for multiple internal servers without 6 months of lead time" for at least another year. Insecure mixed-non-TLS deployments are a hard requirement during this (what I sincerely hope will be only a short) transitional period.

For our projects, I have "temporarily" gone back to the keycloak-js library, as its OIDC implementation is pure OIDC with no proprietary garbage. It works fine, although I have not tested against non-keycloak OIDC providers. Worth considering as a workaround.

(I"temporarily" in quotes, because the configurations are not compatible, and returning to a pure OIDC client is likely to be delayed for a long time.)

Addendum: I get it.

I completely understand that the combination of "we don't want to pull in a third party library" and "the browsers all decided to put a non-crypto library into the crypto module" means it simply won't be available, and that for public web-facing OIDC implementations HTTPS is a hard requirement anyway, and that oidc-client-ts is a library not designed to be used by applications running in a secured corporate environment, so this will not become a requirement and likely to be closed as WONTFIX at any moment. :D I'm simply commenting to put on record that this isn't a "dev" issue, and makes the library a non-starter for corporate deployment at this time. (It's not just that it's a whiny pita in the testbed!)

@Badisi
Copy link
Contributor

Badisi commented Dec 12, 2024

Guys, this is javascript, you don't like something or you don't have access to something, then replace it or provide it.

SubtleCrypto is exposed through the window object if the context if considered "secured" (i.e. https), otherwise it's undefined. So make a polyfill that replicates what's being used by oidc-client-ts and import it only when you are in development.

Looking at oidc-client-ts source code, we can see it uses:

crypto.randomUUID();

await crypto.subtle.digest("SHA-256", data);

await crypto.subtle.exportKey("jwk", keyPair.publicKey);

await crypto.subtle.sign(
    {name: "ECDSA", hash: { name: "SHA-256"}},
    privateKey,
    new TextEncoder().encode(encodedToken)
);

await window.crypto.subtle.generateKey(
    {name: "ECDSA", namedCurve: "P-256"},
    false,
    ["sign", "verify"]
);

Based on that, we could have a polyfill (using node-forge for better layer compatibility with SubtleCrypto) that would look like (NOT TESTED):

// subtlecrypto.polyfill.ts

(async () => {
    if (!globalThis.crypto) {
        globalThis.crypto = {};
    }

    const forge = await import("node-forge");

    if (!crypto.randomUUID) {
        crypto.randomUUID = function () {
            const randomBytes = crypto.getRandomValues(new Uint8Array(16));
            randomBytes[6] = (randomBytes[6] & 0x0f) | 0x40; // UUID version 4
            randomBytes[8] = (randomBytes[8] & 0x3f) | 0x80; // UUID variant 1
            return [...randomBytes]
                .map((b, i) =>
                    [4, 6, 8, 10].includes(i) ? `-${b.toString(16).padStart(2, "0")}` : b.toString(16).padStart(2, "0")
                )
                .join("");
        };
    }

    if (!crypto.subtle) {
        crypto.subtle = {
            async digest(algorithm, data) {
                if (algorithm !== "SHA-256") {
                    throw new Error("Only SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                return new Uint8Array(md.digest().bytes().split("").map(c => c.charCodeAt(0))).buffer;
            },

            async generateKey(algorithm, extractable, keyUsages) {
                if (algorithm.name !== "ECDSA" || algorithm.namedCurve !== "P-256") {
                    throw new Error("Only ECDSA with P-256 is supported.");
                }
                const keys = forge.pki.ec.generateKeyPair({ namedCurve: "P-256" });
                return {
                    privateKey: keys.privateKey,
                    publicKey: keys.publicKey,
                };
            },

            async exportKey(format, key) {
                if (format === "jwk") {
                    if (key.type === "private") {
                        return forge.pki.privateKeyToJwk(key);
                    } else if (key.type === "public") {
                        return forge.pki.publicKeyToJwk(key);
                    }
                }
                throw new Error("Unsupported export key format or type.");
            },

            async sign(algorithm, privateKey, data) {
                if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
                    throw new Error("Only ECDSA with SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                const signature = privateKey.sign(md);
                return new Uint8Array(forge.util.createBuffer(signature).bytes().split("").map(c => c.charCodeAt(0)));
            },

            async verify(algorithm, publicKey, signature, data) {
                if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
                    throw new Error("Only ECDSA with SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                return publicKey.verify(md.digest().bytes(), forge.util.createBuffer(signature).getBytes());
            },
        };
    }
})();

If someone is willing to try that polyfill (and improve it if needed), this is a file that could be store on the repo and the usage could be explained in the doc for such use cases.

@chicco785
Copy link

Guys, this is javascript, you don't like something or you don't have access to something, then replace it or provide it.

SubtleCrypto is exposed through the window object if the context if considered "secured" (i.e. https), otherwise it's undefined. So make a polyfill that replicates what's being used by oidc-client-ts and import it only when you are in development.

Looking at oidc-client-ts source code, we can see it uses:

crypto.randomUUID();

await crypto.subtle.digest("SHA-256", data);

await crypto.subtle.exportKey("jwk", keyPair.publicKey);

await crypto.subtle.sign(
    {name: "ECDSA", hash: { name: "SHA-256"}},
    privateKey,
    new TextEncoder().encode(encodedToken)
);

await window.crypto.subtle.generateKey(
    {name: "ECDSA", namedCurve: "P-256"},
    false,
    ["sign", "verify"]
);

Based on that, we could have a polyfill (using node-forge for better layer compatibility with SubtleCrypto) that would look like (NOT TESTED):

// subtlecrypto.polyfill.ts

(async () => {
    if (!globalThis.crypto) {
        globalThis.crypto = {};
    }

    const forge = await import("node-forge");

    if (!crypto.randomUUID) {
        crypto.randomUUID = function () {
            const randomBytes = crypto.getRandomValues(new Uint8Array(16));
            randomBytes[6] = (randomBytes[6] & 0x0f) | 0x40; // UUID version 4
            randomBytes[8] = (randomBytes[8] & 0x3f) | 0x80; // UUID variant 1
            return [...randomBytes]
                .map((b, i) =>
                    [4, 6, 8, 10].includes(i) ? `-${b.toString(16).padStart(2, "0")}` : b.toString(16).padStart(2, "0")
                )
                .join("");
        };
    }

    if (!crypto.subtle) {
        crypto.subtle = {
            async digest(algorithm, data) {
                if (algorithm !== "SHA-256") {
                    throw new Error("Only SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                return new Uint8Array(md.digest().bytes().split("").map(c => c.charCodeAt(0))).buffer;
            },

            async generateKey(algorithm, extractable, keyUsages) {
                if (algorithm.name !== "ECDSA" || algorithm.namedCurve !== "P-256") {
                    throw new Error("Only ECDSA with P-256 is supported.");
                }
                const keys = forge.pki.ec.generateKeyPair({ namedCurve: "P-256" });
                return {
                    privateKey: keys.privateKey,
                    publicKey: keys.publicKey,
                };
            },

            async exportKey(format, key) {
                if (format === "jwk") {
                    if (key.type === "private") {
                        return forge.pki.privateKeyToJwk(key);
                    } else if (key.type === "public") {
                        return forge.pki.publicKeyToJwk(key);
                    }
                }
                throw new Error("Unsupported export key format or type.");
            },

            async sign(algorithm, privateKey, data) {
                if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
                    throw new Error("Only ECDSA with SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                const signature = privateKey.sign(md);
                return new Uint8Array(forge.util.createBuffer(signature).bytes().split("").map(c => c.charCodeAt(0)));
            },

            async verify(algorithm, publicKey, signature, data) {
                if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
                    throw new Error("Only ECDSA with SHA-256 is supported.");
                }
                const md = forge.md.sha256.create();
                md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
                return publicKey.verify(md.digest().bytes(), forge.util.createBuffer(signature).getBytes());
            },
        };
    }
})();

If someone is willing to try that polyfill (and improve it if needed), this is a file that could be store on the repo and the usage could be explained in the doc for such use cases.

@Badisi not really a js developer - till you mentioned polyfill i didn't know they existed :) Still, I am happy to try and contribute as needed ;) What I wanted to avoid is opening a PR and invest time on it if it won't be approved by design :)

In my understanding polyfill will just work without needs of changes to the main library, so it will be only to be documented as example and any developer should then take the decision to add it or not to his code.

@Badisi
Copy link
Contributor

Badisi commented Dec 12, 2024

@chicco785, polyfill is just a "name".

  • Usually "polyfill" symbolize a piece of code that will provide something that doesn't exists (ex: something that exists on modern browsers but do not exists on older browsers, or something that does not exists on modern browsers yet)

    • the idea here is to provide window.crypto.randomUUID and window.crypto.subtle when they are not
  • You also have the idea of "monkey patching": that patches things directly in the original code (i.e. in oidc-client-ts directly) (but that's something we should avoid here).

So yes, you got it, the main idea is to provide a "polyfill" file that users could download and import in their code when they see fit (i.e. when they are running in dev mode).

Happy to assist if you need more guidance 😉

@chicco785
Copy link

  • Usually "polyfill" symbolize a piece of code that will provide something that doesn't exists (ex: something that exists on modern browsers but do not exists on older browsers, or something that does not exists on modern browsers yet)

Thanks :) I am mostly GOlanging, so I am not used to think you can override something dynamically so easily ;)

@chicco785
Copy link

i am using it in a react app, i managed to make it work importing it synchronously.

// index.js

if (import.meta.env.DEV && !window.isSecureContext) {
  import('./lib/subtlecrypto.polyfill.js').then(({ setupCryptoPolyfill }) => setupCryptoPolyfill());
}
...



// ./lib/subtlecrypto.polyfill.js

export function setupCryptoPolyfill() {
if (!window.isSecureContext) {
    console.warn(
      "Your environment is not secure (not HTTPS). Mocking Crypto.subtle for development purposes."
    );
  
    if (!globalThis.crypto) {
      globalThis.crypto = {};
    }
  
    const forge = import("node-forge");
  
    globalThis.crypto.subtle = {
      digest: async (algorithm, data) => {
        if (algorithm !== "SHA-256") {
          throw new Error("Only SHA-256 is supported.");
        }
        const md = forge.md.sha256.create();
        md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
        return new Uint8Array(md.digest().bytes().split("").map(c => c.charCodeAt(0))).buffer;
      },
  
      generateKey: async (algorithm, extractable, keyUsages) => {
        if (algorithm.name !== "ECDSA" || algorithm.namedCurve !== "P-256") {
            throw new Error("Only ECDSA with P-256 is supported.");
        }
        const keys = forge.pki.ec.generateKeyPair({ namedCurve: "P-256" });
        return {
            privateKey: keys.privateKey,
            publicKey: keys.publicKey,
        };
      },
  
      exportKey: async(format, key) =>  {
        if (format === "jwk") {
            if (key.type === "private") {
                return forge.pki.privateKeyToJwk(key);
            } else if (key.type === "public") {
                return forge.pki.publicKeyToJwk(key);
            }
        }
        throw new Error("Unsupported export key format or type.");
      },
  
      sign: async (algorithm, privateKey, data)  => {
          if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
              throw new Error("Only ECDSA with SHA-256 is supported.");
          }
          const md = forge.md.sha256.create();
          md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
          const signature = privateKey.sign(md);
          return new Uint8Array(forge.util.createBuffer(signature).bytes().split("").map(c => c.charCodeAt(0)));
      },
  
      verify: async (algorithm, publicKey, signature, data) => {
          if (algorithm.name !== "ECDSA" || algorithm.hash.name !== "SHA-256") {
              throw new Error("Only ECDSA with SHA-256 is supported.");
          }
          const md = forge.md.sha256.create();
          md.update(forge.util.createBuffer(new Uint8Array(data)).getBytes());
          return publicKey.verify(md.digest().bytes(), forge.util.createBuffer(signature).getBytes());
      },
    };
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

6 participants