@mp-lb/doctrine-secrets 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Doctrine Secrets
2
+
3
+ Decrypt Doctrine encrypted text files from CI or other Node.js runtimes.
4
+
5
+ ## CLI
6
+
7
+ Store the external recipient secret in your CI secret manager:
8
+
9
+ ```sh
10
+ doctrine-secrets decrypt secrets.enc.json > secrets.json
11
+ ```
12
+
13
+ The CLI reads the recipient secret from `DOCTRINE_RECIPIENT_SECRET` by default.
14
+
15
+ ```sh
16
+ DOCTRINE_RECIPIENT_SECRET='doctrine-recipient-v1_...' doctrine-secrets decrypt secrets.enc.json
17
+ ```
18
+
19
+ ## Library
20
+
21
+ ```ts
22
+ import { decryptDoctrineEncryptedFileText } from "@mp-lb/doctrine-secrets";
23
+
24
+ const plaintext = await decryptDoctrineEncryptedFileText({
25
+ encryptedText,
26
+ recipientSecret: process.env.DOCTRINE_RECIPIENT_SECRET!,
27
+ });
28
+ ```
@@ -0,0 +1 @@
1
+ export declare const base64ToBytes: (value: string) => Uint8Array;
@@ -0,0 +1 @@
1
+ export const base64ToBytes = (value) => new Uint8Array(Buffer.from(value, "base64"));
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./runCli.js";
3
+ runCli(process.argv.slice(2)).catch((error) => {
4
+ const message = error instanceof Error ? error.message : String(error);
5
+ console.error(message);
6
+ process.exitCode = 1;
7
+ });
@@ -0,0 +1,3 @@
1
+ export declare const doctrineRecipientSecretPrefix = "doctrine-recipient-v1_";
2
+ export declare const encryptedFileCipher = "AES-GCM";
3
+ export declare const encryptedFileVersion = 2;
@@ -0,0 +1,3 @@
1
+ export const doctrineRecipientSecretPrefix = "doctrine-recipient-v1_";
2
+ export const encryptedFileCipher = "AES-GCM";
3
+ export const encryptedFileVersion = 2;
@@ -0,0 +1,3 @@
1
+ import type { webcrypto } from "node:crypto";
2
+ export type AesCryptoKey = webcrypto.CryptoKey;
3
+ export type AesKeyUsage = webcrypto.KeyUsage;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { AesCryptoKey } from "./cryptoTypes";
2
+ export declare const decryptBytes: ({ data, iv, key, }: {
3
+ data: string;
4
+ iv: string;
5
+ key: AesCryptoKey;
6
+ }) => Promise<Uint8Array>;
@@ -0,0 +1,3 @@
1
+ import { encryptedFileCipher } from "./constants.js";
2
+ import { base64ToBytes } from "./base64ToBytes.js";
3
+ export const decryptBytes = async ({ data, iv, key, }) => new Uint8Array(await crypto.subtle.decrypt({ iv: base64ToBytes(iv), name: encryptedFileCipher }, key, base64ToBytes(data)));
@@ -0,0 +1,2 @@
1
+ import type { DecryptDoctrineEncryptedFileInput } from "./types";
2
+ export declare const decryptDoctrineEncryptedFile: ({ encryptedText, recipientSecret, }: DecryptDoctrineEncryptedFileInput) => Promise<Uint8Array>;
@@ -0,0 +1,19 @@
1
+ import { decryptBytes } from "./decryptBytes.js";
2
+ import { importAesKey } from "./importAesKey.js";
3
+ import { importRecipientSecretKey } from "./importRecipientSecretKey.js";
4
+ import { parseDoctrineEncryptedFilePayload } from "./parseDoctrineEncryptedFilePayload.js";
5
+ import { unwrapFileKeyBytes } from "./unwrapFileKeyBytes.js";
6
+ export const decryptDoctrineEncryptedFile = async ({ encryptedText, recipientSecret, }) => {
7
+ const payload = parseDoctrineEncryptedFilePayload(encryptedText);
8
+ const recipientKey = await importRecipientSecretKey(recipientSecret);
9
+ const fileKeyBytes = await unwrapFileKeyBytes({
10
+ recipientKey,
11
+ recipients: payload.recipients,
12
+ });
13
+ const fileKey = await importAesKey(fileKeyBytes, ["decrypt"]);
14
+ return decryptBytes({
15
+ data: payload.data,
16
+ iv: payload.iv,
17
+ key: fileKey,
18
+ });
19
+ };
@@ -0,0 +1,2 @@
1
+ import type { DecryptDoctrineEncryptedFileInput } from "./types";
2
+ export declare const decryptDoctrineEncryptedFileText: (input: DecryptDoctrineEncryptedFileInput) => Promise<string>;
@@ -0,0 +1,3 @@
1
+ import { decryptDoctrineEncryptedFile } from "./decryptDoctrineEncryptedFile.js";
2
+ const textDecoder = new TextDecoder();
3
+ export const decryptDoctrineEncryptedFileText = async (input) => textDecoder.decode(await decryptDoctrineEncryptedFile(input));
@@ -0,0 +1,2 @@
1
+ import type { AesCryptoKey, AesKeyUsage } from "./cryptoTypes";
2
+ export declare const importAesKey: (bytes: Uint8Array, usages: AesKeyUsage[]) => Promise<AesCryptoKey>;
@@ -0,0 +1,2 @@
1
+ import { encryptedFileCipher } from "./constants.js";
2
+ export const importAesKey = async (bytes, usages) => crypto.subtle.importKey("raw", bytes, { length: 256, name: encryptedFileCipher }, false, usages);
@@ -0,0 +1,2 @@
1
+ import type { AesCryptoKey } from "./cryptoTypes";
2
+ export declare const importRecipientSecretKey: (recipientSecret: string) => Promise<AesCryptoKey>;
@@ -0,0 +1,9 @@
1
+ import { doctrineRecipientSecretPrefix } from "./constants.js";
2
+ import { base64ToBytes } from "./base64ToBytes.js";
3
+ import { importAesKey } from "./importAesKey.js";
4
+ export const importRecipientSecretKey = async (recipientSecret) => {
5
+ if (!recipientSecret.startsWith(doctrineRecipientSecretPrefix)) {
6
+ throw new Error("Unsupported Doctrine recipient secret.");
7
+ }
8
+ return importAesKey(base64ToBytes(recipientSecret.slice(doctrineRecipientSecretPrefix.length)), ["decrypt"]);
9
+ };
@@ -0,0 +1,5 @@
1
+ export { doctrineRecipientSecretPrefix, encryptedFileCipher, encryptedFileVersion, } from "./constants";
2
+ export { decryptDoctrineEncryptedFile } from "./decryptDoctrineEncryptedFile";
3
+ export { decryptDoctrineEncryptedFileText } from "./decryptDoctrineEncryptedFileText";
4
+ export { parseDoctrineEncryptedFilePayload } from "./parseDoctrineEncryptedFilePayload";
5
+ export type { DecryptDoctrineEncryptedFileInput, DoctrineEncryptedFilePayload, DoctrineEncryptedRecipient, DoctrineEncryptedValue, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { doctrineRecipientSecretPrefix, encryptedFileCipher, encryptedFileVersion, } from "./constants.js";
2
+ export { decryptDoctrineEncryptedFile } from "./decryptDoctrineEncryptedFile.js";
3
+ export { decryptDoctrineEncryptedFileText } from "./decryptDoctrineEncryptedFileText.js";
4
+ export { parseDoctrineEncryptedFilePayload } from "./parseDoctrineEncryptedFilePayload.js";
@@ -0,0 +1,2 @@
1
+ import type { DoctrineEncryptedFilePayload } from "./types";
2
+ export declare const parseDoctrineEncryptedFilePayload: (encryptedText: string) => DoctrineEncryptedFilePayload;
@@ -0,0 +1,12 @@
1
+ import { encryptedFileCipher, encryptedFileVersion } from "./constants.js";
2
+ export const parseDoctrineEncryptedFilePayload = (encryptedText) => {
3
+ const parsed = JSON.parse(encryptedText);
4
+ if (parsed.doctrineEncryptedFile !== encryptedFileVersion ||
5
+ parsed.cipher !== encryptedFileCipher ||
6
+ typeof parsed.data !== "string" ||
7
+ typeof parsed.iv !== "string" ||
8
+ !Array.isArray(parsed.recipients)) {
9
+ throw new Error("Unsupported Doctrine encrypted file format.");
10
+ }
11
+ return parsed;
12
+ };
@@ -0,0 +1 @@
1
+ export declare const readEncryptedFile: (filePath: string) => Promise<string>;
@@ -0,0 +1,2 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export const readEncryptedFile = async (filePath) => readFile(filePath, "utf8");
@@ -0,0 +1 @@
1
+ export declare const readRecipientSecretFromEnv: () => string;
@@ -0,0 +1,7 @@
1
+ export const readRecipientSecretFromEnv = () => {
2
+ const recipientSecret = process.env.DOCTRINE_RECIPIENT_SECRET;
3
+ if (!recipientSecret) {
4
+ throw new Error("DOCTRINE_RECIPIENT_SECRET is required.");
5
+ }
6
+ return recipientSecret;
7
+ };
@@ -0,0 +1 @@
1
+ export declare const runCli: (args: string[]) => Promise<void>;
package/dist/runCli.js ADDED
@@ -0,0 +1,19 @@
1
+ import { decryptDoctrineEncryptedFileText } from "./decryptDoctrineEncryptedFileText.js";
2
+ import { readEncryptedFile } from "./readEncryptedFile.js";
3
+ import { readRecipientSecretFromEnv } from "./readRecipientSecretFromEnv.js";
4
+ import { usage } from "./usage.js";
5
+ export const runCli = async (args) => {
6
+ const [command, filePath] = args;
7
+ if (command === "--help" || command === "-h") {
8
+ process.stdout.write(usage);
9
+ return;
10
+ }
11
+ if (command !== "decrypt" || !filePath) {
12
+ throw new Error(usage);
13
+ }
14
+ const plaintext = await decryptDoctrineEncryptedFileText({
15
+ encryptedText: await readEncryptedFile(filePath),
16
+ recipientSecret: readRecipientSecretFromEnv(),
17
+ });
18
+ process.stdout.write(plaintext);
19
+ };
@@ -0,0 +1,22 @@
1
+ import type { encryptedFileCipher, encryptedFileVersion } from "./constants";
2
+ export type DoctrineEncryptedValue = {
3
+ cipher: typeof encryptedFileCipher;
4
+ data: string;
5
+ iv: string;
6
+ };
7
+ export type DoctrineEncryptedRecipient = {
8
+ label?: string;
9
+ recipientId: string;
10
+ wrappedDataKey: DoctrineEncryptedValue;
11
+ };
12
+ export type DoctrineEncryptedFilePayload = {
13
+ cipher: typeof encryptedFileCipher;
14
+ data: string;
15
+ doctrineEncryptedFile: typeof encryptedFileVersion;
16
+ iv: string;
17
+ recipients: DoctrineEncryptedRecipient[];
18
+ };
19
+ export type DecryptDoctrineEncryptedFileInput = {
20
+ encryptedText: string;
21
+ recipientSecret: string;
22
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { DoctrineEncryptedRecipient } from "./types";
2
+ import type { AesCryptoKey } from "./cryptoTypes";
3
+ export declare const unwrapFileKeyBytes: ({ recipientKey, recipients, }: {
4
+ recipientKey: AesCryptoKey;
5
+ recipients: DoctrineEncryptedRecipient[];
6
+ }) => Promise<Uint8Array>;
@@ -0,0 +1,16 @@
1
+ import { decryptBytes } from "./decryptBytes.js";
2
+ export const unwrapFileKeyBytes = async ({ recipientKey, recipients, }) => {
3
+ for (const recipient of recipients) {
4
+ try {
5
+ return await decryptBytes({
6
+ data: recipient.wrappedDataKey.data,
7
+ iv: recipient.wrappedDataKey.iv,
8
+ key: recipientKey,
9
+ });
10
+ }
11
+ catch {
12
+ // Only one recipient should match a given external recipient secret.
13
+ }
14
+ }
15
+ throw new Error("No recipient matched the Doctrine recipient secret.");
16
+ };
@@ -0,0 +1 @@
1
+ export declare const usage = "Doctrine Secrets\n\nUsage:\n doctrine-secrets decrypt <secrets.enc.json>\n\nEnvironment:\n DOCTRINE_RECIPIENT_SECRET External recipient secret for the encrypted file\n";
package/dist/usage.js ADDED
@@ -0,0 +1,8 @@
1
+ export const usage = `Doctrine Secrets
2
+
3
+ Usage:
4
+ doctrine-secrets decrypt <secrets.enc.json>
5
+
6
+ Environment:
7
+ DOCTRINE_RECIPIENT_SECRET External recipient secret for the encrypted file
8
+ `;
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@mp-lb/doctrine-secrets",
3
+ "version": "0.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/mp-lb/hyperstore.git",
17
+ "directory": "packages/doctrine-secrets"
18
+ },
19
+ "bin": {
20
+ "doctrine-secrets": "dist/cli.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "package.json"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "scripts": {
34
+ "build": "rm -rf dist && tsc --project tsconfig.build.json && node scripts/fix-esm-imports.mjs && chmod +x dist/cli.js",
35
+ "dev": "tsc --watch --project tsconfig.json",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run"
38
+ }
39
+ }