Skip to content

Commit

Permalink
Add unwrapKey to typescript interface (#5792)
Browse files Browse the repository at this point in the history
  • Loading branch information
beejones authored Nov 2, 2023
1 parent 264ae7a commit e759f99
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 80 deletions.
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

0 comments on commit e759f99

Please sign in to comment.