This commit is contained in:
Tuan Dang
2022-12-11 17:06:23 -05:00
34 changed files with 659 additions and 354 deletions
@@ -217,7 +217,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
try { try {
const { email, code } = req.body; const { email, code } = req.body;
user = await User.findOne({ email }); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
@@ -257,7 +257,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
error: 'Failed email magic link confirmation' error: 'Failed email magic link verification for organization invitation'
}); });
} }
+177 -4
View File
@@ -1,11 +1,121 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; 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 = {}; 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 * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req * @param req
@@ -43,7 +153,7 @@ export const srp1 = async (req: Request, res: Response) => {
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
error: 'Failed to start change password process' error: 'Failed to start change password process'
@@ -110,7 +220,7 @@ export const changePassword = async (req: Request, res: Response) => {
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
error: 'Failed to change password. Try again?' error: 'Failed to change password. Try again?'
@@ -180,10 +290,73 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to update backup private key' 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'
});
}
+42 -2
View File
@@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
import { passwordController } from '../controllers'; import { passwordController } from '../controllers';
import { passwordLimiter } from '../helpers/rateLimiter'; import { passwordLimiter } from '../helpers/rateLimiter';
@@ -27,6 +27,33 @@ router.post(
passwordController.changePassword 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( router.post(
'/backup-private-key', '/backup-private-key',
passwordLimiter, passwordLimiter,
@@ -41,4 +68,17 @@ router.post(
passwordController.createBackupPrivateKey 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> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Email Verification</title> <title>Organization Invitation</title>
</head> </head>
<body> <body>
<h2>Infisical</h2> <h2>Infisical</h2>
@@ -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 [email protected]</p>
</body>
</html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Email Verification</title> <title>Project Invitation</title>
</head> </head>
<body> <body>
<h2>Infisical</h2> <h2>Infisical</h2>
+5
View File
@@ -0,0 +1,5 @@
---
title: "Vercel"
---
Coming soon.
+3 -3
View File
@@ -12,7 +12,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | | [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
| Kubernetes | Platform | Coming soon | | Kubernetes | Platform | Coming soon |
| [Heroku](/integrations/cloud/heroku) | Cloud | Available | | [Heroku](/integrations/cloud/heroku) | Cloud | Available |
| Vercel | Cloud | Coming soon | | [Vercel](/integrations/cloud/vercel) | Cloud | Coming soon |
| AWS | Cloud | Coming soon | | AWS | Cloud | Coming soon |
| GCP | Cloud | Coming soon | | GCP | Cloud | Coming soon |
| Azure | 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 | | [Gatsby](/integrations/frameworks/gatsby) | Framework | Available |
| [Remix](/integrations/frameworks/remix) | Framework | Available | | [Remix](/integrations/frameworks/remix) | Framework | Available |
| [Vite](/integrations/frameworks/vite) | 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 | | [Django](/integrations/frameworks/django) | Framework | Available |
| [Flask](/integrations/frameworks/flask) | 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 | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
+2 -1
View File
@@ -130,7 +130,8 @@
{ {
"group": "Cloud", "group": "Cloud",
"pages": [ "pages": [
"integrations/cloud/heroku" "integrations/cloud/heroku",
"integrations/cloud/vercel"
] ]
}, },
{ {
@@ -1,39 +1,39 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from 'react';
import { useRouter } from "next/router"; import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Dialog, Transition } from '@headlessui/react';
import { Dialog, Transition } from "@headlessui/react"; import nacl from 'tweetnacl';
import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { envMapping } from "../../../public/data/frequentConstants"; import { envMapping } from '../../../public/data/frequentConstants';
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric
} from "../../utilities/cryptography/crypto"; } from '../../utilities/cryptography/crypto';
import Button from "../buttons/Button"; import Button from '../buttons/Button';
import InputField from "../InputField"; import InputField from '../InputField';
import ListBox from "../Listbox"; import ListBox from '../Listbox';
const expiryMapping = { const expiryMapping = {
"1 day": 86400, '1 day': 86400,
"7 days": 604800, '7 days': 604800,
"1 month": 2592000, '1 month': 2592000,
'6 months': 15552000,
'12 months': 31104000
}; };
const AddServiceTokenDialog = ({ const AddServiceTokenDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId, workspaceId,
workspaceName, workspaceName
}) => { }) => {
const router = useRouter(); const [serviceToken, setServiceToken] = useState('');
const [serviceToken, setServiceToken] = useState(""); const [serviceTokenName, setServiceTokenName] = useState('');
const [serviceTokenName, setServiceTokenName] = useState(""); const [serviceTokenEnv, setServiceTokenEnv] = useState('Development');
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const generateServiceToken = async () => { const generateServiceToken = async () => {
@@ -43,7 +43,7 @@ const AddServiceTokenDialog = ({
ciphertext: latestFileKey.latestKey.encryptedKey, ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce, nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey, publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY"), privateKey: localStorage.getItem('PRIVATE_KEY')
}); });
// generate new public/private key pair // generate new public/private key pair
@@ -55,7 +55,7 @@ const AddServiceTokenDialog = ({
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({ const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey, publicKey,
privateKey, privateKey
}); });
let newServiceToken = await addServiceToken({ let newServiceToken = await addServiceToken({
@@ -65,16 +65,16 @@ const AddServiceTokenDialog = ({
expiresIn: expiryMapping[serviceTokenExpiresIn], expiresIn: expiryMapping[serviceTokenExpiresIn],
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce, nonce
}); });
const serviceToken = newServiceToken + "," + privateKey; const serviceToken = newServiceToken + ',' + privateKey;
setServiceToken(serviceToken); setServiceToken(serviceToken);
}; };
function copyToClipboard() { function copyToClipboard() {
// Get the text field // Get the text field
var copyText = document.getElementById("serviceToken"); var copyText = document.getElementById('serviceToken');
// Select the text field // Select the text field
copyText.select(); copyText.select();
@@ -91,8 +91,8 @@ const AddServiceTokenDialog = ({
const closeAddServiceTokenModal = () => { const closeAddServiceTokenModal = () => {
closeModal(); closeModal();
setServiceTokenName(""); setServiceTokenName('');
setServiceToken(""); setServiceToken('');
}; };
return ( return (
@@ -122,7 +122,7 @@ const AddServiceTokenDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" 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.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 <Dialog.Title
as="h3" as="h3"
@@ -155,12 +155,12 @@ const AddServiceTokenDialog = ({
selected={serviceTokenEnv} selected={serviceTokenEnv}
onChange={setServiceTokenEnv} onChange={setServiceTokenEnv}
data={[ data={[
"Development", 'Development',
"Staging", 'Staging',
"Production", 'Production',
"Testing", 'Testing'
]} ]}
width="full" isFull={true}
text="Environment: " text="Environment: "
/> />
</div> </div>
@@ -168,8 +168,14 @@ const AddServiceTokenDialog = ({
<ListBox <ListBox
selected={serviceTokenExpiresIn} selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn} onChange={setServiceTokenExpiresIn}
data={["1 day", "7 days", "1 month"]} data={[
width="full" '1 day',
'7 days',
'1 month',
'6 months',
'12 months'
]}
isFull={true}
text="Expires in: " text="Expires in: "
/> />
</div> </div>
@@ -181,7 +187,7 @@ const AddServiceTokenDialog = ({
text="Add Service Token" text="Add Service Token"
textDisabled="Add Service Token" textDisabled="Add Service Token"
size="md" size="md"
active={serviceTokenName == "" ? false : true} active={serviceTokenName == '' ? false : true}
/> />
</div> </div>
</div> </div>
@@ -1,9 +1,8 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from 'react';
import { faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classnames from "classnames";
import { Notification as NotificationType } from "./NotificationProvider"; import { Notification as NotificationType } from './NotificationProvider';
interface NotificationProps { interface NotificationProps {
notification: Required<NotificationType>; notification: Required<NotificationType>;
@@ -12,7 +11,7 @@ interface NotificationProps {
const Notification = ({ const Notification = ({
notification, notification,
clearNotification, clearNotification
}: NotificationProps) => { }: NotificationProps) => {
const timeout = useRef<number>(); const timeout = useRef<number>();
@@ -37,22 +36,29 @@ const Notification = ({
return ( return (
<div <div
className={classnames( 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"
"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",
}
)}
role="alert" 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 <button
className="bg-white/5 rounded-lg p-3" className="rounded-lg"
onClick={() => clearNotification(notification.text)} onClick={() => clearNotification(notification.text)}
> >
<FontAwesomeIcon className="text-white" icon={faXmarkCircle} /> <FontAwesomeIcon
className="text-white w-4 h-3 hover:text-red"
icon={faX}
/>
</button> </button>
</div> </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 = { export type Notification = {
text: string; text: string;
@@ -15,7 +15,7 @@ type NotificationContextState = {
}; };
const NotificationContext = createContext<NotificationContextState>({ const NotificationContext = createContext<NotificationContextState>({
createNotification: () => console.log("createNotification not set!"), createNotification: () => console.log('createNotification not set!')
}); });
export const useNotificationContext = () => useContext(NotificationContext); export const useNotificationContext = () => useContext(NotificationContext);
@@ -37,8 +37,8 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
const createNotification = ({ const createNotification = ({
text, text,
type = "success", type = 'success',
timeoutMs = 2000, timeoutMs = 5000
}: Notification) => { }: Notification) => {
const doesNotifExist = notifications.some((notif) => notif.text === text); const doesNotifExist = notifications.some((notif) => notif.text === text);
@@ -54,7 +54,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => {
return ( return (
<NotificationContext.Provider <NotificationContext.Provider
value={{ value={{
createNotification, createNotification
}} }}
> >
<Notifications <Notifications
@@ -1,5 +1,5 @@
import Notification from "./Notification"; import Notification from './Notification';
import { Notification as NotificationType } from "./NotificationProvider"; import { Notification as NotificationType } from './NotificationProvider';
interface NoticationsProps { interface NoticationsProps {
notifications: Required<NotificationType>[]; notifications: Required<NotificationType>[];
@@ -8,14 +8,14 @@ interface NoticationsProps {
const Notifications = ({ const Notifications = ({
notifications, notifications,
clearNotification, clearNotification
}: NoticationsProps) => { }: NoticationsProps) => {
if (!notifications.length) { if (!notifications.length) {
return null; return null;
} }
return ( 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) => ( {notifications.map((notif) => (
<Notification <Notification
key={notif.text} key={notif.text}
+6 -10
View File
@@ -54,20 +54,19 @@ const attemptLogin = async (
await login2(email, clientProof); await login2(email, clientProof);
SecurityClient.setToken(token); SecurityClient.setToken(token);
const privateKey = Aes256Gcm.decrypt( const privateKey = Aes256Gcm.decrypt({
encryptedPrivateKey, ciphertext: encryptedPrivateKey,
iv, iv,
tag, tag,
password secret: password
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size), 32 + (password.slice(0, 32).length - new Blob([password]).size),
'0' '0'
) )
); });
saveTokenToLocalStorage({ saveTokenToLocalStorage({
token,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
@@ -114,11 +113,8 @@ const attemptLogin = async (
'personal' 'personal'
], ],
DB_USERNAME: ['user1234', 'personal'], DB_USERNAME: ['user1234', 'personal'],
DB_PASSWORD: ['ah8jak3hk8dhiu4dw7whxwe1l', 'personal'], DB_PASSWORD: ['example_password', 'personal'],
TWILIO_AUTH_TOKEN: [ TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'],
'hgSIwDAKvz8PJfkj6xkzYqzGmAP3HLuG',
'shared'
],
WEBSITE_URL: ['http://localhost:3000', 'shared'], WEBSITE_URL: ['http://localhost:3000', 'shared'],
STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', '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;
@@ -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 changePassword2 from '~/pages/api/auth/ChangePassword2';
import SRP1 from "~/pages/api/auth/SRP1"; import SRP1 from '~/pages/api/auth/SRP1';
import Aes256Gcm from "./aes-256-gcm"; import Aes256Gcm from './aes-256-gcm';
const nacl = require("tweetnacl"); const nacl = require('tweetnacl');
nacl.util = require("tweetnacl-util"); nacl.util = require('tweetnacl-util');
const jsrp = require("jsrp"); const jsrp = require('jsrp');
const clientOldPassword = new jsrp.client(); const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client(); const clientNewPassword = new jsrp.client();
@@ -34,7 +34,7 @@ const changePassword = async (
clientOldPassword.init( clientOldPassword.init(
{ {
username: email, username: email,
password: currentPassword, password: currentPassword
}, },
async () => { async () => {
const clientPublicKey = clientOldPassword.getPublicKey(); const clientPublicKey = clientOldPassword.getPublicKey();
@@ -42,13 +42,13 @@ const changePassword = async (
let serverPublicKey, salt; let serverPublicKey, salt;
try { try {
const res = await SRP1({ const res = await SRP1({
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey
}); });
serverPublicKey = res.serverPublicKey; serverPublicKey = res.serverPublicKey;
salt = res.salt; salt = res.salt;
} catch (err) { } catch (err) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
console.log("Wrong current password", err, 1); console.log('Wrong current password', err, 1);
} }
clientOldPassword.setSalt(salt); clientOldPassword.setSalt(salt);
@@ -58,27 +58,27 @@ const changePassword = async (
clientNewPassword.init( clientNewPassword.init(
{ {
username: email, username: email,
password: newPassword, password: newPassword
}, },
async () => { async () => {
clientNewPassword.createVerifier(async (err, result) => { clientNewPassword.createVerifier(async (err, result) => {
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø) // The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
let { ciphertext, iv, tag } = Aes256Gcm.encrypt( const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
localStorage.getItem("PRIVATE_KEY"), text: localStorage.getItem('PRIVATE_KEY'),
newPassword secret: newPassword
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(
32 + 32 +
(newPassword.slice(0, 32).length - (newPassword.slice(0, 32).length -
new Blob([newPassword]).size), new Blob([newPassword]).size),
"0" '0'
) )
); });
if (ciphertext) { if (ciphertext) {
localStorage.setItem("encryptedPrivateKey", ciphertext); localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem("iv", iv); localStorage.setItem('iv', iv);
localStorage.setItem("tag", tag); localStorage.setItem('tag', tag);
let res; let res;
try { try {
@@ -88,14 +88,14 @@ const changePassword = async (
tag, tag,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
clientProof, clientProof
}); });
if (res.status == 400) { if (res.status == 400) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
} else if (res.status == 200) { } else if (res.status == 200) {
setPasswordChanged(true); setPasswordChanged(true);
setCurrentPassword(""); setCurrentPassword('');
setNewPassword(""); setNewPassword('');
} }
} catch (err) { } catch (err) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
@@ -108,7 +108,7 @@ const changePassword = async (
} }
); );
} catch (error) { } catch (error) {
console.log("Something went wrong during changing the password"); console.log('Something went wrong during changing the password');
} }
return true; return true;
}; };
@@ -1,12 +1,12 @@
const nacl = require("tweetnacl"); const nacl = require('tweetnacl');
nacl.util = require("tweetnacl-util"); nacl.util = require('tweetnacl-util');
const aes = require("./aes-256-gcm"); import aes from './aes-256-gcm';
type encryptAsymmetricProps = { type encryptAsymmetricProps = {
plaintext: string; plaintext: string;
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
} };
/** /**
* Return assymmetrically encrypted [plaintext] using [publicKey] where * Return assymmetrically encrypted [plaintext] using [publicKey] where
@@ -19,7 +19,11 @@ type encryptAsymmetricProps = {
* @returns {String} ciphertext - base64-encoded ciphertext * @returns {String} ciphertext - base64-encoded ciphertext
* @returns {String} nonce - base64-encoded nonce * @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 nonce = nacl.randomBytes(24);
const ciphertext = nacl.box( const ciphertext = nacl.box(
nacl.util.decodeUTF8(plaintext), nacl.util.decodeUTF8(plaintext),
@@ -30,7 +34,7 @@ const encryptAssymmetric = ({ plaintext, publicKey, privateKey }: encryptAsymmet
return { return {
ciphertext: nacl.util.encodeBase64(ciphertext), ciphertext: nacl.util.encodeBase64(ciphertext),
nonce: nacl.util.encodeBase64(nonce), nonce: nacl.util.encodeBase64(nonce)
}; };
}; };
@@ -39,7 +43,7 @@ type decryptAsymmetricProps = {
nonce: string; nonce: string;
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
} };
/** /**
* Return assymmetrically decrypted [ciphertext] using [privateKey] where * Return assymmetrically decrypted [ciphertext] using [privateKey] where
@@ -49,9 +53,13 @@ type decryptAsymmetricProps = {
* @param {String} obj.nonce - nonce * @param {String} obj.nonce - nonce
* @param {String} obj.publicKey - base64-encoded public key of the sender * @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} 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( const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext), nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce), nacl.util.decodeBase64(nonce),
@@ -65,7 +73,7 @@ const decryptAssymmetric = ({ ciphertext, nonce, publicKey, privateKey }: decryp
type encryptSymmetricProps = { type encryptSymmetricProps = {
plaintext: string; plaintext: string;
key: string; key: string;
} };
/** /**
* Return symmetrically encrypted [plaintext] using [key]. * Return symmetrically encrypted [plaintext] using [key].
@@ -73,15 +81,18 @@ type encryptSymmetricProps = {
* @param {String} obj.plaintext - plaintext to encrypt * @param {String} obj.plaintext - plaintext to encrypt
* @param {String} obj.key - 16-byte hex key * @param {String} obj.key - 16-byte hex key
*/ */
const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object => { const encryptSymmetric = ({
plaintext,
key
}: encryptSymmetricProps): object => {
let ciphertext, iv, tag; let ciphertext, iv, tag;
try { try {
const obj = aes.encrypt(plaintext, key); const obj = aes.encrypt({ text: plaintext, secret: key });
ciphertext = obj.ciphertext; ciphertext = obj.ciphertext;
iv = obj.iv; iv = obj.iv;
tag = obj.tag; tag = obj.tag;
} catch (err) { } catch (err) {
console.log("Failed to perform encryption"); console.log('Failed to perform encryption');
console.log(err); console.log(err);
process.exit(1); process.exit(1);
} }
@@ -89,7 +100,7 @@ const encryptSymmetric = ({ plaintext, key }: encryptSymmetricProps): object =>
return { return {
ciphertext, ciphertext,
iv, iv,
tag, tag
}; };
}; };
@@ -98,7 +109,7 @@ type decryptSymmetricProps = {
iv: string; iv: string;
tag: string; tag: string;
key: string; key: string;
} };
/** /**
* Return symmetrically decypted [ciphertext] using [iv], [tag], * Return symmetrically decypted [ciphertext] using [iv], [tag],
@@ -110,12 +121,17 @@ type decryptSymmetricProps = {
* @param {String} obj.key - 32-byte hex key * @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; let plaintext;
try { try {
plaintext = aes.decrypt(ciphertext, iv, tag, key); plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
} catch (err) { } catch (err) {
console.log("Failed to perform decryption"); console.log('Failed to perform decryption');
process.exit(1); process.exit(1);
} }
@@ -126,5 +142,5 @@ export {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric, decryptSymmetric,
encryptAssymmetric, encryptAssymmetric,
encryptSymmetric, encryptSymmetric
}; };
@@ -1,7 +1,6 @@
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey'; import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
import SRP1 from '~/pages/api/auth/SRP1'; import SRP1 from '~/pages/api/auth/SRP1';
import { tempLocalStorage } from '../checks/tempLocalStorage';
import generateBackupPDF from '../generateBackupPDF'; import generateBackupPDF from '../generateBackupPDF';
import Aes256Gcm from './aes-256-gcm'; import Aes256Gcm from './aes-256-gcm';
@@ -12,21 +11,22 @@ const clientPassword = new jsrp.client();
const clientKey = new jsrp.client(); const clientKey = new jsrp.client();
const crypto = require('crypto'); const crypto = require('crypto');
interface Props { interface BackupKeyProps {
email: string; email: string;
password: string; password: string;
personalName: string; personalName: string;
setBackupKeyError: any; setBackupKeyError: (value: boolean) => void;
setBackupKeyIssued: any; setBackupKeyIssued: (value: boolean) => void;
} }
/** /**
* This function loggs in the user (whether it's right after signup, or a normal login) * This function issue a backup key for a user
* @param {*} email * @param {obkect} obj
* @param {*} password * @param {string} obj.email - email of a user issuing a backup key
* @param {*} setErrorLogin * @param {string} obj.password - password of a user issuing a backup key
* @param {*} router * @param {string} obj.personalName - name of a user issuing a backup key
* @param {*} isSignUp * @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 * @returns
*/ */
const issueBackupKey = async ({ const issueBackupKey = async ({
@@ -35,7 +35,7 @@ const issueBackupKey = async ({
personalName, personalName,
setBackupKeyError, setBackupKeyError,
setBackupKeyIssued setBackupKeyIssued
}: Props) => { }: BackupKeyProps) => {
try { try {
setBackupKeyError(false); setBackupKeyError(false);
setBackupKeyIssued(false); setBackupKeyIssued(false);
@@ -72,14 +72,11 @@ const issueBackupKey = async ({
}, },
async () => { async () => {
clientKey.createVerifier( clientKey.createVerifier(
async (_: any, result: { salt: string; verifier: string }) => { async (err: any, result: { salt: string; verifier: string }) => {
// TODO: Fix this const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment text: String(localStorage.getItem('PRIVATE_KEY')),
// @ts-ignore secret: generatedKey
const { ciphertext, iv, tag } = Aes256Gcm.encrypt( });
tempLocalStorage('PRIVATE_KEY'),
generatedKey
);
const res = await issueBackupPrivateKey({ const res = await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext, encryptedPrivateKey: ciphertext,
@@ -90,10 +87,14 @@ const issueBackupKey = async ({
clientProof clientProof
}); });
if (res && res.status == 400) { if (res?.status == 400) {
setBackupKeyError(true); setBackupKeyError(true);
} else if (res && res.status == 200) { } else if (res?.status == 200) {
generateBackupPDF(personalName, email, generatedKey); generateBackupPDF({
personalName,
personalEmail: email,
generatedKey
});
setBackupKeyIssued(true); setBackupKeyIssued(true);
} }
} }
File diff suppressed because one or more lines are too long
-3
View File
@@ -3,9 +3,6 @@ import SecurityClient from '~/utilities/SecurityClient';
/** /**
* This function is used to check if the user is authenticated. * 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. * To do that, we get their tokens from cookies, and verify if they are good.
* @param {*} req
* @param {*} res
* @returns
*/ */
const checkAuth = async () => { const checkAuth = async () => {
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', { 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 * This route check the verification code from the email that user just recieved
* @param {*} email * @param {object} obj
* @param {*} code * @param {string} obj.email
* @param {string} obj.code
* @returns * @returns
*/ */
const checkEmailVerificationCode = ({ email, code }: Props) => { const checkEmailVerificationCode = ({ email, code }: Props) => {
@@ -15,16 +15,18 @@ interface Props {
/** /**
* This function is called in the end of the signup process. * This function is called in the end of the signup process.
* It sends all the necessary nformation to the server. * It sends all the necessary nformation to the server.
* @param {*} email * @param {object} obj
* @param {*} firstName * @param {string} obj.email - email of the user completing signup
* @param {*} lastName * @param {string} obj.firstName - first name of the user completing signup
* @param {*} workspace * @param {string} obj.lastName - last name of the user completing sign up
* @param {*} publicKey * @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
* @param {*} ciphertext * @param {string} obj.publicKey - public key of the user completing signup
* @param {*} iv * @param {string} obj.ciphertext
* @param {*} tag * @param {string} obj.iv
* @param {*} salt * @param {string} obj.tag
* @param {*} verifier * @param {string} obj.salt
* @param {string} obj.verifier
* @param {string} obj.token - token that confirms a user's identity
* @returns * @returns
*/ */
const completeAccountInformationSignup = ({ const completeAccountInformationSignup = ({
@@ -14,15 +14,17 @@ interface Props {
/** /**
* This function is called in the end of the signup process. * This function is called in the end of the signup process.
* It sends all the necessary nformation to the server. * It sends all the necessary nformation to the server.
* @param {*} email * @param {object} obj
* @param {*} firstName * @param {string} obj.email - email of the user completing signupinvite flow
* @param {*} lastName * @param {string} obj.firstName - first name of the user completing signupinvite flow
* @param {*} publicKey * @param {string} obj.lastName - last name of the user completing signupinvite flow
* @param {*} ciphertext * @param {string} obj.publicKey - public key of the user completing signupinvite flow
* @param {*} iv * @param {string} obj.ciphertext
* @param {*} tag * @param {string} obj.iv
* @param {*} salt * @param {string} obj.tag
* @param {*} verifier * @param {string} obj.salt
* @param {string} obj.verifier
* @param {string} obj.token - token that confirms a user's identity
* @returns * @returns
*/ */
const completeAccountInformationSignupInvite = ({ 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 * 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 = ({ const issueBackupPrivateKey = ({
encryptedPrivateKey, encryptedPrivateKey,
+1 -1
View File
@@ -6,7 +6,7 @@ interface Props {
/** /**
* This is the first step of the change password process (pake) * This is the first step of the change password process (pake)
* @param {*} clientPublicKey * @param {string} clientPublicKey
* @returns * @returns
*/ */
const SRP1 = ({ clientPublicKey }: Props) => { const SRP1 = ({ clientPublicKey }: Props) => {
@@ -5,8 +5,9 @@ interface Props {
/** /**
* This route verifies the signup invite link * This route verifies the signup invite link
* @param {*} email * @param {object} obj
* @param {*} code * @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 * @returns
*/ */
const verifySignupInvite = ({ email, code }: Props) => { const verifySignupInvite = ({ email, code }: Props) => {
+2 -2
View File
@@ -2,8 +2,8 @@ import SecurityClient from '~/utilities/SecurityClient';
/** /**
* This function fetches the encrypted secrets from the .env file * This function fetches the encrypted secrets from the .env file
* @param {*} workspaceId * @param {string} workspaceId - project is for which a user is trying to get secrets
* @param {*} env * @param {string} env - environment of a project for which a user is trying ot get secrets
* @returns * @returns
*/ */
const getSecrets = async (workspaceId: string, env: string) => { const getSecrets = async (workspaceId: string, env: string) => {
+5 -2
View File
@@ -9,8 +9,11 @@ interface Props {
/** /**
* This function uploads the encrypted .env file * This function uploads the encrypted .env file
* @param {*} req * @param {object} obj
* @param {*} res * @param {string} obj.workspaceId
* @param {} obj.secrets
* @param {} obj.keys
* @param {string} obj.environment
* @returns * @returns
*/ */
const uploadSecrets = async ({ const uploadSecrets = async ({
@@ -7,7 +7,10 @@ interface Props {
} }
/** /**
* This is the first step of the change password process (pake) * 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 * @returns
*/ */
const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => { const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) => {
+2 -2
View File
@@ -385,14 +385,14 @@ export default function Dashboard() {
if (nameErrors) { if (nameErrors) {
return createNotification({ return createNotification({
text: 'Solve all name errors first!', text: 'Solve all name errors before saving secrets.',
type: 'error' type: 'error'
}); });
} }
if (duplicatesExist) { if (duplicatesExist) {
return createNotification({ return createNotification({
text: "Your secrets weren't saved; please fix the conflicts first.", text: 'Remove duplicated secret names before saving.',
type: 'error' type: 'error'
}); });
} }
+1 -1
View File
@@ -47,7 +47,7 @@ const learningItem = ({
> >
<div <div
onClick={async () => { onClick={async () => {
if (userAction) { if (userAction && userAction != 'first_time_secrets_pushed') {
await registerUserAction({ await registerUserAction({
action: userAction action: userAction
}); });
+5 -6
View File
@@ -1,6 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactCodeInput from 'react-code-input'; import ReactCodeInput from 'react-code-input';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@@ -181,15 +180,15 @@ export default function SignUp() {
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt( const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
PRIVATE_KEY, text: PRIVATE_KEY,
password secret: password
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size), 32 + (password.slice(0, 32).length - new Blob([password]).size),
'0' '0'
) )
) as { ciphertext: string; iv: string; tag: string }; }) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
+48 -47
View File
@@ -1,38 +1,38 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import Head from "next/head"; import Head from 'next/head';
import Image from "next/image"; import Image from 'next/image';
import Link from "next/link"; import Link from 'next/link';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button"; import Button from '~/components/basic/buttons/Button';
import InputField from "~/components/basic/InputField"; import InputField from '~/components/basic/InputField';
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm"; import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey"; import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey';
import attemptLogin from "~/utilities/attemptLogin"; import attemptLogin from '~/utilities/attemptLogin';
import passwordCheck from "~/utilities/checks/PasswordCheck"; import passwordCheck from '~/utilities/checks/PasswordCheck';
import completeAccountInformationSignupInvite from "./api/auth/CompleteAccountInformationSignupInvite"; import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from "./api/auth/VerifySignupInvite"; import verifySignupInvite from './api/auth/VerifySignupInvite';
const nacl = require("tweetnacl"); const nacl = require('tweetnacl');
const jsrp = require("jsrp"); const jsrp = require('jsrp');
nacl.util = require("tweetnacl-util"); nacl.util = require('tweetnacl-util');
const client = new jsrp.client(); const client = new jsrp.client();
const queryString = require("query-string"); const queryString = require('query-string');
export default function SignupInvite() { export default function SignupInvite() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState('');
const [firstNameError, setFirstNameError] = useState(false); const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false); const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const router = useRouter(); 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 [email, setEmail] = useState(parsedUrl.to);
const token = parsedUrl.token; const token = parsedUrl.token;
const [errorLogin, setErrorLogin] = useState(false); const [errorLogin, setErrorLogin] = useState(false);
@@ -74,21 +74,22 @@ export default function SignupInvite() {
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt( const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
PRIVATE_KEY, text: PRIVATE_KEY,
password secret: password
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size), 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( client.init(
{ {
username: email, username: email,
password: password, password: password
}, },
async () => { async () => {
client.createVerifier(async (err, result) => { client.createVerifier(async (err, result) => {
@@ -102,17 +103,17 @@ export default function SignupInvite() {
tag, tag,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
token: verificationToken, token: verificationToken
}); });
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
if (!errorCheck && response.status == "200") { if (!errorCheck && response.status == '200') {
response = await response.json(); response = await response.json();
localStorage.setItem("publicKey", PUBLIC_KEY); localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem("encryptedPrivateKey", ciphertext); localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem("iv", iv); localStorage.setItem('iv', iv);
localStorage.setItem("tag", tag); localStorage.setItem('tag', tag);
try { try {
await attemptLogin( await attemptLogin(
@@ -126,7 +127,7 @@ export default function SignupInvite() {
setStep(3); setStep(3);
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
console.log("Error", error); console.log('Error', error);
} }
} }
}); });
@@ -155,14 +156,14 @@ export default function SignupInvite() {
onButtonPressed={async () => { onButtonPressed={async () => {
const response = await verifySignupInvite({ const response = await verifySignupInvite({
email, email,
code: token, code: token
}); });
if (response.status == 200) { if (response.status == 200) {
setVerificationToken((await response.json()).token); setVerificationToken((await response.json()).token);
setStep(2); setStep(2);
} else { } else {
console.log("ERROR", response); console.log('ERROR', response);
router.push("/requestnewinvite"); router.push('/requestnewinvite');
} }
}} }}
size="lg" size="lg"
@@ -211,7 +212,7 @@ export default function SignupInvite() {
setPasswordErrorLength, setPasswordErrorLength,
setPasswordErrorNumber, setPasswordErrorNumber,
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
currentErrorCheck: false, currentErrorCheck: false
}); });
}} }}
type="password" type="password"
@@ -244,7 +245,7 @@ export default function SignupInvite() {
)} )}
<div <div
className={`${ className={`${
passwordErrorLength ? "text-gray-400" : "text-gray-600" passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`} } text-sm`}
> >
14 characters 14 characters
@@ -264,7 +265,7 @@ export default function SignupInvite() {
)} )}
<div <div
className={`${ className={`${
passwordErrorLowerCase ? "text-gray-400" : "text-gray-600" passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`} } text-sm`}
> >
1 lowercase character 1 lowercase character
@@ -284,7 +285,7 @@ export default function SignupInvite() {
)} )}
<div <div
className={`${ className={`${
passwordErrorNumber ? "text-gray-400" : "text-gray-600" passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`} } text-sm`}
> >
1 number 1 number
@@ -335,11 +336,11 @@ export default function SignupInvite() {
await issueBackupKey({ await issueBackupKey({
email, email,
password, password,
personalName: firstName + " " + lastName, personalName: firstName + ' ' + lastName,
setBackupKeyError, setBackupKeyError,
setBackupKeyIssued, setBackupKeyIssued
}); });
router.push("/dashboard/"); router.push('/dashboard/');
}} }}
size="lg" size="lg"
/> />