Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
"jimp": "1.6.0",
"jose": "^6.2.3",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
Expand Down
49 changes: 49 additions & 0 deletions src/core/lib/RSA.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import forge from "node-forge";
import * as asn1js from "asn1js";

export const MD_ALGORITHMS = {
"SHA-1": forge.md.sha1,
Expand All @@ -15,3 +16,51 @@ export const MD_ALGORITHMS = {
"SHA-384": forge.md.sha384,
"SHA-512": forge.md.sha512,
};

const rsaEncryptionOID = "1.2.840.113549.1.1.1";

/**
* Convert PKCS#1 RSA private key (PEM) to PKCS#8 PEM
* @param {string} originalPem
* @returns {string}
*/
export function pkcs1ToPkcs8(originalPem) {
// remove PEM headers
const b64 = originalPem
.replace(/-----BEGIN RSA PRIVATE KEY-----/g, "")
.replace(/-----END RSA PRIVATE KEY-----/g, "")
.replace(/\s+/g, "");

const pkcs1Der = Uint8Array.from(atob(b64), c => c.charCodeAt(0)).buffer;

// PKCS#8 structure:
// PrivateKeyInfo ::= SEQUENCE {
// version INTEGER,
// privateKeyAlgorithm AlgorithmIdentifier,
// privateKey OCTET STRING
// }

const pkcs8Schema = new asn1js.Sequence({
value: [
new asn1js.Integer({ value: 0 }),
new asn1js.Sequence({
value: [
// rsaEncryption OID
new asn1js.ObjectIdentifier({ value: rsaEncryptionOID }),
new asn1js.Null()
]
}),
new asn1js.OctetString({ valueHex: pkcs1Der })
]
});

const pkcs8Der = pkcs8Schema.toBER(false);

const pkcs8B64 = btoa(
String.fromCharCode(...new Uint8Array(pkcs8Der))
);

const lines = pkcs8B64.match(/.{1,64}/g).join("\n");

return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`;
}
42 changes: 34 additions & 8 deletions src/core/operations/JWTSign.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import jwt from "jsonwebtoken";
import { SignJWT, importPKCS8 } from "jose";
import OperationError from "../errors/OperationError.mjs";
import {JWT_ALGORITHMS} from "../lib/JWT.mjs";

import {pkcs1ToPkcs8} from "../lib/RSA.mjs";

/**
* JWT Sign operation
Expand Down Expand Up @@ -50,21 +50,47 @@ class JWTSign extends Operation {
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
async run(input, args) {
const [key, algorithm, header] = args;

let secret;
try {
return jwt.sign(input, key, {
algorithm: algorithm === "None" ? "none" : algorithm,
header: JSON.parse(header || "{}")
});
if (key.startsWith("-----BEGIN RSA PRIVATE KEY-----")) {
secret = await importPKCS8(pkcs1ToPkcs8(key), algorithm);
} else if (key.startsWith("-----BEGIN PRIVATE KEY-----")) {
secret = await importPKCS8(key, algorithm);
} else {
secret = new TextEncoder().encode(key);
}
} catch (err) {
throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA.

${err}`);
}
}

const fullHeader = { alg: algorithm, typ: "JWT" };
try {
if (header !== "{}") {
Object.assign(fullHeader, JSON.parse(header));
}
} catch (err) {
throw new OperationError(`Header must be a valid (or empty) json object.

${err}`);
}

try {
const token = await new SignJWT(input)
.setProtectedHeader(fullHeader)
.sign(secret);

return token;
} catch (err) {
throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA.

${err}`);
}
};
}

export default JWTSign;
2 changes: 1 addition & 1 deletion tests/browser/02_ops.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ module.exports = {
// testOp(browser, "JSON Minify", "test input", "test_output");
// testOp(browser, "JSON to CSV", "test input", "test_output");
// testOp(browser, "JWT Decode", "test input", "test_output");
// testOp(browser, "JWT Sign", "test input", "test_output");
testOp(browser, "JWT Sign", '{"a":{"b":1}}', "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjp7ImIiOjF9fQ.5PSBsZ9_B-qZa8H3l9tRAEV6qt8NgEHNJaoyjVcnTsU", ["A-key-of-256-bits-or-larger-as-per-RFC7518", "HS256", "{}"]);
// testOp(browser, "JWT Verify", "test input", "test_output");
// testOp(browser, "JavaScript Beautify", "test input", "test_output");
// testOp(browser, "JavaScript Minify", "test input", "test_output");
Expand Down
17 changes: 15 additions & 2 deletions tests/operations/tests/JWTSign.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ TestRegister.addTests([
}
],
},
{
name: "JWT Sign: HS256, invalid header",
input: inputObject,
expectedOutput: `Header must be a valid (or empty) json object.

SyntaxError: Unexpected token 'h', "this is not JSON" is not valid JSON`,
recipeConfig: [
{
op: "JWT Sign",
args: [hsKey, "HS256", "this is not JSON"],
}
],
},
{
name: "JWT Sign: HS256 with custom header",
input: inputObject,
Expand Down Expand Up @@ -142,7 +155,7 @@ TestRegister.addTests([
input: inputObject,
expectedOutput: `Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA.

Error: "alg" parameter "ES384" requires curve "secp384r1".`,
DataError: Named curve mismatch`,
recipeConfig: [
{
op: "JWT Sign",
Expand Down Expand Up @@ -189,7 +202,7 @@ Error: "alg" parameter "ES384" requires curve "secp384r1".`,
input: inputObject,
expectedOutput: `Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded private key for RSA and ECDSA.

Error: secretOrPrivateKey has a minimum key size of 2048 bits for RS256`,
TypeError: RS256 requires key modulusLength to be 2048 bits or larger`,
recipeConfig: [
{
op: "JWT Sign",
Expand Down