mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical into bot
This commit is contained in:
@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email });
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed email magic link confirmation'
|
||||
error: 'Failed email magic link verification for organization invitation'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,121 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, BackupPrivateKey } from '../models';
|
||||
import { User, Token, BackupPrivateKey } from '../models';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
|
||||
/**
|
||||
* Password reset step 1: Send email verification link to email [email]
|
||||
* for account recovery.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
let email: string;
|
||||
try {
|
||||
email = req.body.email;
|
||||
|
||||
const user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(403).send({
|
||||
error: 'Failed to send email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
await Token.findOneAndUpdate(
|
||||
{ email },
|
||||
{
|
||||
email,
|
||||
token,
|
||||
createdAt: new Date()
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
await sendMail({
|
||||
template: 'passwordReset.handlebars',
|
||||
subjectLine: 'Infisical password reset',
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: SITE_URL + '/password-reset'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to send email for account recovery'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email for account recovery to ${email}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Password reset step 2: Verify email verification link sent to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user doesn't exist with email [email] or
|
||||
// hasn't even completed their account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
|
||||
// generate temporary password-reset token
|
||||
token = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||
secret: JWT_SIGNUP_SECRET
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed email verification for password reset'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
user,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to start change password process'
|
||||
@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
error: 'Failed to change password. Try again?'
|
||||
@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update backup private key'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
let backupPrivateKey;
|
||||
try {
|
||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email});
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
backupPrivateKey
|
||||
});
|
||||
}
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email});
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get backup private key'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully reset password'
|
||||
});
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { passwordController } from '../controllers';
|
||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||
|
||||
@ -27,6 +27,33 @@ router.post(
|
||||
passwordController.changePassword
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.post(
|
||||
'/email/password-reset',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordReset
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.post(
|
||||
'/email/password-reset-verify',
|
||||
passwordLimiter,
|
||||
body('email').exists().trim().notEmpty().isEmail(),
|
||||
body('code').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.emailPasswordResetVerify
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.get(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
requireSignupAuth,
|
||||
passwordController.getBackupPrivateKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
@ -41,4 +68,17 @@ router.post(
|
||||
passwordController.createBackupPrivateKey
|
||||
);
|
||||
|
||||
export default router;
|
||||
// NEW
|
||||
router.post(
|
||||
'/password-reset',
|
||||
requireSignupAuth,
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd
|
||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||
validateRequest,
|
||||
passwordController.resetPassword
|
||||
);
|
||||
|
||||
export default router;
|
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Email Verification</title>
|
||||
<title>Organization Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
15
backend/src/templates/passwordReset.handlebars
Normal file
15
backend/src/templates/passwordReset.handlebars
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Account Recovery</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>Reset your password</h2>
|
||||
<p>Someone requested a password reset.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
|
||||
<p>If you didn't initiate this request, please contact us immediately at team@infisical.com</p>
|
||||
</body>
|
||||
</html>
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Email Verification</title>
|
||||
<title>Project Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
5
docs/integrations/cloud/vercel.mdx
Normal file
5
docs/integrations/cloud/vercel.mdx
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Vercel"
|
||||
---
|
||||
|
||||
Coming soon.
|
@ -12,7 +12,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||
| Kubernetes | Platform | Coming soon |
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| Vercel | Cloud | Coming soon |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Coming soon |
|
||||
| AWS | Cloud | Coming soon |
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
@ -31,8 +31,8 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
|
||||
| [Remix](/integrations/frameworks/remix) | Framework | Available |
|
||||
| [Vite](/integrations/frameworks/vite) | Framework | Available |
|
||||
| [Fiber](/integrations/frameworks/fiber) | Framework | Coming soon |
|
||||
| [Fiber](/integrations/frameworks/fiber) | Framework | Available |
|
||||
| [Django](/integrations/frameworks/django) | Framework | Available |
|
||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Coming soon |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||
|
@ -130,7 +130,8 @@
|
||||
{
|
||||
"group": "Cloud",
|
||||
"pages": [
|
||||
"integrations/cloud/heroku"
|
||||
"integrations/cloud/heroku",
|
||||
"integrations/cloud/vercel"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,39 +1,39 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import nacl from "tweetnacl";
|
||||
import { Fragment, useState } from 'react';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import { envMapping } from '../../../public/data/frequentConstants';
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
import ListBox from "../Listbox";
|
||||
encryptAssymmetric
|
||||
} from '../../utilities/cryptography/crypto';
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
import ListBox from '../Listbox';
|
||||
|
||||
const expiryMapping = {
|
||||
"1 day": 86400,
|
||||
"7 days": 604800,
|
||||
"1 month": 2592000,
|
||||
'1 day': 86400,
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000
|
||||
};
|
||||
|
||||
const AddServiceTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
workspaceName
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||
const [serviceToken, setServiceToken] = useState('');
|
||||
const [serviceTokenName, setServiceTokenName] = useState('');
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState('Development');
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
|
||||
const generateServiceToken = async () => {
|
||||
@ -43,7 +43,7 @@ const AddServiceTokenDialog = ({
|
||||
ciphertext: latestFileKey.latestKey.encryptedKey,
|
||||
nonce: latestFileKey.latestKey.nonce,
|
||||
publicKey: latestFileKey.latestKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY"),
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY')
|
||||
});
|
||||
|
||||
// generate new public/private key pair
|
||||
@ -55,7 +55,7 @@ const AddServiceTokenDialog = ({
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
let newServiceToken = await addServiceToken({
|
||||
@ -65,16 +65,16 @@ const AddServiceTokenDialog = ({
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
nonce
|
||||
});
|
||||
|
||||
const serviceToken = newServiceToken + "," + privateKey;
|
||||
const serviceToken = newServiceToken + ',' + privateKey;
|
||||
setServiceToken(serviceToken);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
var copyText = document.getElementById("serviceToken");
|
||||
var copyText = document.getElementById('serviceToken');
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
@ -91,8 +91,8 @@ const AddServiceTokenDialog = ({
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
closeModal();
|
||||
setServiceTokenName("");
|
||||
setServiceToken("");
|
||||
setServiceTokenName('');
|
||||
setServiceToken('');
|
||||
};
|
||||
|
||||
return (
|
||||
@ -122,7 +122,7 @@ const AddServiceTokenDialog = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
{serviceToken == "" ? (
|
||||
{serviceToken == '' ? (
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
@ -155,12 +155,12 @@ const AddServiceTokenDialog = ({
|
||||
selected={serviceTokenEnv}
|
||||
onChange={setServiceTokenEnv}
|
||||
data={[
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
'Development',
|
||||
'Staging',
|
||||
'Production',
|
||||
'Testing'
|
||||
]}
|
||||
width="full"
|
||||
isFull={true}
|
||||
text="Environment: "
|
||||
/>
|
||||
</div>
|
||||
@ -168,8 +168,14 @@ const AddServiceTokenDialog = ({
|
||||
<ListBox
|
||||
selected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={["1 day", "7 days", "1 month"]}
|
||||
width="full"
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months'
|
||||
]}
|
||||
isFull={true}
|
||||
text="Expires in: "
|
||||
/>
|
||||
</div>
|
||||
@ -181,7 +187,7 @@ const AddServiceTokenDialog = ({
|
||||
text="Add Service Token"
|
||||
textDisabled="Add Service Token"
|
||||
size="md"
|
||||
active={serviceTokenName == "" ? false : true}
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import classnames from "classnames";
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { Notification as NotificationType } from "./NotificationProvider";
|
||||
import { Notification as NotificationType } from './NotificationProvider';
|
||||
|
||||
interface NotificationProps {
|
||||
notification: Required<NotificationType>;
|
||||
@ -12,7 +11,7 @@ interface NotificationProps {
|
||||
|
||||
const Notification = ({
|
||||
notification,
|
||||
clearNotification,
|
||||
clearNotification
|
||||
}: NotificationProps) => {
|
||||
const timeout = useRef<number>();
|
||||
|
||||
@ -37,22 +36,29 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
"w-full flex items-center justify-between px-4 py-3 rounded pointer-events-auto",
|
||||
{
|
||||
"bg-green-600": notification.type === "success",
|
||||
"bg-red-500": notification.type === "error",
|
||||
"bg-blue-500": notification.type === "info",
|
||||
}
|
||||
)}
|
||||
className="relative w-full flex items-center justify-between px-4 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
|
||||
role="alert"
|
||||
>
|
||||
<p className="text-white text-sm font-bold">{notification.text}</p>
|
||||
{notification.type === 'error' && (
|
||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
{notification.type === 'success' && (
|
||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
{notification.type === 'info' && (
|
||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">
|
||||
{notification.text}
|
||||
</p>
|
||||
<button
|
||||
className="bg-white/5 rounded-lg p-3"
|
||||
className="rounded-lg"
|
||||
onClick={() => clearNotification(notification.text)}
|
||||
>
|
||||
<FontAwesomeIcon className="text-white" icon={faXmarkCircle} />
|
||||
<FontAwesomeIcon
|
||||
className="text-white w-4 h-3 hover:text-red"
|
||||
icon={faX}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
|
||||
import Notifications from "./Notifications";
|
||||
import Notifications from './Notifications';
|
||||
|
||||
type NotificationType = "success" | "error" | "info";
|
||||
type NotificationType = 'success' | 'error' | 'info';
|
||||
|
||||
export type Notification = {
|
||||
text: string;
|
||||
@ -15,7 +15,7 @@ type NotificationContextState = {
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextState>({
|
||||
createNotification: () => console.log("createNotification not set!"),
|
||||
createNotification: () => console.log('createNotification not set!')
|
||||
});
|
||||
|
||||
export const useNotificationContext = () => useContext(NotificationContext);
|
||||
@ -37,8 +37,8 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
|
||||
const createNotification = ({
|
||||
text,
|
||||
type = "success",
|
||||
timeoutMs = 2000,
|
||||
type = 'success',
|
||||
timeoutMs = 5000
|
||||
}: Notification) => {
|
||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||
|
||||
@ -54,7 +54,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
createNotification,
|
||||
createNotification
|
||||
}}
|
||||
>
|
||||
<Notifications
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Notification from "./Notification";
|
||||
import { Notification as NotificationType } from "./NotificationProvider";
|
||||
import Notification from './Notification';
|
||||
import { Notification as NotificationType } from './NotificationProvider';
|
||||
|
||||
interface NoticationsProps {
|
||||
notifications: Required<NotificationType>[];
|
||||
@ -8,14 +8,14 @@ interface NoticationsProps {
|
||||
|
||||
const Notifications = ({
|
||||
notifications,
|
||||
clearNotification,
|
||||
clearNotification
|
||||
}: NoticationsProps) => {
|
||||
if (!notifications.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse bottom-1 gap-y-2 w-96 h-full right-1 pointer-events-none">
|
||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse bottom-1 gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
||||
{notifications.map((notif) => (
|
||||
<Notification
|
||||
key={notif.text}
|
||||
|
@ -54,20 +54,19 @@ const attemptLogin = async (
|
||||
await login2(email, clientProof);
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt(
|
||||
encryptedPrivateKey,
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
password
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
token,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
@ -114,11 +113,8 @@ const attemptLogin = async (
|
||||
'personal'
|
||||
],
|
||||
DB_USERNAME: ['user1234', 'personal'],
|
||||
DB_PASSWORD: ['ah8jak3hk8dhiu4dw7whxwe1l', 'personal'],
|
||||
TWILIO_AUTH_TOKEN: [
|
||||
'hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG',
|
||||
'shared'
|
||||
],
|
||||
DB_PASSWORD: ['example_password', 'personal'],
|
||||
TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'],
|
||||
WEBSITE_URL: ['http://localhost:3000', 'shared'],
|
||||
STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared']
|
||||
},
|
||||
|
@ -1,63 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const BLOCK_SIZE_BYTES = 16; // 128 bit
|
||||
|
||||
/**
|
||||
* Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
class Aes256Gcm {
|
||||
/**
|
||||
* No need to run the constructor. The class only has static methods.
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Encrypts text with AES 256 GCM.
|
||||
* @param {string} text - Cleartext to encode.
|
||||
* @param {string} secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {object}
|
||||
*/
|
||||
static encrypt(text, secret) {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, "utf8", "base64");
|
||||
ciphertext += cipher.final("base64");
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString("base64"),
|
||||
tag: cipher.getAuthTag().toString("base64"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES 256 CGM encrypted text.
|
||||
* @param {string} ciphertext - Base64-encoded ciphertext.
|
||||
* @param {string} iv - The base64-encoded initialization vector.
|
||||
* @param {string} tag - The base64-encoded authentication tag generated by getAuthTag().
|
||||
* @param {string} secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {string}
|
||||
*/
|
||||
static decrypt(ciphertext, iv, tag, secret) {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, "base64")
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
||||
cleartext += decipher.final("utf8");
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Aes256Gcm;
|
82
frontend/components/utilities/cryptography/aes-256-gcm.ts
Normal file
82
frontend/components/utilities/cryptography/aes-256-gcm.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16; // 128 bit
|
||||
|
||||
interface EncryptProps {
|
||||
text: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface DecryptProps {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface EncryptOutputProps {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
class Aes256Gcm {
|
||||
/**
|
||||
* No need to run the constructor. The class only has static methods.
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Encrypts text with AES 256 GCM.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.text - Cleartext to encode.
|
||||
* @param {string} obj.secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {object}
|
||||
*/
|
||||
// { ciphertext: string; iv: string; tag: string; }
|
||||
static encrypt({ text, secret }: EncryptProps): EncryptOutputProps {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES 256 CGM encrypted text.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.ciphertext - Base64-encoded ciphertext.
|
||||
* @param {string} obj.iv - The base64-encoded initialization vector.
|
||||
* @param {string} obj.tag - The base64-encoded authentication tag generated by getAuthTag().
|
||||
* @param {string} obj.secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {string}
|
||||
*/
|
||||
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
}
|
||||
|
||||
export default Aes256Gcm;
|
@ -1,11 +1,11 @@
|
||||
import changePassword2 from "~/pages/api/auth/ChangePassword2";
|
||||
import SRP1 from "~/pages/api/auth/SRP1";
|
||||
import changePassword2 from '~/pages/api/auth/ChangePassword2';
|
||||
import SRP1 from '~/pages/api/auth/SRP1';
|
||||
|
||||
import Aes256Gcm from "./aes-256-gcm";
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
const jsrp = require("jsrp");
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const jsrp = require('jsrp');
|
||||
const clientOldPassword = new jsrp.client();
|
||||
const clientNewPassword = new jsrp.client();
|
||||
|
||||
@ -34,7 +34,7 @@ const changePassword = async (
|
||||
clientOldPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: currentPassword,
|
||||
password: currentPassword
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
@ -42,13 +42,13 @@ const changePassword = async (
|
||||
let serverPublicKey, salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey: clientPublicKey,
|
||||
clientPublicKey: clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
} catch (err) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log("Wrong current password", err, 1);
|
||||
console.log('Wrong current password', err, 1);
|
||||
}
|
||||
|
||||
clientOldPassword.setSalt(salt);
|
||||
@ -58,27 +58,27 @@ const changePassword = async (
|
||||
clientNewPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: newPassword,
|
||||
password: newPassword
|
||||
},
|
||||
async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
|
||||
let { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
localStorage.getItem("PRIVATE_KEY"),
|
||||
newPassword
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: localStorage.getItem('PRIVATE_KEY'),
|
||||
secret: newPassword
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 +
|
||||
(newPassword.slice(0, 32).length -
|
||||
new Blob([newPassword]).size),
|
||||
"0"
|
||||
'0'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
if (ciphertext) {
|
||||
localStorage.setItem("encryptedPrivateKey", ciphertext);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
|
||||
let res;
|
||||
try {
|
||||
@ -88,14 +88,14 @@ const changePassword = async (
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof,
|
||||
clientProof
|
||||
});
|
||||
if (res.status == 400) {
|
||||
setCurrentPasswordError(true);
|
||||
} else if (res.status == 200) {
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
}
|
||||
} catch (err) {
|
||||
setCurrentPasswordError(true);
|
||||
@ -108,7 +108,7 @@ const changePassword = async (
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Something went wrong during changing the password");
|
||||
console.log('Something went wrong during changing the password');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
const aes = require("./aes-256-gcm");
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
import aes from './aes-256-gcm';
|
||||
|
||||
type encryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
@ -19,7 +19,11 @@ type encryptAsymmetricProps = {
|
||||
* @returns {String} ciphertext - base64-encoded ciphertext
|
||||
* @returns {String} nonce - base64-encoded nonce
|
||||
*/
|
||||
const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmetricProps): object => {
|
||||
const encryptAssymmetric = ({
|
||||
plaintext,
|
||||
publicKey,
|
||||
privateKey
|
||||
}: encryptAsymmetricProps): object => {
|
||||
const nonce = nacl.randomBytes(24);
|
||||
const ciphertext = nacl.box(
|
||||
nacl.util.decodeUTF8(plaintext),
|
||||
@ -30,7 +34,7 @@ const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmet
|
||||
|
||||
return {
|
||||
ciphertext: nacl.util.encodeBase64(ciphertext),
|
||||
nonce: nacl.util.encodeBase64(nonce),
|
||||
nonce: nacl.util.encodeBase64(nonce)
|
||||
};
|
||||
};
|
||||
|
||||
@ -39,7 +43,7 @@ type decryptAsymmetricProps = {
|
||||
nonce: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
|
||||
@ -49,9 +53,13 @@ type decryptAsymmetricProps = {
|
||||
* @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)
|
||||
* @param {String} plaintext - UTF8 plaintext
|
||||
*/
|
||||
const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryptAsymmetricProps): string => {
|
||||
const decryptAssymmetric = ({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
privateKey
|
||||
}: decryptAsymmetricProps): string => {
|
||||
const plaintext = nacl.box.open(
|
||||
nacl.util.decodeBase64(ciphertext),
|
||||
nacl.util.decodeBase64(nonce),
|
||||
@ -65,7 +73,7 @@ const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryp
|
||||
type encryptSymmetricProps = {
|
||||
plaintext: string;
|
||||
key: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using [key].
|
||||
@ -73,15 +81,18 @@ type encryptSymmetricProps = {
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
* @param {String} obj.key - 16-byte hex key
|
||||
*/
|
||||
const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => {
|
||||
const encryptSymmetric = ({
|
||||
plaintext,
|
||||
key
|
||||
}: encryptSymmetricProps): object => {
|
||||
let ciphertext, iv, tag;
|
||||
try {
|
||||
const obj = aes.encrypt(plaintext, key);
|
||||
const obj = aes.encrypt({ text: plaintext, secret: key });
|
||||
ciphertext = obj.ciphertext;
|
||||
iv = obj.iv;
|
||||
tag = obj.tag;
|
||||
} catch (err) {
|
||||
console.log("Failed to perform encryption");
|
||||
console.log('Failed to perform encryption');
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
@ -89,7 +100,7 @@ const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object =>
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
};
|
||||
};
|
||||
|
||||
@ -98,7 +109,7 @@ type decryptSymmetricProps = {
|
||||
iv: string;
|
||||
tag: string;
|
||||
key: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return symmetrically decypted [ciphertext] using [iv], [tag],
|
||||
@ -110,12 +121,17 @@ type decryptSymmetricProps = {
|
||||
* @param {String} obj.key - 32-byte hex key
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({ ciphertext, iv, tag, key }: decryptSymmetricProps): string => {
|
||||
const decryptSymmetric = ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
}: decryptSymmetricProps): string => {
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = aes.decrypt(ciphertext, iv, tag, key);
|
||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||
} catch (err) {
|
||||
console.log("Failed to perform decryption");
|
||||
console.log('Failed to perform decryption');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -126,5 +142,5 @@ export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
|
||||
import SRP1 from '~/pages/api/auth/SRP1';
|
||||
|
||||
import { tempLocalStorage } from '../checks/tempLocalStorage';
|
||||
import generateBackupPDF from '../generateBackupPDF';
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
|
||||
@ -12,21 +11,22 @@ const clientPassword = new jsrp.client();
|
||||
const clientKey = new jsrp.client();
|
||||
const crypto = require('crypto');
|
||||
|
||||
interface Props {
|
||||
interface BackupKeyProps {
|
||||
email: string;
|
||||
password: string;
|
||||
personalName: string;
|
||||
setBackupKeyError: any;
|
||||
setBackupKeyIssued: any;
|
||||
setBackupKeyError: (value: boolean) => void;
|
||||
setBackupKeyIssued: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {*} email
|
||||
* @param {*} password
|
||||
* @param {*} setErrorLogin
|
||||
* @param {*} router
|
||||
* @param {*} isSignUp
|
||||
* This function issue a backup key for a user
|
||||
* @param {obkect} obj
|
||||
* @param {string} obj.email - email of a user issuing a backup key
|
||||
* @param {string} obj.password - password of a user issuing a backup key
|
||||
* @param {string} obj.personalName - name of a user issuing a backup key
|
||||
* @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
|
||||
* @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
|
||||
* @returns
|
||||
*/
|
||||
const issueBackupKey = async ({
|
||||
@ -35,7 +35,7 @@ const issueBackupKey = async ({
|
||||
personalName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
}: Props) => {
|
||||
}: BackupKeyProps) => {
|
||||
try {
|
||||
setBackupKeyError(false);
|
||||
setBackupKeyIssued(false);
|
||||
@ -72,14 +72,11 @@ const issueBackupKey = async ({
|
||||
},
|
||||
async () => {
|
||||
clientKey.createVerifier(
|
||||
async (_: any, result: { salt: string; verifier: string }) => {
|
||||
// TODO: Fix this
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
tempLocalStorage('PRIVATE_KEY'),
|
||||
generatedKey
|
||||
);
|
||||
async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: String(localStorage.getItem('PRIVATE_KEY')),
|
||||
secret: generatedKey
|
||||
});
|
||||
|
||||
const res = await issueBackupPrivateKey({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
@ -90,10 +87,14 @@ const issueBackupKey = async ({
|
||||
clientProof
|
||||
});
|
||||
|
||||
if (res && res.status == 400) {
|
||||
if (res?.status == 400) {
|
||||
setBackupKeyError(true);
|
||||
} else if (res && res.status == 200) {
|
||||
generateBackupPDF(personalName, email, generatedKey);
|
||||
} else if (res?.status == 200) {
|
||||
generateBackupPDF({
|
||||
personalName,
|
||||
personalEmail: email,
|
||||
generatedKey
|
||||
});
|
||||
setBackupKeyIssued(true);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -3,9 +3,6 @@ import SecurityClient from '~/utilities/SecurityClient';
|
||||
/**
|
||||
* This function is used to check if the user is authenticated.
|
||||
* To do that, we get their tokens from cookies, and verify if they are good.
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @returns
|
||||
*/
|
||||
const checkAuth = async () => {
|
||||
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
|
||||
|
@ -5,8 +5,9 @@ interface Props {
|
||||
|
||||
/**
|
||||
* This route check the verification code from the email that user just recieved
|
||||
* @param {*} email
|
||||
* @param {*} code
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email
|
||||
* @param {string} obj.code
|
||||
* @returns
|
||||
*/
|
||||
const checkEmailVerificationCode = ({ email, code }: Props) => {
|
||||
|
@ -15,16 +15,18 @@ interface Props {
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {*} email
|
||||
* @param {*} firstName
|
||||
* @param {*} lastName
|
||||
* @param {*} workspace
|
||||
* @param {*} publicKey
|
||||
* @param {*} ciphertext
|
||||
* @param {*} iv
|
||||
* @param {*} tag
|
||||
* @param {*} salt
|
||||
* @param {*} verifier
|
||||
* @param {object} obj
|
||||
* @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.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
|
||||
* @param {string} obj.iv
|
||||
* @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 = ({
|
||||
|
@ -14,15 +14,17 @@ interface Props {
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {*} email
|
||||
* @param {*} firstName
|
||||
* @param {*} lastName
|
||||
* @param {*} publicKey
|
||||
* @param {*} ciphertext
|
||||
* @param {*} iv
|
||||
* @param {*} tag
|
||||
* @param {*} salt
|
||||
* @param {*} verifier
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of the user completing signupinvite flow
|
||||
* @param {string} obj.firstName - first name of the user completing signupinvite flow
|
||||
* @param {string} obj.lastName - last name of the user completing signupinvite flow
|
||||
* @param {string} obj.publicKey - public key of the user completing signupinvite flow
|
||||
* @param {string} obj.ciphertext
|
||||
* @param {string} obj.iv
|
||||
* @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 completeAccountInformationSignupInvite = ({
|
||||
|
@ -11,6 +11,14 @@ interface Props {
|
||||
|
||||
/**
|
||||
* This is the route that issues a backup private key that will afterwards be added into a pdf
|
||||
* @param {object} obj
|
||||
* @param {string} obj.encryptedPrivateKey
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.clientProof
|
||||
* @returns
|
||||
*/
|
||||
const issueBackupPrivateKey = ({
|
||||
encryptedPrivateKey,
|
||||
|
@ -6,7 +6,7 @@ interface Props {
|
||||
|
||||
/**
|
||||
* This is the first step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @param {string} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const SRP1 = ({ clientPublicKey }: Props) => {
|
||||
|
@ -5,8 +5,9 @@ interface Props {
|
||||
|
||||
/**
|
||||
* This route verifies the signup invite link
|
||||
* @param {*} email
|
||||
* @param {*} code
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email that a user is trying to verify
|
||||
* @param {string} obj.code - code that a user received to the abovementioned email
|
||||
* @returns
|
||||
*/
|
||||
const verifySignupInvite = ({ email, code }: Props) => {
|
||||
|
@ -2,8 +2,8 @@ import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function fetches the encrypted secrets from the .env file
|
||||
* @param {*} workspaceId
|
||||
* @param {*} env
|
||||
* @param {string} workspaceId - project is for which a user is trying to get secrets
|
||||
* @param {string} env - environment of a project for which a user is trying ot get secrets
|
||||
* @returns
|
||||
*/
|
||||
const getSecrets = async (workspaceId: string, env: string) => {
|
||||
|
@ -9,8 +9,11 @@ interface Props {
|
||||
|
||||
/**
|
||||
* This function uploads the encrypted .env file
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId
|
||||
* @param {} obj.secrets
|
||||
* @param {} obj.keys
|
||||
* @param {string} obj.environment
|
||||
* @returns
|
||||
*/
|
||||
const uploadSecrets = async ({
|
||||
|
@ -7,7 +7,10 @@ interface Props {
|
||||
}
|
||||
/**
|
||||
* This is the first step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @param {object} obj
|
||||
* @param {object} obj.workspaceId - project id for which we want to authorize the integration
|
||||
* @param {object} obj.code
|
||||
* @param {object} obj.integration - integration which a user is trying to turn on
|
||||
* @returns
|
||||
*/
|
||||
const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => {
|
||||
|
@ -385,14 +385,14 @@ export default function Dashboard() {
|
||||
|
||||
if (nameErrors) {
|
||||
return createNotification({
|
||||
text: 'Solve all name errors first!',
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicatesExist) {
|
||||
return createNotification({
|
||||
text: "Your secrets weren't saved; please fix the conflicts first.",
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ const learningItem = ({
|
||||
>
|
||||
<div
|
||||
onClick={async () => {
|
||||
if (userAction) {
|
||||
if (userAction && userAction != 'first_time_secrets_pushed') {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@ -181,15 +180,15 @@ export default function SignUp() {
|
||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
PRIVATE_KEY,
|
||||
password
|
||||
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 };
|
||||
}) as { ciphertext: string; iv: string; tag: string };
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
|
||||
|
@ -1,38 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import InputField from "~/components/basic/InputField";
|
||||
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
|
||||
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
|
||||
import attemptLogin from "~/utilities/attemptLogin";
|
||||
import passwordCheck from "~/utilities/checks/PasswordCheck";
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import InputField from '~/components/basic/InputField';
|
||||
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
||||
import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey';
|
||||
import attemptLogin from '~/utilities/attemptLogin';
|
||||
import passwordCheck from '~/utilities/checks/PasswordCheck';
|
||||
|
||||
import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite";
|
||||
import verifySignupInvite from "./api/auth/VerifySignupInvite";
|
||||
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
||||
import verifySignupInvite from './api/auth/VerifySignupInvite';
|
||||
|
||||
const nacl = require("tweetnacl");
|
||||
const jsrp = require("jsrp");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
const nacl = require('tweetnacl');
|
||||
const jsrp = require('jsrp');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const client = new jsrp.client();
|
||||
const queryString = require("query-string");
|
||||
const queryString = require('query-string');
|
||||
|
||||
export default function SignupInvite() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [password, setPassword] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
||||
const [email, setEmail] = useState(parsedUrl.to);
|
||||
const token = parsedUrl.token;
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
@ -74,21 +74,22 @@ export default function SignupInvite() {
|
||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt(
|
||||
PRIVATE_KEY,
|
||||
password
|
||||
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"
|
||||
'0'
|
||||
)
|
||||
);
|
||||
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
|
||||
});
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password,
|
||||
password: password
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err, result) => {
|
||||
@ -102,17 +103,17 @@ export default function SignupInvite() {
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken,
|
||||
token: verificationToken
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (!errorCheck && response.status == "200") {
|
||||
if (!errorCheck && response.status == '200') {
|
||||
response = await response.json();
|
||||
|
||||
localStorage.setItem("publicKey", PUBLIC_KEY);
|
||||
localStorage.setItem("encryptedPrivateKey", ciphertext);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
|
||||
try {
|
||||
await attemptLogin(
|
||||
@ -126,7 +127,7 @@ export default function SignupInvite() {
|
||||
setStep(3);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.log("Error", error);
|
||||
console.log('Error', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -155,14 +156,14 @@ export default function SignupInvite() {
|
||||
onButtonPressed={async () => {
|
||||
const response = await verifySignupInvite({
|
||||
email,
|
||||
code: token,
|
||||
code: token
|
||||
});
|
||||
if (response.status == 200) {
|
||||
setVerificationToken((await response.json()).token);
|
||||
setStep(2);
|
||||
} else {
|
||||
console.log("ERROR", response);
|
||||
router.push("/requestnewinvite");
|
||||
console.log('ERROR', response);
|
||||
router.push('/requestnewinvite');
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
@ -211,7 +212,7 @@ export default function SignupInvite() {
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
currentErrorCheck: false,
|
||||
currentErrorCheck: false
|
||||
});
|
||||
}}
|
||||
type="password"
|
||||
@ -244,7 +245,7 @@ export default function SignupInvite() {
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorLength ? "text-gray-400" : "text-gray-600"
|
||||
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
14 characters
|
||||
@ -264,7 +265,7 @@ export default function SignupInvite() {
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"
|
||||
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
1 lowercase character
|
||||
@ -284,7 +285,7 @@ export default function SignupInvite() {
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorNumber ? "text-gray-400" : "text-gray-600"
|
||||
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
1 number
|
||||
@ -335,11 +336,11 @@ export default function SignupInvite() {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: firstName + " " + lastName,
|
||||
personalName: firstName + ' ' + lastName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push("/dashboard/");
|
||||
router.push('/dashboard/');
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
|
Reference in New Issue
Block a user