Skip to content

Commit 78bb990

Browse files
authored
feat(apify): add decryption for input secrets (#83)
1 parent adc2fad commit 78bb990

File tree

6 files changed

+98
-20
lines changed

6 files changed

+98
-20
lines changed

Diff for: package-lock.json

+33-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
"lint:fix": "eslint packages/*/src test --fix"
5050
},
5151
"devDependencies": {
52+
"@apify/consts": "^2.4.1",
5253
"@apify/eslint-config-ts": "^0.2.3",
54+
"@apify/input_secrets": "^1.1.1",
5355
"@apify/tsconfig": "^0.1.0",
5456
"@commitlint/config-conventional": "^17.0.3",
5557
"@types/content-type": "^1.1.5",
@@ -64,15 +66,15 @@
6466
"@typescript-eslint/parser": "5.39.0",
6567
"commitlint": "^17.0.3",
6668
"crawlee": "^3.0.0",
67-
"playwright": "*",
68-
"puppeteer": "*",
6969
"eslint": "~8.24.0",
7070
"fs-extra": "^10.1.0",
7171
"gen-esm-wrapper": "^1.1.3",
7272
"husky": "^8.0.1",
7373
"jest": "^29.1.2",
7474
"lerna": "^5.0.0",
7575
"lint-staged": "^13.0.3",
76+
"playwright": "*",
77+
"puppeteer": "*",
7678
"rimraf": "^3.0.2",
7779
"ts-jest": "^29.0.3",
7880
"ts-node": "^10.8.2",

Diff for: packages/apify/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,17 @@
5454
"access": "public"
5555
},
5656
"dependencies": {
57-
"@apify/consts": "^2.3.0",
57+
"@apify/consts": "^2.4.1",
58+
"@apify/input_secrets": "^1.1.0",
5859
"@apify/log": "^2.1.4",
5960
"@apify/timeout": "^0.3.0",
60-
"@apify/utilities": "^2.1.5",
61+
"@apify/utilities": "^2.2.2",
6162
"@crawlee/core": "^3.0.0",
6263
"@crawlee/types": "^3.0.0",
6364
"@crawlee/utils": "^3.0.0",
64-
"semver": "^7.3.7",
6565
"apify-client": "^2.6.0",
6666
"ow": "^0.28.1",
67+
"semver": "^7.3.7",
6768
"ws": "^7.5.9"
6869
}
6970
}

Diff for: packages/apify/src/actor.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import ow from 'ow';
2+
import { createPrivateKey } from 'node:crypto';
3+
import { decryptInputSecrets } from '@apify/input_secrets';
24
import { ENV_VARS, INTEGER_ENV_VARS } from '@apify/consts';
35
import { addTimeoutToPromise } from '@apify/timeout';
46
import log from '@apify/log';
@@ -646,7 +648,17 @@ export class Actor<Data extends Dictionary = Dictionary> {
646648
* @ignore
647649
*/
648650
async getInput<T = Dictionary | string | Buffer>(): Promise<T | null> {
649-
return this.getValue<T>(this.config.get('inputKey'));
651+
const inputSecretsPrivateKeyFile = this.config.get('inputSecretsPrivateKeyFile');
652+
const inputSecretsPrivateKeyPassphrase = this.config.get('inputSecretsPrivateKeyPassphrase');
653+
const input = await this.getValue<T>(this.config.get('inputKey'));
654+
if (ow.isValid(input, ow.object.nonEmpty) && inputSecretsPrivateKeyFile && inputSecretsPrivateKeyPassphrase) {
655+
const privateKey = createPrivateKey({
656+
key: Buffer.from(inputSecretsPrivateKeyFile, 'base64'),
657+
passphrase: inputSecretsPrivateKeyPassphrase,
658+
});
659+
return decryptInputSecrets<T>({ input, privateKey });
660+
}
661+
return input;
650662
}
651663

652664
/**

Diff for: packages/apify/src/configuration.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface ConfigurationOptions extends CoreConfigurationOptions {
1818
proxyStatusUrl?: string;
1919
isAtHome?: boolean;
2020
userId?: string;
21+
inputSecretsPrivateKeyPassphrase?: string;
22+
inputSecretsPrivateKeyFile?: string;
2123
}
2224

2325
/**
@@ -129,6 +131,8 @@ export class Configuration extends CoreConfiguration {
129131
APIFY_PROXY_PASSWORD: 'proxyPassword',
130132
APIFY_PROXY_STATUS_URL: 'proxyStatusUrl',
131133
APIFY_PROXY_PORT: 'proxyPort',
134+
APIFY_INPUT_SECRETS_PRIVATE_KEY_FILE: 'inputSecretsPrivateKeyFile',
135+
APIFY_INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE: 'inputSecretsPrivateKeyPassphrase',
132136
};
133137

134138
protected static override INTEGER_VARS = [...super.INTEGER_VARS, 'proxyPort', 'containerPort', 'metamorphAfterSleepMillis'];

Diff for: test/apify/actor.test.ts

+40-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { createPublicKey } from 'node:crypto';
12
import { ACT_JOB_STATUSES, ENV_VARS, KEY_VALUE_STORE_KEYS, WEBHOOK_EVENT_TYPES } from '@apify/consts';
23
import log from '@apify/log';
4+
import { encryptInputSecrets } from '@apify/input_secrets';
35
import type { ApifyEnv } from 'apify';
46
import { Actor, ProxyConfiguration } from 'apify';
57
import type { WebhookUpdateData } from 'apify-client';
@@ -45,6 +47,14 @@ const setEnv = (env: ApifyEnv) => {
4547
if (env.memoryMbytes) process.env.APIFY_MEMORY_MBYTES = env.memoryMbytes.toString();
4648
};
4749

50+
const testingPublicKey = createPublicKey({
51+
// eslint-disable-next-line max-len
52+
key: Buffer.from('LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF0dis3NlNXbklhOFFKWC94RUQxRQpYdnBBQmE3ajBnQnVYenJNUU5adjhtTW1RU0t2VUF0TmpOL2xacUZpQ0haZUQxU2VDcGV1MnFHTm5XbGRxNkhUCnh5cXJpTVZEbFNKaFBNT09QSENISVNVdFI4Tk5lR1Y1MU0wYkxJcENabHcyTU9GUjdqdENWejVqZFRpZ1NvYTIKQWxrRUlRZWQ4UVlDKzk1aGJoOHk5bGcwQ0JxdEdWN1FvMFZQR2xKQ0hGaWNuaWxLVFFZay9MZzkwWVFnUElPbwozbUppeFl5bWFGNmlMZTVXNzg1M0VHWUVFVWdlWmNaZFNjaGVBMEdBMGpRSFVTdnYvMEZjay9adkZNZURJOTVsCmJVQ0JoQjFDbFg4OG4wZUhzUmdWZE5vK0NLMDI4T2IvZTZTK1JLK09VaHlFRVdPTi90alVMdGhJdTJkQWtGcmkKOFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==', 'base64'),
53+
});
54+
// eslint-disable-next-line max-len
55+
const testingPrivateKeyFile = 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpQcm9jLVR5cGU6IDQsRU5DUllQVEVECkRFSy1JbmZvOiBERVMtRURFMy1DQkMsNTM1QURERjIzNUQ4QkFGOQoKMXFWUzl0S0FhdkVhVUVFMktESnpjM3plMk1lZkc1dmVEd2o1UVJ0ZkRaMXdWNS9VZmIvcU5sVThTSjlNaGhKaQp6RFdrWExueUUzSW0vcEtITVZkS0czYWZkcFRtcis2TmtidXptd0dVMk0vSWpzRjRJZlpad0lGbGJoY09jUnp4CmZmWVIvTlVyaHNrS1RpNGhGV0lBUDlLb3Z6VDhPSzNZY3h6eVZQWUxYNGVWbWt3UmZzeWkwUU5Xb0tGT3d0ZC8KNm9HYzFnd2piRjI5ZDNnUThZQjFGWmRLa1AyMTJGbkt1cTIrUWgvbE1zTUZrTHlTQTRLTGJ3ZG1RSXExbE1QUwpjbUNtZnppV3J1MlBtNEZoM0dmWlQyaE1JWHlIRFdEVzlDTkxKaERodExOZ2RRamFBUFpVT1E4V2hwSkE5MS9vCjJLZzZ3MDd5Z2RCcVd5dTZrc0pXcjNpZ1JpUEJ5QmVNWEpEZU5HY3NhaUZ3Q2c5eFlja1VORXR3NS90WlRsTjIKSEdZV0NpVU5Ed0F2WllMUHR1SHpIOFRFMGxsZm5HR0VuVC9QQlp1UHV4andlZlRleE1mdzFpbGJRU3lkcy9HMgpOOUlKKzkydms0N0ZXR2NOdGh1Q3lCbklva0NpZ0c1ZlBlV2IwQTdpdjk0UGtwRTRJZ3plc0hGQ0ZFQWoxWldLCnpQdFRBQlkwZlJrUzBNc3UwMHYxOXloTTUrdFUwYkVCZWo2eWpzWHRoYzlwS01hcUNIZWlQTC9TSHRkaWsxNVMKQmU4Sml4dVJxZitUeGlYWWVuNTg2aDlzTFpEYzA3cGpkUGp2NVNYRnBYQjhIMlVxQ0tZY2p4R3RvQWpTV0pjWApMNHc3RHNEby80bVg1N0htR09iamlCN1ZyOGhVWEJDdFh2V0dmQXlmcEFZNS9vOXowdm4zREcxaDc1NVVwdDluCkF2MFZrbm9qcmJVYjM1ZlJuU1lYTVltS01LSnpNRlMrdmFvRlpwV0ZjTG10cFRWSWNzc0JGUEYyZEo3V1c0WHMKK0d2Vkl2eFl3S2wyZzFPTE1TTXRZa09vekdlblBXTzdIdU0yMUVKVGIvbHNEZ25GaTkrYWRGZHBLY3R2cm0zdgpmbW1HeG5pRmhLU05GU0xtNms5YStHL2pjK3NVQVBhb2FZNEQ3NHVGajh0WGp0eThFUHdRRGxVUGRVZld3SE9PClF3bVgyMys1REh4V0VoQy91Tm8yNHNNY2ZkQzFGZUpBV281bUNuVU5vUVVmMStNRDVhMzNJdDhhMmlrNUkxUWoKeSs1WGpRaG0xd3RBMWhWTWE4aUxBR0toT09lcFRuK1VBZHpyS0hvNjVtYzNKbGgvSFJDUXJabnVxWkErK0F2WgpjeWU0dWZGWC8xdmRQSTdLb2Q0MEdDM2dlQnhweFFNYnp1OFNUcGpOcElJRkJvRVc5dFRhemUzeHZXWnV6dDc0CnFjZS8xWURuUHBLeW5lM0xGMk94VWoyYWVYUW5YQkpYcGhTZTBVTGJMcWJtUll4bjJKWkl1d09RNHV5dm94NjUKdG9TWGNac054dUs4QTErZXNXR3JSN3pVc0djdU9QQTFERE9Ja2JjcGtmRUxMNjk4RTJRckdqTU9JWnhrcWdxZQoySE5VNktWRmV2NzdZeEJDbm1VcVdXZEhYMjcyU2NPMUYzdWpUdFVnRVBNWGN0aEdBckYzTWxEaUw1Q0k0RkhqCnhHc3pVemxzalRQTmpiY2MzdUE2MjVZS3VVZEI2c1h1Rk5NUHk5UDgwTzBpRWJGTXl3MWxmN2VpdFhvaUUxWVoKc3NhMDVxTUx4M3pPUXZTLzFDdFpqaFp4cVJMRW5pQ3NWa2JVRlVYclpodEU4dG94bGpWSUtpQ25qbitORmtqdwo2bTZ1anpBSytZZHd2Nk5WMFB4S0gwUk5NYVhwb1lmQk1oUmZ3dGlaS3V3Y2hyRFB5UEhBQ2J3WXNZOXdtUE9rCnpwdDNxWi9JdDVYTmVqNDI0RzAzcGpMbk1sd1B1T1VzYmFQUWQ2VHU4TFhsckZReUVjTXJDNHdjUTA1SzFVN3kKM1NNN3RFaTlnbjV3RjY1YVI5eEFBR0grTUtMMk5WNnQrUmlTazJVaWs1clNmeDE4Mk9wYmpSQ2grdmQ4UXhJdwotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=';
56+
const testingPrivateKeyPassphrase = 'pwd1234';
57+
4858
describe('Actor', () => {
4959
const localStorageEmulator = new MemoryStorageEmulator();
5060

@@ -937,23 +947,43 @@ describe('Actor', () => {
937947
});
938948

939949
describe('Actor.getInput', () => {
950+
const TestingActor = new Actor();
951+
940952
test('should work', async () => {
941-
const defaultStore = await KeyValueStore.open();
942-
// Uses default value.
943-
const oldGet = defaultStore.getValue;
944-
// @ts-expect-error TODO use spyOn instead of this
945-
defaultStore.getValue = async (key) => expect(key).toEqual(KEY_VALUE_STORE_KEYS.INPUT);
946-
await Actor.getInput();
953+
const mockGetValue = jest.spyOn(TestingActor, 'getValue');
954+
mockGetValue.mockImplementation(async (key) => expect(key).toEqual(KEY_VALUE_STORE_KEYS.INPUT));
955+
956+
await TestingActor.getInput();
947957

948958
// Uses value from env var.
949959
process.env[ENV_VARS.INPUT_KEY] = 'some-value';
950-
// @ts-expect-error TODO use spyOn instead of this
951-
defaultStore.getValue = async (key) => expect(key).toBe('some-value');
952-
await Actor.getInput();
960+
mockGetValue.mockImplementation(async (key) => expect(key).toBe('some-value'));
961+
await TestingActor.getInput();
953962

954963
delete process.env[ENV_VARS.INPUT_KEY];
964+
mockGetValue.mockRestore();
965+
});
955966

956-
defaultStore.getValue = oldGet;
967+
test('should work with input secrets', async () => {
968+
const mockGetValue = jest.spyOn(TestingActor, 'getValue');
969+
const originalInput = { secret: 'foo', nonSecret: 'bar' };
970+
const likeInputSchema = { properties: { secret: { type: 'string', isSecret: true } }, nonSecret: { type: 'string' } };
971+
const encryptedInput = encryptInputSecrets({ input: originalInput, inputSchema: likeInputSchema, publicKey: testingPublicKey });
972+
// Checks if encrypts the right value
973+
expect(encryptedInput.secret.startsWith('ENCRYPTED_')).toBe(true);
974+
expect(encryptedInput.nonSecret).toBe(originalInput.nonSecret);
975+
976+
mockGetValue.mockImplementation(async (key) => encryptedInput);
977+
978+
process.env[ENV_VARS.INPUT_SECRETS_PRIVATE_KEY_FILE] = testingPrivateKeyFile;
979+
process.env[ENV_VARS.INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE] = testingPrivateKeyPassphrase;
980+
const input = await TestingActor.getInput();
981+
982+
expect(input).toStrictEqual(originalInput);
983+
984+
delete process.env[ENV_VARS.INPUT_SECRETS_PRIVATE_KEY_FILE];
985+
delete process.env[ENV_VARS.INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE];
986+
mockGetValue.mockRestore();
957987
});
958988
});
959989

0 commit comments

Comments
 (0)