Merge pull request #3973 from Infisical/daniel/fips-initative

feat(fips): native FIPS support
This commit is contained in:
Daniel Hougaard
2025-07-15 19:03:51 +04:00
committed by GitHub
167 changed files with 3503 additions and 1619 deletions
+10 -13
View File
@@ -5,9 +5,8 @@ import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
@@ -16,7 +15,7 @@ import { onRequestError } from "@app/hooks/api/reactQuery";
import InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/password/checkPassword";
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "../utilities/cryptography/crypto";
import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2";
@@ -77,6 +76,7 @@ export default function UserInfoStep({
}: UserInfoStepProps): JSX.Element {
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
const { config } = useServerConfig();
const [errors, setErrors] = useState<Errors>({});
@@ -109,12 +109,9 @@ export default function UserInfoStep({
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
localStorage.setItem("PRIVATE_KEY", privateKey);
const pair = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", pair.privateKey);
client.init(
{
@@ -145,7 +142,7 @@ export default function UserInfoStep({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
text: pair.privateKey,
secret: key
});
@@ -168,7 +165,7 @@ export default function UserInfoStep({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
publicKey: pair.publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
@@ -189,11 +186,11 @@ export default function UserInfoStep({
}
saveTokenToLocalStorage({
publicKey,
publicKey: pair.publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
privateKey: pair.privateKey
});
const userOrgs = await fetchOrganizations();
@@ -2,48 +2,55 @@
// eslint-disable-next-line
import argon2 from "argon2-browser/dist/argon2-bundled.min.js";
import nacl from "tweetnacl";
import { decodeBase64, decodeUTF8, encodeBase64, encodeUTF8 } from "tweetnacl-util";
import { encodeBase64 as naclEncodeBase64 } from "tweetnacl-util";
import aes from "./aes-256-gcm";
/**
* Return new base64, NaCl, public-private key pair.
* @returns {Object} obj
* @returns {String} obj.publicKey - base64, NaCl, public key
* @returns {String} obj.privateKey - base64, NaCl, private key
*/
const generateKeyPair = () => {
const encodeBase64 = (uint8Array: Uint8Array) => btoa(String.fromCharCode(...uint8Array));
const decodeBase64 = (base64String: string) =>
new Uint8Array([...atob(base64String)].map((c) => c.charCodeAt(0)));
const generateKeyPair = async (fipsEnabled: boolean) => {
if (fipsEnabled) {
if (!crypto || !crypto.subtle) {
throw new Error("Web Crypto API not available");
}
// browser version of how the key format we expect on the backend for asymmetric encryption
const result = await crypto.subtle.generateKey(
{
name: "X25519"
},
true, // extractable
["deriveKey", "deriveBits"]
);
// Type guard
if (!("publicKey" in result)) {
throw new Error("Expected CryptoKeyPair but got CryptoKey");
}
const keyPair = result as CryptoKeyPair;
const publicKeyBytes = await crypto.subtle.exportKey("spki", keyPair.publicKey);
const privateKeyBytes = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
return {
publicKey: btoa(String.fromCharCode(...new Uint8Array(publicKeyBytes))),
privateKey: btoa(String.fromCharCode(...new Uint8Array(privateKeyBytes)))
};
}
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = naclEncodeBase64(secretKeyUint8Array);
const publicKey = naclEncodeBase64(publicKeyUint8Array);
return {
publicKey: encodeBase64(pair.publicKey),
privateKey: encodeBase64(pair.secretKey)
publicKey,
privateKey
};
};
type EncryptAsymmetricProps = {
plaintext: string;
publicKey: string;
privateKey: string;
};
/**
* Verify that private key [privateKey] is the one that corresponds to
* the public key [publicKey]
* @param {Object}
* @param {String} - base64-encoded Nacl private key
* @param {String} - base64-encoded Nacl public key
*/
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
const derivedPublicKey = encodeBase64(
nacl.box.keyPair.fromSecretKey(decodeBase64(privateKey)).publicKey
);
if (derivedPublicKey !== publicKey) {
throw new Error("Failed to verify private key");
}
};
/**
* Derive a key from password [password] and salt [salt] using Argon2id
* @param {Object} obj
@@ -88,71 +95,6 @@ const deriveArgonKey = async ({
return derivedKey;
};
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where
* [publicKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.plaintext - plaintext to encrypt
* @param {String} obj.publicKey - public key of the recipient
* @param {String} obj.privateKey - private key of the sender (current user)
* @returns {Object} obj
* @returns {String} ciphertext - base64-encoded ciphertext
* @returns {String} nonce - base64-encoded nonce
*/
const encryptAssymmetric = ({
plaintext,
publicKey,
privateKey
}: EncryptAsymmetricProps): {
ciphertext: string;
nonce: string;
} => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
decodeUTF8(plaintext),
nonce,
decodeBase64(publicKey),
decodeBase64(privateKey)
);
return {
ciphertext: encodeBase64(ciphertext),
nonce: encodeBase64(nonce)
};
};
type DecryptAsymmetricProps = {
ciphertext: string;
nonce: string;
publicKey: string;
privateKey: string;
};
/**
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
* [privateKey] likely belongs to the recipient.
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.nonce - nonce
* @param {String} obj.publicKey - base64-encoded public key of the sender
* @param {String} obj.privateKey - base64-encoded private key of the receiver (current user)
*/
const decryptAssymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey
}: DecryptAsymmetricProps): string => {
const plaintext = nacl.box.open(
decodeBase64(ciphertext),
decodeBase64(nonce),
decodeBase64(publicKey),
decodeBase64(privateKey)
);
return encodeUTF8(plaintext!);
};
type EncryptSymmetricProps = {
plaintext: string;
key: string;
@@ -226,11 +168,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
};
export {
decryptAssymmetric,
decodeBase64,
decryptSymmetric,
deriveArgonKey,
encryptAssymmetric,
encodeBase64,
encryptSymmetric,
generateKeyPair,
verifyPrivateKey
generateKeyPair
};
+1
View File
@@ -49,6 +49,7 @@ export type TServerConfig = {
authConsentContent?: string;
pageFrameContent?: string;
invalidatingCache: boolean;
fipsEnabled: boolean;
envOverrides?: Record<string, string>;
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { encodeBase64 } from "tweetnacl-util";
import { encodeBase64 } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { cmekKeys } from "@app/hooks/api/cmeks/queries";
import {
@@ -1,15 +1,9 @@
/* eslint-disable no-param-reassign */
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecret, SecretType, SecretV3RawSanitized } from "../secrets/types";
import {
TGetSecretApprovalRequestCount,
TGetSecretApprovalRequestDetails,
@@ -41,79 +35,6 @@ export const secretApprovalRequestKeys = {
]
};
export const decryptSecrets = (
encryptedSecrets: EncryptedSecret[],
decryptFileKey: UserWsKeyPair
) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const personalSecrets: Record<string, { id: string; value: string }> = {};
const secrets: SecretV3RawSanitized[] = [];
encryptedSecrets.forEach((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret: SecretV3RawSanitized = {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
secretValueHidden: encSecret.secretValueHidden,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
reminderRepeatDays: encSecret.secretReminderRepeatDays,
reminderNote: encSecret.secretReminderNote,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version,
skipMultilineEncoding: encSecret.skipMultilineEncoding
};
if (encSecret.type === SecretType.Personal) {
personalSecrets[decryptedSecret.key] = {
id: encSecret.id,
value: secretValue
};
} else {
secrets.push(decryptedSecret);
}
});
secrets.forEach((sec) => {
if (personalSecrets?.[sec.key]) {
sec.idOverride = personalSecrets[sec.key].id;
sec.valueOverride = personalSecrets[sec.key].value;
sec.overrideAction = "modified";
}
});
return secrets;
};
const fetchSecretApprovalRequestList = async ({
workspaceId,
environment,
+1 -1
View File
@@ -27,7 +27,7 @@ export * from "./secrets/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/types";
export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
export type { OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
export type { TWebhook } from "./webhooks/types";
export type {
CreateEnvironmentDTO,
-1
View File
@@ -1,5 +1,4 @@
export {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useRemoveMyDuplicateAccounts,
useRevokeMySessionById,
+1 -42
View File
@@ -1,51 +1,10 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { userKeys } from "./query-keys";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsE2EE = () => {
const queryClient = useQueryClient();
return useMutation<object, object, AddUserToWsDTOE2EE>({
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce,
publicKey: decryptKey.sender.publicKey,
privateKey: userPrivateKey
});
const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
plaintext: key,
publicKey: userPublicKey,
privateKey: userPrivateKey
});
return {
orgMembershipId,
workspaceEncryptedKey: inviteeCipherText,
workspaceEncryptedNonce: inviteeNonce
};
});
const { data } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/memberships`, {
members: newWsMembers
});
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(workspaceId) });
}
});
};
import { AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient();
-11
View File
@@ -1,5 +1,4 @@
import { MfaMethod } from "../auth/types";
import { UserWsKeyPair } from "../keys/types";
import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types";
export enum AuthMethod {
@@ -129,16 +128,6 @@ export type TWorkspaceUser = {
deniedPermissions: any[];
};
export type AddUserToWsDTOE2EE = {
workspaceId: string;
decryptKey: UserWsKeyPair;
userPrivateKey: string;
members: {
orgMembershipId: string;
userPublicKey: string;
}[];
};
export type AddUserToWsDTONonE2EE = {
projectId: string;
usernames: string[];
+7 -9
View File
@@ -1,11 +1,9 @@
import crypto from "crypto";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
export const generateUserBackupKey = async (email: string, password: string) => {
@@ -54,15 +52,15 @@ export const generateUserBackupKey = async (email: string, password: string) =>
return generatedKey;
};
export const generateUserPassKey = async (email: string, password: string) => {
export const generateUserPassKey = async (
email: string,
password: string,
fipsEnabled: boolean
) => {
// eslint-disable-next-line new-cap
const client = new jsrp.client();
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
const { publicKey, privateKey } = await generateKeyPair(fipsEnabled);
await new Promise((resolve) => {
client.init({ username: email, password }, () => resolve(null));
@@ -1,12 +1,14 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useServerConfig, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useGetServerRootKmsEncryptionDetails,
@@ -29,6 +31,7 @@ export const EncryptionPageForm = () => {
const { data: rootKmsDetails } = useGetServerRootKmsEncryptionDetails();
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
const { config } = useServerConfig();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -117,14 +120,27 @@ export const EncryptionPageForm = () => {
)}
</div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
<div className="flex w-full items-center justify-between">
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
{config.fipsEnabled && (
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
<div>
<Badge className="flex items-center gap-2" variant="primary">
FIPS Mode: Enabled
<FontAwesomeIcon icon={faInfoCircle} />
</Badge>
</div>
</Tooltip>
)}
</div>
</form>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
@@ -49,7 +49,11 @@ export const SignUpPage = () => {
// avoid multi submission
if (isSubmitting) return;
try {
const { privateKey, ...userPass } = await generateUserPassKey(email, password);
const { privateKey, ...userPass } = await generateUserPassKey(
email,
password,
config.fipsEnabled
);
const res = await createAdminUser({
email,
password,
@@ -8,17 +8,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { Mfa } from "@app/components/auth/Mfa";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useToggle } from "@app/hooks";
import {
completeAccountSignupInvite,
@@ -68,6 +67,7 @@ export const SignupInvitePage = () => {
const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { config } = useServerConfig();
const loggedIn = isLoggedIn();
@@ -95,11 +95,7 @@ export const SignupInvitePage = () => {
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", privateKey);
@@ -4,15 +4,14 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { Mfa } from "@app/components/auth/Mfa";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
@@ -65,6 +64,7 @@ export const UserInfoSSOStep = ({
const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const navigate = useNavigate();
const { config } = useServerConfig();
useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -94,11 +94,7 @@ export const UserInfoSSOStep = ({
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", privateKey);
client.init(
@@ -51,7 +51,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isPending &&
caCerts?.map((caCert, index) => {
caCerts?.map?.((caCert, index) => {
const isLastItem = index === caCerts.length - 1;
const caCertObj = new x509.X509Certificate(caCert.certificate);
return (
@@ -3,10 +3,10 @@ import { useForm } from "react-hook-form";
import { faCheckCircle, faCopy, faInfoCircle, faLockOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { decodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
import {
Button,
FormControl,
@@ -2,10 +2,10 @@ import { Controller, useForm } from "react-hook-form";
import { faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { decodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
import {
Badge,
Button,
@@ -3,35 +3,8 @@ import crypto from "crypto";
import { NavigateFn } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { localStorageService } from "@app/helpers/localStorage";
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: botPublicKey,
privateKey: PRIVATE_KEY
});
return { encryptedKey: ciphertext, nonce };
};
import { TCloudIntegration } from "@app/hooks/api/types";
export const createIntegrationMissingEnvVarsNotification = (
slug: string,
@@ -1,18 +1,12 @@
import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
import { SecretSharingSection } from "../SecretSharingSection";
import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection";
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
const { currentWorkspace } = useWorkspace();
return (
<div>
<EnvironmentSection />
@@ -22,7 +16,6 @@ export const ProjectGeneralTab = () => {
<SecretSnapshotsLegacySection />
<PointInTimeVersionLimitSection />
<BackfillSecretReferenceSecretion />
{currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
</div>
);
};
@@ -1,93 +0,0 @@
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useNameWorkspaceSecrets } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
export const RebuildSecretIndicesSection = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const [isIndexing, setIsIndexing] = useToggle();
const { data: decryptFileKey } = useGetUserWsKey(currentWorkspace.id);
if (!currentWorkspace) return null;
const onRebuildIndices = async () => {
if (!currentWorkspace?.id) return;
setIsIndexing.on();
try {
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id);
if (!currentWorkspace || !decryptFileKey) {
return;
}
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
secretId: encryptedSecret.id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace.id,
secretsToUpdate
});
createNotification({
text: "Successfully rebuilt secret indices",
type: "success"
});
} catch (err) {
console.log(err);
} finally {
setIsIndexing.off();
}
};
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
if (!isAdmin) {
return null;
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Rebuild Secret Indices</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
This will rebuild indices of all secrets in the project.
</p>
<Button
variant="outline_bg"
isLoading={isIndexing}
onClick={onRebuildIndices}
isDisabled={!isAdmin}
>
Rebuild Secret Indices
</Button>
</div>
);
};