JamesBroadberry / deno-bcrypt

A port of jBCrypt to TypeScript for use as a Deno module
ISC License
67 stars 5 forks source link

Deno Deploy Support #26

Open oscartbeaumont opened 2 years ago

oscartbeaumont commented 2 years ago

Currently hash and compare are not supported when deploying code on Deno Deploy. This is due to Worker not being defined refer to upstream issue.

However, until Worker is implemented with Deno Deploy you can use the hashSync and compareSync methods.

I have included a helper below which allows for easily switching between these two methods for anyone working on a codebase designed to run efficiently locally and inside Deno Deploy.

import {
  hash as hashPromise,
  hashSync,
  compare as comparePromise,
  compareSync,
} from "https://deno.land/x/bcrypt/mod.ts";

export const isRunningInDenoDeploy = Deno.permissions?.query === undefined; // This is crude check for if the code in running in Deno Deploy. It works for now but may not work in the future.

export const hash: typeof hashPromise = isRunningInDenoDeploy
  ? (plaintext: string, salt: string | undefined = undefined) =>
      new Promise((res) => res(hashSync(plaintext, salt)))
  : hashPromise;
export const compare: typeof comparePromise = isRunningInDenoDeploy
  ? (plaintext: string, hash: string) =>
      new Promise((res) => res(compareSync(plaintext, hash)))
  : comparePromise;

I primarily created this issue to help anyone who runs into the same issue. It would be cool to see the helpers I included added to this library but I think this would be unlikely given how hacky it is.

l3onh4rd commented 2 years ago

Wow, nice! I was here to create a ticket that it isn't running on Deno Deploy. Thank you very much for your help and work ;)

JamesBroadberry commented 2 years ago

Thanks for raising the issue, I'm sure it'll help a lot of people out. However, this operation is blocking so running it non-async isn't the intended use case and can cause issues (e.g. if many requests hit a server at once) so while it may work for you, I don't think adding it to the library would be wise.

I'll close the issue if/when deno-deploy supports workers (please let me know since I don't follow deno-deploy closely) and leave it open for now so others can find it. Thanks again for sharing your workaround!

psi-4ward commented 2 years ago

This issues hits also deno compile. Anyone has a way to detect this case?

ralyodio commented 2 years ago

I’m finding a weird issue that just started this week. compare function returns false for older accounts and true for new accounts created this week.

If I restart systemd which I use to start deno it sometimes works for older accounts but then newer ones can’t login.

I’m completely baffled. Deployed to Ubuntu 20.04 using latest stable deno.

Y suspicion is a recent deno upgrade broke it.

oscartbeaumont commented 2 years ago

I don't maintain this repository but it looks like someone reported this as issue #27. I don't understand how it is related to this issue about Deno Deploy so it's discussion should be done in a separate issue unless it has a direct link.

obskur123 commented 2 years ago

Love you dawg, i was just searching for this

KorigamiK commented 2 years ago

Thank you so much man, this really helped !

Currently hash and compare are not supported when deploying code on Deno Deploy. This is due to Worker not being defined refer to upstream issue.

However, until Worker is implemented with Deno Deploy you can use the hashSync and compareSync methods.

I have included a helper below which allows for easily switching between these two methods for anyone working on a codebase designed to run efficiently locally and inside Deno Deploy.

import {
  hash as hashPromise,
  hashSync,
  compare as comparePromise,
  compareSync,
} from "https://deno.land/x/bcrypt/mod.ts";

export const isRunningInDenoDeploy = Deno.permissions?.query === undefined; // This is crude check for if the code in running in Deno Deploy. It works for now but may not work in the future.

export const hash: typeof hashPromise = isRunningInDenoDeploy
  ? (plaintext: string, salt: string | undefined = undefined) =>
      new Promise((res) => res(hashSync(plaintext, salt)))
  : hashPromise;
export const compare: typeof comparePromise = isRunningInDenoDeploy
  ? (plaintext: string, hash: string) =>
      new Promise((res) => res(compareSync(plaintext, hash)))
  : comparePromise;

I primarily created this issue to help anyone who runs into the same issue. It would be cool to see the helpers I included added to this library but I think this would be unlikely given how hacky it is.

KorigamiK commented 2 years ago

isRunningInDenoDeploy should be checked through the environment variables in the project.

AshwinDeTaeye commented 1 year ago

Is there any status update concerning this? SInce deno deploy has the web crypto API, it might be the Worker never will be implemented.

Is there a way we could use the web crypto API instead of the sync ways with bcrypt?

JamesBroadberry commented 1 year ago

@AshwinDeTaeye This is a problem with Deno Deploy not supporting Workers. If you need to run your app on Deno Deploy, it seems that the only option right now is to have these calls blocking and use the sync methods.

I'm not quite sure what you mean by using the Web Crypto API instead of Workers.

Upstream issue previously mentioned here: https://github.com/denoland/deploy_feedback/issues/171

AshwinDeTaeye commented 1 year ago

Deno deploy does support the Web Crypto API. https://deno.com/deploy/docs/runtime-api I suppose there should be async ways of achieving hash and compare through this API.

What i found: https://bradyjoslin.com/blog/encryption-webcrypto/

Do you think this could be done with this API?

https://doc.deno.land/deno/stable/~/SubtleCrypto

JamesBroadberry commented 1 year ago

To be honest, I've not really kept up with Deno anymore and I'm not prepared to overhaul this code just to support Deno Deploy because it doesn't support Workers. If you do manage to find a way, please do keep us posted so others interested in the thread know. Thanks 😄

AshwinDeTaeye commented 1 year ago

We can use the standard lib from Deno, Web Crypto, to be able to do async hashing. Following worked for me in Deno deploy:

const buff_to_base64 = (buff: number[] | Uint8Array) =>
  btoa(String.fromCharCode.apply(null, buff));

const base64_to_buf = (b64: string) =>
  Uint8Array.from(atob(b64), (c) => c.charCodeAt(null));

const enc = new TextEncoder();
const dec = new TextDecoder();

const getPasswordKey = async (password: any) =>
  await window.crypto.subtle.importKey(
    "raw",
    enc.encode(password),
    "PBKDF2",
    false,
    ['deriveBits', "deriveKey"]
  );

const deriveKey = async (passwordKey: any, salt: any, keyUsage: any[]) =>
  await window.crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 250000,
      hash: "SHA-256",
    },
    passwordKey,
    { name: "AES-GCM", length: 256 },
    false,
    keyUsage
  );

export async function encryptData(secretData: string | undefined, password: any) {
  try {
    const salt = window.crypto.getRandomValues(new Uint8Array(16));
    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const passwordKey = await getPasswordKey(password);
    const aesKey = await deriveKey(passwordKey, salt, ["encrypt"]);
    const encryptedContent = await window.crypto.subtle.encrypt(
      {
        name: "AES-GCM",
        iv: iv,
      },
      aesKey,
      enc.encode(secretData)
    );

    const encryptedContentArr = new Uint8Array(encryptedContent);
    let buff = new Uint8Array(
      salt.byteLength + iv.byteLength + encryptedContentArr.byteLength
    );
    buff.set(salt, 0);
    buff.set(iv, salt.byteLength);
    buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
    const base64Buff = buff_to_base64(buff);
    return base64Buff;
  } catch (e) {
    console.log(`Error - ${e}`);
    return "";
  }
}

export async function decryptData(
  encryptedData: string,
  password: string | undefined
) {
  try {
    const encryptedDataBuff = base64_to_buf(encryptedData);
    const salt = encryptedDataBuff.slice(0, 16);
    const iv = encryptedDataBuff.slice(16, 16 + 12);
    const data = encryptedDataBuff.slice(16 + 12);
    const passwordKey = await getPasswordKey(password);
    const aesKey = await deriveKey(passwordKey, salt, ["decrypt"]);
    const decryptedContent = await window.crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: iv,
      },
      aesKey,
      data
    );
    return dec.decode(decryptedContent);
  } catch (e) {
    console.log(`Error - ${e}`);
    return "";
  }
}
AL1L commented 1 year ago

For the proposed solution in the OP, I think a more sensible way to detect if it'll work is by using

// deno-lint-ignore no-explicit-any
export const isRunningInDenoDeploy = (globalThis as any).Worker === undefined;
derikvanschaik commented 1 year ago

For the proposed solution in the OP, I think a more sensible way to detect if it'll work is by using

// deno-lint-ignore no-explicit-any
export const isRunningInDenoDeploy = (globalThis as any).Worker === undefined;

^ This suggestion worked for me, however an alternative solution could be to set an environment variable such as Deno.env.Get('DENO_ENV') === 'production'

AL1L commented 1 year ago

For the proposed solution in the OP, I think a more sensible way to detect if it'll work is by using

// deno-lint-ignore no-explicit-any
export const isRunningInDenoDeploy = (globalThis as any).Worker === undefined;

^ This suggestion worked for me, however an alternative solution could be to set an environment variable such as Deno.env.Get('DENO_ENV') === 'production'

That's actually what I ended up going with, because of another library that I needed to know which environment I was in. Just didn't post it here lol