mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 19:28:33 +00:00
feat(secret-sharing): server-side encryption
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.string("iv").notNullable().alter();
|
||||||
|
t.string("tag").notNullable().alter();
|
||||||
|
t.string("encryptedValue").notNullable().alter();
|
||||||
|
|
||||||
|
t.dropColumn("encryptedSecret");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,15 @@
|
|||||||
|
|
||||||
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(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
@@ -22,7 +24,8 @@ 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()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
|||||||
@@ -917,7 +917,8 @@ export const registerRoutes = async (
|
|||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
|
|||||||
@@ -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,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
encryptedValue: z.string(),
|
secretValue: z.string(),
|
||||||
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().uuid(),
|
||||||
|
hashedHex: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,7 +117,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
...req.body,
|
...req.body,
|
||||||
accessType: SecretSharingAccessType.Anyone
|
accessType: SecretSharingAccessType.Anyone
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id, hashedHex: sharedSecret.hashedHex };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,17 +131,15 @@ 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().uuid(),
|
||||||
|
hashedHex: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -156,7 +153,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.body
|
...req.body
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id, hashedHex: sharedSecret.hashedHex };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
@@ -5,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
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,6 +22,7 @@ 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>;
|
||||||
@@ -26,7 +30,8 @@ export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServic
|
|||||||
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 +39,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
accessType,
|
accessType,
|
||||||
@@ -59,19 +61,28 @@ 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 = await kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
|
const encryptedSecret = await encryptWithRoot({
|
||||||
|
plainText: Buffer.from(secretValue)
|
||||||
|
});
|
||||||
|
const hashedHex = crypto.createHash("sha256").update(secretValue).digest("hex").substring(0, 13);
|
||||||
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({
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
encryptedValue: null,
|
||||||
|
|
||||||
|
encryptedSecret: encryptedSecret.cipherTextBlob,
|
||||||
|
hashedHex,
|
||||||
|
|
||||||
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,12 @@ export const secretSharingServiceFactory = ({
|
|||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id, hashedHex: newSharedSecret.hashedHex };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async ({
|
const createPublicSharedSecret = async ({
|
||||||
password,
|
password,
|
||||||
encryptedValue,
|
secretValue,
|
||||||
hashedHex,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
@@ -104,24 +112,33 @@ 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)
|
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)n
|
||||||
if (encryptedValue.length > 13000) {
|
if (secretValue.length > 10_000) {
|
||||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = await kmsService.encryptWithRootKey();
|
||||||
|
const encrypted = await encryptWithRoot({
|
||||||
|
plainText: Buffer.from(secretValue)
|
||||||
|
});
|
||||||
|
|
||||||
|
const hashedHex = crypto.createHash("sha256").update(secretValue).digest("hex").substring(0, 13);
|
||||||
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({
|
||||||
password: hashedPassword,
|
encryptedValue: null,
|
||||||
encryptedValue,
|
iv: null,
|
||||||
|
tag: null,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
iv,
|
encryptedSecret: encrypted.cipherTextBlob,
|
||||||
tag,
|
|
||||||
|
password: hashedPassword,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id, hashedHex: newSharedSecret.hashedHex };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async ({
|
const getSharedSecrets = async ({
|
||||||
@@ -222,6 +239,16 @@ 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 decrypt = await kmsService.decryptWithRootKey();
|
||||||
|
|
||||||
|
decryptedSecretValue = await decrypt({
|
||||||
|
cipherTextBlob: 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, sharedSecretId);
|
||||||
|
|
||||||
@@ -229,6 +256,9 @@ export const secretSharingServiceFactory = ({
|
|||||||
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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ export type TSharedSecret = {
|
|||||||
tag: string;
|
tag: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreatedSharedSecret = {
|
||||||
|
id: string;
|
||||||
|
hashedHex: 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 +30,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 +47,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 { id, hashedHex } = await createSharedSecret.mutateAsync({
|
||||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
|
||||||
plaintext: secret,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
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}-${hashedHex}`);
|
||||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
|
||||||
hashedHex
|
|
||||||
)}-${encodeURIComponent(key)}`
|
|
||||||
);
|
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
setCopyTextSecret("secret");
|
setCopyTextSecret("secret");
|
||||||
|
|||||||
@@ -1,23 +1,44 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
if (urlEncodedKey) {
|
||||||
|
const [hashedHex, key] = urlEncodedKey ? urlEncodedKey.toString().split("-") : ["", ""];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id as string,
|
||||||
|
hashedHex,
|
||||||
|
key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// its like this {uuid}-{hex} so example: idpart1-idpart2-idpart3-idpart4-hex
|
||||||
|
const extractedId = id?.toString().split("-").slice(0, 5).join("-");
|
||||||
|
const extractedHex = id?.toString().split("-").slice(5).join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: extractedId || "",
|
||||||
|
hashedHex: extractedHex || "",
|
||||||
|
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 +46,7 @@ export const ViewSecretPublicPage = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isFetching
|
isFetching
|
||||||
} = useGetActiveSharedSecretById({
|
} = useGetActiveSharedSecretById({
|
||||||
sharedSecretId: id as string,
|
sharedSecretId: id,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
password
|
password
|
||||||
});
|
});
|
||||||
@@ -80,7 +101,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user