budibase/packages/backend-core/src/security/encryption.ts

149 lines
3.8 KiB
TypeScript
Raw Normal View History

import crypto from "crypto"
2023-06-09 17:26:48 +02:00
import fs from "fs"
import zlib from "zlib"
import env from "../environment"
2023-06-12 12:49:38 +02:00
import { join } from "path"
2022-02-14 19:32:09 +01:00
const ALGO = "aes-256-ctr"
const SEPARATOR = "-"
const ITERATIONS = 10000
const STRETCH_LENGTH = 32
2023-06-12 17:27:19 +02:00
const SALT_LENGTH = 16
const IV_LENGTH = 16
export enum SecretOption {
API = "api",
ENCRYPTION = "encryption",
}
2023-04-12 03:29:30 +02:00
export function getSecret(secretOption: SecretOption): string {
let secret, secretName
switch (secretOption) {
case SecretOption.ENCRYPTION:
secret = env.ENCRYPTION_KEY
secretName = "ENCRYPTION_KEY"
break
case SecretOption.API:
default:
secret = env.API_ENCRYPTION_KEY
secretName = "API_ENCRYPTION_KEY"
break
}
if (!secret) {
throw new Error(`Secret "${secretName}" has not been set in environment.`)
}
return secret
}
2023-06-12 18:31:08 +02:00
function stretchString(secret: string, salt: Buffer) {
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
}
2022-02-14 19:32:09 +01:00
export function encrypt(
input: string,
secretOption: SecretOption = SecretOption.API
) {
2023-06-12 17:27:19 +02:00
const salt = crypto.randomBytes(SALT_LENGTH)
const stretched = stretchString(getSecret(secretOption), salt)
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
2022-02-14 19:32:09 +01:00
const base = cipher.update(input)
const final = cipher.final()
const encrypted = Buffer.concat([base, final]).toString("hex")
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
2022-02-14 19:32:09 +01:00
}
export function decrypt(
input: string,
secretOption: SecretOption = SecretOption.API
) {
const [salt, encrypted] = input.split(SEPARATOR)
const saltBuffer = Buffer.from(salt, "hex")
const stretched = stretchString(getSecret(secretOption), saltBuffer)
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
2022-02-14 19:32:09 +01:00
const base = decipher.update(Buffer.from(encrypted, "hex"))
const final = decipher.final()
return Buffer.concat([base, final]).toString()
}
2023-06-09 17:26:48 +02:00
2023-06-12 12:49:38 +02:00
export async function encryptFile(
{ dir, filename }: { dir: string; filename: string },
secret: string
) {
const outputFileName = `${filename}.enc`
2023-06-09 17:26:48 +02:00
2023-06-12 12:49:38 +02:00
const filePath = join(dir, filename)
2023-06-09 17:26:48 +02:00
const inputFile = fs.createReadStream(filePath)
2023-06-12 12:49:38 +02:00
const outputFile = fs.createWriteStream(join(dir, outputFileName))
2023-06-09 17:26:48 +02:00
2023-06-12 17:27:19 +02:00
const salt = crypto.randomBytes(SALT_LENGTH)
const iv = crypto.randomBytes(IV_LENGTH)
2023-06-09 17:26:48 +02:00
const stretched = stretchString(secret, salt)
2023-06-12 17:27:19 +02:00
const cipher = crypto.createCipheriv(ALGO, stretched, iv)
2023-06-09 17:26:48 +02:00
2023-06-12 17:27:19 +02:00
outputFile.write(salt)
outputFile.write(iv)
2023-06-09 17:26:48 +02:00
2023-06-12 17:27:19 +02:00
inputFile.pipe(cipher).pipe(outputFile)
2023-06-09 17:26:48 +02:00
2023-06-12 12:49:38 +02:00
return new Promise<{ filename: string; dir: string }>(r => {
2023-06-09 17:26:48 +02:00
outputFile.on("finish", () => {
2023-06-12 12:49:38 +02:00
r({
filename: outputFileName,
dir,
})
2023-06-09 17:26:48 +02:00
})
})
}
2023-06-12 17:27:19 +02:00
export async function decryptFile(
inputPath: string,
outputPath: string,
secret: string
) {
const inputFile = fs.createReadStream(inputPath)
const outputFile = fs.createWriteStream(outputPath)
const salt = await readBytes(inputFile, SALT_LENGTH)
const iv = await readBytes(inputFile, IV_LENGTH)
const stretched = stretchString(secret, salt)
const decipher = crypto.createDecipheriv(ALGO, stretched, iv)
fs.createReadStream(inputPath, { start: SALT_LENGTH + IV_LENGTH })
.pipe(decipher)
.pipe(outputFile)
return new Promise<void>(r => {
outputFile.on("finish", () => {
r()
})
})
}
function readBytes(stream: fs.ReadStream, length: number) {
return new Promise<Buffer>((resolve, reject) => {
let bytesRead = 0
const data: Buffer[] = []
stream.on("readable", () => {
let chunk
while ((chunk = stream.read(length - bytesRead)) !== null) {
data.push(chunk)
bytesRead += chunk.length
}
resolve(Buffer.concat(data))
})
stream.on("end", () => {
reject(new Error("Insufficient data in the stream."))
})
stream.on("error", (error: any) => {
reject(error)
})
})
}