Skip to content

Commit

Permalink
feat(ext/crypto): implement AES-GCM encryption (#13119)
Browse files Browse the repository at this point in the history
  • Loading branch information
littledivy authored Jan 5, 2022
1 parent 9778545 commit c74eb7a
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 91 deletions.
56 changes: 56 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 @@ -1418,6 +1418,34 @@ Deno.test(async function testImportEcSpkiPkcs8() {
}
});

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]),
);
});

async function roundTripSecretJwk(
jwk: JsonWebKey,
algId: AlgorithmIdentifier | HmacImportParams,
Expand Down
65 changes: 65 additions & 0 deletions ext/crypto/00_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
];

const simpleAlgorithmDictionaries = {
AesGcmParams: { iv: "BufferSource", additionalData: "BufferSource" },
RsaHashedKeyGenParams: { hash: "HashAlgorithmIdentifier" },
EcKeyGenParams: {},
HmacKeyGenParams: { hash: "HashAlgorithmIdentifier" },
Expand Down Expand Up @@ -123,6 +124,7 @@
"encrypt": {
"RSA-OAEP": "RsaOaepParams",
"AES-CBC": "AesCbcParams",
"AES-GCM": "AesGcmParams",
"AES-CTR": "AesCtrParams",
},
"decrypt": {
Expand Down Expand Up @@ -3502,6 +3504,69 @@
// 4.
return cipherText.buffer;
}
case "AES-GCM": {
normalizedAlgorithm.iv = copyBuffer(normalizedAlgorithm.iv);

// 1.
if (data.byteLength > (2 ** 39) - 256) {
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) {
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
31 changes: 26 additions & 5 deletions ext/crypto/01_webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,23 @@
},
];

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

webidl.converters.AesCbcParams = webidl
.createDictionaryConverter("AesCbcParams", dictAesCbcParams);
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"],
},
];

const dictAesCtrParams = [
...dictAlgorithm,
Expand All @@ -419,6 +431,15 @@
},
];

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

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

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

webidl.converters.AesCtrParams = webidl
.createDictionaryConverter("AesCtrParams", dictAesCtrParams);

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"
ctr = "0.8.0"
Expand Down
82 changes: 80 additions & 2 deletions ext/crypto/encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ use crate::shared::*;
use aes::cipher::NewCipher;
use aes::BlockEncrypt;
use aes::NewBlockCipher;
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 ctr::Ctr;

use block_modes::BlockMode;
Expand Down Expand Up @@ -53,6 +60,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,
},
#[serde(rename = "AES-CTR", rename_all = "camelCase")]
AesCtr {
#[serde(with = "serde_bytes")]
Expand All @@ -74,6 +90,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),
EncryptAlgorithm::AesCtr {
counter,
ctr_length,
Expand All @@ -89,7 +111,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 @@ -129,7 +151,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 @@ -161,6 +183,62 @@ 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)
}

fn encrypt_aes_ctr_gen<B, F>(
key: &[u8],
counter: &[u8],
Expand Down
Loading

0 comments on commit c74eb7a

Please sign in to comment.