mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 09:30:09 +00:00
Merge pull request #3973 from Infisical/daniel/fips-initative
feat(fips): native FIPS support
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ export type TServerConfig = {
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
fipsEnabled: boolean;
|
||||
envOverrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
export {
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useRemoveMyDuplicateAccounts,
|
||||
useRevokeMySessionById,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
-8
@@ -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(
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-28
@@ -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,
|
||||
|
||||
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
-93
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user