refactor(node/crypto): scrypt polyfill to rust (#18746)

This commit is contained in:
Levente Kurusa 2023-04-18 14:29:10 +02:00 committed by GitHub
parent 74aee8b305
commit 530963c34c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 179 deletions

33
Cargo.lock generated
View File

@ -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"

View File

@ -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", () => {

View File

@ -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

View File

@ -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<i32, AnyError> {
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, &params, 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<ZeroCopyBuf, AnyError> {
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?
}

View File

@ -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,

View File

@ -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);
}