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

feat(ext/crypto): implement importKey and deriveBits for Pbkdf2 #11642

Merged
merged 22 commits into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 148 additions & 9 deletions ext/crypto/00_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
RsaPssParams: {},
EcdsaParams: { hash: "HashAlgorithmIdentifier" },
HmacImportParams: { hash: "HashAlgorithmIdentifier" },
Pbkdf2Params: { hash: "HashAlgorithmIdentifier", salt: "BufferSource" },
RsaOaepParams: { label: "BufferSource" },
};

Expand Down Expand Up @@ -86,6 +87,10 @@
},
"importKey": {
"HMAC": "HmacImportParams",
"PBKDF2": null,
},
"deriveBits": {
"PBKDF2": "Pbkdf2Params",
},
"encrypt": {
"RSA-OAEP": "RsaOaepParams",
Expand Down Expand Up @@ -657,18 +662,18 @@

const normalizedAlgorithm = normalizeAlgorithm(algorithm, "importKey");

if (
ArrayPrototypeFind(
keyUsages,
(u) => !ArrayPrototypeIncludes(["sign", "verify"], u),
) !== undefined
) {
throw new DOMException("Invalid key usages", "SyntaxError");
}

switch (normalizedAlgorithm.name) {
// https://w3c.github.io/webcrypto/#hmac-operations
case "HMAC": {
if (
ArrayPrototypeFind(
keyUsages,
(u) => !ArrayPrototypeIncludes(["sign", "verify"], u),
) !== undefined
) {
throw new DOMException("Invalid key usages", "SyntaxError");
}

switch (format) {
case "raw": {
const hash = normalizedAlgorithm.hash;
Expand Down Expand Up @@ -726,6 +731,52 @@
// TODO(@littledivy): RSASSA-PKCS1-v1_5
// TODO(@littledivy): RSA-PSS
// TODO(@littledivy): ECDSA
case "PBKDF2": {
// 1.
if (format !== "raw") {
throw new DOMException("Format not supported", "NotSupportedError");
}

// 2.
if (
ArrayPrototypeFind(
keyUsages,
(u) => !ArrayPrototypeIncludes(["deriveKey", "deriveBits"], u),
) !== undefined
) {
throw new DOMException("Invalid key usages", "SyntaxError");
}

// 3.
if (extractable !== false) {
throw new DOMException(
"Key must not be extractable",
"SyntaxError",
);
}

// 4.
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
type: "raw",
data: keyData,
});

// 5-9.
const algorithm = {
name: "PBKDF2",
};
const key = constructKey(
"secret",
false,
usageIntersection(keyUsages, recognisedUsages),
algorithm,
handle,
);

// 10.
return key;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
Expand Down Expand Up @@ -782,6 +833,48 @@
}
}

/**
* @param {AlgorithmIdentifier} algorithm
* @param {CryptoKey} baseKey
* @param {number} length
* @returns {Promise<ArrayBuffer>}
*/
async deriveBits(algorithm, baseKey, length) {
webidl.assertBranded(this, SubtleCrypto);
const prefix = "Failed to execute 'deriveBits' on 'SubtleCrypto'";
webidl.requiredArguments(arguments.length, 3, { prefix });
algorithm = webidl.converters.AlgorithmIdentifier(algorithm, {
prefix,
context: "Argument 1",
});
baseKey = webidl.converters.CryptoKey(baseKey, {
prefix,
context: "Argument 2",
});
length = webidl.converters["unsigned long"](length, {
prefix,
context: "Argument 3",
});

// 2.
const normalizedAlgorithm = normalizeAlgorithm(algorithm, "deriveBits");
// 4-6.
const result = await deriveBits(normalizedAlgorithm, baseKey, length);
// 7.
if (normalizedAlgorithm.name !== baseKey[_algorithm].name) {
throw new DOMException("InvalidAccessError", "Invalid algorithm name");
}
// 8.
if (!ArrayPrototypeIncludes(baseKey[_usages], "deriveBits")) {
throw new DOMException(
"InvalidAccessError",
"baseKey usages does not contain `deriveBits`",
);
}
// 9-10.
return result;
}

/**
* @param {string} algorithm
* @param {CryptoKey} key
Expand Down Expand Up @@ -1185,6 +1278,52 @@
}
}

async function deriveBits(normalizedAlgorithm, baseKey, length) {
switch (normalizedAlgorithm.name) {
case "PBKDF2": {
// 1.
if (length == null || length == 0 || length % 8 !== 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check if there's e.g. a WPT test that tests non-numeric inputs?

The logic is correct because e.g. "bad" % 8 evaluates to NaN and NaN !== 0 but I suspect that if it somehow managed to slip through to Rust land, it'd get coerced to zero.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis All the input types are always valid. They are validated by various webidl converters at the top of this function.

Copy link
Member Author

@littledivy littledivy Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only null as non-numeric length is tested in WPT.

Wouldn't the WebIDL converter (unsigned long) also complain? Edit: Whoops Github doesn't update in realtime

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will. Strings will already fail parsing there, so will never reach this statement.

throw new DOMException("Invalid length", "OperationError");
}

if (normalizedAlgorithm.iterations == 0) {
throw new DOMException(
"iterations must not be zero",
"OperationError",
);
}

const handle = baseKey[_handle];
const keyData = WeakMapPrototypeGet(KEY_STORE, handle);

if (ArrayBufferIsView(normalizedAlgorithm.salt)) {
normalizedAlgorithm.salt = new Uint8Array(
normalizedAlgorithm.salt.buffer,
normalizedAlgorithm.salt.byteOffset,
normalizedAlgorithm.salt.byteLength,
);
} else {
normalizedAlgorithm.salt = new Uint8Array(normalizedAlgorithm.salt);
}
normalizedAlgorithm.salt = TypedArrayPrototypeSlice(
normalizedAlgorithm.salt,
);

const buf = await core.opAsync("op_crypto_derive_bits", {
key: keyData,
algorithm: "PBKDF2",
hash: normalizedAlgorithm.hash.name,
iterations: normalizedAlgorithm.iterations,
length,
}, normalizedAlgorithm.salt);

return buf.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
}

const subtle = webidl.createBranded(SubtleCrypto);

class Crypto {
Expand Down
23 changes: 23 additions & 0 deletions ext/crypto/01_webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@
webidl.converters.HmacImportParams = webidl
.createDictionaryConverter("HmacImportParams", dictHmacImportParams);

const dictPbkdf2Params = [
...dictAlgorithm,
{
key: "hash",
converter: webidl.converters.HashAlgorithmIdentifier,
required: true,
},
{
key: "iterations",
converter: (V, opts) =>
webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }),
required: true,
},
{
key: "salt",
converter: webidl.converters["BufferSource"],
required: true,
},
];

webidl.converters.Pbkdf2Params = webidl
.createDictionaryConverter("Pbkdf2Params", dictPbkdf2Params);

webidl.converters.CryptoKey = webidl.createInterfaceConverter(
"CryptoKey",
CryptoKey,
Expand Down
2 changes: 2 additions & 0 deletions ext/crypto/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,6 @@ pub enum Algorithm {
AesKw,
#[serde(rename = "HMAC")]
Hmac,
#[serde(rename = "PBKDF2")]
Pbkdf2,
}
42 changes: 42 additions & 0 deletions ext/crypto/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use serde::Deserialize;

use std::cell::RefCell;
use std::convert::TryInto;
use std::num::NonZeroU32;
use std::rc::Rc;

use lazy_static::lazy_static;
Expand All @@ -27,6 +28,7 @@ use rand::SeedableRng;
use ring::digest;
use ring::hmac::Algorithm as HmacAlgorithm;
use ring::hmac::Key as HmacKey;
use ring::pbkdf2;
use ring::rand as RingRand;
use ring::rand::SecureRandom;
use ring::signature::EcdsaKeyPair;
Expand Down Expand Up @@ -74,6 +76,7 @@ pub fn init(maybe_seed: Option<u64>) -> Extension {
("op_crypto_generate_key", op_async(op_crypto_generate_key)),
("op_crypto_sign_key", op_async(op_crypto_sign_key)),
("op_crypto_verify_key", op_async(op_crypto_verify_key)),
("op_crypto_derive_bits", op_async(op_crypto_derive_bits)),
("op_crypto_encrypt_key", op_async(op_crypto_encrypt_key)),
("op_crypto_decrypt_key", op_async(op_crypto_decrypt_key)),
("op_crypto_subtle_digest", op_async(op_crypto_subtle_digest)),
Expand Down Expand Up @@ -519,6 +522,45 @@ pub async fn op_crypto_verify_key(
Ok(verification)
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeriveKeyArg {
key: KeyData,
algorithm: Algorithm,
hash: Option<CryptoHash>,
length: usize,
iterations: Option<u32>,
}

pub async fn op_crypto_derive_bits(
_state: Rc<RefCell<OpState>>,
args: DeriveKeyArg,
zero_copy: Option<ZeroCopyBuf>,
) -> Result<ZeroCopyBuf, AnyError> {
let zero_copy = zero_copy.ok_or_else(null_opbuf)?;
let salt = &*zero_copy;
let algorithm = args.algorithm;
match algorithm {
Algorithm::Pbkdf2 => {
let algorithm = match args.hash.ok_or_else(not_supported)? {
CryptoHash::Sha1 => pbkdf2::PBKDF2_HMAC_SHA1,
CryptoHash::Sha256 => pbkdf2::PBKDF2_HMAC_SHA256,
CryptoHash::Sha384 => pbkdf2::PBKDF2_HMAC_SHA384,
CryptoHash::Sha512 => pbkdf2::PBKDF2_HMAC_SHA512,
};

// This will never panic. We have already checked length earlier.
let iterations =
NonZeroU32::new(args.iterations.ok_or_else(not_supported)?).unwrap();
let secret = args.key.data;
let mut out = vec![0; args.length / 8];
littledivy marked this conversation as resolved.
Show resolved Hide resolved
pbkdf2::derive(algorithm, iterations, salt, &secret, &mut out);
Ok(out.into())
}
_ => Err(type_error("Unsupported algorithm".to_string())),
}
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncryptArg {
Expand Down
Loading