Skip to content

Commit

Permalink
Merge pull request #9 from invisal/rsa-export-key
Browse files Browse the repository at this point in the history
RSAKey can export to different key format
  • Loading branch information
invisal authored Sep 8, 2020
2 parents 83468d9 + 993aa0b commit 4b41e18
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 16 deletions.
11 changes: 11 additions & 0 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/rsa/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface RSAKey {
export interface RSAKeyParams {
n: bigint;
e?: bigint;
d?: bigint;
Expand Down
121 changes: 121 additions & 0 deletions src/rsa/export_key.ts
Original file line number Diff line number Diff line change
@@ -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";
}
16 changes: 8 additions & 8 deletions src/rsa/import_key.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");

Expand All @@ -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)),
Expand All @@ -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)),
Expand All @@ -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)),
Expand All @@ -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],
Expand All @@ -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);
Expand Down
9 changes: 4 additions & 5 deletions src/rsa/mod.ts
Original file line number Diff line number Diff line change
@@ -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]]];

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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));
}
}
3 changes: 2 additions & 1 deletion src/rsa/rsa_js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
55 changes: 55 additions & 0 deletions src/rsa/rsa_key.ts
Original file line number Diff line number Diff line change
@@ -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 pem(): 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;
}
}
3 changes: 2 additions & 1 deletion src/rsa/rsa_wc.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/utility/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
42 changes: 42 additions & 0 deletions tests/rsa/rsa.export_key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RSA } from "./../../mod.ts";
import {
assertEquals,
} from "https://deno.land/[email protected]/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).pem(), privateKeyRaw);
assertEquals(RSA.importKey(publicKeyRaw).pem(), 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).pem(), 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);
});

0 comments on commit 4b41e18

Please sign in to comment.