From 68d1803a6d8d0efc8bfac6898e1797a8135278a2 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 8 Sep 2020 17:46:05 +0700 Subject: [PATCH 1/3] RSAKey can export to different key format --- src/helper.ts | 11 +++ src/rsa/common.ts | 2 +- src/rsa/export_key.ts | 121 +++++++++++++++++++++++++++++++ src/rsa/import_key.ts | 16 ++-- src/rsa/mod.ts | 9 +-- src/rsa/rsa_js.ts | 3 +- src/rsa/rsa_key.ts | 55 ++++++++++++++ src/rsa/rsa_wc.ts | 3 +- src/utility/encode.ts | 11 +++ tests/rsa/rsa.export_key.test.ts | 42 +++++++++++ 10 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 src/rsa/export_key.ts create mode 100644 src/rsa/rsa_key.ts create mode 100644 tests/rsa/rsa.export_key.test.ts diff --git a/src/helper.ts b/src/helper.ts index 719b8d6..76cf879 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -29,6 +29,17 @@ export function concat(...arg: (Uint8Array | number[])[]) { return c; } +export function bignum_to_byte(n: bigint): number[] { + const bytes = []; + while (n > 0) { + bytes.push(Number(n & 255n)); + n = n >> 8n; + } + + bytes.reverse(); + return bytes; +} + export function random_bytes(length: number): Uint8Array { const n = new Uint8Array(length); for (let i = 0; i < length; i++) n[i] = ((Math.random() * 254) | 0) + 1; diff --git a/src/rsa/common.ts b/src/rsa/common.ts index bccde66..31b71d3 100644 --- a/src/rsa/common.ts +++ b/src/rsa/common.ts @@ -1,4 +1,4 @@ -export interface RSAKey { +export interface RSAKeyParams { n: bigint; e?: bigint; d?: bigint; diff --git a/src/rsa/export_key.ts b/src/rsa/export_key.ts new file mode 100644 index 0000000..d3c73d6 --- /dev/null +++ b/src/rsa/export_key.ts @@ -0,0 +1,121 @@ +import { RSAKeyParams } from "./common.ts"; +import { bignum_to_byte } from "../helper.ts"; +import { encode } from "./../../src/utility/encode.ts"; + +function ber_size_bytes(size: number): number[] { + // The BER Length + // The second component in the TLV structure of a BER element is the length. + // This specifies the size in bytes of the encoded value. For the most part, + // this uses a straightforward binary encoding of the integer value + // (for example, if the encoded value is five bytes long, then it is encoded as + // 00000101 binary, or 0x05 hex), but if the value is longer than 127 bytes then + // it is necessary to use multiple bytes to encode the length. In that case, the + // first byte has the leftmost bit set to one and the remaining seven bits are + // used to specify the number of bytes required to encode the full length. For example, + // if there are 500 bytes in the length (hex 0x01F4), then the encoded length will actually + // consist of three bytes: 82 01 F4. + // + // Note that there is an alternate form for encoding the length called the indefinite form. + // In this mechanism, only a part of the length is given at a time, similar to the chunked encoding + // that is available in HTTP 1.1. However, this form is not used in LDAP, as specified in RFC 2251 + // section 5.1. + // https://docs.oracle.com/cd/E19476-01/821-0510/def-basic-encoding-rules.html + + if (size <= 127) return [size]; + + const bytes = []; + while (size > 0) { + bytes.push(size & 0xff); + size = size >> 8; + } + + bytes.reverse(); + return [0x80 + bytes.length, ...bytes]; +} + +function add_line_break(base64_str: string): string { + const lines = []; + for (let i = 0; i < base64_str.length; i += 64) { + lines.push(base64_str.substr(i, 64)); + } + + return lines.join("\n"); +} + +function ber_generate_integer_list(order: number[][]) { + let content: number[] = []; + + for (const item of order) { + if ((item[0] & 0x80) > 0) { + content = content.concat( + [0x02, ...ber_size_bytes(item.length + 1), 0x0, ...item], + ); + } else { + content = content.concat( + [0x02, ...ber_size_bytes(item.length), ...item], + ); + } + } + + return content; +} + +export function rsa_export_pkcs8_public(key: RSAKeyParams) { + const n = bignum_to_byte(key.n); + const e = bignum_to_byte(key.e || 0n); + + // deno-fmt-ignore + const other = [0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00]; + + // Key sequence + const content = ber_generate_integer_list([n, e]); + const keySequence = [ + 0x30, + ...ber_size_bytes(content.length), + ...content, + ]; + + // Bitstring + const bitString = [ + 0x03, + ...ber_size_bytes(keySequence.length + 1), + 0x00, + ...keySequence, + ]; + + const ber = [ + 0x30, + ...ber_size_bytes(other.length + bitString.length), + ...other, + ...bitString, + ]; + + return "-----BEGIN PUBLIC KEY-----\n" + + add_line_break(encode.binary(ber).base64()) + + "\n-----END PUBLIC KEY-----\n"; +} + +export function rsa_export_pkcs8_private(key: RSAKeyParams) { + const n = bignum_to_byte(key.n); + const e = bignum_to_byte(key.e || 0n); + const d = bignum_to_byte(key.d || 0n); + const q = bignum_to_byte(key.q || 0n); + const p = bignum_to_byte(key.p || 0n); + const dp = bignum_to_byte(key.dp || 0n); + const dq = bignum_to_byte(key.dq || 0n); + const qi = bignum_to_byte(key.qi || 0n); + + const content = ber_generate_integer_list([n, e, d, p, q, dp, dq, qi]); + + const ber = encode.binary([ + 0x30, + ...ber_size_bytes(content.length + 3), + 0x02, + 0x01, + 0x00, + ...content, + ]).base64(); + + return "-----BEGIN RSA PRIVATE KEY-----\n" + add_line_break(ber) + + "\n-----END RSA PRIVATE KEY-----\n"; +} diff --git a/src/rsa/import_key.ts b/src/rsa/import_key.ts index c6214be..e4e37b9 100644 --- a/src/rsa/import_key.ts +++ b/src/rsa/import_key.ts @@ -1,5 +1,5 @@ import { encode } from "./../../src/utility/encode.ts"; -import { JSONWebKey, RSAKey } from "./common.ts"; +import { JSONWebKey, RSAKeyParams } from "./common.ts"; import { get_key_size, base64_to_binary } from "../helper.ts"; import { ber_decode, ber_simple } from "./basic_encoding_rule.ts"; import { os2ip } from "./primitives.ts"; @@ -31,7 +31,7 @@ function detect_format(key: string | JSONWebKey): RSAImportKeyFormat { * * @param key PEM encoded key format */ -function rsa_import_jwk(key: JSONWebKey): RSAKey { +function rsa_import_jwk(key: JSONWebKey): RSAKeyParams { if (typeof key !== "object") throw new TypeError("Invalid JWK format"); if (!key.n) throw new TypeError("RSA key requires n"); @@ -56,7 +56,7 @@ function rsa_import_jwk(key: JSONWebKey): RSAKey { * * @param key */ -function rsa_import_pem_cert(key: string): RSAKey { +function rsa_import_pem_cert(key: string): RSAKeyParams { const trimmedKey = key.substr(27, key.length - 53); const parseKey = ber_simple( ber_decode(base64_to_binary(trimmedKey)), @@ -75,7 +75,7 @@ function rsa_import_pem_cert(key: string): RSAKey { * * @param key PEM encoded key format */ -function rsa_import_pem_private(key: string): RSAKey { +function rsa_import_pem_private(key: string): RSAKeyParams { const trimmedKey = key.substr(31, key.length - 61); const parseKey = ber_simple( ber_decode(base64_to_binary(trimmedKey)), @@ -100,7 +100,7 @@ function rsa_import_pem_private(key: string): RSAKey { * * @param key PEM encoded key format */ -function rsa_import_pem_public(key: string): RSAKey { +function rsa_import_pem_public(key: string): RSAKeyParams { const trimmedKey = key.substr(26, key.length - 51); const parseKey = ber_simple( ber_decode(base64_to_binary(trimmedKey)), @@ -119,10 +119,10 @@ function rsa_import_pem_public(key: string): RSAKey { * * @param key PEM encoded key format */ -function rsa_import_pem(key: string): RSAKey { +function rsa_import_pem(key: string): RSAKeyParams { if (typeof key !== "string") throw new TypeError("PEM key must be string"); - const maps: [string, (key: string) => RSAKey][] = [ + const maps: [string, (key: string) => RSAKeyParams][] = [ ["-----BEGIN RSA PRIVATE KEY-----", rsa_import_pem_private], ["-----BEGIN PUBLIC KEY-----", rsa_import_pem_public], ["-----BEGIN CERTIFICATE-----", rsa_import_pem_cert], @@ -144,7 +144,7 @@ function rsa_import_pem(key: string): RSAKey { export function rsa_import_key( key: string | JSONWebKey, format: RSAImportKeyFormat, -): RSAKey { +): RSAKeyParams { const finalFormat = format === "auto" ? detect_format(key) : format; if (finalFormat === "jwk") return rsa_import_jwk(key as JSONWebKey); diff --git a/src/rsa/mod.ts b/src/rsa/mod.ts index ef9c8fb..02eab87 100644 --- a/src/rsa/mod.ts +++ b/src/rsa/mod.ts @@ -1,10 +1,9 @@ -import { RSAKey, RSAOption, RSASignOption, JSONWebKey } from "./common.ts"; -import { ber_decode, ber_simple } from "./basic_encoding_rule.ts"; -import { base64_to_binary, get_key_size, str2bytes } from "./../helper.ts"; +import { RSAOption, RSASignOption, JSONWebKey } from "./common.ts"; import { WebCryptoRSA } from "./rsa_wc.ts"; import { PureRSA } from "./rsa_js.ts"; import { RawBinary } from "../binary.ts"; import { rsa_import_key } from "./import_key.ts"; +import { RSAKey } from "./rsa_key.ts"; type RSAPublicKeyFormat = [[string, null], [[bigint, bigint]]]; @@ -97,7 +96,7 @@ export class RSA { key: string | JSONWebKey, format: "auto" | "jwk" | "pem" = "auto", ): RSAKey { - return rsa_import_key(key, format); + return this.importKey(key, format); } /** @@ -110,6 +109,6 @@ export class RSA { key: string | JSONWebKey, format: "auto" | "jwk" | "pem" = "auto", ): RSAKey { - return rsa_import_key(key, format); + return new RSAKey(rsa_import_key(key, format)); } } diff --git a/src/rsa/rsa_js.ts b/src/rsa/rsa_js.ts index 1d06d0b..ee7730b 100644 --- a/src/rsa/rsa_js.ts +++ b/src/rsa/rsa_js.ts @@ -7,8 +7,9 @@ import { rsa_pkcs1_sign, } from "./rsa_internal.ts"; import { RawBinary } from "./../binary.ts"; -import { RSAKey, RSAOption, RSASignOption } from "./common.ts"; +import { RSAOption, RSASignOption } from "./common.ts"; import { createHash } from "../hash.ts"; +import { RSAKey } from "./rsa_key.ts"; export class PureRSA { static async encrypt(key: RSAKey, message: Uint8Array, options: RSAOption) { diff --git a/src/rsa/rsa_key.ts b/src/rsa/rsa_key.ts new file mode 100644 index 0000000..caf3b39 --- /dev/null +++ b/src/rsa/rsa_key.ts @@ -0,0 +1,55 @@ +import { RSAKeyParams, JSONWebKey } from "./common.ts"; +import { encode } from "./../../src/utility/encode.ts"; +import { + rsa_export_pkcs8_private, + rsa_export_pkcs8_public, +} from "./export_key.ts"; + +export class RSAKey { + public n: bigint; + public e?: bigint; + public d?: bigint; + public p?: bigint; + public q?: bigint; + public dp?: bigint; + public dq?: bigint; + public qi?: bigint; + public length: number; + + constructor(params: RSAKeyParams) { + this.n = params.n; + this.e = params.e; + this.d = params.d; + this.p = params.p; + this.q = params.q; + this.dp = params.dp; + this.dq = params.dq; + this.qi = params.qi; + this.length = params.length; + } + + public pkcs8(): string { + if (this.d) { + return rsa_export_pkcs8_private(this); + } else { + return rsa_export_pkcs8_public(this); + } + } + + public jwk(): JSONWebKey { + let jwk: JSONWebKey = { + kty: "RSA", + n: encode.bigint(this.n).base64url(), + }; + + if (this.d) jwk = { ...jwk, d: encode.bigint(this.d).base64url() }; + if (this.e) jwk = { ...jwk, e: encode.bigint(this.e).base64url() }; + if (this.p) jwk = { ...jwk, p: encode.bigint(this.p).base64url() }; + if (this.q) jwk = { ...jwk, q: encode.bigint(this.q).base64url() }; + if (this.dp) jwk = { ...jwk, dp: encode.bigint(this.dp).base64url() }; + if (this.dq) jwk = { ...jwk, dq: encode.bigint(this.dq).base64url() }; + if (this.qi) jwk = { ...jwk, qi: encode.bigint(this.qi).base64url() }; + + return jwk; + } +} diff --git a/src/rsa/rsa_wc.ts b/src/rsa/rsa_wc.ts index cb961b8..a8f46c8 100644 --- a/src/rsa/rsa_wc.ts +++ b/src/rsa/rsa_wc.ts @@ -1,4 +1,5 @@ -import { RSAKey, RSAOption } from "./common.ts"; +import { RSAOption } from "./common.ts"; +import { RSAKey } from "./rsa_key.ts"; function big_base64(m?: bigint) { if (m === undefined) return undefined; diff --git a/src/utility/encode.ts b/src/utility/encode.ts index 1198452..387bbb9 100644 --- a/src/utility/encode.ts +++ b/src/utility/encode.ts @@ -14,6 +14,17 @@ export class encode { return output; } + static bigint(n: bigint) { + const bytes = []; + while (n > 0) { + bytes.push(Number(n & 255n)); + n = n >> 8n; + } + + bytes.reverse(); + return new RawBinary(bytes); + } + static string(data: string) { return new RawBinary(new TextEncoder().encode(data)); } diff --git a/tests/rsa/rsa.export_key.test.ts b/tests/rsa/rsa.export_key.test.ts new file mode 100644 index 0000000..d832404 --- /dev/null +++ b/tests/rsa/rsa.export_key.test.ts @@ -0,0 +1,42 @@ +import { RSA } from "./../../mod.ts"; +import { + assertEquals, +} from "https://deno.land/std@0.63.0/testing/asserts.ts"; + +const privateKeyRaw = Deno.readTextFileSync("./tests/rsa/private.pem"); +const publicKeyRaw = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArlKJ591/fYCKhdflQSNi\n" + + "xBhWutUtW5y3l5vFzTxiKE4e9jykJ0Sr7U6GkwjmvplTV7Wgx4zhRr3tYrMqmQ+s\n" + + "/byRK3f2bb+zXF9+fnKGuP7Fp2oYprW3MKxKgNxjRzmx2x7LaV11dHFQv6oigeV2\n" + + "cyY5XB/GnEWUyHY7fCJIJIRdxuskt+77NAU0vrA/ntbWzFFsPP5xWJ8ns/ojTvwu\n" + + "+LT++fpBD3X1nTUR/LzlRgGxGqPHYRCHvY8B2FSPL8ukqfXI3LkvCM77zeR5lwPq\n" + + "IqDFVWcP6TNsOXccqDtBiA3+A6TS3nGmOu3NbZdefkzJlXq2D0xuW6ql0WqBM0Vu\n" + + "bwIDAQAB\n" + + "-----END PUBLIC KEY-----\n"; + +Deno.test("RSA - PKCS8 to PKCS8", async () => { + assertEquals(RSA.importKey(privateKeyRaw).pkcs8(), privateKeyRaw); + assertEquals(RSA.importKey(publicKeyRaw).pkcs8(), publicKeyRaw); +}); + +Deno.test("RSA - JWK to PKCS8", async () => { + const jwk = { + kty: "RSA", + n: "rlKJ591_fYCKhdflQSNixBhWutUtW5y3l5vFzTxiKE4e9jykJ0Sr7U6GkwjmvplTV7Wgx4zhRr3tYrMqmQ-s_byRK3f2bb-zXF9-fnKGuP7Fp2oYprW3MKxKgNxjRzmx2x7LaV11dHFQv6oigeV2cyY5XB_GnEWUyHY7fCJIJIRdxuskt-77NAU0vrA_ntbWzFFsPP5xWJ8ns_ojTvwu-LT--fpBD3X1nTUR_LzlRgGxGqPHYRCHvY8B2FSPL8ukqfXI3LkvCM77zeR5lwPqIqDFVWcP6TNsOXccqDtBiA3-A6TS3nGmOu3NbZdefkzJlXq2D0xuW6ql0WqBM0Vubw", + e: "AQAB", + }; + + assertEquals(RSA.importKey(jwk).pkcs8(), publicKeyRaw); +}); + +Deno.test("RSA - PKCS8 to JWK", async () => { + const jwk = { + kty: "RSA", + n: "rlKJ591_fYCKhdflQSNixBhWutUtW5y3l5vFzTxiKE4e9jykJ0Sr7U6GkwjmvplTV7Wgx4zhRr3tYrMqmQ-s_byRK3f2bb-zXF9-fnKGuP7Fp2oYprW3MKxKgNxjRzmx2x7LaV11dHFQv6oigeV2cyY5XB_GnEWUyHY7fCJIJIRdxuskt-77NAU0vrA_ntbWzFFsPP5xWJ8ns_ojTvwu-LT--fpBD3X1nTUR_LzlRgGxGqPHYRCHvY8B2FSPL8ukqfXI3LkvCM77zeR5lwPqIqDFVWcP6TNsOXccqDtBiA3-A6TS3nGmOu3NbZdefkzJlXq2D0xuW6ql0WqBM0Vubw", + e: "AQAB", + }; + + const actualJwk = RSA.importKey(publicKeyRaw).jwk(); + assertEquals(actualJwk.n, jwk.n); + assertEquals(actualJwk.e, jwk.e); +}); From ce1e5016656628de2546bdce766a80fb78cecd44 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 8 Sep 2020 17:51:26 +0700 Subject: [PATCH 2/3] Rename it to pkcs8 to PEM --- src/rsa/rsa_key.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rsa/rsa_key.ts b/src/rsa/rsa_key.ts index caf3b39..080d936 100644 --- a/src/rsa/rsa_key.ts +++ b/src/rsa/rsa_key.ts @@ -28,7 +28,7 @@ export class RSAKey { this.length = params.length; } - public pkcs8(): string { + public pem(): string { if (this.d) { return rsa_export_pkcs8_private(this); } else { From 993aa0bb33f42a07b7283dfcf2ae4a66415f819c Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 8 Sep 2020 17:52:10 +0700 Subject: [PATCH 3/3] Rename the method pem in test --- tests/rsa/rsa.export_key.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rsa/rsa.export_key.test.ts b/tests/rsa/rsa.export_key.test.ts index d832404..7f86b53 100644 --- a/tests/rsa/rsa.export_key.test.ts +++ b/tests/rsa/rsa.export_key.test.ts @@ -15,8 +15,8 @@ const publicKeyRaw = "-----BEGIN PUBLIC KEY-----\n" + "-----END PUBLIC KEY-----\n"; Deno.test("RSA - PKCS8 to PKCS8", async () => { - assertEquals(RSA.importKey(privateKeyRaw).pkcs8(), privateKeyRaw); - assertEquals(RSA.importKey(publicKeyRaw).pkcs8(), publicKeyRaw); + assertEquals(RSA.importKey(privateKeyRaw).pem(), privateKeyRaw); + assertEquals(RSA.importKey(publicKeyRaw).pem(), publicKeyRaw); }); Deno.test("RSA - JWK to PKCS8", async () => { @@ -26,7 +26,7 @@ Deno.test("RSA - JWK to PKCS8", async () => { e: "AQAB", }; - assertEquals(RSA.importKey(jwk).pkcs8(), publicKeyRaw); + assertEquals(RSA.importKey(jwk).pem(), publicKeyRaw); }); Deno.test("RSA - PKCS8 to JWK", async () => {