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
};