Handle missing encryption key for the EncryptedMetadataManager (#159)

* Handle missing encryption key for the EncryptedMetadataManager

* Fancy up the documentation

* Throw an error instead of process exit
This commit is contained in:
Krzysztof Wolski 2023-01-23 15:39:42 +01:00 committed by GitHub
parent 5a4d316228
commit 51284efa00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 128 additions and 54 deletions

View file

@ -66,3 +66,19 @@ const settings = new MetadataManager({
mutateMetadata: (md) => mutateMetadata(client, md), mutateMetadata: (md) => mutateMetadata(client, md),
}); });
``` ```
# EncryptedMetadataManager
This manager encrypts add the layer of encryption for all the stored data.
To operate correctly, the encryption key needs to be passed to the constructor:
```ts
new EncryptedMetadataManager({
encryptionKey: process.env.SECRET_KEY, // secrets should be saved in the environment variables, never in the source code
fetchMetadata: () => fetchAllMetadata(client),
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
});
```
> **Warning**
> If encryption key won't be passed, the application will exit.

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { import {
DecryptCallback, DecryptCallback,
@ -19,66 +19,109 @@ const entryForDomainY = { key: "a__y.com", value: "domain y value" };
const metadata = [initialEntry, entryForDomainX, entryForDomainY, encryptedEntry]; const metadata = [initialEntry, entryForDomainX, entryForDomainY, encryptedEntry];
describe("settings-manager", () => { describe("settings-manager", () => {
describe("metadata-manager", () => { describe("encrypted-metadata-manager", () => {
const fetchMock = vi.fn(async () => metadata); describe("Constructor", () => {
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]); const initialEnv = { ...process.env };
const manager = new EncryptedMetadataManager({
fetchMetadata: fetchMock, afterEach(() => {
mutateMetadata: mutateMock, process.env = { ...initialEnv };
encryptionKey: "key", vi.resetModules();
});
it("Process exit should be called when no encryption key is set if the environment type is production", async () => {
// @ts-expect-error
process.env.NODE_ENV = "production";
const fetchMock = vi.fn(async () => metadata);
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
expect(
() =>
new EncryptedMetadataManager({
fetchMetadata: fetchMock,
mutateMetadata: mutateMock,
// @ts-expect-error
encryptionKey: undefined,
})
).toThrowError(
"Encryption key for the EncryptedMetadataManager has not been set. Setting it for the production environments is necessary. You can find more in the documentation: https://github.com/saleor/saleor-app-sdk/blob/main/docs/settings-manager.md"
);
});
it("If env type is different than production (development/test) use placeholder value", async () => {
const fetchMock = vi.fn(async () => metadata);
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
const manager = new EncryptedMetadataManager({
fetchMetadata: fetchMock,
mutateMetadata: mutateMock,
// @ts-expect-error
encryptionKey: undefined,
});
// @ts-expect-error
expect(manager.encryptionKey).toBe("CHANGE_ME");
});
}); });
beforeEach(() => { describe("Manager operations", () => {
vi.restoreAllMocks(); const fetchMock = vi.fn(async () => metadata);
}); const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
const manager = new EncryptedMetadataManager({
it("Set encrypted value in the metadata", async () => {
const newEntry = { key: "new", value: "new value" };
await manager.set(newEntry);
const mutateValue = mutateMock.mock.lastCall![0][0].value;
// Encrypted value should be encrypted, alphanumeric value and different than input
expect(mutateValue).toMatch(/^[\d\w]+$/);
expect(mutateValue).not.toEqual(newEntry.key);
// Set method should populate cache with updated values, so fetch is never called
expect(fetchMock).toBeCalledTimes(0);
});
it("Get encrypted data from metadata", async () => {
const value = await manager.get(encryptedEntry.key);
expect(value).toMatch("new value");
// make sure encrypted metadata is different than decrypted value
expect(value).not.toEqual(encryptedEntry.value);
expect(fetchMock).toBeCalledTimes(0);
});
it("Use custom encryption callbacks", async () => {
const newEntry = { key: "new", value: "new value" };
// dummy encryption - join value and string together
const customEncrypt: EncryptCallback = (value, secret) => value + secret;
// dummy decryption - remove secret from end of the "encrypted" value
const customDecrypt: DecryptCallback = (value, secret) =>
value.substr(0, value.length - secret.length);
const customManager = new EncryptedMetadataManager({
fetchMetadata: fetchMock, fetchMetadata: fetchMock,
mutateMetadata: mutateMock, mutateMetadata: mutateMock,
encryptionKey: "key", encryptionKey: "key",
encryptionMethod: customEncrypt,
decryptionMethod: customDecrypt,
}); });
await customManager.set(newEntry); beforeEach(() => {
// value send to the API should be encrypted with custom method vi.restoreAllMocks();
const mutateValue = mutateMock.mock.lastCall![0][0].value; });
expect(mutateValue).toMatch("new valuekey");
// value from get should be "decrypted" using custom method it("Set encrypted value in the metadata", async () => {
expect(await customManager.get(newEntry.key)).toMatch("new value"); const newEntry = { key: "new", value: "new value" };
// Set method should populate cache with updated values, so fetch is never called
expect(fetchMock).toBeCalledTimes(0); await manager.set(newEntry);
const mutateValue = mutateMock.mock.lastCall![0][0].value;
// Encrypted value should be encrypted, alphanumeric value and different than input
expect(mutateValue).toMatch(/^[\d\w]+$/);
expect(mutateValue).not.toEqual(newEntry.key);
// Set method should populate cache with updated values, so fetch is never called
expect(fetchMock).toBeCalledTimes(0);
});
it("Get encrypted data from metadata", async () => {
const value = await manager.get(encryptedEntry.key);
expect(value).toMatch("new value");
// make sure encrypted metadata is different than decrypted value
expect(value).not.toEqual(encryptedEntry.value);
expect(fetchMock).toBeCalledTimes(0);
});
it("Use custom encryption callbacks", async () => {
const newEntry = { key: "new", value: "new value" };
// dummy encryption - join value and string together
const customEncrypt: EncryptCallback = (value, secret) => value + secret;
// dummy decryption - remove secret from end of the "encrypted" value
const customDecrypt: DecryptCallback = (value, secret) =>
value.substr(0, value.length - secret.length);
const customManager = new EncryptedMetadataManager({
fetchMetadata: fetchMock,
mutateMetadata: mutateMock,
encryptionKey: "key",
encryptionMethod: customEncrypt,
decryptionMethod: customDecrypt,
});
await customManager.set(newEntry);
// value send to the API should be encrypted with custom method
const mutateValue = mutateMock.mock.lastCall![0][0].value;
expect(mutateValue).toMatch("new valuekey");
// value from get should be "decrypted" using custom method
expect(await customManager.get(newEntry.key)).toMatch("new value");
// Set method should populate cache with updated values, so fetch is never called
expect(fetchMock).toBeCalledTimes(0);
});
}); });
}); });
}); });

View file

@ -71,7 +71,22 @@ export class EncryptedMetadataManager implements SettingsManager {
fetchMetadata, fetchMetadata,
mutateMetadata, mutateMetadata,
}); });
this.encryptionKey = encryptionKey; if (encryptionKey) {
this.encryptionKey = encryptionKey;
} else {
console.warn("Encrypted Metadata Manager secret key has not been set.");
if (process.env.NODE_ENV === "production") {
console.error("Can't start the application without the secret key.");
throw new Error(
"Encryption key for the EncryptedMetadataManager has not been set. Setting it for the production environments is necessary. You can find more in the documentation: https://github.com/saleor/saleor-app-sdk/blob/main/docs/settings-manager.md"
);
}
console.warn(
"WARNING: Encrypted Metadata Manager encryption key has not been set. For production deployments, it need's to be set"
);
console.warn("Using placeholder value for the development.");
this.encryptionKey = "CHANGE_ME";
}
this.encryptionMethod = encryptionMethod || encrypt; this.encryptionMethod = encryptionMethod || encrypt;
this.decryptionMethod = decryptionMethod || decrypt; this.decryptionMethod = decryptionMethod || decrypt;
} }