Search code examples
node.jsencryptiongoogle-cloud-platformgoogle-cloud-kms

Node Google Cloud KMS encryption seems to work but decryption fails


This is my first ever Stack Overflow question!

Anyway, I'm trying to set up decryption of DB connect secrets using Cloud KMS for a Node API I have running in App Engine. In order to get this working I've been testing it locally. I used the gcloud CLI to encrypt the secrets then uploaded them to a Cloud Storage bucket (under a different project than the API if that matters). Pulling down the encrypted secrets in the API went fine but when I tried to decrypt these secrets I've been getting:

Error: 3 INVALID_ARGUMENT: Decryption failed: verify that 'name' refers to the correct CryptoKey.

I checked and rechecked that I had the correct project ID, keyring ID, key ID.

I tried encoding the encrypted secrets in base64 before uploading to the storage bucket. I tried hardcoding the encoded-and-encrypted secrets in the API. Neither of these worked.

So just for a sanity check I rewrote the code to simply encrypt a string then decrypt it using the same cryptoKeyPath for both right in the API. The encryption seems to work but I'm still getting the above error during decryption.

(Some Cloud Storage code is still there but isn't being used until decryption gets figured out).

const Storage = require('@google-cloud/storage');
console.log(process.env.GOOGLE_APPLICATION_CREDENTIALS);

// if running in production we need to get the .env file from a storage bucket and decrypt.
const addSecretsToEnv = async () => {
    // setup for storage bucket
    const bucketName=<bucketName>;
    const fileName=<fileName>;
    const storage = new Storage.Storage();
    const file = storage.bucket(bucketName).file(fileName);

    // setup for KMS
    const client = new kms.KeyManagementServiceClient();
    const locationId = 'global';
    const projectId = <projectId>;
    const keyRingID = <keyRingID>;
    const keyID = <keyID>;

    try {
        const formattedName = client.cryptoKeyPath(
            projectId,
            locationId,
            keyRingID,
            keyID,
        );

        const [result] = await client.encrypt({
            name: formattedName,
            plainText: 'help me!!!'
        });

        console.log(typeof result);
        console.log(result);

        const cipherText = result.ciphertext;
        console.log(typeof cipherText);
        console.log(cipherText);

        const [decrypted] = await client.decrypt({
            name: formattedName,  
            cipherText,
        });

        console.log(decrypted);

    } catch(error) {
        console.log(error);
    }
}

module.exports = {
    addSecretsToEnv
};

I have authentication set up through the GOOGLE_APPLICATION_CREDENTIALS env variable which is pointing to a JSON key file for a service account that has both the Cloud KMS CryptoKey Encrypter/Decrypter AND Cloud KMS Admin roles (added the admin role in desperation).

Can anybody help me out here?

Thanks in advance.


Solution

  • The capital T is your culprit. In Node, keys without values get expanded to their object name. For example, given:

    let foo = "banana";
    

    Passing foo into an object like this:

    doTheThing({ foo });
    

    expands into this:

    doTheThing({ foo: foo }); // which is { foo: "banana" }
    

    When you use plainText or cipherText, they are expanded to {plainText: "..."} and {cipherText: "..."} respectively in the Node object. Unfortunately those aren't recognized fields, but they are silently ignored. Thus, effectively, you aren't passing in any plaintext or ciphertext to either API call.

    Encrypting the empty string is valid, but decrypting an empty string is not. That is why you are not getting an error on encrypt.

    To fix this, replace plainText and cipherText with plaintext and ciphertext respectively. It is my personal recommendation that you instead be explicit about the parameter calls to the function:

    const [decrypted] = await client.decrypt({
      name: "projects/p/...",  
      ciphertext: myCiphertext,
    });
    

    Otherwise a subtle renaming of a variable can drastically break the code in very obscure ways.