diff --git a/Cargo.lock b/Cargo.lock index c4f0906478..61cb5a5e50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,6 +1171,7 @@ dependencies = [ "regex", "ripemd", "rsa", + "scrypt", "serde", "sha-1 0.10.0", "sha2", @@ -3157,6 +3158,17 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "path-clean" version = "0.1.0" @@ -3821,6 +3833,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3851,6 +3872,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.0" diff --git a/cli/tests/unit_node/internal/scrypt_test.ts b/cli/tests/unit_node/internal/scrypt_test.ts index e98a1a3af5..d65cd27fff 100644 --- a/cli/tests/unit_node/internal/scrypt_test.ts +++ b/cli/tests/unit_node/internal/scrypt_test.ts @@ -2,8 +2,11 @@ import { scrypt, scryptSync } from "node:crypto"; import { Buffer } from "node:buffer"; import { assertEquals } from "../../../../test_util/std/testing/asserts.ts"; +import { deferred } from "../../../../test_util/std/async/deferred.ts"; + +Deno.test("scrypt works correctly", async () => { + const promise = deferred(); -Deno.test("scrypt works correctly", () => { scrypt("password", "salt", 32, (err, key) => { if (err) throw err; assertEquals( @@ -43,10 +46,15 @@ Deno.test("scrypt works correctly", () => { 115, ]), ); + promise.resolve(true); }); + + await promise; }); -Deno.test("scrypt works with options", () => { +Deno.test("scrypt works with options", async () => { + const promise = deferred(); + scrypt( "password", "salt", @@ -93,8 +101,11 @@ Deno.test("scrypt works with options", () => { 71, ]), ); + promise.resolve(true); }, ); + + await promise; }); Deno.test("scryptSync works correctly", () => { diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 75d85ac0bc..76424daa09 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -38,6 +38,7 @@ rand.workspace = true regex.workspace = true ripemd = "0.1.3" rsa.workspace = true +scrypt = "0.11.0" serde = "1.0.149" sha-1 = "0.10.0" sha2.workspace = true diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index b45f361448..26392da4c0 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -1,4 +1,5 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use deno_core::error::generic_error; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::op; @@ -542,3 +543,89 @@ pub fn op_node_random_int(min: i32, max: i32) -> Result { Ok(dist.sample(&mut rng)) } + +#[allow(clippy::too_many_arguments)] +fn scrypt( + password: StringOrBuffer, + salt: StringOrBuffer, + keylen: u32, + cost: u32, + block_size: u32, + parallelization: u32, + _maxmem: u32, + output_buffer: &mut [u8], +) -> Result<(), AnyError> { + // Construct Params + let params = scrypt::Params::new( + cost as u8, + block_size, + parallelization, + keylen as usize, + ) + .unwrap(); + + // Call into scrypt + let res = scrypt::scrypt(&password, &salt, ¶ms, output_buffer); + if res.is_ok() { + Ok(()) + } else { + // TODO(lev): key derivation failed, so what? + Err(generic_error("scrypt key derivation failed")) + } +} + +#[op] +pub fn op_node_scrypt_sync( + password: StringOrBuffer, + salt: StringOrBuffer, + keylen: u32, + cost: u32, + block_size: u32, + parallelization: u32, + maxmem: u32, + output_buffer: &mut [u8], +) -> Result<(), AnyError> { + scrypt( + password, + salt, + keylen, + cost, + block_size, + parallelization, + maxmem, + output_buffer, + ) +} + +#[op] +pub async fn op_node_scrypt_async( + password: StringOrBuffer, + salt: StringOrBuffer, + keylen: u32, + cost: u32, + block_size: u32, + parallelization: u32, + maxmem: u32, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut output_buffer = vec![0u8; keylen as usize]; + let res = scrypt( + password, + salt, + keylen, + cost, + block_size, + parallelization, + maxmem, + &mut output_buffer, + ); + + if res.is_ok() { + Ok(output_buffer.into()) + } else { + // TODO(lev): rethrow the error? + Err(generic_error("scrypt failure")) + } + }) + .await? +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index b09cb1c905..d363c444af 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -196,6 +196,8 @@ deno_core::extension!(deno_node, crypto::op_node_sign, crypto::op_node_verify, crypto::op_node_random_int, + crypto::op_node_scrypt_sync, + crypto::op_node_scrypt_async, crypto::x509::op_node_x509_parse, crypto::x509::op_node_x509_ca, crypto::x509::op_node_x509_check_email, diff --git a/ext/node/polyfills/internal/crypto/scrypt.ts b/ext/node/polyfills/internal/crypto/scrypt.ts index 5e65869068..4383355318 100644 --- a/ext/node/polyfills/internal/crypto/scrypt.ts +++ b/ext/node/polyfills/internal/crypto/scrypt.ts @@ -24,9 +24,11 @@ SOFTWARE. */ import { Buffer } from "ext:deno_node/buffer.ts"; -import { pbkdf2Sync as pbkdf2 } from "ext:deno_node/internal/crypto/pbkdf2.ts"; import { HASH_DATA } from "ext:deno_node/internal/crypto/types.ts"; +const { core } = globalThis.__bootstrap; +const { ops } = core; + type Opts = Partial<{ N: number; cost: number; @@ -55,166 +57,6 @@ const fixOpts = (opts?: Opts) => { return out; }; -function blockxor(S: Buffer, Si: number, D: Buffer, Di: number, len: number) { - let i = -1; - while (++i < len) D[Di + i] ^= S[Si + i]; -} -function arraycopy( - src: Buffer, - srcPos: number, - dest: Buffer, - destPos: number, - length: number, -) { - src.copy(dest, destPos, srcPos, srcPos + length); -} - -const R = (a: number, b: number) => (a << b) | (a >>> (32 - b)); - -class ScryptRom { - B: Buffer; - r: number; - N: number; - p: number; - XY: Buffer; - V: Buffer; - B32: Int32Array; - x: Int32Array; - _X: Buffer; - constructor(b: Buffer, r: number, N: number, p: number) { - this.B = b; - this.r = r; - this.N = N; - this.p = p; - this.XY = Buffer.allocUnsafe(256 * r); - this.V = Buffer.allocUnsafe(128 * r * N); - this.B32 = new Int32Array(16); // salsa20_8 - this.x = new Int32Array(16); // salsa20_8 - this._X = Buffer.allocUnsafe(64); // blockmix_salsa8 - } - - run() { - const p = this.p | 0; - const r = this.r | 0; - for (let i = 0; i < p; i++) this.scryptROMix(i, r); - - return this.B; - } - - scryptROMix(i: number, r: number) { - const blockStart = i * 128 * r; - const offset = (2 * r - 1) * 64; - const blockLen = 128 * r; - const B = this.B; - const N = this.N | 0; - const V = this.V; - const XY = this.XY; - B.copy(XY, 0, blockStart, blockStart + blockLen); - for (let i1 = 0; i1 < N; i1++) { - XY.copy(V, i1 * blockLen, 0, blockLen); - this.blockmix_salsa8(blockLen); - } - - let j: number; - for (let i2 = 0; i2 < N; i2++) { - j = XY.readUInt32LE(offset) & (N - 1); - blockxor(V, j * blockLen, XY, 0, blockLen); - this.blockmix_salsa8(blockLen); - } - XY.copy(B, blockStart, 0, blockLen); - } - - blockmix_salsa8(blockLen: number) { - const BY = this.XY; - const r = this.r; - const _X = this._X; - arraycopy(BY, (2 * r - 1) * 64, _X, 0, 64); - let i; - for (i = 0; i < 2 * r; i++) { - blockxor(BY, i * 64, _X, 0, 64); - this.salsa20_8(); - arraycopy(_X, 0, BY, blockLen + i * 64, 64); - } - for (i = 0; i < r; i++) { - arraycopy(BY, blockLen + i * 2 * 64, BY, i * 64, 64); - arraycopy(BY, blockLen + (i * 2 + 1) * 64, BY, (i + r) * 64, 64); - } - } - - salsa20_8() { - const B32 = this.B32; - const B = this._X; - const x = this.x; - - let i; - for (i = 0; i < 16; i++) { - B32[i] = (B[i * 4 + 0] & 0xff) << 0; - B32[i] |= (B[i * 4 + 1] & 0xff) << 8; - B32[i] |= (B[i * 4 + 2] & 0xff) << 16; - B32[i] |= (B[i * 4 + 3] & 0xff) << 24; - } - - for (i = 0; i < 16; i++) x[i] = B32[i]; - - for (i = 0; i < 4; i++) { - x[4] ^= R(x[0] + x[12], 7); - x[8] ^= R(x[4] + x[0], 9); - x[12] ^= R(x[8] + x[4], 13); - x[0] ^= R(x[12] + x[8], 18); - x[9] ^= R(x[5] + x[1], 7); - x[13] ^= R(x[9] + x[5], 9); - x[1] ^= R(x[13] + x[9], 13); - x[5] ^= R(x[1] + x[13], 18); - x[14] ^= R(x[10] + x[6], 7); - x[2] ^= R(x[14] + x[10], 9); - x[6] ^= R(x[2] + x[14], 13); - x[10] ^= R(x[6] + x[2], 18); - x[3] ^= R(x[15] + x[11], 7); - x[7] ^= R(x[3] + x[15], 9); - x[11] ^= R(x[7] + x[3], 13); - x[15] ^= R(x[11] + x[7], 18); - x[1] ^= R(x[0] + x[3], 7); - x[2] ^= R(x[1] + x[0], 9); - x[3] ^= R(x[2] + x[1], 13); - x[0] ^= R(x[3] + x[2], 18); - x[6] ^= R(x[5] + x[4], 7); - x[7] ^= R(x[6] + x[5], 9); - x[4] ^= R(x[7] + x[6], 13); - x[5] ^= R(x[4] + x[7], 18); - x[11] ^= R(x[10] + x[9], 7); - x[8] ^= R(x[11] + x[10], 9); - x[9] ^= R(x[8] + x[11], 13); - x[10] ^= R(x[9] + x[8], 18); - x[12] ^= R(x[15] + x[14], 7); - x[13] ^= R(x[12] + x[15], 9); - x[14] ^= R(x[13] + x[12], 13); - x[15] ^= R(x[14] + x[13], 18); - } - for (i = 0; i < 16; i++) B32[i] += x[i]; - - let bi; - - for (i = 0; i < 16; i++) { - bi = i * 4; - B[bi + 0] = (B32[i] >> 0) & 0xff; - B[bi + 1] = (B32[i] >> 8) & 0xff; - B[bi + 2] = (B32[i] >> 16) & 0xff; - B[bi + 3] = (B32[i] >> 24) & 0xff; - } - } - - clean() { - this.XY.fill(0); - this.V.fill(0); - this._X.fill(0); - this.B.fill(0); - for (let i = 0; i < 16; i++) { - this.B32[i] = 0; - this.x[i] = 0; - } - } -} - export function scryptSync( password: HASH_DATA, salt: HASH_DATA, @@ -226,17 +68,22 @@ export function scryptSync( const blen = p * 128 * r; if (32 * r * (N + 2) * 4 + blen > maxmem) { - throw new Error("excedes max memory"); + throw new Error("exceeds max memory"); } - const b = pbkdf2(password, salt, 1, blen, "sha256"); + const buf = Buffer.alloc(keylen); + ops.op_node_scrypt_sync( + password, + salt, + keylen, + Math.log2(N), + r, + p, + maxmem, + buf.buffer, + ); - const scryptRom = new ScryptRom(b, r, N, p); - const out = scryptRom.run(); - - const fin = pbkdf2(password, out, 1, keylen, "sha256"); - scryptRom.clean(); - return fin; + return buf; } type Callback = (err: unknown, result?: Buffer) => void; @@ -256,17 +103,24 @@ export function scrypt( const blen = p * 128 * r; if (32 * r * (N + 2) * 4 + blen > maxmem) { - throw new Error("excedes max memory"); + throw new Error("exceeds max memory"); } try { - const b = pbkdf2(password, salt, 1, blen, "sha256"); - - const scryptRom = new ScryptRom(b, r, N, p); - const out = scryptRom.run(); - const result = pbkdf2(password, out, 1, keylen, "sha256"); - scryptRom.clean(); - cb(null, result); + core.opAsync( + "op_node_scrypt_async", + password, + salt, + keylen, + Math.log2(N), + r, + p, + maxmem, + ).then( + (buf: Uint8Array) => { + cb(null, Buffer.from(buf.buffer)); + }, + ); } catch (err: unknown) { return cb(err); }