Merge pull request #2482 from Infisical/daniel/shorter-share-url

feat(secret-sharing): server-side encryption
This commit is contained in:
Daniel Hougaard
2024-10-03 17:48:12 +04:00
committed by GitHub
15 changed files with 193 additions and 122 deletions
+8 -8
View File
@@ -208,20 +208,20 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId;
};
const encryptWithRootKey = async () => {
const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ plainText }: { plainText: Buffer }) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, ROOT_ENCRYPTION_KEY);
return Promise.resolve({ cipherTextBlob: encryptedPlainTextBlob });
return (plainTextBuffer: Buffer) => {
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
return encryptedBuffer;
};
};
const decryptWithRootKey = async () => {
const decryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY);
return Promise.resolve(decryptedBlob);
return (cipherTextBuffer: Buffer) => {
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
};
};
@@ -1,10 +1,14 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { z } from "zod";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import {
@@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory;
kmsService: TKmsServiceFactory;
};
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
export const secretSharingServiceFactory = ({
permissionService,
secretSharingDAL,
orgDAL
orgDAL,
kmsService
}: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async ({
actor,
@@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
orgId,
actorAuthMethod,
actorOrgId,
encryptedValue,
hashedHex,
iv,
tag,
secretValue,
name,
password,
accessType,
@@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
}
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
if (encryptedValue.length > 13000) {
if (secretValue.length > 10_000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex");
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
identifier: id,
iv: null,
tag: null,
encryptedValue: null,
encryptedSecret,
name,
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt),
expiresAfterViews,
userId: actorId,
@@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
accessType
});
return { id: newSharedSecret.id };
const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
return { id: idToReturn };
};
const createPublicSharedSecret = async ({
password,
encryptedValue,
hashedHex,
iv,
tag,
secretValue,
expiresAt,
expiresAfterViews,
accessType
@@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
}
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
if (encryptedValue.length > 13000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex");
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
identifier: id,
encryptedValue: null,
iv: null,
tag: null,
encryptedSecret,
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt),
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
};
const getSharedSecrets = async ({
@@ -162,25 +173,30 @@ export const secretSharingServiceFactory = ({
};
};
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing) => {
const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
await secretSharingDAL.updateById(sharedSecret.id, { $decr: { expiresAfterViews: 1 } });
}
await secretSharingDAL.updateById(sharedSecretId, {
await secretSharingDAL.updateById(sharedSecret.id, {
lastViewedAt: new Date()
});
};
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
/** Get's password-less secret. validates all secret's requested (must be fresh). */
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
});
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
})
: await secretSharingDAL.findOne({
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
});
if (!sharedSecret)
throw new NotFoundError({
message: "Shared secret not found"
@@ -222,13 +238,23 @@ export const secretSharingServiceFactory = ({
}
}
// If encryptedSecret is set, we know that this secret has been encrypted using KMS, and we can therefore do server-side decryption.
let decryptedSecretValue: Buffer | undefined;
if (sharedSecret.encryptedSecret) {
const decryptWithRoot = kmsService.decryptWithRootKey();
decryptedSecretValue = decryptWithRoot(sharedSecret.encryptedSecret);
}
// decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
await $decrementSecretViewCount(sharedSecret);
return {
isPasswordProtected,
secret: {
...sharedSecret,
...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString()
}),
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
@@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findById(sharedSecretId)
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
return deletedSharedSecret;
};
@@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
};
export type TCreatePublicSharedSecretDTO = {
encryptedValue: string;
hashedHex: string;
iv: string;
tag: string;
secretValue: string;
expiresAt: string;
expiresAfterViews?: number;
password?: string;
@@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string;
hashedHex: string;
hashedHex?: string;
orgId?: string;
password?: string;
};
+3 -5
View File
@@ -141,16 +141,14 @@ export const slackServiceFactory = ({
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
const decrypt = await kmsService.decryptWithRootKey();
const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) {
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString();
slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
}
if (serverCfg.encryptedSlackClientSecret) {
slackClientSecret = (
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
).toString();
slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
}
if (!slackClientId || !slackClientSecret) {
@@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
}
}
const encryptWithRoot = await kmsService.encryptWithRootKey();
const encryptWithRoot = kmsService.encryptWithRootKey();
if (data.slackClientId) {
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({
plainText: Buffer.from(data.slackClientId)
});
const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
updatedData.encryptedSlackClientId = encryptedClientId;
updatedData.slackClientId = undefined;
}
if (data.slackClientSecret) {
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({
plainText: Buffer.from(data.slackClientSecret)
});
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
updatedData.encryptedSlackClientSecret = encryptedClientSecret;
updatedData.slackClientSecret = undefined;
@@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
let clientId = "";
let clientSecret = "";
const decrypt = await kmsService.decryptWithRootKey();
const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) {
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString();
clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
}
if (serverCfg.encryptedSlackClientSecret) {
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString();
clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
}
return {