mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2482 from Infisical/daniel/shorter-share-url
feat(secret-sharing): server-side encryption
This commit is contained in:
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,16 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
expiresAt: z.date(),
|
||||
userId: 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"),
|
||||
name: z.string().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>;
|
||||
|
||||
@@ -923,7 +923,8 @@ export const registerRoutes = async (
|
||||
const secretSharingService = secretSharingServiceFactory({
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL
|
||||
orgDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
|
||||
@@ -55,10 +55,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
hashedHex: z.string().min(1),
|
||||
hashedHex: z.string().min(1).optional(),
|
||||
password: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
@@ -73,7 +73,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
accessType: true
|
||||
})
|
||||
.extend({
|
||||
orgName: z.string().optional()
|
||||
orgName: z.string().optional(),
|
||||
secretValue: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -99,17 +100,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
secretValue: z.string().max(10_000),
|
||||
password: z.string().optional(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -132,17 +130,14 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
body: z.object({
|
||||
name: z.string().max(50).optional(),
|
||||
password: z.string().optional(),
|
||||
encryptedValue: z.string(),
|
||||
hashedHex: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
secretValue: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -168,7 +163,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sharedSecretId: z.string().uuid()
|
||||
sharedSecretId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SecretSharingSchema
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,13 +3,21 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretSharingKeys } from "./queries";
|
||||
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
||||
import {
|
||||
TCreatedSharedSecret,
|
||||
TCreateSharedSecretRequest,
|
||||
TDeleteSharedSecretRequest,
|
||||
TSharedSecret
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
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;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||
@@ -20,7 +28,7 @@ export const useCreatePublicSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
inputData
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const secretSharingKeys = {
|
||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...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",
|
||||
arg
|
||||
]
|
||||
@@ -46,7 +46,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
password
|
||||
}: {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
hashedHex: string | null;
|
||||
password?: string;
|
||||
}) => {
|
||||
return useQuery<TViewSharedSecretResponse>(
|
||||
@@ -55,7 +55,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||
{
|
||||
hashedHex,
|
||||
...(hashedHex && { hashedHex }),
|
||||
password
|
||||
}
|
||||
);
|
||||
@@ -63,7 +63,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
return data;
|
||||
},
|
||||
{
|
||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex)
|
||||
enabled: Boolean(sharedSecretId)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,13 +13,14 @@ export type TSharedSecret = {
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type TCreatedSharedSecret = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name?: string;
|
||||
password?: string;
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secretValue: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews?: number;
|
||||
accessType?: SecretSharingAccessType;
|
||||
@@ -28,6 +29,7 @@ export type TCreateSharedSecretRequest = {
|
||||
export type TViewSharedSecretResponse = {
|
||||
isPasswordProtected: boolean;
|
||||
secret: {
|
||||
secretValue?: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
@@ -44,4 +46,3 @@ export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
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 { 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 { useTimedReset } from "@app/hooks";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
|
||||
@@ -79,30 +76,16 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
try {
|
||||
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({
|
||||
name,
|
||||
password,
|
||||
encryptedValue: ciphertext,
|
||||
hashedHex,
|
||||
iv,
|
||||
tag,
|
||||
secretValue: secret,
|
||||
expiresAt,
|
||||
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
||||
accessType
|
||||
});
|
||||
|
||||
setSecretLink(
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||
hashedHex
|
||||
)}-${encodeURIComponent(key)}`
|
||||
);
|
||||
setSecretLink(`${window.location.origin}/shared/secret/${id}`);
|
||||
reset();
|
||||
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
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 = () => {
|
||||
const router = useRouter();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
const { hashedHex, key, id } = extractDetailsFromUrl(router);
|
||||
|
||||
const {
|
||||
data: fetchSecret,
|
||||
@@ -25,7 +44,7 @@ export const ViewSecretPublicPage = () => {
|
||||
isLoading,
|
||||
isFetching
|
||||
} = useGetActiveSharedSecretById({
|
||||
sharedSecretId: id as string,
|
||||
sharedSecretId: id,
|
||||
hashedHex,
|
||||
password
|
||||
});
|
||||
@@ -80,7 +99,7 @@ export const ViewSecretPublicPage = () => {
|
||||
)}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{!error && fetchSecret?.secret && key && (
|
||||
{!error && fetchSecret?.secret && (
|
||||
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
|
||||
)}
|
||||
{error && !isInvalidCredential && <SecretErrorContainer />}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||
|
||||
type Props = {
|
||||
secret: TViewSharedSecretResponse["secret"];
|
||||
secretKey: string;
|
||||
secretKey: string | null;
|
||||
};
|
||||
|
||||
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
@@ -25,6 +25,10 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
});
|
||||
|
||||
const decryptedSecret = useMemo(() => {
|
||||
if (secret.secretValue) {
|
||||
return secret.secretValue;
|
||||
}
|
||||
|
||||
if (secret && secret.encryptedValue && key) {
|
||||
const res = decryptSymmetric({
|
||||
ciphertext: secret.encryptedValue,
|
||||
|
||||
Reference in New Issue
Block a user