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 AES-GCM encryption #13119

Merged
merged 12 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from 9 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
65 changes: 65 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions cli/tests/unit/webcrypto_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1310,3 +1310,31 @@ Deno.test(async function testImportEcSpkiPkcs8() {
assertEquals(new Uint8Array(expPrivateKeySPKI), spki);*/
}
});

Deno.test(async function testAesGcmEncrypt() {
const key = await crypto.subtle.importKey(
"raw",
new Uint8Array(16),
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);

// deno-fmt-ignore
const iv = new Uint8Array([0,1,2,3,4,5,6,7,8,9,10,11]);
const data = new Uint8Array([1, 2, 3]);

const cipherText = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData: new Uint8Array() },
key,
data,
);

assert(cipherText instanceof ArrayBuffer);
assertEquals(cipherText.byteLength, 19);
assertEquals(
new Uint8Array(cipherText),
// deno-fmt-ignore
new Uint8Array([50,223,112,178,166,156,255,110,125,138,95,141,82,47,14,164,134,247,22]),
);
});
66 changes: 66 additions & 0 deletions ext/crypto/00_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
];

const simpleAlgorithmDictionaries = {
AesGcmParams: { iv: "BufferSource", additionalData: "BufferSource" },
RsaHashedKeyGenParams: { hash: "HashAlgorithmIdentifier" },
EcKeyGenParams: {},
HmacKeyGenParams: { hash: "HashAlgorithmIdentifier" },
Expand Down Expand Up @@ -126,6 +127,7 @@
"encrypt": {
"RSA-OAEP": "RsaOaepParams",
"AES-CBC": "AesCbcParams",
"AES-GCM": "AesGcmParams",
},
"decrypt": {
"RSA-OAEP": "RsaOaepParams",
Expand Down Expand Up @@ -3429,6 +3431,70 @@
// 4.
return cipherText.buffer;
}
case "AES-GCM": {
normalizedAlgorithm.iv = copyBuffer(normalizedAlgorithm.iv);

// 1.
if (data.byteLength > (2 ** 39) - 256) {
bnoordhuis marked this conversation as resolved.
Show resolved Hide resolved
throw new DOMException(
"Plaintext too large",
"OperationError",
);
}

// 2.
// We only support 96-bit nonce for now.
if (normalizedAlgorithm.iv.byteLength !== 12) {
throw new DOMException(
"Initialization vector length not supported",
"NotSupportedError",
);
}

// 3.
if (normalizedAlgorithm.additionalData !== undefined) {
if (normalizedAlgorithm.additionalData.byteLength > (2 ** 64) - 1) {
bnoordhuis marked this conversation as resolved.
Show resolved Hide resolved
throw new DOMException(
"Additional data too large",
"OperationError",
);
}
}

// 4.
if (normalizedAlgorithm.tagLength == undefined) {
normalizedAlgorithm.tagLength = 128;
} else if (
!ArrayPrototypeIncludes(
[32, 64, 96, 104, 112, 120, 128],
normalizedAlgorithm.tagLength,
)
) {
throw new DOMException(
"Invalid tag length",
"OperationError",
);
}

// 5.
if (normalizedAlgorithm.additionalData) {
normalizedAlgorithm.additionalData = copyBuffer(
normalizedAlgorithm.additionalData,
);
}
// 6-7.
const cipherText = await core.opAsync("op_crypto_encrypt", {
key: keyData,
algorithm: "AES-GCM",
length: key[_algorithm].length,
iv: normalizedAlgorithm.iv,
additionalData: normalizedAlgorithm.additionalData,
tagLength: normalizedAlgorithm.tagLength,
}, data);

// 8.
return cipherText.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
Expand Down
21 changes: 21 additions & 0 deletions ext/crypto/01_webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,33 @@
},
];

const dictAesGcmParams = [
...dictAlgorithm,
{
key: "iv",
converter: webidl.converters["BufferSource"],
required: true,
},
{
key: "tagLength",
converter: (V, opts) =>
webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }),
},
{
key: "additionalData",
converter: webidl.converters["BufferSource"],
},
];

webidl.converters.AesDerivedKeyParams = webidl
.createDictionaryConverter("AesDerivedKeyParams", dictAesDerivedKeyParams);

webidl.converters.AesCbcParams = webidl
.createDictionaryConverter("AesCbcParams", dictAesCbcParams);

webidl.converters.AesGcmParams = webidl
.createDictionaryConverter("AesGcmParams", dictAesGcmParams);

webidl.converters.CryptoKey = webidl.createInterfaceConverter(
"CryptoKey",
CryptoKey,
Expand Down
1 change: 1 addition & 0 deletions ext/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ path = "lib.rs"

[dependencies]
aes = "0.7.5"
aes-gcm = "0.9.4"
base64 = "0.13.0"
block-modes = "0.8.1"
deno_core = { version = "0.111.0", path = "../../core" }
Expand Down
83 changes: 81 additions & 2 deletions ext/crypto/encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ use std::cell::RefCell;
use std::rc::Rc;

use crate::shared::*;

use aes_gcm::aead::generic_array::typenum::U12;
use aes_gcm::aes::Aes192;
use aes_gcm::AeadInPlace;
use aes_gcm::Aes128Gcm;
use aes_gcm::Aes256Gcm;
use aes_gcm::NewAead;
use aes_gcm::Nonce;
use block_modes::BlockMode;
use deno_core::error::type_error;
use deno_core::error::AnyError;
Expand Down Expand Up @@ -41,6 +49,15 @@ pub enum EncryptAlgorithm {
iv: Vec<u8>,
length: usize,
},
#[serde(rename = "AES-GCM", rename_all = "camelCase")]
AesGcm {
#[serde(with = "serde_bytes")]
iv: Vec<u8>,
#[serde(with = "serde_bytes")]
additional_data: Option<Vec<u8>>,
length: usize,
tag_length: usize,
},
}
pub async fn op_crypto_encrypt(
_state: Rc<RefCell<OpState>>,
Expand All @@ -55,6 +72,12 @@ pub async fn op_crypto_encrypt(
EncryptAlgorithm::AesCbc { iv, length } => {
encrypt_aes_cbc(key, length, iv, &data)
}
EncryptAlgorithm::AesGcm {
iv,
additional_data,
length,
tag_length,
} => encrypt_aes_gcm(key, length, tag_length, iv, additional_data, &data),
};
let buf = tokio::task::spawn_blocking(fun).await.unwrap()?;
Ok(buf.into())
Expand All @@ -65,7 +88,7 @@ fn encrypt_rsa_oaep(
hash: ShaHash,
label: Vec<u8>,
data: &[u8],
) -> Result<Vec<u8>, deno_core::anyhow::Error> {
) -> Result<Vec<u8>, AnyError> {
let label = String::from_utf8_lossy(&label).to_string();

let public_key = key.as_rsa_public_key()?;
Expand Down Expand Up @@ -105,7 +128,7 @@ fn encrypt_aes_cbc(
length: usize,
iv: Vec<u8>,
data: &[u8],
) -> Result<Vec<u8>, deno_core::anyhow::Error> {
) -> Result<Vec<u8>, AnyError> {
let key = key.as_secret_key()?;
let ciphertext = match length {
128 => {
Expand Down Expand Up @@ -136,3 +159,59 @@ fn encrypt_aes_cbc(
};
Ok(ciphertext)
}

fn encrypt_aes_gcm(
key: RawKeyData,
length: usize,
tag_length: usize,
iv: Vec<u8>,
additional_data: Option<Vec<u8>>,
data: &[u8],
) -> Result<Vec<u8>, AnyError> {
let key = key.as_secret_key()?;
let additional_data = additional_data.unwrap_or_default();

// Fixed 96-bit nonce
if iv.len() != 12 {
return Err(type_error("iv length not equal to 12"));
}

let nonce = Nonce::from_slice(&iv);

let mut ciphertext = data.to_vec();
let tag = match length {
128 => {
let cipher = Aes128Gcm::new_from_slice(key)
.map_err(|_| operation_error("Encryption failed"))?;
cipher
.encrypt_in_place_detached(nonce, &additional_data, &mut ciphertext)
.map_err(|_| operation_error("Encryption failed"))?
}
192 => {
type Aes192Gcm = aes_gcm::AesGcm<Aes192, U12>;

let cipher = Aes192Gcm::new_from_slice(key)
.map_err(|_| operation_error("Encryption failed"))?;
cipher
.encrypt_in_place_detached(nonce, &additional_data, &mut ciphertext)
.map_err(|_| operation_error("Encryption failed"))?
}
256 => {
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|_| operation_error("Encryption failed"))?;
cipher
.encrypt_in_place_detached(nonce, &additional_data, &mut ciphertext)
.map_err(|_| operation_error("Encryption failed"))?
}
_ => return Err(type_error("invalid length")),
};

// Truncated tag to the specified tag length.
// `tag` is fixed to be 16 bytes long and (tag_length / 8) is always <= 16
let tag = &tag[..(tag_length / 8)];

// C | T
ciphertext.extend_from_slice(tag);

Ok(ciphertext)
}
Loading