Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Quit on decryption failure #381

Merged
merged 5 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint && pnpm format && pnpm test:api && pnpm test:api-client
pnpm lint && pnpm format && pnpm test:api && pnpm test:api-client
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"db:format": "pnpm dlx prisma format --schema=src/prisma/schema.prisma",
"db:reset": "pnpx dotenv-cli -e ../../.env -- pnpm dlx prisma migrate reset --force --schema=src/prisma/schema.prisma",
"sourcemaps": "sentry-cli sourcemaps inject ./dist && sentry-cli sourcemaps upload ./dist || echo 'Failed to upload source maps to Sentry'",
"e2e:prepare": "cd ../../ && docker compose down && docker compose -f docker-compose-test.yml up -d && cd apps/api && pnpm db:generate-types && cross-env NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' pnpm run db:deploy-migrations",
"e2e:prepare": "cd ../../ && docker compose down && docker compose -f docker-compose-test.yml up -d && sleep 3 && cd apps/api && pnpm db:generate-types && cross-env NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' pnpm run db:deploy-migrations",
"e2e": "pnpm run e2e:prepare && cross-env NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' jest --runInBand --config=jest.e2e-config.ts --coverage --coverageDirectory=../../coverage-e2e/api --coverageReporters=json && pnpm run e2e:teardown",
"e2e:teardown": "cd ../../ && docker compose -f docker-compose-test.yml down",
"unit": "pnpm db:generate-types && jest --config=jest.config.ts"
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"cli-table": "^0.3.11",
"colors": "^1.4.0",
"commander": "^12.1.0",
"eccrypto": "^1.1.6",
"figlet": "^1.7.0",
"fs": "0.0.1-security",
"nodemon": "^3.1.4",
Expand Down
11 changes: 9 additions & 2 deletions apps/cli/src/commands/init.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export default class InitCommand extends BaseCommand {
long: '--overwrite',
description: 'Overwrite existing configuration',
defaultValue: false
},
{
short: '-q',
long: '--quit-on-decryption-failure',
description: 'Quit on decryption failure',
defaultValue: false
}
]
}
Expand All @@ -52,7 +58,7 @@ export default class InitCommand extends BaseCommand {

async action({ options }: CommandActionData): Promise<void> {
let { workspace, project, environment, privateKey } = options
const { overwrite } = options
const { overwrite, quitOnDecryptionFailure } = options

intro('Configure the project for live-updates')

Expand Down Expand Up @@ -85,7 +91,8 @@ export default class InitCommand extends BaseCommand {
await writeProjectRootConfig({
workspace,
project,
environment
environment,
quitOnDecryptionFailure
})

await writePrivateKeyConfig({
Expand Down
37 changes: 29 additions & 8 deletions apps/cli/src/commands/run.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
import { Logger } from '@/util/logger'
import type {
ClientRegisteredResponse,
Configuration
Configuration,
RunData
} from '@/types/command/run.types'

import { decrypt } from '@/util/decrypt'

export default class RunCommand extends BaseCommand {
private processEnvironmentalVariables = {}

Expand All @@ -29,7 +32,7 @@ export default class RunCommand extends BaseCommand {
getName(): string {
return 'run'
}

getDescription(): string {
return 'Run a command'
}
Expand All @@ -52,9 +55,9 @@ export default class RunCommand extends BaseCommand {
}

private async fetchConfigurations(): Promise<
ProjectRootConfig & { privateKey: string }
RunData
> {
const { environment, project, workspace } = await fetchProjectRootConfig()
const { environment, project, workspace, quitOnDecryptionFailure } = await fetchProjectRootConfig()
const privateKeyConfig = await fetchPrivateKeyConfig()
const privateKey =
privateKeyConfig[`${workspace}_${project}_${environment}`]
Expand All @@ -67,7 +70,8 @@ export default class RunCommand extends BaseCommand {
environment,
project,
workspace,
privateKey
privateKey,
quitOnDecryptionFailure
}
}

Expand All @@ -78,11 +82,13 @@ export default class RunCommand extends BaseCommand {
return 'ws'
}

private async connectToSocket(data: ProjectRootConfig) {
private async connectToSocket(data: RunData) {
Logger.info('Connecting to socket...')
const host = this.baseUrl.substring(this.baseUrl.lastIndexOf('/') + 1)
const websocketUrl = `${this.getWebsocketType(this.baseUrl)}://${host}/change-notifier`

const privateKey = data.privateKey
const quitOnDecryptionFailure = data.quitOnDecryptionFailure

const ioClient = io(websocketUrl, {
autoConnect: false,
extraHeaders: {
Expand All @@ -102,8 +108,23 @@ export default class RunCommand extends BaseCommand {

ioClient.on('configuration-updated', async (data: Configuration) => {
Logger.info(
`Configuration change received from API (name: ${data.name}, value: ${data.value})`
`Configuration change received from API (name: ${data.name})`
)

if (!data.isPlaintext) {
try {
data.value = await decrypt(privateKey, data.value)
} catch (error) {
if (quitOnDecryptionFailure) {
Logger.error(`Decryption failed for ${data.name}. Stopping the process.`);
process.exit(1)
} else {
Logger.warn(`Decryption failed for ${data.name}. Skipping this configuration.`);
return;
}
}
}

this.processEnvironmentalVariables[data.name] = data.value
this.shouldRestart = true
})
Expand Down
6 changes: 6 additions & 0 deletions apps/cli/src/types/command/run.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ProjectRootConfig } from "../index.types"

export interface Configuration {
name: string
value: string
Expand All @@ -9,3 +11,7 @@ export interface ClientRegisteredResponse {
projectId: string
environmentId: string
}

export interface RunData extends ProjectRootConfig{
privateKey: string
}
1 change: 1 addition & 0 deletions apps/cli/src/types/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ProjectRootConfig {
workspace: string
project: string
environment: string
quitOnDecryptionFailure: boolean
}

export interface ProfileConfig {
Expand Down
22 changes: 22 additions & 0 deletions apps/cli/src/util/decrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as eccrypto from 'eccrypto'

export const decrypt = async (
privateKey: string,
data: string
): Promise<string> => {
const parsed = JSON.parse(data)

const eicesData = {
iv: Buffer.from(parsed.iv),
ephemPublicKey: Buffer.from(parsed.ephemPublicKey),
ciphertext: Buffer.from(parsed.ciphertext),
mac: Buffer.from(parsed.mac)
}

const decrypted = await eccrypto.decrypt(
Buffer.from(privateKey, 'hex'),
eicesData
)

return decrypted.toString()
}
Loading
Loading