typage icon indicating copy to clipboard operation
typage copied to clipboard

Version issue with age-encrypted sops files

Open humphd opened this issue 1 year ago • 4 comments

Thank you for making this. I couldn't believe it when I went looking for a TS age implementation, and lo and behold, you had made an official one. Amazing!

My current use case is being able to decrypt pieces of an age-encrypted sops file in JS. We

Here's an example of the kind of thing I want to parse, where I need to decrypt the value key, and my AGE public key is listed as a recipient:

value: ENC[AES256_GCM,data:asgm,iv:535n5Dj8DJ+XY5KuAYK2nGPKpA2H5Er7eLNPChQxEWg=,tag:TEohl6v4sHcrQK8IAx8p8w==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age:
        - recipient: age19j4d6v9j7rx5fs629fu387qz4zmlpsqjexa4s08tkfrrmfdl5cwqjlaupd
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqTXJvbVVsSzVMM1RIcUpY
            RTBPZTdwN3RZUGJDV0p0eDFCaTJzZm1YU0RFCk1YbFg3djFBR3RjQmduaTBBUlFy
            WFR2S2JacC8xUnh4Y29GMk8wK3NGREUKLS0tIFFvRGlHNmt1RGtVVEZ3eUpWbk96
            a1lpeVFqVDlZaHRFV1c5V0pMbXI4RkkKrLaOy3LVv9Uap07S8xQi+CJr9i2tcbZR
            VAgOMocpDRQU6AsiU+suZQ0X+Zz9Obb1oRTez84FSBwoOojYBbjLxA==
            -----END AGE ENCRYPTED FILE-----
    lastmodified: "2024-03-15T13:31:49Z"
    mac: ENC[AES256_GCM,data:/qN53oo7iWrZRLm8OopnkK/4IpOYTb+wo2PA3EHQIzuWxnpigxdbnCA8bWdB4v79Xaeu8AJUZOa5kzWh8+we9mPI8M2zj6rwYnbuqrYnF/wzGmEW6PQylw2LN8WNKnlNRWf/a3XP1/v2LXyQCkdtmadejEgG3BjH5gmKLWskl2E=,iv:g+Ppyo9LxFgoCOZTCnesdwwE91d2j8isg91KfXPFntM=,tag:I4b0IyqjxbTJ6+yrCHvtjw==,type:str]
    pgp: []
    unencrypted_suffix: _unencrypted
    version: 3.8.1

Here's my first attempt to get that decryption key:

import age from "age-encryption";
import { readFile } from "node:fs/promises";
import yaml from "js-yaml";

async function getPublicAgeKey(privateAgeKey: string) {
    const { identityToRecipient } = await age();
    return identityToRecipient(privateAgeKey);
}

async function decryptSopsFile(sopsFile: string, privateAgeKey: string) {
    const { Decrypter } = await age();
    const doc: any = yaml.load(await readFile(sopsFile, "utf8"));
    const ageConfig = doc.sops.age;

    const pubKey = await getPublicAgeKey(privateAgeKey);
    const config = ageConfig.find((config: any) => config.recipient === pubKey);

    const decrypter = new Decrypter();
    decrypter.addIdentity(privateAgeKey);
    const decryptionKey = decrypter.decrypt(config.enc.trim(), "text");

    // TODO ...
}

async function main() {
    await decryptSopsFile(process.env.SOPS_FILE, process.env.AGE_KEY);
}

main();

When I run this, I get the following error:

/workspaces/DeepStructure/node_modules/.pnpm/[email protected]/node_modules/age-encryption/dist/format.js:91
        throw Error("invalid version " + versionLine);
              ^


Error: invalid version null
    at parseHeader (/workspaces/DeepStructure/node_modules/.pnpm/[email protected]/node_modules/age-encryption/dist/format.js:91:15)
    at Decrypter.decrypt (/workspaces/DeepStructure/node_modules/.pnpm/[email protected]/node_modules/age-encryption/dist/index.js:115:19)
    at decryptSopsFile (/workspaces/DeepStructure/misc/sops/sops-js/src/sops.ts:20:37)
    at main (/workspaces/DeepStructure/misc/sops/sops-js/src/index.ts:32:5)

Which seems to be https://github.com/FiloSottile/typage/blob/d0744544906d115825c358698a58ba259bc83f23/lib/format.ts#L109

On my system I'm using:

$ age --version
v1.1.1

Do I need to pass more info in order to be able to do this? Use a different version somehow? Or maybe it's not possible?

Thanks for helping me understand what is and isn't possible.

humphd avatar Mar 15 '24 13:03 humphd

I played with this some more and was able to get it. I needed to extract the base64 encoded key:

function getEncryptionKeyForRecipient(
    sopsFile: string,
    privateAgeKey: string
) {
    const { Decrypter } = await age();
    const doc = await loadSopsFile(sopsFile);
    if (!Array.isArray(doc?.sops?.age)) {
        throw new Error("missing sops age metadata");
    }

    const sopsAgeConfig = doc.sops.age;
    const pubKey = await getPublicAgeKey(privateAgeKey);
    const { enc } = sopsAgeConfig.find(
        (config: SopsAgeConfig) => config.recipient === pubKey
    );
    if (!enc) {
        throw new Error("no matching recipient found in age config");
    }

    const decrypter = new Decrypter();
    decrypter.addIdentity(privateAgeKey);

    const regex =
        /-----BEGIN AGE ENCRYPTED FILE-----\s*([\s\S]*?)\s*-----END AGE ENCRYPTED FILE-----/;
    const matches = enc.match(regex);

    if (!(matches && matches[1])) {
        throw new Error("unable to extract age encryption key");
    }

    const base64String = matches[1].trim();
    const encrypted = Buffer.from(base64String, "base64");
    const decryptionKey = decrypter.decrypt(encrypted, "uint8array");

    return decryptionKey;
}

I'm surprised that I couldn't use the whole -----BEGIN AGE ENCRYPTED FILE-----... block, but perhaps that's just my own ignorance showing.

humphd avatar Mar 17 '24 14:03 humphd

I ended up making an npm package to work with sops and age in TS/JS: https://github.com/humphd/sops-age

Thanks for making this!

humphd avatar Mar 27 '24 01:03 humphd

Hello! Sorry for the late response but I was apparently not "Watching" this repository. No idea how that happened.

It's not you, the armored encoding (PEM with AGE ENCRYPTED FILE type) is not supported, which is something that 1) I should have made clearer, 2) I should fix, and 3) I should have thought had a good chance of coming up in a TS setting.

Thank you for working on this and making sops-age.

FiloSottile avatar Jul 20 '24 17:07 FiloSottile

@FiloSottile I hate how GitHub does this, so you end up missing notifications for your own repos (happens to me a lot). Thanks for the encouragement!

humphd avatar Jul 22 '24 12:07 humphd