From 1fad36d62f825dbc56964cbdac60918011cce9e6 Mon Sep 17 00:00:00 2001 From: David Humphrey Date: Tue, 25 Feb 2025 08:42:40 -0500 Subject: [PATCH 1/2] Support bare SOPS objects in decryptSops --- README.md | 32 ++++++++++++++++++++++++++++++++ package.json | 2 +- src/index.ts | 6 ++++++ src/sops-file.ts | 22 +++++++++++++++++++++- tests/unit/index.test.ts | 11 +++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 445ed22..6703ab4 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,38 @@ const decrypted = await decryptSops({ }); ``` +### `decryptSops(sopsObject, options?)` + +Decrypts SOPS-encrypted content directly from a pre-parsed SOPS object. + +```js +const sopsObject = { + secret: + "ENC[AES256_GCM,data:trrpgezXug4Dq9T/inwkMA==,iv:glPwxoY2UuHO91vlJRaqYtFkPY1VsWvkJtfkEKZJdns=,tag:v7DbOYl7C5gdQRdW6BVoLw==,type:str]", + sops: { + kms: null, + gcp_kms: null, + azure_kv: null, + hc_vault: null, + age: [ + { + recipient: + "age1je6kjhzuhdjy3fqptpttxjh5k8q46vygzlgtpuq3030c947pc5tqz9dqvr", + enc: "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsYVh6WTdzNnArL1E1RVVw\nZEoyU2hqZVZuVjR3bFJ2dlZaOGQ4VUVla1hVCll3MDZrY1VZeWtPNzVPUGNFWjBK\ncDZLVFZheU4wK0hBOTNmRFcwUkE4OFkKLS0tIHl3Y0x5Sk9lVDRCQkR3QzNzRWY4\ncFNPWFZqdEtyUmFhL3pLOHJvNlhuTTAKNLKVIFujPtmpYo/Oycit0JbcfPVN2TN5\nG9emUjK1XVOwkNda0olhEt4KAjSBV7dYt8luOL8VQeR33PadX7RK3A==\n-----END AGE ENCRYPTED FILE-----\n", + }, + ], + lastmodified: "2024-12-25T09:03:17Z", + mac: "ENC[AES256_GCM,data:pjgkspXlmS1uFtR5yRZXcXISNEOk2/L4lN1zJoo49kgbABun2EwpZ2wfhPUDEDKn7kfmKuSOl4xQ/adUHVJh6bOHyQTf9lfH2BekCy828BIODowzk2tR5uiin8bB5q5VQfNJIaYEn3EWpGaOupEaNTZOyi5ML+WXB8s6w53Wg0w=,iv:wKEh9xSZ5RtsNdlRcEpYnbLKmUV6yitneJQhVt7qBSM=,tag:CmfHyYRe2/GVEexW5A8OWg==,type:str]", + pgp: null, + unencrypted_suffix: "_unencrypted", + version: "3.9.2", + }, +}; + +// Assumes SOPS_AGE_KEY is set in the env +const decrypted = await decryptSops(sopsObject); +``` + ### Options The `decryptSops` function accepts the following options: diff --git a/package.json b/package.json index 2f41b0f..61c932a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sops-age", - "version": "3.2.0", + "version": "3.3.0", "description": "sops age decryption for JavaScript", "repository": "humphd/sops-age", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 27a93aa..83baec5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,12 @@ export async function decryptSops( return decrypt(sopsData, decryptOptions); } + // Case 3. An bare input object with no options (AGE key must be set in the env) + if (inputOrOptions && isSopsInput(inputOrOptions)) { + const sopsData = await parseSops(inputOrOptions); + return decrypt(sopsData, {}); + } + throw new Error( "Invalid options: when no input given, you must specify one of `path` or `url`", ); diff --git a/src/sops-file.ts b/src/sops-file.ts index c7b36d6..bfe6e9a 100644 --- a/src/sops-file.ts +++ b/src/sops-file.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { loadFromFile } from "./runtime.js"; export type SopsInput = + | object | string | File | Blob @@ -31,7 +32,22 @@ export function isSopsInput(value: unknown): value is SopsInput { return true; } - // Check for object types + // Check for an already parsed, sops-like object + if ( + typeof value === "object" && + value !== null && + "sops" in value && + typeof (value as any).sops === "object" && + (value as any).sops !== null && + Array.isArray((value as any).sops.age) && + typeof (value as any).sops.mac === "string" && + typeof (value as any).sops.lastmodified === "string" && + typeof (value as any).sops.version === "string" + ) { + return true; + } + + // Look for other types we can handle if (typeof value === "object") { return ( value instanceof File || @@ -114,6 +130,10 @@ async function inputToString(input: SopsInput): Promise { return new TextDecoder().decode(concatenated); } + if (typeof input === "object") { + return JSON.stringify(input); + } + throw new Error(`Unsupported input type: ${typeof input}`); } diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 9d5b4cc..1ca0e35 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -15,6 +15,17 @@ const AGE_SECRET_KEY = "AGE-SECRET-KEY-1QXRVEJH9S4NQU0FPD6V79ESZQ8S6PXH3L8V40EVPTHFH6KNKD4DQ7SKC4P"; describe("decryptSops()", () => { + test.only("with valid object input", async () => { + const value = await decryptSops(test_secret_enc_json); + expect(value).toEqual(test_secret_json); + }); + + test("with invalid object input", async () => { + await expect( + decryptSops({ missing: "sops", invalid: true }), + ).rejects.toThrow(); + }); + test("with direct input", async () => { const value = await decryptSops(JSON.stringify(test_secret_enc_json), { secretKey: AGE_SECRET_KEY, From b693ed615309905e92c37cf48805e71ae6e0d0c7 Mon Sep 17 00:00:00 2001 From: David Humphrey Date: Tue, 25 Feb 2025 08:45:55 -0500 Subject: [PATCH 2/2] Remove .only --- tests/unit/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 1ca0e35..9b4bf05 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -15,7 +15,7 @@ const AGE_SECRET_KEY = "AGE-SECRET-KEY-1QXRVEJH9S4NQU0FPD6V79ESZQ8S6PXH3L8V40EVPTHFH6KNKD4DQ7SKC4P"; describe("decryptSops()", () => { - test.only("with valid object input", async () => { + test("with valid object input", async () => { const value = await decryptSops(test_secret_enc_json); expect(value).toEqual(test_secret_json); });