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
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("iv").nullable().alter();
t.string("tag").nullable().alter();
t.string("encryptedValue").nullable().alter();
t.binary("encryptedSecret").nullable();
t.string("hashedHex").nullable().alter();
t.string("identifier", 64).nullable();
t.unique("identifier");
t.index("identifier");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("encryptedSecret");
t.dropColumn("identifier");
});
}
}
+9 -5
View File
@@ -5,14 +5,16 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({ export const SecretSharingSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
encryptedValue: z.string(), encryptedValue: z.string().nullable().optional(),
iv: z.string(), iv: z.string().nullable().optional(),
tag: z.string(), tag: z.string().nullable().optional(),
hashedHex: z.string(), hashedHex: z.string().nullable().optional(),
expiresAt: z.date(), expiresAt: z.date(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(),
@@ -22,7 +24,9 @@ export const SecretSharingSchema = z.object({
accessType: z.string().default("anyone"), accessType: z.string().default("anyone"),
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional(), lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional() password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional()
}); });
export type TSecretSharing = z.infer<typeof SecretSharingSchema>; export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
+2 -1
View File
@@ -923,7 +923,8 @@ export const registerRoutes = async (
const secretSharingService = secretSharingServiceFactory({ const secretSharingService = secretSharingServiceFactory({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL orgDAL,
kmsService
}); });
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
@@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
params: z.object({ params: z.object({
id: z.string().uuid() id: z.string()
}), }),
body: z.object({ body: z.object({
hashedHex: z.string().min(1), hashedHex: z.string().min(1).optional(),
password: z.string().optional() password: z.string().optional()
}), }),
response: { response: {
@@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
accessType: true accessType: true
}) })
.extend({ .extend({
orgName: z.string().optional() orgName: z.string().optional(),
secretValue: z.string().optional()
}) })
.optional() .optional()
}) })
@@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
body: z.object({ body: z.object({
encryptedValue: z.string(), secretValue: z.string().max(10_000),
password: z.string().optional(), password: z.string().optional(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
expiresAt: z.string(), expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional() expiresAfterViews: z.number().min(1).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid() id: z.string()
}) })
} }
}, },
@@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
body: z.object({ body: z.object({
name: z.string().max(50).optional(), name: z.string().max(50).optional(),
password: z.string().optional(), password: z.string().optional(),
encryptedValue: z.string(), secretValue: z.string(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
expiresAt: z.string(), expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional(), expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}), }),
response: { response: {
200: z.object({ 200: z.object({
id: z.string().uuid() id: z.string()
}) })
} }
}, },
@@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
schema: { schema: {
params: z.object({ params: z.object({
sharedSecretId: z.string().uuid() sharedSecretId: z.string()
}), }),
response: { response: {
200: SecretSharingSchema 200: SecretSharingSchema
+8 -8
View File
@@ -208,20 +208,20 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
}; };
const encryptWithRootKey = async () => { const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); 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); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return ({ cipherTextBlob }: { cipherTextBlob: Buffer }) => {
const decryptedBlob = cipher.decrypt(cipherTextBlob, ROOT_ENCRYPTION_KEY); return (cipherTextBuffer: Buffer) => {
return Promise.resolve(decryptedBlob); return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
}; };
}; };
@@ -1,10 +1,14 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { z } from "zod";
import { TSecretSharing } from "@app/db/schemas"; import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types"; import { SecretSharingAccessType } from "@app/lib/types";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import { import {
@@ -19,14 +23,18 @@ type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory; secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
kmsService: TKmsServiceFactory;
}; };
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>; export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
const isUuidV4 = (uuid: string) => z.string().uuid().safeParse(uuid).success;
export const secretSharingServiceFactory = ({ export const secretSharingServiceFactory = ({
permissionService, permissionService,
secretSharingDAL, secretSharingDAL,
orgDAL orgDAL,
kmsService
}: TSecretSharingServiceFactoryDep) => { }: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async ({ const createSharedSecret = async ({
actor, actor,
@@ -34,10 +42,7 @@ export const secretSharingServiceFactory = ({
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
encryptedValue, secretValue,
hashedHex,
iv,
tag,
name, name,
password, password,
accessType, accessType,
@@ -59,19 +64,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" }); 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 (secretValue.length > 10_000) {
if (encryptedValue.length > 13000) {
throw new BadRequestError({ message: "Shared secret value too long" }); 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 hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
identifier: id,
iv: null,
tag: null,
encryptedValue: null,
encryptedSecret,
name, name,
password: hashedPassword, password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
expiresAfterViews, expiresAfterViews,
userId: actorId, userId: actorId,
@@ -79,15 +90,14 @@ export const secretSharingServiceFactory = ({
accessType accessType
}); });
return { id: newSharedSecret.id }; const idToReturn = `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}`;
return { id: idToReturn };
}; };
const createPublicSharedSecret = async ({ const createPublicSharedSecret = async ({
password, password,
encryptedValue, secretValue,
hashedHex,
iv,
tag,
expiresAt, expiresAt,
expiresAfterViews, expiresAfterViews,
accessType accessType
@@ -104,24 +114,25 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" }); 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) const encryptWithRoot = kmsService.encryptWithRootKey();
if (encryptedValue.length > 13000) { const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
throw new BadRequestError({ message: "Shared secret value too long" });
}
const id = crypto.randomBytes(32).toString("hex");
const hashedPassword = password ? await bcrypt.hash(password, 10) : null; const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
identifier: id,
encryptedValue: null,
iv: null,
tag: null,
encryptedSecret,
password: hashedPassword, password: hashedPassword,
encryptedValue,
hashedHex,
iv,
tag,
expiresAt: new Date(expiresAt), expiresAt: new Date(expiresAt),
expiresAfterViews, expiresAfterViews,
accessType accessType
}); });
return { id: newSharedSecret.id }; return { id: `${Buffer.from(newSharedSecret.identifier!, "hex").toString("base64url")}` };
}; };
const getSharedSecrets = async ({ 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; const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) { if (expiresAfterViews) {
// decrement view count if view count expiry set // 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() 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 getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({ const sharedSecret = isUuidV4(sharedSecretId)
id: sharedSecretId, ? await secretSharingDAL.findOne({
hashedHex id: sharedSecretId,
}); hashedHex
})
: await secretSharingDAL.findOne({
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
});
if (!sharedSecret) if (!sharedSecret)
throw new NotFoundError({ throw new NotFoundError({
message: "Shared secret not found" 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. // decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId); await $decrementSecretViewCount(sharedSecret);
return { return {
isPasswordProtected, isPasswordProtected,
secret: { secret: {
...sharedSecret, ...sharedSecret,
...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString()
}),
orgName: orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName ? orgName
@@ -241,7 +267,16 @@ export const secretSharingServiceFactory = ({
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); 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); 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; return deletedSharedSecret;
}; };
@@ -19,10 +19,7 @@ export type TSharedSecretPermission = {
}; };
export type TCreatePublicSharedSecretDTO = { export type TCreatePublicSharedSecretDTO = {
encryptedValue: string; secretValue: string;
hashedHex: string;
iv: string;
tag: string;
expiresAt: string; expiresAt: string;
expiresAfterViews?: number; expiresAfterViews?: number;
password?: string; password?: string;
@@ -31,7 +28,7 @@ export type TCreatePublicSharedSecretDTO = {
export type TGetActiveSharedSecretByIdDTO = { export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string; sharedSecretId: string;
hashedHex: string; hashedHex?: string;
orgId?: string; orgId?: string;
password?: string; password?: string;
}; };
+3 -5
View File
@@ -141,16 +141,14 @@ export const slackServiceFactory = ({
let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string; let slackClientId = appCfg.WORKFLOW_SLACK_CLIENT_ID as string;
let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string; let slackClientSecret = appCfg.WORKFLOW_SLACK_CLIENT_SECRET as string;
const decrypt = await kmsService.decryptWithRootKey(); const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) { if (serverCfg.encryptedSlackClientId) {
slackClientId = (await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientId) })).toString(); slackClientId = decrypt(Buffer.from(serverCfg.encryptedSlackClientId)).toString();
} }
if (serverCfg.encryptedSlackClientSecret) { if (serverCfg.encryptedSlackClientSecret) {
slackClientSecret = ( slackClientSecret = decrypt(Buffer.from(serverCfg.encryptedSlackClientSecret)).toString();
await decrypt({ cipherTextBlob: Buffer.from(serverCfg.encryptedSlackClientSecret) })
).toString();
} }
if (!slackClientId || !slackClientSecret) { if (!slackClientId || !slackClientSecret) {
@@ -122,20 +122,16 @@ export const superAdminServiceFactory = ({
} }
} }
const encryptWithRoot = await kmsService.encryptWithRootKey(); const encryptWithRoot = kmsService.encryptWithRootKey();
if (data.slackClientId) { if (data.slackClientId) {
const { cipherTextBlob: encryptedClientId } = await encryptWithRoot({ const encryptedClientId = encryptWithRoot(Buffer.from(data.slackClientId));
plainText: Buffer.from(data.slackClientId)
});
updatedData.encryptedSlackClientId = encryptedClientId; updatedData.encryptedSlackClientId = encryptedClientId;
updatedData.slackClientId = undefined; updatedData.slackClientId = undefined;
} }
if (data.slackClientSecret) { if (data.slackClientSecret) {
const { cipherTextBlob: encryptedClientSecret } = await encryptWithRoot({ const encryptedClientSecret = encryptWithRoot(Buffer.from(data.slackClientSecret));
plainText: Buffer.from(data.slackClientSecret)
});
updatedData.encryptedSlackClientSecret = encryptedClientSecret; updatedData.encryptedSlackClientSecret = encryptedClientSecret;
updatedData.slackClientSecret = undefined; updatedData.slackClientSecret = undefined;
@@ -270,14 +266,14 @@ export const superAdminServiceFactory = ({
let clientId = ""; let clientId = "";
let clientSecret = ""; let clientSecret = "";
const decrypt = await kmsService.decryptWithRootKey(); const decrypt = kmsService.decryptWithRootKey();
if (serverCfg.encryptedSlackClientId) { if (serverCfg.encryptedSlackClientId) {
clientId = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientId })).toString(); clientId = decrypt(serverCfg.encryptedSlackClientId).toString();
} }
if (serverCfg.encryptedSlackClientSecret) { if (serverCfg.encryptedSlackClientSecret) {
clientSecret = (await decrypt({ cipherTextBlob: serverCfg.encryptedSlackClientSecret })).toString(); clientSecret = decrypt(serverCfg.encryptedSlackClientSecret).toString();
} }
return { return {
@@ -3,13 +3,21 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretSharingKeys } from "./queries"; import { secretSharingKeys } from "./queries";
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types"; import {
TCreatedSharedSecret,
TCreateSharedSecretRequest,
TDeleteSharedSecretRequest,
TSharedSecret
} from "./types";
export const useCreateSharedSecret = () => { export const useCreateSharedSecret = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => { mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData); const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing",
inputData
);
return data; return data;
}, },
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets()) onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
@@ -20,7 +28,7 @@ export const useCreatePublicSharedSecret = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => { mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TSharedSecret>( const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/public", "/api/v1/secret-sharing/public",
inputData inputData
); );
@@ -8,7 +8,7 @@ export const secretSharingKeys = {
allSharedSecrets: () => ["sharedSecrets"] as const, allSharedSecrets: () => ["sharedSecrets"] as const,
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) => specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const, [...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
getSecretById: (arg: { id: string; hashedHex: string; password?: string }) => [ getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
"shared-secret", "shared-secret",
arg arg
] ]
@@ -46,7 +46,7 @@ export const useGetActiveSharedSecretById = ({
password password
}: { }: {
sharedSecretId: string; sharedSecretId: string;
hashedHex: string; hashedHex: string | null;
password?: string; password?: string;
}) => { }) => {
return useQuery<TViewSharedSecretResponse>( return useQuery<TViewSharedSecretResponse>(
@@ -55,7 +55,7 @@ export const useGetActiveSharedSecretById = ({
const { data } = await apiRequest.post<TViewSharedSecretResponse>( const { data } = await apiRequest.post<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${sharedSecretId}`, `/api/v1/secret-sharing/public/${sharedSecretId}`,
{ {
hashedHex, ...(hashedHex && { hashedHex }),
password password
} }
); );
@@ -63,7 +63,7 @@ export const useGetActiveSharedSecretById = ({
return data; return data;
}, },
{ {
enabled: Boolean(sharedSecretId) && Boolean(hashedHex) enabled: Boolean(sharedSecretId)
} }
); );
}; };
@@ -13,13 +13,14 @@ export type TSharedSecret = {
tag: string; tag: string;
}; };
export type TCreatedSharedSecret = {
id: string;
};
export type TCreateSharedSecretRequest = { export type TCreateSharedSecretRequest = {
name?: string; name?: string;
password?: string; password?: string;
encryptedValue: string; secretValue: string;
hashedHex: string;
iv: string;
tag: string;
expiresAt: Date; expiresAt: Date;
expiresAfterViews?: number; expiresAfterViews?: number;
accessType?: SecretSharingAccessType; accessType?: SecretSharingAccessType;
@@ -28,6 +29,7 @@ export type TCreateSharedSecretRequest = {
export type TViewSharedSecretResponse = { export type TViewSharedSecretResponse = {
isPasswordProtected: boolean; isPasswordProtected: boolean;
secret: { secret: {
secretValue?: string;
encryptedValue: string; encryptedValue: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -44,4 +46,3 @@ export enum SecretSharingAccessType {
Anyone = "anyone", Anyone = "anyone",
Organization = "organization" Organization = "organization"
} }
@@ -1,5 +1,3 @@
import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
@@ -8,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2"; import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api"; import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
@@ -79,30 +76,16 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
try { try {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
const key = crypto.randomBytes(16).toString("hex");
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: secret,
key
});
const { id } = await createSharedSecret.mutateAsync({ const { id } = await createSharedSecret.mutateAsync({
name, name,
password, password,
encryptedValue: ciphertext, secretValue: secret,
hashedHex,
iv,
tag,
expiresAt, expiresAt,
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit), expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
accessType accessType
}); });
setSecretLink( setSecretLink(`${window.location.origin}/shared/secret/${id}`);
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
hashedHex
)}-${encodeURIComponent(key)}`
);
reset(); reset();
setCopyTextSecret("secret"); setCopyTextSecret("secret");
@@ -1,23 +1,42 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { NextRouter, useRouter } from "next/router";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
import { PasswordContainer,SecretContainer, SecretErrorContainer } from "./components"; import { PasswordContainer, SecretContainer, SecretErrorContainer } from "./components";
const extractDetailsFromUrl = (router: NextRouter) => {
const { id, key: urlEncodedKey } = router.query;
const idString = id as string;
if (urlEncodedKey) {
const [hashedHex, key] = urlEncodedKey ? urlEncodedKey.toString().split("-") : ["", ""];
return {
id: idString,
hashedHex,
key
};
}
return {
id: idString,
hashedHex: null,
key: null
};
};
export const ViewSecretPublicPage = () => { export const ViewSecretPublicPage = () => {
const router = useRouter(); const router = useRouter();
const [password, setPassword] = useState<string>(); const [password, setPassword] = useState<string>();
const { id, key: urlEncodedPublicKey } = router.query;
const [hashedHex, key] = urlEncodedPublicKey const { hashedHex, key, id } = extractDetailsFromUrl(router);
? urlEncodedPublicKey.toString().split("-")
: ["", ""];
const { const {
data: fetchSecret, data: fetchSecret,
@@ -25,7 +44,7 @@ export const ViewSecretPublicPage = () => {
isLoading, isLoading,
isFetching isFetching
} = useGetActiveSharedSecretById({ } = useGetActiveSharedSecretById({
sharedSecretId: id as string, sharedSecretId: id,
hashedHex, hashedHex,
password password
}); });
@@ -80,7 +99,7 @@ export const ViewSecretPublicPage = () => {
)} )}
{!isLoading && ( {!isLoading && (
<> <>
{!error && fetchSecret?.secret && key && ( {!error && fetchSecret?.secret && (
<SecretContainer secret={fetchSecret.secret} secretKey={key} /> <SecretContainer secret={fetchSecret.secret} secretKey={key} />
)} )}
{error && !isInvalidCredential && <SecretErrorContainer />} {error && !isInvalidCredential && <SecretErrorContainer />}
@@ -15,7 +15,7 @@ import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
type Props = { type Props = {
secret: TViewSharedSecretResponse["secret"]; secret: TViewSharedSecretResponse["secret"];
secretKey: string; secretKey: string | null;
}; };
export const SecretContainer = ({ secret, secretKey: key }: Props) => { export const SecretContainer = ({ secret, secretKey: key }: Props) => {
@@ -25,6 +25,10 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
}); });
const decryptedSecret = useMemo(() => { const decryptedSecret = useMemo(() => {
if (secret.secretValue) {
return secret.secretValue;
}
if (secret && secret.encryptedValue && key) { if (secret && secret.encryptedValue && key) {
const res = decryptSymmetric({ const res = decryptSymmetric({
ciphertext: secret.encryptedValue, ciphertext: secret.encryptedValue,