Merge pull request #339 from Infisical/mfa

MFA
This commit is contained in:
Maidul Islam
2023-02-19 18:33:11 -05:00
committed by GitHub
100 changed files with 3513 additions and 1734 deletions
+147
View File
@@ -0,0 +1,147 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
/**
* 1st step of login - user enters their username and password
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {Function} obj.setEmail - function to set the email of user
* @param {String} obj.password - password of user
* @param {String} obj.setPassword - function to set the password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function LoginStep ({
email,
setEmail,
password,
setPassword,
setStep
}: {
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin(email, password);
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={async () => handleLogin()}
loading={isLoading}
size="lg"
/>
</div>
</div>
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
+158
View File
@@ -0,0 +1,158 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState } from 'react';
import ReactCodeInput from 'react-code-input';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useSendMfaToken } from '@app/hooks/api/auth';
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
// The style for the verification code input
const props = {
inputStyle: {
fontFamily: 'monospace',
margin: '4px',
MozAppearance: 'textfield',
width: '55px',
borderRadius: '5px',
fontSize: '24px',
height: '55px',
paddingLeft: '7',
backgroundColor: '#0d1117',
color: 'white',
border: '1px solid #2d2f33',
textAlign: 'center',
outlineColor: '#8ca542',
borderColor: '#2d2f33'
}
} as const;
interface VerifyMfaTokenError {
response: {
data: {
context: {
code: string;
triesLeft: number;
}
},
status: number;
}
}
/**
* 2nd step of login - users enter their MFA code
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.password - password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function MFAStep({
email,
password
}: {
email: string;
password: string;
}): JSX.Element {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [mfaCode, setMfaCode] = useState('');
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
const { t } = useTranslation();
const sendMfaToken = useSendMfaToken();
const handleLoginMfa = async () => {
try {
if (mfaCode.length !== 6) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
mfaToken: mfaCode
});
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
const error = err as VerifyMfaTokenError;
if (error?.response?.status === 500) {
window.location.reload();
} else if (error?.response?.data?.context?.triesLeft) {
setTriesLeft(error?.response?.data?.context?.triesLeft);
if (error.response.data.context.triesLeft === 0) {
window.location.reload();
}
}
setIsLoading(false);
}
}
const handleResendMfaCode = async () => {
try {
await sendMfaToken.mutateAsync({ email });
} catch (err) {
console.error(err);
}
}
return (
<form className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">{t('mfa:step2-message')}</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
<div className="hidden md:block">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
{...props}
className="mt-6 mb-2"
/>
</div>
{typeof triesLeft === 'number' && <Error text={`${t('mfa:step2-code-error')} ${triesLeft}`} />}
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button
text={t('mfa:verify') ?? ''}
onButtonPressed={() => handleLoginMfa()}
size="lg"
/>
</div>
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">{t('mfa:step2-resend-alert')}</span>
<u
className={`font-normal ${
isLoading
? 'text-bunker-400'
: 'text-primary-700 hover:text-primary duration-200'
}`}
>
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
{isLoading
? t('mfa:step2-resend-progress')
: t('mfa:step2-resend-submit')}
</button>
</u>
</div>
<p className="text-sm text-bunker-400 pb-2">{t('mfa:step2-spam-alert')}</p>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'mfa']);
@@ -87,7 +87,6 @@ export default function Navbar() {
}, []);
const closeApp = async () => {
console.log('Logging out...');
await logout();
router.push('/login');
};
@@ -31,7 +31,7 @@ export default function DonwloadBackupPDFStep({
return (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<p className="text-4xl text-center font-semibold flex justify-center text-primary">
{t('signup:step4-message')}
</p>
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
@@ -42,7 +42,7 @@ export default function DonwloadBackupPDFStep({
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t('signup:step4-description3')}
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-4 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button
text="Download PDF"
onButtonPressed={async () => {
+93 -44
View File
@@ -1,5 +1,6 @@
import crypto from 'crypto';
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -8,18 +9,21 @@ import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
import attemptLogin from '../utilities/attemptLogin';
import passwordCheck from '../utilities/checks/PasswordCheck';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
import SecurityClient from '../utilities/SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps {
verificationToken: string;
incrementStep: () => void;
email: string;
password: string;
@@ -44,7 +48,6 @@ interface UserInfoStepProps {
* @param {string} obj.setLastName - function managing the state of user's last name
*/
export default function UserInfoStep({
verificationToken,
incrementStep,
email,
password,
@@ -62,7 +65,6 @@ export default function UserInfoStep({
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const router = useRouter();
// Verifies if the information that the users entered (name, workspace)
// is there, and if the password matches the criteria.
@@ -94,17 +96,9 @@ export default function UserInfoStep({
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
localStorage.setItem('PRIVATE_KEY', privateKey);
client.init(
{
@@ -113,35 +107,90 @@ export default function UserInfoStep({
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
organizationName: `${firstName}'s organization`,
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken
});
try {
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
organizationName: `${firstName}'s organization`
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(response.token);
try {
await attemptLogin(email, password, () => {}, router, true, false);
incrementStep();
} catch (error) {
setIsLoading(false);
}
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({
organizationId: orgId,
projectName: 'Example Project'
});
localStorage.setItem('orgData.id', orgId);
localStorage.setItem('projectData.id', project._id);
incrementStep();
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
@@ -153,7 +202,7 @@ export default function UserInfoStep({
return (
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-primary">
{t('signup:step3-message')}
</p>
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
@@ -258,4 +307,4 @@ export default function UserInfoStep({
</div>
</div>
);
}
}
@@ -1,7 +1,19 @@
import { getAuthToken, setAuthToken } from '@app/reactQuery';
import {
getAuthToken,
setAuthToken,
setMfaTempToken,
setSignupTempToken} from '@app/reactQuery';
// depreciated: go for apiRequest module in config/api
export default class SecurityClient {
static setSignupToken(tokenStr: string) {
setSignupTempToken(tokenStr);
}
static setMfaToken(tokenStr: string) {
setMfaTempToken(tokenStr);
}
static setToken(tokenStr: string) {
setAuthToken(tokenStr);
}
+99 -189
View File
@@ -1,20 +1,12 @@
/* eslint-disable prefer-destructuring */
import crypto from 'crypto';
import jsrp from 'jsrp';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import login1 from '@app/pages/api/auth/Login1';
import login2 from '@app/pages/api/auth/Login2';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import getUser from '@app/pages/api/user/getUser';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import KeyService from '@app/services/KeyService';
import { encryptAssymmetric } from './cryptography/crypto';
import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
@@ -22,212 +14,130 @@ import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean;
}
/**
* This function logs in the user (whether it's right after signup, or a normal login)
* @param {string} email - email of the user logging in
* @param {string} password - password of the user logging in
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
* @param {*} router
* @param {boolean} isSignUp - whether this log in is a part of signup
* @param {boolean} isLogin - ?
* @returns
* Return whether or not login is successful for user with email [email]
* and password [password]
* @param {string} email - email of user to log in
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
email: string,
password: string,
setErrorLogin: (value: boolean) => void,
router: any,
isSignUp: boolean,
isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
password: string
): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
const clientPublicKey = client.getPublicKey();
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
// if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2(
email,
clientProof
);
if (mfaEnabled) {
// case: MFA is enabled
SecurityClient.setToken(token);
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
const privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
orgToLogin = localStorage.getItem('orgData.id');
} else {
orgToLogin = userOrgsData[0];
localStorage.setItem('orgData.id', orgToLogin);
}
let orgUserProjects = await getOrganizationUserProjects({
orgId: orgToLogin
});
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
let projectToLogin;
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
projectToLogin = localStorage.getItem('projectData.id');
} else {
try {
projectToLogin = orgUserProjects[0];
localStorage.setItem('projectData.id', projectToLogin);
} catch (error) {
console.log('ERROR: User likely has no projects. ', error);
}
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
if (isSignUp) {
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY
}) as { ciphertext: string; nonce: string };
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
const secretsToBeAdded: SecretDataProps[] = [
{
pos: 0,
key: 'DATABASE_URL',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: String(localStorage.getItem('projectData.id')),
env: 'dev'
resolve({
mfaEnabled,
success: true
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: String(localStorage.getItem('projectData.id'))
} else if (
!mfaEnabled &&
encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
if (orgUserProjects.length > 0) {
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
resolve({
mfaEnabled: false,
success: true
});
}
if (isLogin) {
if (localStorage.getItem('projectData.id') !== "undefined") {
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
} else {
router.push("/noprojects");
}
}
} catch (error) {
console.log(error);
setErrorLogin(true);
console.log('Login response not available');
} catch (err) {
reject(err);
}
}
);
} catch (error) {
console.log('Something went wrong during authentication');
}
return true;
});
};
export default attemptLogin;
export default attemptLogin;
@@ -0,0 +1,103 @@
/* eslint-disable prefer-destructuring */
import jsrp from 'jsrp';
import login1 from '@app/pages/api/auth/Login1';
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import KeyService from '@app/services/KeyService';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
mfaToken
}: {
email: string;
password: string;
mfaToken: string;
}): Promise<Boolean> => {
return new Promise((resolve, reject) => {
client.init({
username: email,
password
}, async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1(email, clientPublicKey);
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken('');
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
resolve(true);
} catch (err) {
reject(err);
}
});
});
}
export default attemptLoginMfa;
@@ -9,14 +9,14 @@ const BLOCK_SIZE_BYTES = 16; // 128 bit
interface EncryptProps {
text: string;
secret: string;
secret: string | Buffer;
}
interface DecryptProps {
ciphertext: string;
iv: string;
tag: string;
secret: string;
secret: string | Buffer;
}
interface EncryptOutputProps {
@@ -1,14 +1,20 @@
/* eslint-disable new-cap */
import crypto from 'crypto';
import jsrp from 'jsrp';
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
import SRP1 from '@app/pages/api/auth/SRP1';
import { saveTokenToLocalStorage } from '../saveTokenToLocalStorage';
import Aes256Gcm from './aes-256-gcm';
import { deriveArgonKey } from './crypto';
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
// TODO: modify this function
/**
* This function loggs in the user (whether it's right after signup, or a normal login)
* @param {*} email
@@ -63,43 +69,75 @@ const changePassword = async (
},
async () => {
clientNewPassword.createVerifier(async (err, result) => {
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem('PRIVATE_KEY') as string,
secret: newPassword
.slice(0, 32)
.padStart(
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
'0'
)
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
if (ciphertext) {
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
let res;
try {
res = await changePassword2({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
});
let res;
try {
res = await changePassword2({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
if (res && res.status === 400) {
setCurrentPasswordError(true);
} else if (res && res.status === 200) {
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
}
} catch (error) {
if (res && res.status === 400) {
setCurrentPasswordError(true);
console.log(error);
} else if (res && res.status === 200) {
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
}
} catch (error) {
setCurrentPasswordError(true);
console.log(error);
}
});
}
@@ -1,3 +1,5 @@
import argon2 from 'argon2-browser';
import aes from './aes-256-gcm';
const nacl = require('tweetnacl');
@@ -9,6 +11,50 @@ type EncryptAsymmetricProps = {
privateKey: string;
};
/**
* Derive a key from password [password] and salt [salt] using Argon2id
* @param {Object} obj
* @param {String} obj.password - password to derive key from
* @param {String} obj.salt - salt to derive key from
* @param {Number} obj.mem - used memory, in KiB
* @param {Number} obj.time - number of iterations
* @param {Number} obj.parallelism - desired parallelism
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
* @returns
*/
const deriveArgonKey = async ({
password,
salt,
mem,
time,
parallelism,
hashLen
}: {
password: string;
salt: string;
mem: number;
time: number;
parallelism: number;
hashLen: number;
}) => {
let derivedKey;
try {
derivedKey = await argon2.hash({
pass: password,
salt,
type: argon2.ArgonType.Argon2id,
mem,
time,
parallelism,
hashLen
});
} catch (err) {
console.error(err);
}
return derivedKey;
}
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where
* [publicKey] likely belongs to the recipient.
@@ -138,4 +184,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
return plaintext;
};
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
export {
decryptAssymmetric,
decryptSymmetric,
deriveArgonKey,
encryptAssymmetric,
encryptSymmetric
};
@@ -1,12 +1,18 @@
interface Props {
publicKey: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
privateKey: string;
privateKey?: string;
}
export const saveTokenToLocalStorage = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
@@ -14,11 +20,38 @@ export const saveTokenToLocalStorage = ({
privateKey,
}: Props) => {
try {
localStorage.setItem("publicKey", publicKey);
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
if (protectedKey) {
localStorage.setItem("protectedKey", protectedKey);
}
if (protectedKeyIV) {
localStorage.setItem("protectedKeyIV", protectedKeyIV);
}
if (protectedKeyTag) {
localStorage.setItem("protectedKeyTag", protectedKeyTag);
}
if (publicKey) {
localStorage.setItem("publicKey", publicKey);
}
if (privateKey) {
localStorage.setItem("PRIVATE_KEY", privateKey);
}
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
localStorage.setItem("PRIVATE_KEY", privateKey);
} catch (err) {
if (err instanceof Error) {
throw new Error(
+14 -2
View File
@@ -1,6 +1,9 @@
import axios from 'axios';
import { getAuthToken } from '@app/reactQuery';
import {
getAuthToken,
getMfaTempToken,
getSignupTempToken} from '@app/reactQuery';
export const apiRequest = axios.create({
baseURL: '/',
@@ -10,8 +13,17 @@ export const apiRequest = axios.create({
});
apiRequest.interceptors.request.use((config) => {
const signupTempToken = getSignupTempToken();
const mfaTempToken = getMfaTempToken();
const token = getAuthToken();
if (token && config.headers) {
if (signupTempToken && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${signupTempToken}`;
} else if (mfaTempToken && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${mfaTempToken}`;
} else if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
+85
View File
@@ -0,0 +1,85 @@
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
/**
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
*/
const decryptPrivateKeyHelper = async ({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) => {
let privateKey;
try {
if (encryptionVersion === 1) {
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
const derivedKey = await deriveArgonKey({
password,
salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to generate derived key');
const key = Aes256Gcm.decrypt({
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag,
secret: Buffer.from(derivedKey.hash)
});
// decrypt back the private key
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: Buffer.from(key, 'hex')
});
} else {
throw new Error('Insufficient details to decrypt private key');
}
} catch (err) {
console.error(err);
throw new Error('Failed to decrypt private key');
}
return privateKey;
}
export { decryptPrivateKeyHelper };
+140
View File
@@ -0,0 +1,140 @@
import crypto from 'crypto';
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getUser from '@app/pages/api/user/getUser';
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
const secretsToBeAdded = [
{
pos: 0,
key: 'DATABASE_URL',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
const initProjectHelper = async ({
organizationId,
projectName
}: {
organizationId: string;
projectName: string;
}) => {
let project;
try {
// create new project
project = await createWorkspace({
workspaceName: projectName,
organizationId
});
// create and upload new (encrypted) project key
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) throw new Error('Failed to find private key');
const user = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadKeys(project._id, user._id, ciphertext, nonce);
// encrypt and upload secrets to new project
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: project._id,
env: 'dev'
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: project._id
});
} catch (err) {
console.error('Failed to init project in organization', err);
}
return project;
}
export {
initProjectHelper
}
+4 -1
View File
@@ -1 +1,4 @@
export { useGetAuthToken } from './queries';
export {
useGetAuthToken,
useSendMfaToken,
useVerifyMfaToken} from './queries'
+27 -2
View File
@@ -1,14 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { GetAuthTokenAPI } from './types';
import {
GetAuthTokenAPI,
SendMfaTokenDTO,
VerifyMfaTokenDTO,
VerifyMfaTokenRes} from './types';
const authKeys = {
getAuthToken: ['token'] as const
};
export const useSendMfaToken = () => {
return useMutation<{}, {}, SendMfaTokenDTO>({
mutationFn: async ({ email }) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/send', { email });
return data;
}
});
}
export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
mutationFn: async ({ email, mfaCode }) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
email,
mfaToken: mfaCode
});
return data;
}
});
}
// Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls
const fetchAuthToken = async () => {
+21
View File
@@ -1,3 +1,24 @@
export type GetAuthTokenAPI = {
token: string;
};
export type SendMfaTokenDTO = {
email: string;
}
export type VerifyMfaTokenDTO = {
email: string;
mfaCode: string;
}
export type VerifyMfaTokenRes = {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
+14 -6
View File
@@ -1,15 +1,23 @@
import { UserWsKeyPair } from '../keys/types';
export type User = {
seenIps: string[];
_id: string;
email: string;
createdAt: Date;
updatedAt: Date;
__v: number;
firstName: string;
lastName: string;
email?: string;
firstName?: string;
lastName?: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
isMfaEnabled: boolean;
seenIps: string[];
_id: string;
__v: number;
};
export type OrgUser = {
+1 -1
View File
@@ -1,3 +1,3 @@
export { useLeaveConfirm } from './useLeaveConfirm';
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
export { useLeaveConfirm } from './useLeaveConfirm';
@@ -93,6 +93,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
@@ -75,8 +75,18 @@ export const Navbar = () => {
const closeApp = async () => {
try {
console.log('Logging out...');
console.log('Logging out...')
await logout.mutateAsync();
localStorage.removeItem('protectedKey');
localStorage.removeItem('protectedKeyIV');
localStorage.removeItem('protectedKeyTag');
localStorage.removeItem('publicKey');
localStorage.removeItem('encryptedPrivateKey');
localStorage.removeItem('iv');
localStorage.removeItem('tag');
localStorage.removeItem('PRIVATE_KEY');
localStorage.removeItem('orgData.id');
localStorage.removeItem('projectData.id');
router.push('/login');
} catch (error) {
console.error(error);
+22 -6
View File
@@ -1,12 +1,15 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
clientProof: string;
}
/**
@@ -14,7 +17,17 @@ interface Props {
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
const changePassword2 = ({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: Props) =>
SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST',
headers: {
@@ -22,9 +35,12 @@ const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientP
},
body: JSON.stringify({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
@@ -1,15 +1,20 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
organizationName: string;
iv: string;
tag: string;
salt: string;
verifier: string;
token: string;
}
/**
@@ -19,6 +24,9 @@ interface Props {
* @param {string} obj.email - email of the user completing signup
* @param {string} obj.firstName - first name of the user completing signup
* @param {string} obj.lastName - last name of the user completing sign up
* @param {string} obj.protectedKey - protected key in encryption version 2
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
* @param {string} obj.publicKey - public key of the user completing signup
* @param {string} obj.ciphertext
@@ -26,39 +34,40 @@ interface Props {
* @param {string} obj.tag
* @param {string} obj.salt
* @param {string} obj.verifier
* @param {string} obj.token - token that confirms a user's identity
* @returns
*/
const completeAccountInformationSignup = ({
const completeAccountInformationSignup = async ({
email,
firstName,
lastName,
organizationName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token}`
},
body: JSON.stringify({
email,
firstName,
lastName,
publicKey,
encryptedPrivateKey: ciphertext,
organizationName,
iv,
tag,
salt,
verifier
})
organizationName
}: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
});
return data;
}
export default completeAccountInformationSignup;
@@ -1,16 +1,24 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
iv: string;
tag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
}
// missing token?
// TODO: add to SecurityClient
/**
* This function is called in the end of the signup process.
* It sends all the necessary nformation to the server.
@@ -27,34 +35,36 @@ interface Props {
* @param {string} obj.token - token that confirms a user's identity
* @returns
*/
const completeAccountInformationSignupInvite = ({
const completeAccountInformationSignupInvite = async ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token}`
},
body: JSON.stringify({
email,
firstName,
lastName,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
salt,
verifier
})
verifier
}: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/invite', {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
return data;
}
export default completeAccountInformationSignupInvite;
+2 -2
View File
@@ -10,7 +10,7 @@ interface Login1 {
* @returns
*/
const login1 = async (email: string, clientPublicKey: string) => {
const response = await fetch("/api/v1/auth/login1", {
const response = await fetch("/api/v2/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -25,7 +25,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
const data = (await response.json()) as unknown as Login1;
return data;
}
throw new Error("Wrong password");
};
+10 -5
View File
@@ -1,9 +1,14 @@
interface Login2Response {
encryptedPrivateKey: string;
iv: string;
publicKey: string;
tag: string;
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
/**
@@ -13,7 +18,7 @@ interface Login2Response {
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const response = await fetch('/api/v1/auth/login2', {
const response = await fetch('/api/v2/auth/login2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+28 -17
View File
@@ -4,27 +4,38 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
* This route logs the user out. Note: the user should authorized to do this.
* We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
*/
const logout = async () =>
SecurityClient.fetchCall('/api/v1/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
}).then((res) => {
const logout = async () => {
try {
const res = await SecurityClient.fetchCall('/api/v1/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (res?.status === 200) {
SecurityClient.setToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem('publicKey', '');
localStorage.setItem('encryptedPrivateKey', '');
localStorage.setItem('iv', '');
localStorage.setItem('tag', '');
localStorage.setItem('PRIVATE_KEY', '');
console.log('User logged out', res);
localStorage.removeItem('protectedKey');
localStorage.removeItem('protectedKeyIV');
localStorage.removeItem('protectedKeyTag');
localStorage.removeItem('publicKey');
localStorage.removeItem('encryptedPrivateKey');
localStorage.removeItem('iv');
localStorage.removeItem('tag');
localStorage.removeItem('PRIVATE_KEY');
localStorage.removeItem('orgData.id');
localStorage.removeItem('projectData.id');
return res;
}
console.log('Failed to log out');
return undefined;
});
} catch (error) {
console.log('Error logging out', error);
}
return undefined;
};
export default logout;
@@ -1,10 +1,13 @@
interface Props {
verificationToken: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
verificationToken: string;
}
/**
@@ -19,22 +22,28 @@ interface Props {
* @returns
*/
const resetPasswordOnAccountRecovery = ({
verificationToken,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
verifier,
verificationToken,
}: Props) => fetch('/api/v1/password/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ verificationToken}`
Authorization: `Bearer ${verificationToken}`
},
body: JSON.stringify({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
@@ -0,0 +1,25 @@
import { apiRequest } from "@app/config/request";
/**
* Verify MFA token [mfaToken] for user with email [email]
* @param {object} obj
* @param {string} obj.email - email of user
* @param {string} obj.mfaToken - MFA cod/token to verify
* @returns
*/
const verifyMfaToken = async ({
email,
mfaToken
}: {
email: string;
mfaToken: string;
}) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
email,
mfaToken
});
return data;
}
export default verifyMfaToken;
@@ -0,0 +1,32 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
isMfaEnabled: boolean;
}
/**
* Update the user's MFA-enabled status to [isMfaEnabled]
* @param {Object} obj
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
* @returns {User} user - user with updated MFA-enabled status
*/
const updateMyMfaEnabled = async ({
isMfaEnabled
}: Props) =>
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isMfaEnabled,
})
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).user;
}
console.log('Failed to update MFA status');
return undefined;
});
export default updateMyMfaEnabled;
+31 -104
View File
@@ -4,28 +4,23 @@ import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox';
import attemptLogin from '@app/components/utilities/attemptLogin';
import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const router = useRouter();
const [step, setStep] = useState(1);
const { t } = useTranslation();
const lang = router.locale ?? 'en';
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
@@ -45,29 +40,34 @@ export default function Login() {
}
};
if (isLoggedIn()) {
setIsAlreadyLoggedIn(true);
redirectToDashboard();
}
}, []);
/**
* This function check if the user entered the correct credentials and should be allowed to log in.
*/
const loginCheck = async () => {
if (!email || !password) {
return;
const renderStep = (loginStep: number) => {
// TODO: add MFA step
switch (loginStep) {
case 1:
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return (
<MFAStep
email={email}
password={password}
/>
);
default:
return <div />
}
setIsLoading(true);
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
setTimeout(() => {
setIsLoading(false);
}, 2000);
});
};
if (isAlreadyLoggedIn) {
return null
}
return (
@@ -84,80 +84,7 @@ export default function Login() {
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
</div>
</Link>
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={loginCheck}
loading={isLoading}
size="lg"
/>
</div>
</div>
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
</div> */}
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
{renderStep(step)}
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="w-48 mx-auto">
<ListBox
@@ -173,4 +100,4 @@ export default function Login() {
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
export const getStaticProps = getTranslatedStaticProps(['auth', 'login', 'mfa']);
+48 -14
View File
@@ -1,3 +1,5 @@
import crypto from 'crypto';
import { useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
@@ -12,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { deriveArgonKey } from '../components/utilities/cryptography/crypto';
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
@@ -39,6 +42,7 @@ export default function PasswordReset() {
const getEncryptedKeyHandler = async () => {
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
@@ -64,14 +68,6 @@ export default function PasswordReset() {
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: privateKey,
secret: newPassword
.slice(0, 32)
.padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
client.init(
{
username: email,
@@ -79,13 +75,51 @@ export default function PasswordReset() {
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
const response = await resetPasswordOnAccountRecovery({
verificationToken,
encryptedPrivateKey: ciphertext,
iv,
tag,
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
verifier: result.verifier
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const response = await resetPasswordOnAccountRecovery({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken
});
// if everything works, go the main dashboard page.
@@ -14,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import changePassword from '@app/components/utilities/cryptography/changePassword';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
import getAPIKeys from '../../api/apiKey/getAPIKeys';
@@ -100,6 +101,7 @@ export default function PersonalSettings() {
/>
</div>
</div>
<SecuritySection />
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
+3 -3
View File
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
@@ -28,7 +29,6 @@ export default function SignUp() {
const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1);
const router = useRouter();
const [verificationToken, setVerificationToken] = useState('');
const { t } = useTranslation();
@@ -59,7 +59,8 @@ export default function SignUp() {
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
setVerificationToken((await response.json()).token);
const {token} = await response.json();
SecurityClient.setSignupToken(token);
setStep(3);
} else {
setCodeError(true);
@@ -94,7 +95,6 @@ export default function SignUp() {
/>
) : step === 3 ? (
<UserInfoStep
verificationToken={verificationToken}
incrementStep={incrementStep}
email={email}
password={password}
+85 -37
View File
@@ -1,5 +1,7 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from 'crypto';
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
@@ -17,7 +19,12 @@ import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLocalStorage';
import SecurityClient from '@app/components/utilities/SecurityClient';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite';
@@ -75,17 +82,10 @@ export default function SignupInvite() {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
localStorage.setItem('PRIVATE_KEY', privateKey);
client.init(
{
@@ -94,35 +94,83 @@ export default function SignupInvite() {
},
async () => {
client.createVerifier(async (err, result) => {
let response = await completeAccountInformationSignupInvite({
email,
firstName,
lastName,
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken
});
try {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
// if everything works, go the main dashboard page.
if (!errorCheck && response.status === 200) {
response = await response.json();
if (!derivedKey) throw new Error('Failed to derive key from password');
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const {
token: jwtToken
} = await completeAccountInformationSignupInvite({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(jwtToken);
try {
await attemptLogin(email, password, setErrorLogin, router, false, false);
setStep(3);
} catch (error) {
setIsLoading(false);
console.log('Error', error);
}
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
setStep(3);
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
@@ -152,7 +200,7 @@ export default function SignupInvite() {
// user will have temp token if doesn't have an account
// then continue with account setup workflow
if (res?.token) {
setVerificationToken(res.token);
SecurityClient.setSignupToken(res.token);
setStep(2);
} else {
// user will be redirected to dashboard
+10
View File
@@ -1,6 +1,8 @@
import { QueryClient } from '@tanstack/react-query';
// this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ['infisical__signup-temp-token'];
export const MFA_TEMP_TOKEN_CACHE_KEY = ['infisical__mfa-temp-token'];
export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token'];
export const queryClient = new QueryClient({
@@ -13,9 +15,17 @@ export const queryClient = new QueryClient({
});
// set token in memory cache
export const setSignupTempToken = (token: string) =>
queryClient.setQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY, token);
export const setMfaTempToken = (token: string) =>
queryClient.setQueryData(MFA_TEMP_TOKEN_CACHE_KEY, token);
export const setAuthToken = (token: string) =>
queryClient.setQueryData(AUTH_TOKEN_CACHE_KEY, token);
export const getSignupTempToken = () => queryClient.getQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY) as string;
export const getMfaTempToken = () => queryClient.getQueryData(MFA_TEMP_TOKEN_CACHE_KEY) as string;
export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string;
export const isLoggedIn = () => Boolean(getAuthToken());
+56
View File
@@ -0,0 +1,56 @@
import { decryptPrivateKeyHelper } from '@app/helpers/key';
/**
* Class to handle key actions
*/
class KeyService {
/** Return the user's decrypted private key
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
* @returns {String} privateKey - decrypted private key
*/
static async decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) {
return decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
}
}
export default KeyService;
+26
View File
@@ -0,0 +1,26 @@
import { initProjectHelper } from '@app/helpers/project';
class ProjectService {
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
static async initProject({
organizationId,
projectName
}: {
organizationId: string;
projectName: string
}) {
return initProjectHelper({
organizationId,
projectName
});
}
}
export default ProjectService;
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import { Checkbox } from '@app/components/v2';
import { useGetUser } from '../../../../hooks/api';
import { User } from '../../../../hooks/api/types';
import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
export const SecuritySection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') {
setIsMfaEnabled(user.isMfaEnabled);
}
}, [user]);
const toggleMfa = async (state: boolean) => {
try {
const newUser: User = await updateMyMfaEnabled({
isMfaEnabled: state
});
if (newUser) {
setIsMfaEnabled(newUser.isMfaEnabled);
}
} catch (err) {
console.error(err);
}
}
return (
<form>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
Two-factor Authentication
</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="isTwoFAEnabled"
isChecked={isMfaEnabled}
onCheckedChange={(state) => {
toggleMfa(state as boolean);
}}
>
Enable 2-factor authentication via your personal email.
</Checkbox>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { SecuritySection } from './SecuritySection';
@@ -314,10 +314,6 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name}
onProjectNameChange={onRenameWorkspace}
/>
<AutoCapitalizationSection
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection
environments={currentWorkspace?.environments || []}
@@ -339,6 +335,10 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''}
onCreateTag={onCreateWsTag}
/>
<AutoCapitalizationSection
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
@@ -14,7 +14,7 @@ export const AutoCapitalizationSection = ({
const { t } = useTranslation();
return (
<form>
<div className="mb-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
{t('settings-project:auto-capitalization')}
</p>