diff --git a/README.md b/README.md index 5558cfd..5f95f62 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Finds a private key through various user-(un)specified methods. Order of precede 3. `PRIVATE_KEY_PATH` environment variable or explicit `env.PRIVATE_KEY_PATH` option 4. Any file w/ `.pem` extension in current working dir +Supports both PKCS1 (i.e `-----BEGIN RSA PRIVATE KEY-----`) and PKCS8 (i.e `-----BEGIN PRIVATE KEY-----`). + ## Usage diff --git a/src/index.ts b/src/index.ts index 80aa419..6536103 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import { resolve } from "path"; import { existsSync, readdirSync, readFileSync } from "fs"; - import { VERSION } from "./version"; type Options = { @@ -13,8 +12,19 @@ type Options = { cwd?: string; }; -const begin = "-----BEGIN RSA PRIVATE KEY-----"; -const end = "-----END RSA PRIVATE KEY-----"; +const pkcs1Begin = "-----BEGIN RSA PRIVATE KEY-----"; +const pkcs1End = "-----END RSA PRIVATE KEY-----"; + +const pkcs8Begin = "-----BEGIN PRIVATE KEY-----"; +const pkcs8End = "-----END PRIVATE KEY-----"; + +function isPKCS1(privateKey: string): boolean { + return privateKey.includes(pkcs1Begin) && privateKey.includes(pkcs1End); +} + +function isPKCS8(privateKey: string): boolean { + return privateKey.includes(pkcs8Begin) && privateKey.includes(pkcs8End); +} export function getPrivateKey(options: Options = {}): string | null { const env = options.env || process.env; @@ -32,15 +42,31 @@ export function getPrivateKey(options: Options = {}): string | null { privateKey = Buffer.from(privateKey, "base64").toString(); } - if (privateKey.includes(begin) && privateKey.includes(end)) { - // newlines are escaped - if (privateKey.indexOf("\\n") !== -1) { - privateKey = privateKey.replace(/\\n/g, "\n"); + // newlines are potentially escaped + if (privateKey.indexOf("\\n") !== -1) { + privateKey = privateKey.replace(/\\n/g, "\n"); + } + + if (isPKCS1(privateKey)) { + // newlines are missing + if (privateKey.indexOf("\n") === -1) { + privateKey = addNewlines({ + privateKey, + begin: pkcs1Begin, + end: pkcs1End, + }); } + return privateKey; + } + if (isPKCS8(privateKey)) { // newlines are missing if (privateKey.indexOf("\n") === -1) { - privateKey = addNewlines(privateKey); + privateKey = addNewlines({ + privateKey, + begin: pkcs8Begin, + end: pkcs8End, + }); } return privateKey; } @@ -76,7 +102,15 @@ function isBase64(str: string): boolean { return Buffer.from(str, "base64").toString("base64") === str; } -function addNewlines(privateKey: string): string { +function addNewlines({ + privateKey, + begin, + end, +}: { + privateKey: string; + begin: string; + end: string; +}): string { const middleLength = privateKey.length - begin.length - end.length - 2; const middle = privateKey.substr(begin.length + 1, middleLength); return `${begin}\n${middle.trim().replace(/\s+/g, "\n")}\n${end}`; diff --git a/test/get-private-key.test.ts b/test/get-private-key.test.ts index d882acb..997e0be 100644 --- a/test/get-private-key.test.ts +++ b/test/get-private-key.test.ts @@ -9,18 +9,30 @@ const existsSync = fs.existsSync as jest.Mock; const readdirSync = fs.readdirSync as jest.Mock; const readFileSync = fs.readFileSync as jest.Mock; -const PRIVATE_KEY = +const PRIVATE_KEY_PKCS1 = "-----BEGIN RSA PRIVATE KEY-----\n7HjkPK\nKLm395\nAIBII\n-----END RSA PRIVATE KEY-----"; -const PRIVATE_KEY_NO_NEWLINES = +const PRIVATE_KEY_PKCS1_NO_NEWLINES = "-----BEGIN RSA PRIVATE KEY----- 7HjkPK KLm395 AIBII -----END RSA PRIVATE KEY-----"; -const PRIVATE_KEY_NO_NEWLINES_MULTIPLE_SPACES = +const PRIVATE_KEY_PKCS1_NO_NEWLINES_MULTIPLE_SPACES = "-----BEGIN RSA PRIVATE KEY----- 7HjkPK KLm395 AIBII -----END RSA PRIVATE KEY-----"; -const PRIVATE_KEY_ESCAPED_NEWLINES = +const PRIVATE_KEY_PKCS1_ESCAPED_NEWLINES = "-----BEGIN RSA PRIVATE KEY-----\\n7HjkPK\\nKLm395\\nAIBII\\n-----END RSA PRIVATE KEY-----"; +const PRIVATE_KEY_PKCS8 = + "-----BEGIN PRIVATE KEY-----\n7HjkPK\nKLm395\nAIBII\n-----END PRIVATE KEY-----"; + +const PRIVATE_KEY_PKCS8_NO_NEWLINES = + "-----BEGIN PRIVATE KEY----- 7HjkPK KLm395 AIBII -----END PRIVATE KEY-----"; + +const PRIVATE_KEY_PKCS8_NO_NEWLINES_MULTIPLE_SPACES = + "-----BEGIN PRIVATE KEY----- 7HjkPK KLm395 AIBII -----END PRIVATE KEY-----"; + +const PRIVATE_KEY_PKCS8_ESCAPED_NEWLINES = + "-----BEGIN PRIVATE KEY-----\\n7HjkPK\\nKLm395\\nAIBII\\n-----END PRIVATE KEY-----"; + describe("getPrivateKey", () => { beforeEach(() => { jest.resetAllMocks(); @@ -34,90 +46,267 @@ describe("getPrivateKey", () => { }); describe("no environment variables", () => { - it("returns null if called without arguments", () => { - const result = getPrivateKey(); - expect(result).toBeNull(); - }); + describe("PKCS1 format", () => { + const PRIVATE_KEY = PRIVATE_KEY_PKCS1; - it("{ filepath } option", () => { - const result = getPrivateKey({ filepath: "test.pem" }); - expect(readFileSync).toHaveBeenCalledTimes(1); - expect(readFileSync).toHaveBeenCalledWith( - resolve(process.cwd(), "test.pem"), - "utf-8" - ); - expect(result).toEqual("test.pem content"); - }); + it("returns null if called without arguments", () => { + const result = getPrivateKey(); + expect(result).toBeNull(); + }); - it("{ env: { PRIVATE_KEY } }", () => { - const result = getPrivateKey({ - env: { - PRIVATE_KEY, - }, + it("{ filepath } option", () => { + const result = getPrivateKey({ filepath: "test.pem" }); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); }); - expect(result).toEqual(PRIVATE_KEY); - }); - it("{ env: { PRIVATE_KEY_PATH } }", () => { - const result = getPrivateKey({ - env: { - PRIVATE_KEY_PATH: "test.pem", - }, + it("{ env: { PRIVATE_KEY } }", () => { + const result = getPrivateKey({ + env: { + PRIVATE_KEY, + }, + }); + expect(result).toEqual(PRIVATE_KEY); }); - expect(existsSync).toHaveBeenCalledTimes(1); - expect(existsSync).toHaveBeenCalledWith( - resolve(process.cwd(), "test.pem") - ); - expect(readFileSync).toHaveBeenCalledTimes(1); - expect(readFileSync).toHaveBeenCalledWith( - resolve(process.cwd(), "test.pem"), - "utf-8" - ); - expect(result).toEqual("test.pem content"); - }); + it("{ env: { PRIVATE_KEY_PATH } }", () => { + const result = getPrivateKey({ + env: { + PRIVATE_KEY_PATH: "test.pem", + }, + }); + expect(existsSync).toHaveBeenCalledTimes(1); + expect(existsSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem") + ); - it("{ filepath, env }", () => { - const result = getPrivateKey({ - filepath: "test.pem", - env: { PRIVATE_KEY }, + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); }); - expect(readFileSync).toHaveBeenCalledTimes(1); - expect(readFileSync).toHaveBeenCalledWith( - resolve(process.cwd(), "test.pem"), - "utf-8" - ); - expect(result).toEqual("test.pem content"); - }); - it("single test.pem file in current working directory", () => { - readdirSync.mockReturnValue(["test.pem"]); - const result = getPrivateKey(); - expect(readFileSync).toHaveBeenCalledTimes(1); - expect(readFileSync).toHaveBeenCalledWith( - resolve(process.cwd(), "test.pem"), - "utf-8" - ); - expect(result).toEqual("test.pem content"); - }); + it("{ filepath, env }", () => { + const result = getPrivateKey({ + filepath: "test.pem", + env: { PRIVATE_KEY }, + }); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); - it("two *.pem files in current working directory", () => { - readdirSync.mockReturnValue(["test1.pem", "test2.pem"]); - expect(() => getPrivateKey()).toThrow( - `[@probot/get-private-key] More than one file found: \"test1.pem, test2.pem\". Set { filepath } option or set one of the environment variables: PRIVATE_KEY, PRIVATE_KEY_PATH` - ); + it("single test.pem file in current working directory", () => { + readdirSync.mockReturnValue(["test.pem"]); + const result = getPrivateKey(); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("two *.pem files in current working directory", () => { + readdirSync.mockReturnValue(["test1.pem", "test2.pem"]); + expect(() => getPrivateKey()).toThrow( + `[@probot/get-private-key] More than one file found: \"test1.pem, test2.pem\". Set { filepath } option or set one of the environment variables: PRIVATE_KEY, PRIVATE_KEY_PATH` + ); + }); + + it("{ cwd }", () => { + const result = getPrivateKey({ + cwd: "/app/current", + }); + expect(result).toEqual(null); + expect(readdirSync).toHaveBeenCalledWith("/app/current"); + }); }); - it("{ cwd }", () => { - const result = getPrivateKey({ - cwd: "/app/current", + describe("PKCS8 format", () => { + const PRIVATE_KEY = PRIVATE_KEY_PKCS8; + + it("returns null if called without arguments", () => { + const result = getPrivateKey(); + expect(result).toBeNull(); + }); + + it("{ filepath } option", () => { + const result = getPrivateKey({ filepath: "test.pem" }); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("{ env: { PRIVATE_KEY } }", () => { + const result = getPrivateKey({ + env: { + PRIVATE_KEY, + }, + }); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("{ env: { PRIVATE_KEY_PATH } }", () => { + const result = getPrivateKey({ + env: { + PRIVATE_KEY_PATH: "test.pem", + }, + }); + expect(existsSync).toHaveBeenCalledTimes(1); + expect(existsSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem") + ); + + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("{ filepath, env }", () => { + const result = getPrivateKey({ + filepath: "test.pem", + env: { PRIVATE_KEY }, + }); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("single test.pem file in current working directory", () => { + readdirSync.mockReturnValue(["test.pem"]); + const result = getPrivateKey(); + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("two *.pem files in current working directory", () => { + readdirSync.mockReturnValue(["test1.pem", "test2.pem"]); + expect(() => getPrivateKey()).toThrow( + `[@probot/get-private-key] More than one file found: \"test1.pem, test2.pem\". Set { filepath } option or set one of the environment variables: PRIVATE_KEY, PRIVATE_KEY_PATH` + ); + }); + + it("{ cwd }", () => { + const result = getPrivateKey({ + cwd: "/app/current", + }); + expect(result).toEqual(null); + expect(readdirSync).toHaveBeenCalledWith("/app/current"); }); - expect(result).toEqual(null); - expect(readdirSync).toHaveBeenCalledWith("/app/current"); }); }); describe("with environment variables", () => { + describe("PKCS1 format", () => { + const PRIVATE_KEY = PRIVATE_KEY_PKCS1; + const PRIVATE_KEY_NO_NEWLINES = PRIVATE_KEY_PKCS1_NO_NEWLINES; + const PRIVATE_KEY_NO_NEWLINES_MULTIPLE_SPACES = + PRIVATE_KEY_PKCS1_NO_NEWLINES_MULTIPLE_SPACES; + const PRIVATE_KEY_ESCAPED_NEWLINES = PRIVATE_KEY_PKCS1_ESCAPED_NEWLINES; + + it("PRIVATE_KEY", () => { + process.env.PRIVATE_KEY = PRIVATE_KEY; + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("PRIVATE_KEY is base64 encoded", () => { + process.env.PRIVATE_KEY = Buffer.from(PRIVATE_KEY, "utf-8").toString( + "base64" + ); + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("PRIVATE_KEY contains no newlines", () => { + process.env.PRIVATE_KEY = PRIVATE_KEY_NO_NEWLINES; + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("PRIVATE_KEY contains consecutive spaces", () => { + process.env.PRIVATE_KEY = PRIVATE_KEY_NO_NEWLINES_MULTIPLE_SPACES; + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("PRIVATE_KEY contains escaped newlines", () => { + process.env.PRIVATE_KEY = PRIVATE_KEY_ESCAPED_NEWLINES; + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + + it("PRIVATE_KEY invalid", () => { + process.env.PRIVATE_KEY = "invalid"; + expect(() => getPrivateKey()).toThrow( + `[@probot/get-private-key] The contents of "env.PRIVATE_KEY" could not be validated. Please check to ensure you have copied the contents of the .pem file correctly.` + ); + }); + + it("PRIVATE_KEY_PATH", () => { + process.env.PRIVATE_KEY_PATH = "test.pem"; + const result = getPrivateKey(); + expect(existsSync).toHaveBeenCalledTimes(1); + expect(existsSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem") + ); + + expect(readFileSync).toHaveBeenCalledTimes(1); + expect(readFileSync).toHaveBeenCalledWith( + resolve(process.cwd(), "test.pem"), + "utf-8" + ); + expect(result).toEqual("test.pem content"); + }); + + it("PRIVATE_KEY_PATH does not exist", () => { + process.env.PRIVATE_KEY_PATH = "test.pem"; + existsSync.mockReturnValue(false); + expect(() => getPrivateKey()).toThrow( + `[@probot/get-private-key] Private key does not exists at path: "test.pem". Please check to ensure that "env.PRIVATE_KEY_PATH" is correct.` + ); + }); + + it("both PRIVATE_KEY and PRIVATE_KEY_PATH", () => { + process.env.PRIVATE_KEY = PRIVATE_KEY; + process.env.PRIVATE_KEY_PATH = "test.pem"; + + const result = getPrivateKey(); + expect(result).toEqual(PRIVATE_KEY); + }); + }); + }); + + describe("PKCS8 format", () => { + const PRIVATE_KEY = PRIVATE_KEY_PKCS8; + const PRIVATE_KEY_NO_NEWLINES = PRIVATE_KEY_PKCS8_NO_NEWLINES; + const PRIVATE_KEY_NO_NEWLINES_MULTIPLE_SPACES = + PRIVATE_KEY_PKCS8_NO_NEWLINES_MULTIPLE_SPACES; + const PRIVATE_KEY_ESCAPED_NEWLINES = PRIVATE_KEY_PKCS8_ESCAPED_NEWLINES; + it("PRIVATE_KEY", () => { process.env.PRIVATE_KEY = PRIVATE_KEY; const result = getPrivateKey();