diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index f9adb68955..ac8589ddf3 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -1,4 +1,6 @@ import crypto from "crypto" +import fs from "fs" +import zlib from "zlib" import env from "../environment" const ALGO = "aes-256-ctr" @@ -60,3 +62,24 @@ export function decrypt( const final = decipher.final() return Buffer.concat([base, final]).toString() } + +export async function encryptFile(filePath: string, secret: string) { + const outputFilePath = `${filePath}.enc` + + const inputFile = fs.createReadStream(filePath) + const outputFile = fs.createWriteStream(outputFilePath) + + const salt = crypto.randomBytes(RANDOM_BYTES) + const stretched = stretchString(secret, salt) + const cipher = crypto.createCipheriv(ALGO, stretched, salt) + + const encrypted = inputFile.pipe(cipher).pipe(zlib.createGzip()) + + encrypted.pipe(outputFile) + + return new Promise(r => { + outputFile.on("finish", () => { + r(outputFilePath) + }) + }) +} diff --git a/packages/server/src/api/controllers/backup.ts b/packages/server/src/api/controllers/backup.ts index 53e1bd1792..716f5298dd 100644 --- a/packages/server/src/api/controllers/backup.ts +++ b/packages/server/src/api/controllers/backup.ts @@ -4,14 +4,19 @@ import { DocumentType } from "../../db/utils" import { isQsTrue } from "../../utilities" export async function exportAppDump(ctx: any) { - let { appId, excludeRows } = ctx.query + let { appId, excludeRows = false, encryptPassword } = ctx.query // remove the 120 second limit for the request ctx.req.setTimeout(0) const appName = decodeURI(ctx.query.appname) excludeRows = isQsTrue(excludeRows) - const backupIdentifier = `${appName}-export-${new Date().getTime()}.tar.gz` + const extension = encryptPassword ? "data" : "tar.gz" + const backupIdentifier = `${appName}-export-${new Date().getTime()}.${extension}` ctx.attachment(backupIdentifier) - ctx.body = await sdk.backups.streamExportApp(appId, excludeRows) + ctx.body = await sdk.backups.streamExportApp({ + appId, + excludeRows, + encryptPassword, + }) await context.doInAppContext(appId, async () => { const appDb = context.getAppDB() diff --git a/packages/server/src/sdk/app/backups/exports.ts b/packages/server/src/sdk/app/backups/exports.ts index a31ade6d13..6bce7eed5c 100644 --- a/packages/server/src/sdk/app/backups/exports.ts +++ b/packages/server/src/sdk/app/backups/exports.ts @@ -1,4 +1,4 @@ -import { db as dbCore, objectStore } from "@budibase/backend-core" +import { db as dbCore, encryption, objectStore } from "@budibase/backend-core" import { budibaseTempDir } from "../../../utilities/budibaseDir" import { streamFile, createTempFolder } from "../../../utilities/fileSystem" import { ObjectStoreBuckets } from "../../../constants" @@ -31,6 +31,7 @@ interface ExportOpts extends DBDumpOpts { tar?: boolean excludeRows?: boolean excludeLogs?: boolean + encryptPassword?: string } function tarFilesToTmp(tmpDir: string, files: string[]) { @@ -150,7 +151,15 @@ export async function exportApp(appId: string, config?: ExportOpts) { const tarPath = tarFilesToTmp(tmpPath, fs.readdirSync(tmpPath)) // cleanup the tmp export files as tarball returned fs.rmSync(tmpPath, { recursive: true, force: true }) - return tarPath + if (!config.encryptPassword) { + return tarPath + } + + const encryptedTarPath = await encryption.encryptFile( + tarPath, + config.encryptPassword + ) + return encryptedTarPath } // tar not requested, turn the directory where export is else { @@ -164,11 +173,20 @@ export async function exportApp(appId: string, config?: ExportOpts) { * @param {boolean} excludeRows Flag to state whether the export should include data. * @returns {*} a readable stream of the backup which is written in real time */ -export async function streamExportApp(appId: string, excludeRows: boolean) { +export async function streamExportApp({ + appId, + excludeRows, + encryptPassword, +}: { + appId: string + excludeRows: boolean + encryptPassword: string +}) { const tmpPath = await exportApp(appId, { excludeRows, excludeLogs: true, tar: true, + encryptPassword, }) return streamFile(tmpPath) }