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

Deno crashes at createDecipheriv #25279

Closed
silverbucket opened this issue Aug 28, 2024 · 4 comments · Fixed by #25571
Closed

Deno crashes at createDecipheriv #25279

silverbucket opened this issue Aug 28, 2024 · 4 comments · Fixed by #25571
Assignees
Labels
bug Something isn't working correctly node compat

Comments

@silverbucket
Copy link

Version: Deno 1.45.5

$ deno run crypto-crash.ts

============================================================
Deno has panicked. This is a bug in Deno. Please report this
at https://github.com/denoland/deno/issues/new.
If you can reliably reproduce this panic, include the
reproduction steps and re-run with the RUST_BACKTRACE=1 env
var set and include the backtrace in your report.

Platform: macos aarch64
Version: 1.45.5
Args: ["deno", "run", "crypto-crash.ts"]

thread 'main' panicked at /Users/brew/Library/Caches/Homebrew/cargo_cache/registry/src/index.crates.io-6f17d22bba15001f/generic-array-0.14.7/src/lib.rs:572:9:
assertion `left == right` failed
  left: 11
 right: 32
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
import { NonEmptyArray } from "https://deno.land/x/fun@v2.0.0/array.ts";
import { Buffer } from "node:buffer";
import { createDecipheriv } from "node:crypto";
import { string } from "https://deno.land/x/fun@v2.0.0/mod.ts";

const ALGORITHM = "aes-256-cbc";
const split = string.split(":");

function decrypt(text: string, secret: string) {
    const pieces: NonEmptyArray<string> = split(text);
    const firstElement = pieces[0];
    const restElements = pieces.slice(1, pieces.length);
    const iv = Buffer.from(firstElement, "hex");
    const encryptedText = Buffer.from(restElements.join(":"), "hex");
    const decipher = createDecipheriv(ALGORITHM, Buffer.from(secret), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return JSON.parse(decrypted.toString());
}

decrypt("foo:bar", "secretsauce");

The crash is at line:
const encryptedText = Buffer.from(restElements.join(":"), "hex");

@bartlomieju bartlomieju added bug Something isn't working correctly node compat labels Aug 28, 2024
@silverbucket
Copy link
Author

silverbucket commented Aug 28, 2024

I haven't had time to track down why, but when I run some tests against this same method, I get a different crash, later in the method:

============================================================
Deno has panicked. This is a bug in Deno. Please report this
at https://github.com/denoland/deno/issues/new.
If you can reliably reproduce this panic, include the
reproduction steps and re-run with the RUST_BACKTRACE=1 env
var set and include the backtrace in your report.

Platform: macos aarch64
Version: 1.45.5
Args: ["/opt/homebrew/bin/deno", "test", "--fail-fast", "--coverage", "--clean", "--allow-env", "--allow-read", "src/index.test.ts"]

thread 'tokio-runtime-worker' panicked at ext/node/ops/crypto/cipher.rs:399:9:
assertion failed: input.len() == 16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The crash is at line:
decrypted = Buffer.concat([decrypted, decipher.final()]);

@silverbucket
Copy link
Author

silverbucket commented Aug 28, 2024

With the original issue, the problem seems to be using a secret that is 11 chars long, not the expected 32 chars.

...
assertion `left == right` failed
  left: 11
 right: 32
...

However, it seems even with the correct length secret, the decryption does not work (or at least one of the two encrypt or decrypt do not work). Here's a more complete script:

import { NonEmptyArray } from "https://deno.land/x/fun@v2.0.0/array.ts";
import { Buffer } from "node:buffer";
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { string } from "https://deno.land/x/fun@v2.0.0/mod.ts";

const ALGORITHM = "aes-256-cbc";
const IV_LENGTH = 16; // For AES, this is always 16
const split = string.split(":");

const secret = "a test secret.. that is 16 x 2..";

function ensureSecret(secret: string) {
    if (secret.length !== 32) {
        throw new Error(
            "secret must be a 32 char string, length: " + secret.length,
        );
    }
}

function encrypt(json: string, secret: string): string {
    ensureSecret(secret);
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, Buffer.from(secret), iv);
    let encrypted = cipher.update(JSON.stringify(json));
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString("hex") + ":" + encrypted.toString("hex");
}

function decrypt(encryptedString: string, secret: string): string {
    ensureSecret(secret);
    const pieces: NonEmptyArray<string> = split(encryptedString);
    const firstElement = pieces[0];
    const restElements = pieces.slice(1);
    const iv = Buffer.from(firstElement, "hex");
    const encryptedText = Buffer.from(restElements.join(":"), "hex");
    const decipher = createDecipheriv(ALGORITHM, Buffer.from(secret), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
}

const encString = encrypt("foo bar baz", secret);
console.log("decrypted: ", decrypt(encString, secret));

This is based on an original implementation that has been working fine in node, modified for Deno and simplified for the purposes of this example.
https://github.com/sockethub/sockethub/blob/master/packages/crypto/src/index.ts

@kt3k
Copy link
Member

kt3k commented Sep 10, 2024

Encrypting part looks wrong in Deno. The below snippet:

import { Buffer } from "node:buffer";
import { createCipheriv, createDecipheriv } from "node:crypto";

const secret = Buffer.from("a test secret.. that is 16 x 2..");
const iv = Buffer.from("e75f3621f41df73a47efa67cac1053bd", "hex");

const cipher = createCipheriv("aes-256-cbc", secret, iv);
const x = Buffer.concat([cipher.update("foo bar baz"), cipher.final()]);
console.log(x.toString("hex"));

const decipher = createDecipheriv("aes-256-cbc", secret, iv);
const y = Buffer.concat([decipher.update(Buffer.from("bc3b393dafcd0d45ee7b6cce7a4b1fb0", "hex")), decipher.final()]);
console.log(y.toString());

executes in Node and Deno like the below:

$ node c.mjs
5c2f0a9ac809c2e585ca97a879cac090
"foo bar baz"
$ deno c.mjs 
4c63f944d72f04879998b99daaf473de
"foo bar baz"

Also we miss the length check of key and iv. That causes the panic. We should throw RangeError and TypeError for invalid length of those.

@kt3k kt3k self-assigned this Sep 10, 2024
@kt3k kt3k changed the title Deno crash from node:buffer use Deno crashes at createDecipheriv Sep 11, 2024
@kt3k
Copy link
Member

kt3k commented Sep 11, 2024

I think I found the cause of the error. It looks like cipher.update()/decipher.update() can't handle "string" input correctly when the input encoding (the 2nd argument) is not specified. If I specify the 2nd argument of .update() method to 'utf-8' like the below, the given example correctly deciphers the original text:

import { NonEmptyArray } from "https://deno.land/x/fun@v2.0.0/array.ts";
import { Buffer } from "node:buffer";
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { string } from "https://deno.land/x/fun@v2.0.0/mod.ts";

const ALGORITHM = "aes-256-cbc";
const IV_LENGTH = 16; // For AES, this is always 16
const split = string.split(":");

const secret = "a test secret.. that is 16 x 2..";

function ensureSecret(secret: string) {
    if (secret.length !== 32) {
        throw new Error(
            "secret must be a 32 char string, length: " + secret.length,
        );
    }
}

function encrypt(json: string, secret: string): string {
    ensureSecret(secret);
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, Buffer.from(secret), iv);
    let encrypted = cipher.update(JSON.stringify(json));
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString("hex") + ":" + encrypted.toString("hex");
}

function decrypt(encryptedString: string, secret: string): string {
    ensureSecret(secret);
    const pieces: NonEmptyArray<string> = split(encryptedString);
    const firstElement = pieces[0];
    const restElements = pieces.slice(1);
    const iv = Buffer.from(firstElement, "hex");
    const encryptedText = Buffer.from(restElements.join(":"), "hex");
    const decipher = createDecipheriv(ALGORITHM, Buffer.from(secret), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
}

const encString = encrypt("foo bar baz", secret);
console.log("decrypted: ", decrypt(encString, secret));

This executes like:

$ deno example.ts
decrypted:  "foo bar baz"

I'll work on the fix soon

kt3k added a commit that referenced this issue Sep 11, 2024
nyannyacha added a commit to nyannyacha/edge-runtime that referenced this issue Sep 23, 2024
nyannyacha added a commit to supabase/edge-runtime that referenced this issue Sep 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working correctly node compat
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants