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

Add unwrapKey to typescript interface #5792

Merged
merged 12 commits into from
Nov 2, 2023
5 changes: 5 additions & 0 deletions js/ccf-app/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export const generateEddsaKeyPair = ccf.crypto.generateEddsaKeyPair;
*/
export const wrapKey = ccf.crypto.wrapKey;

/**
* @inheritDoc global!CCFCrypto.unwrapKey
*/
export const unwrapKey = ccf.crypto.unwrapKey;

/**
* @inheritDoc global!CCFCrypto.sign
*/
Expand Down
12 changes: 12 additions & 0 deletions js/ccf-app/src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@ export interface CCFCrypto {
wrapAlgo: WrapAlgoParams,
): ArrayBuffer;

/**
* Unwraps a key using a wrapping key.
*
* Constraints on the `key` and `wrappingKey` parameters depend
* on the wrapping algorithm that is used (`wrapAlgo`).
*/
unwrapKey(
key: ArrayBuffer,
wrappingKey: ArrayBuffer,
wrapAlgo: WrapAlgoParams,
): ArrayBuffer;

/**
* Generate a digest (hash) of the given data.
*/
Expand Down
60 changes: 60 additions & 0 deletions js/ccf-app/src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,58 @@ class CCFPolyfill implements CCF {
throw new Error("unsupported wrapAlgo.name");
}
},
unwrapKey(
wrappedKey: ArrayBuffer,
unwrappingKey: ArrayBuffer,
unwrapAlgo: WrapAlgoParams,
): ArrayBuffer {
if (unwrapAlgo.name == "RSA-OAEP") {
return nodeBufToArrBuf(
jscrypto.privateDecrypt(
{
key: Buffer.from(unwrappingKey),
oaepHash: "sha256",
padding: jscrypto.constants.RSA_PKCS1_OAEP_PADDING,
},
new Uint8Array(wrappedKey),
),
);
} else if (unwrapAlgo.name == "AES-KWP") {
const iv = Buffer.from("A65959A6", "hex"); // defined in RFC 5649
const decipher = jscrypto.createDecipheriv(
"id-aes256-wrap-pad",
new Uint8Array(unwrappingKey),
iv,
);
return nodeBufToArrBuf(
Buffer.concat([
decipher.update(new Uint8Array(wrappedKey)),
decipher.final(),
]),
);
} else if (unwrapAlgo.name == "RSA-OAEP-AES-KWP") {
const keyInfo = jscrypto.createPrivateKey(Buffer.from(unwrappingKey));
// asymmetricKeyDetails added in Node.js 15.7.0, we're at 16.
console.log(
`Modulus length: `,
keyInfo?.asymmetricKeyDetails?.modulusLength,
);
const modulusLengthInBytes =
(keyInfo?.asymmetricKeyDetails?.modulusLength || 2048) / 8;

const wrap1 = wrappedKey.slice(0, modulusLengthInBytes);
const wrap2 = wrappedKey.slice(modulusLengthInBytes);
const aesKey = this.unwrapKey(wrap1, unwrappingKey, {
name: "RSA-OAEP",
label: unwrapAlgo.label,
});
return this.unwrapKey(wrap2, aesKey, {
name: "AES-KWP",
});
} else {
throw new Error("unsupported unwrapAlgo.name");
}
},
digest(algorithm: DigestAlgorithm, data: ArrayBuffer): ArrayBuffer {
if (algorithm === "SHA-256") {
return nodeBufToArrBuf(
Expand Down Expand Up @@ -533,6 +585,14 @@ class CCFPolyfill implements CCF {
return this.crypto.wrapKey(key, wrappingKey, parameters);
}

unwrapKey(
key: ArrayBuffer,
wrappingKey: ArrayBuffer,
parameters: WrapAlgoParams,
): ArrayBuffer {
return this.crypto.unwrapKey(key, wrappingKey, parameters);
}

digest(algorithm: DigestAlgorithm, data: ArrayBuffer): ArrayBuffer {
return this.crypto.digest(algorithm, data);
}
Expand Down
60 changes: 0 additions & 60 deletions js/ccf-app/test/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,5 @@
import * as crypto from "crypto";
import forge from "node-forge";
import { WrapAlgoParams } from "../src/global.js";

function nodeBufToArrBuf(buf: Buffer): ArrayBuffer {
// Note: buf.buffer is not safe, see docs.
const arrBuf = new ArrayBuffer(buf.byteLength);
buf.copy(new Uint8Array(arrBuf));
return arrBuf;
}

export function unwrapKey(
wrappedKey: ArrayBuffer,
unwrappingKey: ArrayBuffer,
unwrapAlgo: WrapAlgoParams,
): ArrayBuffer {
if (unwrapAlgo.name == "RSA-OAEP") {
return nodeBufToArrBuf(
crypto.privateDecrypt(
{
key: Buffer.from(unwrappingKey),
oaepHash: "sha256",
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
new Uint8Array(wrappedKey),
),
);
} else if (unwrapAlgo.name == "AES-KWP") {
const iv = Buffer.from("A65959A6", "hex"); // defined in RFC 5649
const decipher = crypto.createDecipheriv(
"id-aes256-wrap-pad",
new Uint8Array(unwrappingKey),
iv,
);
return nodeBufToArrBuf(
Buffer.concat([
decipher.update(new Uint8Array(wrappedKey)),
decipher.final(),
]),
);
} else if (unwrapAlgo.name == "RSA-OAEP-AES-KWP") {
/*
const keyInfo = crypto.createPrivateKey(unwrappingKey);
// asymmetricKeyDetails added in Node.js 15.7.0, we're at 14.
const modulusLengthInBytes = keyInfo.asymmetricKeyDetails.modulusLength / 8;
*/
// For now, hard-coded for the test in polyfill.test.ts.
const modulusLengthInBytes = 2048 / 8;

const wrap1 = wrappedKey.slice(0, modulusLengthInBytes);
const wrap2 = wrappedKey.slice(modulusLengthInBytes);
const aesKey = unwrapKey(wrap1, unwrappingKey, {
name: "RSA-OAEP",
label: unwrapAlgo.label,
});
return unwrapKey(wrap2, aesKey, {
name: "AES-KWP",
});
} else {
throw new Error("unsupported unwrapAlgo.name");
}
}

export function generateSelfSignedCert() {
const keys = crypto.generateKeyPairSync("rsa", {
Expand Down
12 changes: 4 additions & 8 deletions js/ccf-app/test/polyfill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import {
RsaOaepAesKwpParams,
RsaOaepParams,
} from "../src/global.js";
import {
unwrapKey,
generateSelfSignedCert,
generateCertChain,
} from "./crypto.js";
import { generateSelfSignedCert, generateCertChain } from "./crypto.js";

beforeEach(function () {
// clear KV before each test
Expand Down Expand Up @@ -92,7 +88,7 @@ describe("polyfill", function () {
ccf.strToBuf(wrappingKey.publicKey),
wrapAlgo,
);
const unwrapped = unwrapKey(
const unwrapped = ccf.crypto.unwrapKey(
wrapped,
ccf.strToBuf(wrappingKey.privateKey),
wrapAlgo,
Expand All @@ -106,7 +102,7 @@ describe("polyfill", function () {
name: "AES-KWP",
};
const wrapped = ccf.crypto.wrapKey(key, wrappingKey, wrapAlgo);
const unwrapped = unwrapKey(wrapped, wrappingKey, wrapAlgo);
const unwrapped = ccf.crypto.unwrapKey(wrapped, wrappingKey, wrapAlgo);
assert.deepEqual(unwrapped, key);
});
it("performs RSA-OAEP-AES-KWP wrapping correctly", function () {
Expand All @@ -121,7 +117,7 @@ describe("polyfill", function () {
ccf.strToBuf(wrappingKey.publicKey),
wrapAlgo,
);
const unwrapped = unwrapKey(
const unwrapped = ccf.crypto.unwrapKey(
wrapped,
ccf.strToBuf(wrappingKey.privateKey),
wrapAlgo,
Expand Down
144 changes: 140 additions & 4 deletions src/js/crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ namespace ccf::js

try
{
auto algo_name = std::string(*wrap_algo_name_str);
auto algo_name = *wrap_algo_name_str;
if (algo_name == "RSA-OAEP")
{
// key can in principle be arbitrary data (see note on maximum size
Expand Down Expand Up @@ -602,9 +602,12 @@ namespace ccf::js
}
else if (algo_name == "AES-KWP")
{
std::vector<uint8_t> wrapped_key = crypto::ckm_aes_key_wrap_pad(
{wrapping_key, wrapping_key + wrapping_key_size},
{key, key + key_size});
std::vector<uint8_t> privateKey(
wrapping_key, wrapping_key + wrapping_key_size);
std::vector<uint8_t> wrapped_key =
crypto::ckm_aes_key_wrap_pad(privateKey, {key, key + key_size});

OPENSSL_cleanse(&privateKey, sizeof(privateKey));

return JS_NewArrayBufferCopy(
ctx, wrapped_key.data(), wrapped_key.size());
Expand Down Expand Up @@ -660,6 +663,139 @@ namespace ccf::js
}
}

static JSValue js_unwrap_key(
JSContext* ctx, JSValueConst, int argc, JSValueConst* argv)
{
if (argc != 3)
return JS_ThrowTypeError(
ctx, "Passed %d arguments, but expected 3", argc);

// API loosely modeled after
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/unwrapKey.

size_t key_size;
uint8_t* key = JS_GetArrayBuffer(ctx, &key_size, argv[0]);
if (!key)
{
return ccf::js::constants::Exception;
}

size_t unwrapping_key_size;
uint8_t* unwrapping_key =
JS_GetArrayBuffer(ctx, &unwrapping_key_size, argv[1]);
if (!unwrapping_key)
{
return ccf::js::constants::Exception;
}

js::Context& jsctx = *(js::Context*)JS_GetContextOpaque(ctx);

auto parameters = argv[2];
auto wrap_algo_name_val = jsctx(JS_GetPropertyStr(ctx, parameters, "name"));
JS_CHECK_EXC(wrap_algo_name_val);

auto wrap_algo_name_str = jsctx.to_str(wrap_algo_name_val);
if (!wrap_algo_name_str)
{
return ccf::js::constants::Exception;
}

try
{
auto algo_name = *wrap_algo_name_str;
if (algo_name == "RSA-OAEP")
{
// key can in principle be arbitrary data (see note on maximum size
// in rsa_key_pair.h). unwrapping_key is a private RSA key.

auto label_val = jsctx(JS_GetPropertyStr(ctx, parameters, "label"));
JS_CHECK_EXC(label_val);

size_t label_buf_size = 0;
uint8_t* label_buf = JS_GetArrayBuffer(ctx, &label_buf_size, label_val);

std::optional<std::vector<uint8_t>> label_opt = std::nullopt;
if (label_buf && label_buf_size > 0)
{
label_opt = {label_buf, label_buf + label_buf_size};
}

auto pemPrivateUnwrappingKey =
crypto::Pem(unwrapping_key, unwrapping_key_size);
auto unwrapped_key = crypto::ckm_rsa_pkcs_oaep_unwrap(
pemPrivateUnwrappingKey, {key, key + key_size}, label_opt);

OPENSSL_cleanse(
pemPrivateUnwrappingKey.data(), pemPrivateUnwrappingKey.size());

return JS_NewArrayBufferCopy(
ctx, unwrapped_key.data(), unwrapped_key.size());
}
else if (algo_name == "AES-KWP")
{
std::vector<uint8_t> privateKey(
unwrapping_key, unwrapping_key + unwrapping_key_size);
std::vector<uint8_t> unwrapped_key =
crypto::ckm_aes_key_unwrap_pad(privateKey, {key, key + key_size});

OPENSSL_cleanse(&privateKey, sizeof(privateKey));

return JS_NewArrayBufferCopy(
ctx, unwrapped_key.data(), unwrapped_key.size());
}
else if (algo_name == "RSA-OAEP-AES-KWP")
{
auto aes_key_size_value =
jsctx(JS_GetPropertyStr(ctx, parameters, "aesKeySize"));
JS_CHECK_EXC(aes_key_size_value);

int32_t aes_key_size = 0;
if (JS_ToInt32(ctx, &aes_key_size, aes_key_size_value) < 0)
{
return ccf::js::constants::Exception;
}

auto label_val = jsctx(JS_GetPropertyStr(ctx, parameters, "label"));
JS_CHECK_EXC(label_val);

size_t label_buf_size = 0;
uint8_t* label_buf = JS_GetArrayBuffer(ctx, &label_buf_size, label_val);

std::optional<std::vector<uint8_t>> label_opt = std::nullopt;
if (label_buf && label_buf_size > 0)
{
label_opt = {label_buf, label_buf + label_buf_size};
}

auto privPemUnwrappingKey =
crypto::Pem(unwrapping_key, unwrapping_key_size);
auto unwrapped_key = crypto::ckm_rsa_aes_key_unwrap(
privPemUnwrappingKey, {key, key + key_size}, label_opt);

OPENSSL_cleanse(
privPemUnwrappingKey.data(), privPemUnwrappingKey.size());

return JS_NewArrayBufferCopy(
ctx, unwrapped_key.data(), unwrapped_key.size());
}
else
{
return JS_ThrowRangeError(
ctx,
"unsupported key unwrapping algorithm, supported: RSA-OAEP, AES-KWP, "
"RSA-OAEP-AES-KWP");
}
}
catch (std::exception& ex)
{
return JS_ThrowInternalError(ctx, "Failed to unwrap key: %s", ex.what());
}
catch (...)
{
return JS_ThrowRangeError(ctx, "caught unknown exception");
}
}

static JSValue js_sign(
JSContext* ctx, JSValueConst, int argc, JSValueConst* argv)
{
Expand Down
5 changes: 5 additions & 0 deletions src/js/wrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2314,6 +2314,11 @@ namespace ccf::js
ctx, js_generate_eddsa_key_pair, "generateEddsaKeyPair", 1));
JS_SetPropertyStr(
ctx, crypto, "wrapKey", JS_NewCFunction(ctx, js_wrap_key, "wrapKey", 3));
JS_SetPropertyStr(
ctx,
crypto,
"unwrapKey",
JS_NewCFunction(ctx, js_unwrap_key, "unwrapKey", 3));
JS_SetPropertyStr(
ctx, crypto, "digest", JS_NewCFunction(ctx, js_digest, "digest", 2));
JS_SetPropertyStr(
Expand Down
Loading