mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 13:26:16 +00:00
Finish preliminary backwards-compatible transition from user encryption scheme v1 to v2 with argon2 and protected key
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
|||||||
integrationAuth as v1IntegrationAuthRouter
|
integrationAuth as v1IntegrationAuthRouter
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
|
signup as v2SignupRouter,
|
||||||
auth as v2AuthRouter,
|
auth as v2AuthRouter,
|
||||||
users as v2UsersRouter,
|
users as v2UsersRouter,
|
||||||
organizations as v2OrganizationsRouter,
|
organizations as v2OrganizationsRouter,
|
||||||
@@ -110,6 +111,7 @@ app.use('/api/v1/integration', v1IntegrationRouter);
|
|||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
|
app.use('/api/v2/signup', v2SignupRouter);
|
||||||
app.use('/api/v2/auth', v2AuthRouter);
|
app.use('/api/v2/auth', v2AuthRouter);
|
||||||
app.use('/api/v2/users', v2UsersRouter);
|
app.use('/api/v2/users', v2UsersRouter);
|
||||||
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||||
|
|||||||
@@ -165,8 +165,18 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const changePassword = async (req: Request, res: Response) => {
|
export const changePassword = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
const {
|
||||||
req.body;
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
@@ -192,9 +202,13 @@ export const changePassword = async (req: Request, res: Response) => {
|
|||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
@@ -322,9 +336,12 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
export const resetPassword = async (req: Request, res: Response) => {
|
export const resetPassword = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
@@ -332,9 +349,13 @@ export const resetPassword = async (req: Request, res: Response) => {
|
|||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||||
import { User, MembershipOrg } from '../../models';
|
import { User } from '../../models';
|
||||||
import { completeAccount } from '../../helpers/user';
|
|
||||||
import {
|
import {
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
checkEmailVerification,
|
checkEmailVerification,
|
||||||
initializeDefaultOrg
|
|
||||||
} from '../../helpers/signup';
|
} from '../../helpers/signup';
|
||||||
import { issueTokens, createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { INVITED, ACCEPTED } from '../../variables';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||||
@@ -103,201 +99,3 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
|
|||||||
token
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
|
||||||
* signup flow
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
|
||||||
let user, token, refreshToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
organizationName
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
|
||||||
user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: 'Failed to complete account for complete user'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete setting up user's account
|
|
||||||
user = await completeAccount({
|
|
||||||
userId: user._id.toString(),
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user)
|
|
||||||
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
|
||||||
|
|
||||||
// initialize default organization and workspace
|
|
||||||
await initializeDefaultOrg({
|
|
||||||
organizationName,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
|
|
||||||
// update organization membership statuses that are
|
|
||||||
// invited to completed with user attached
|
|
||||||
await MembershipOrg.updateMany(
|
|
||||||
{
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
status: ACCEPTED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueTokens({
|
|
||||||
userId: user._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
token = tokens.token;
|
|
||||||
refreshToken = tokens.refreshToken;
|
|
||||||
|
|
||||||
// sending a welcome email to new users
|
|
||||||
if (process.env.LOOPS_API_KEY) {
|
|
||||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
|
||||||
"email": email,
|
|
||||||
"eventName": "Sign Up",
|
|
||||||
"firstName": firstName,
|
|
||||||
"lastName": lastName
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to complete account setup'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully set up account',
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
|
||||||
* invite flow
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
|
||||||
let user, token, refreshToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
|
||||||
user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: 'Failed to complete account for complete user'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
|
||||||
|
|
||||||
// complete setting up user's account
|
|
||||||
user = await completeAccount({
|
|
||||||
userId: user._id.toString(),
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user)
|
|
||||||
throw new Error('Failed to complete account for non-existent user');
|
|
||||||
|
|
||||||
// update organization membership statuses that are
|
|
||||||
// invited to completed with user attached
|
|
||||||
await MembershipOrg.updateMany(
|
|
||||||
{
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
status: ACCEPTED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueTokens({
|
|
||||||
userId: user._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
token = tokens.token;
|
|
||||||
refreshToken = tokens.refreshToken;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to complete account setup'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully set up account',
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -79,15 +79,11 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
// check to see if user has MFA enabled; if yes then issue MFA-token
|
|
||||||
// TODO: may have to figure out a better token system for tokens with varying expirations
|
|
||||||
// (e.g. for org-invitations vs. auth etc.)
|
|
||||||
try {
|
try {
|
||||||
const { email, clientProof } = req.body;
|
const { email, clientProof } = req.body;
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
@@ -142,6 +138,10 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
// return (access) token in response
|
// return (access) token in response
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
mfaEnabled: false,
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
protectedKey: user.protectedKey ?? null,
|
||||||
|
protectedKeyIV: user.protectedKeyIV ?? null,
|
||||||
|
protectedKeyTag: user.protectedKeyTag ?? null,
|
||||||
token: tokens.token,
|
token: tokens.token,
|
||||||
publicKey: user.publicKey,
|
publicKey: user.publicKey,
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
@@ -182,7 +182,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
@@ -200,6 +200,10 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
// case: user does not have MFA enabled
|
// case: user does not have MFA enabled
|
||||||
// return (access) token in response
|
// return (access) token in response
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
protectedKey: user.protectedKey ?? null,
|
||||||
|
protectedKeyIV: user.protectedKeyIV ?? null,
|
||||||
|
protectedKeyTag: user.protectedKeyTag ?? null,
|
||||||
token: tokens.token,
|
token: tokens.token,
|
||||||
publicKey: user.publicKey,
|
publicKey: user.publicKey,
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from './authController';
|
import * as authController from './authController';
|
||||||
|
import * as signupController from './signupController';
|
||||||
import * as usersController from './usersController';
|
import * as usersController from './usersController';
|
||||||
import * as organizationsController from './organizationsController';
|
import * as organizationsController from './organizationsController';
|
||||||
import * as workspaceController from './workspaceController';
|
import * as workspaceController from './workspaceController';
|
||||||
@@ -10,6 +11,7 @@ import * as environmentController from './environmentController';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
signupController,
|
||||||
usersController,
|
usersController,
|
||||||
organizationsController,
|
organizationsController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { User, MembershipOrg } from '../../models';
|
||||||
|
import { completeAccount } from '../../helpers/user';
|
||||||
|
import {
|
||||||
|
initializeDefaultOrg
|
||||||
|
} from '../../helpers/signup';
|
||||||
|
import { issueTokens } from '../../helpers/auth';
|
||||||
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// TODO: finish
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
|
* signup flow
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
|
let user, token, refreshToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
organizationName
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
organizationName: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// get user
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user || (user && user?.publicKey)) {
|
||||||
|
// case 1: user doesn't exist.
|
||||||
|
// case 2: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to complete account for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete setting up user's account
|
||||||
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||||
|
|
||||||
|
// initialize default organization and workspace
|
||||||
|
await initializeDefaultOrg({
|
||||||
|
organizationName,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userId: user._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
token = tokens.token;
|
||||||
|
refreshToken = tokens.refreshToken;
|
||||||
|
|
||||||
|
// sending a welcome email to new users
|
||||||
|
if (process.env.LOOPS_API_KEY) {
|
||||||
|
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||||
|
"email": email,
|
||||||
|
"eventName": "Sign Up",
|
||||||
|
"firstName": firstName,
|
||||||
|
"lastName": lastName
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to complete account setup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully set up account',
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
|
* invite flow
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||||
|
let user, token, refreshToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// get user
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user || (user && user?.publicKey)) {
|
||||||
|
// case 1: user doesn't exist.
|
||||||
|
// case 2: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to complete account for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||||
|
|
||||||
|
// complete setting up user's account
|
||||||
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to complete account for non-existent user');
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userId: user._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
token = tokens.token;
|
||||||
|
refreshToken = tokens.refreshToken;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to complete account setup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully set up account',
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { User, IUser } from '../models';
|
import { User } from '../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a user under email [email]
|
* Initialize a user under email [email]
|
||||||
@@ -28,10 +28,14 @@ const setupAccount = async ({ email }: { email: string }) => {
|
|||||||
* @param {String} obj.userId - id of user to finish setting up
|
* @param {String} obj.userId - id of user to finish setting up
|
||||||
* @param {String} obj.firstName - first name of user
|
* @param {String} obj.firstName - first name of user
|
||||||
* @param {String} obj.lastName - last name of user
|
* @param {String} obj.lastName - last name of user
|
||||||
|
* @param {Number} obj.encryptionVersion - version of auth encryption scheme used
|
||||||
|
* @param {String} obj.protectedKey - protected key in encryption version 2
|
||||||
|
* @param {String} obj.protectedKeyIV - IV of protected key in encryption version 2
|
||||||
|
* @param {String} obj.protectedKeyTag - tag of protected key in encryption version 2
|
||||||
* @param {String} obj.publicKey - publickey of user
|
* @param {String} obj.publicKey - publickey of user
|
||||||
* @param {String} obj.encryptedPrivateKey - (encrypted) private key of user
|
* @param {String} obj.encryptedPrivateKey - (encrypted) private key of user
|
||||||
* @param {String} obj.iv - iv for (encrypted) private key of user
|
* @param {String} obj.encryptedPrivateKeyIV - iv for (encrypted) private key of user
|
||||||
* @param {String} obj.tag - tag for (encrypted) private key of user
|
* @param {String} obj.encryptedPrivateKeyTag - tag for (encrypted) private key of user
|
||||||
* @param {String} obj.salt - salt for auth SRP
|
* @param {String} obj.salt - salt for auth SRP
|
||||||
* @param {String} obj.verifier - verifier for auth SRP
|
* @param {String} obj.verifier - verifier for auth SRP
|
||||||
* @returns {Object} user - the completed user
|
* @returns {Object} user - the completed user
|
||||||
@@ -40,20 +44,28 @@ const completeAccount = async ({
|
|||||||
userId,
|
userId,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
encryptionVersion,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
encryptionVersion: number;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
encryptedPrivateKeyIV: string;
|
||||||
tag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -67,10 +79,14 @@ const completeAccount = async ({
|
|||||||
{
|
{
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
encryptionVersion,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from 'mongoose';
|
||||||
import { MFA_METHOD_EMAIL } from '../variables';
|
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
email: string;
|
email: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
encryptionVersion: number;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
encryptedPrivateKey?: string;
|
encryptedPrivateKey?: string;
|
||||||
iv?: string;
|
iv?: string;
|
||||||
@@ -28,6 +31,23 @@ const userSchema = new Schema<IUser>(
|
|||||||
lastName: {
|
lastName: {
|
||||||
type: String
|
type: String
|
||||||
},
|
},
|
||||||
|
encryptionVersion: {
|
||||||
|
type: Number,
|
||||||
|
select: false,
|
||||||
|
default: 1 // to resolve backward-compatibility issues
|
||||||
|
},
|
||||||
|
protectedKey: { // introduced as part of encryption version 2
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
protectedKeyIV: { // introduced as part of encryption version 2
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
protectedKeyTag: { // introduced as part of encryption version 2
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
publicKey: {
|
publicKey: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
@@ -36,11 +56,11 @@ const userSchema = new Schema<IUser>(
|
|||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
},
|
},
|
||||||
iv: {
|
iv: { // iv of [encryptedPrivateKey]
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
},
|
},
|
||||||
tag: {
|
tag: { // tag of [encryptedPrivateKey]
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ router.post(
|
|||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
body('clientPublicKey').exists().trim().notEmpty(),
|
body('clientPublicKey').exists().isString().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.srp1
|
passwordController.srp1
|
||||||
);
|
);
|
||||||
@@ -22,11 +22,14 @@ router.post(
|
|||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
body('clientProof').exists().trim().notEmpty(),
|
body('clientProof').exists().trim().notEmpty(),
|
||||||
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
|
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd
|
||||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key
|
||||||
|
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key
|
||||||
|
body('salt').exists().isString().trim().notEmpty(), // part of new pwd
|
||||||
|
body('verifier').exists().isString().trim().notEmpty(), // part of new pwd
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.changePassword
|
passwordController.changePassword
|
||||||
);
|
);
|
||||||
@@ -34,7 +37,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/email/password-reset',
|
'/email/password-reset',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
body('email').exists().trim().notEmpty(),
|
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.emailPasswordReset
|
passwordController.emailPasswordReset
|
||||||
);
|
);
|
||||||
@@ -42,8 +45,8 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/email/password-reset-verify',
|
'/email/password-reset-verify',
|
||||||
passwordLimiter,
|
passwordLimiter,
|
||||||
body('email').exists().trim().notEmpty().isEmail(),
|
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||||
body('code').exists().trim().notEmpty(),
|
body('code').exists().isString().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.emailPasswordResetVerify
|
passwordController.emailPasswordResetVerify
|
||||||
);
|
);
|
||||||
@@ -61,12 +64,12 @@ router.post(
|
|||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
body('clientProof').exists().trim().notEmpty(),
|
body('clientProof').exists().isString().trim().notEmpty(),
|
||||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
||||||
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key
|
body('iv').exists().isString().trim().notEmpty(), // new iv for (backup) private key
|
||||||
body('tag').exists().trim().notEmpty(), // new tag for (backup) private key
|
body('tag').exists().isString().trim().notEmpty(), // new tag for (backup) private key
|
||||||
body('salt').exists().trim().notEmpty(), // salt generated from strong key
|
body('salt').exists().isString().trim().notEmpty(), // salt generated from strong key
|
||||||
body('verifier').exists().trim().notEmpty(), // salt generated from strong key
|
body('verifier').exists().isString().trim().notEmpty(), // salt generated from strong key
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.createBackupPrivateKey
|
passwordController.createBackupPrivateKey
|
||||||
);
|
);
|
||||||
@@ -74,11 +77,14 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/password-reset',
|
'/password-reset',
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // private key encrypted under new pwd
|
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||||
body('tag').exists().trim().notEmpty(), // new tag for private key
|
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||||
body('salt').exists().trim().notEmpty(), // part of new pwd
|
body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd
|
||||||
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key
|
||||||
|
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key
|
||||||
|
body('salt').exists().isString().trim().notEmpty(), // part of new pwd
|
||||||
|
body('verifier').exists().isString().trim().notEmpty(), // part of new pwd
|
||||||
validateRequest,
|
validateRequest,
|
||||||
passwordController.resetPassword
|
passwordController.resetPassword
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { requireSignupAuth, validateRequest } from '../../middleware';
|
import { validateRequest } from '../../middleware';
|
||||||
import { signupController } from '../../controllers/v1';
|
import { signupController } from '../../controllers/v1';
|
||||||
import { authLimiter } from '../../helpers/rateLimiter';
|
import { authLimiter } from '../../helpers/rateLimiter';
|
||||||
|
|
||||||
@@ -22,39 +22,4 @@ router.post(
|
|||||||
signupController.verifyEmailSignup
|
signupController.verifyEmailSignup
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/complete-account/signup',
|
|
||||||
authLimiter,
|
|
||||||
requireSignupAuth,
|
|
||||||
body('email').exists().trim().notEmpty().isEmail(),
|
|
||||||
body('firstName').exists().trim().notEmpty(),
|
|
||||||
body('lastName').exists().trim().notEmpty(),
|
|
||||||
body('publicKey').exists().trim().notEmpty(),
|
|
||||||
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
|
||||||
body('iv').exists().trim().notEmpty(),
|
|
||||||
body('tag').exists().trim().notEmpty(),
|
|
||||||
body('salt').exists().trim().notEmpty(),
|
|
||||||
body('verifier').exists().trim().notEmpty(),
|
|
||||||
body('organizationName').exists().trim().notEmpty(),
|
|
||||||
validateRequest,
|
|
||||||
signupController.completeAccountSignup
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/complete-account/invite',
|
|
||||||
authLimiter,
|
|
||||||
requireSignupAuth,
|
|
||||||
body('email').exists().trim().notEmpty().isEmail(),
|
|
||||||
body('firstName').exists().trim().notEmpty(),
|
|
||||||
body('lastName').exists().trim().notEmpty(),
|
|
||||||
body('publicKey').exists().trim().notEmpty(),
|
|
||||||
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
|
||||||
body('iv').exists().trim().notEmpty(),
|
|
||||||
body('tag').exists().trim().notEmpty(),
|
|
||||||
body('salt').exists().trim().notEmpty(),
|
|
||||||
body('verifier').exists().trim().notEmpty(),
|
|
||||||
validateRequest,
|
|
||||||
signupController.completeAccountInvite
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
|
import signup from './signup';
|
||||||
import users from './users';
|
import users from './users';
|
||||||
import organizations from './organizations';
|
import organizations from './organizations';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
@@ -10,6 +11,7 @@ import environment from "./environment"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
auth,
|
auth,
|
||||||
|
signup,
|
||||||
users,
|
users,
|
||||||
organizations,
|
organizations,
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import { requireSignupAuth, validateRequest } from '../../middleware';
|
||||||
|
import { signupController } from '../../controllers/v2';
|
||||||
|
import { authLimiter } from '../../helpers/rateLimiter';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/complete-account/signup',
|
||||||
|
authLimiter,
|
||||||
|
requireSignupAuth,
|
||||||
|
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||||
|
body('firstName').exists().isString().trim().notEmpty(),
|
||||||
|
body('lastName').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||||
|
body('publicKey').exists().isString().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKey').exists().isString().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(),
|
||||||
|
body('salt').exists().isString().trim().notEmpty(),
|
||||||
|
body('verifier').exists().isString().trim().notEmpty(),
|
||||||
|
body('organizationName').exists().isString().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.completeAccountSignup
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/complete-account/invite',
|
||||||
|
authLimiter,
|
||||||
|
requireSignupAuth,
|
||||||
|
body('email').exists().isString().trim().notEmpty().isEmail(),
|
||||||
|
body('firstName').exists().isString().trim().notEmpty(),
|
||||||
|
body('lastName').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKey').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKeyIV').exists().isString().trim().notEmpty(),
|
||||||
|
body('protectedKeyTag').exists().isString().trim().notEmpty(),
|
||||||
|
body('publicKey').exists().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKey').exists().isString().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(),
|
||||||
|
body('salt').exists().isString().trim().notEmpty(),
|
||||||
|
body('verifier').exists().isString().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.completeAccountInvite
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -6,22 +6,50 @@ Infisical stores a range of data namely user, secrets, keys, organization, proje
|
|||||||
|
|
||||||
## Users
|
## Users
|
||||||
|
|
||||||
The `User` model includes the fields `email`, `firstName`, `lastName`, `publicKey`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `refreshVersion`.
|
The `User` model includes the fields `email`, `firstName`, `lastName`, `publicKey`, `encryptionVersion`, `protectedKey`, `protectedKeyIV`, `protectedKeyTag`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `refreshVersion`.
|
||||||
|
|
||||||
Infisical makes a usability-security tradeoff to give users convenient access to public-private key pairs across different devices upon login, solving key-storage and transfer challenges across device and browser mediums, in exchange for it storing `encryptedPrivateKey`. In any case, private keys are symmetrically encrypted locally by user passwords which are not sent to the server — this is done with SRP.
|
Infisical makes a usability-security tradeoff that is to give users convenient access to public-private key pairs across different devices upon login, solving key-storage and transfer challenges across device and browser mediums, in exchange for it storing `encryptedPrivateKey`.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
`encryptedPrivateKey` is obtained by symmetrically encrypting the user's
|
||||||
|
private key locally with a protected key which is encrypted by the key derived
|
||||||
|
from the user's password and salt. Encryption is done via `AES256-GCM` and key
|
||||||
|
derivation via `argon2id`. The user's password is not sent to the server —
|
||||||
|
this is done with SRP.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretKeyHash`, `secretValueCiphertext`, `secretValueIV`, `secretValueTag`, and `secretValueHash`.
|
The `Secret` model includes the fields `workspace`, `type`, `user`, `environment`, `secretKeyCiphertext`, `secretKeyIV`, `secretKeyTag`, `secretValueCiphertext`, `secretValueIV`, and `secretValueTag`.
|
||||||
|
|
||||||
Each secret is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
|
Each secret is symmetrically encrypted by the key of the project that it belongs to; that key's encrypted copies are stored in a separate `Key` collection.
|
||||||
|
|
||||||
## Keys
|
## Project Keys
|
||||||
|
|
||||||
The `Key` model includes the fields `encryptedKey`, `nonce`, `sender`, `receiver`, and `workspace`.
|
The `Key` model includes the fields `encryptedKey`, `nonce`, `sender`, `receiver`, and `workspace`.
|
||||||
|
|
||||||
Infisical stores copies of project keys, one for each member of a project, encrypted under each member's public key.
|
Infisical stores copies of project keys, one for each member of a project, encrypted under each member's public key.
|
||||||
|
|
||||||
|
## Bots
|
||||||
|
|
||||||
|
The `Bot` model contains the fields `name`, `workspace`, `isActive`, `publicKey`, `encryptedPrivateKey`, `iv`, and `tag`.
|
||||||
|
|
||||||
|
Each project comes with a bot that has its own public-private key pair; its private key is encrypted by the server's symmetric key. If needed, a user can opt-in to share their project key with the bot (i.e. Infisical) to give the platform access to the project's secrets.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Sharing secrets with Infisical so they can be synced to integrations like
|
||||||
|
Vercel, GitHub, and Netlify is something we make sure users consent to before
|
||||||
|
opting in.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## Organizations and Workspaces
|
## Organizations and Workspaces
|
||||||
|
|
||||||
The `Organization`, `Workspace`, `MembershipOrg`, and `Membership` models contain enrollment information for organizations and projects; they are used to check if users are authorized to retrieve select secrets.
|
The `Organization`, `Workspace`, `MembershipOrg`, and `Membership` models contain enrollment information for organizations and projects; they are used to check if users are authorized to retrieve select secrets.
|
||||||
|
|
||||||
|
## Service Tokens
|
||||||
|
|
||||||
|
The `ServiceTokenData` model contains data for service tokens that enable users to fetch secrets from a particular project and environment; each service token data record includes an (encrypted) copy of the project key that it is bound to as well as a validation hash for `bcrypt`.
|
||||||
|
|
||||||
|
## API Keys
|
||||||
|
|
||||||
|
The `APIKeyData` model contains data for API keys that enable users to interact with [Infisical's Open API](https://infisical.com/docs/api-reference/overview/introduction); each API key data record includes a validation hash for `bcrypt`.
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ title: "Mechanics"
|
|||||||
|
|
||||||
## Signup
|
## Signup
|
||||||
|
|
||||||
During account signup, a user confirms their email address via OTP, generates a public-private key pair to be stored locally (private keys are symmetrically encrypted by the user's newly-made password), and forwards SRP-related values and user identifier information to the server. This includes `email`, `firstName`, `lastName`, `publicKey`, `encryptedPrivateKey`, `iv`, `tag`, `salt`, `verifier`, and `organizationName`.
|
During account signup, a user confirms their email address via OTP, generates a public-private key pair to be stored locally, generates a user salt, generates a 256-bit key, and enters their password.
|
||||||
|
|
||||||
|
The 256-bit key is used to encrypt the private key; the 256-bit key itself is then encrypted by a key generated from the user's password and salt with key derivation function `argon2id`. The resulting, 256-bit key the protected key.
|
||||||
|
|
||||||
|
The encrypted private key, protected key, user identifier information, and SRP details are forwarded to the server.
|
||||||
|
|
||||||
Once authenticated via SRP, a user is issued a JWT and refresh token. The JWT token is stored in browser memory under a write-only class `SecurityClient` that appends the token to all future outbound requests requiring authentication. The refresh token is stored in an `HttpOnly` cookie and included in future requests to `/api/token` for JWT token renewal. This design side-steps potential XSS attacks on local storage.
|
Once authenticated via SRP, a user is issued a JWT and refresh token. The JWT token is stored in browser memory under a write-only class `SecurityClient` that appends the token to all future outbound requests requiring authentication. The refresh token is stored in an `HttpOnly` cookie and included in future requests to `/api/token` for JWT token renewal. This design side-steps potential XSS attacks on local storage.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["next.config.js"]
|
||||||
|
}
|
||||||
|
],
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
@@ -87,6 +92,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
ignorePatterns: ['next.config.js'],
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
+29
-1
@@ -1,9 +1,11 @@
|
|||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('next').NextConfig}
|
* @type {import('next').NextConfig}
|
||||||
**/
|
**/
|
||||||
const { i18n } = require("./next-i18next.config.js");
|
const { i18n } = require("./next-i18next.config.js");
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const ContentSecurityPolicy = `
|
const ContentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
@@ -65,7 +67,33 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
webpack: (config, { isServer, webpack }) => {
|
webpack: (config, { isServer, webpack }) => { // config
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.wasm$/,
|
||||||
|
loader: "base64-loader",
|
||||||
|
type: "javascript/auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
config.module.noParse = /\.wasm$/;
|
||||||
|
|
||||||
|
config.module.rules.forEach((rule) => {
|
||||||
|
(rule.oneOf || []).forEach((oneOf) => {
|
||||||
|
if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
|
||||||
|
oneOf.exclude.push(/\.wasm$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
config.resolve.fallback.fs = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform customizations to webpack config
|
||||||
|
config.plugins.push(
|
||||||
|
new webpack.IgnorePlugin({ resourceRegExp: /\/__tests__\// })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Important: return the modified config
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
i18n,
|
i18n,
|
||||||
|
|||||||
Generated
+33
@@ -25,9 +25,12 @@
|
|||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
|
"@types/argon2-browser": "^1.18.1",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"axios-auth-refresh": "^3.3.3",
|
"axios-auth-refresh": "^3.3.3",
|
||||||
|
"base64-loader": "^1.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cookies": "^0.8.0",
|
"cookies": "^0.8.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
@@ -6618,6 +6621,11 @@
|
|||||||
"@testing-library/dom": ">=7.21.4"
|
"@testing-library/dom": ">=7.21.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/argon2-browser": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-PZffP/CqH9m2kovDSRQMfMMxUC3V98I7i7/caa0RB0/nvsXzYbL9bKyqZpNMFmLFGZslROlG1R60ONt7abrwlA=="
|
||||||
|
},
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
||||||
@@ -8027,6 +8035,11 @@
|
|||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/argon2-browser": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
@@ -8621,6 +8634,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-loader": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-loader/-/base64-loader-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA=="
|
||||||
|
},
|
||||||
"node_modules/better-opn": {
|
"node_modules/better-opn": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
|
||||||
@@ -26728,6 +26746,11 @@
|
|||||||
"@babel/runtime": "^7.12.5"
|
"@babel/runtime": "^7.12.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/argon2-browser": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-PZffP/CqH9m2kovDSRQMfMMxUC3V98I7i7/caa0RB0/nvsXzYbL9bKyqZpNMFmLFGZslROlG1R60ONt7abrwlA=="
|
||||||
|
},
|
||||||
"@types/aria-query": {
|
"@types/aria-query": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
|
||||||
@@ -27860,6 +27883,11 @@
|
|||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"argon2-browser": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="
|
||||||
|
},
|
||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
@@ -28295,6 +28323,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||||
},
|
},
|
||||||
|
"base64-loader": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-loader/-/base64-loader-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA=="
|
||||||
|
},
|
||||||
"better-opn": {
|
"better-opn": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz",
|
||||||
|
|||||||
@@ -32,9 +32,12 @@
|
|||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
|
"@types/argon2-browser": "^1.18.1",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"axios-auth-refresh": "^3.3.3",
|
"axios-auth-refresh": "^3.3.3",
|
||||||
|
"base64-loader": "^1.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cookies": "^0.8.0",
|
"cookies": "^0.8.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@@ -14,6 +16,8 @@ import InputField from '../basic/InputField';
|
|||||||
import attemptLogin from '../utilities/attemptLogin';
|
import attemptLogin from '../utilities/attemptLogin';
|
||||||
import passwordCheck from '../utilities/checks/PasswordCheck';
|
import passwordCheck from '../utilities/checks/PasswordCheck';
|
||||||
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||||
|
import { deriveArgonKey } from '../utilities/cryptography/crypto';
|
||||||
|
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
@@ -94,17 +98,9 @@ export default function UserInfoStep({
|
|||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
const secretKeyUint8Array = pair.secretKey;
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
const publicKeyUint8Array = pair.publicKey;
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||||
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||||
|
localStorage.setItem('PRIVATE_KEY', privateKey);
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
|
||||||
text: PRIVATE_KEY,
|
|
||||||
secret: password
|
|
||||||
.slice(0, 32)
|
|
||||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
|
||||||
}) as { ciphertext: string; iv: string; tag: string };
|
|
||||||
|
|
||||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
|
||||||
|
|
||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
@@ -113,35 +109,81 @@ export default function UserInfoStep({
|
|||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
const response = await completeAccountInformationSignup({
|
try {
|
||||||
email,
|
const derivedKey = await deriveArgonKey({
|
||||||
firstName,
|
password,
|
||||||
lastName,
|
salt: result.salt,
|
||||||
organizationName: `${firstName}'s organization`,
|
mem: 65536,
|
||||||
publicKey: PUBLIC_KEY,
|
time: 3,
|
||||||
ciphertext,
|
parallelism: 1,
|
||||||
iv,
|
hashLen: 32
|
||||||
tag,
|
});
|
||||||
salt: result.salt,
|
|
||||||
verifier: result.verifier,
|
|
||||||
token: verificationToken
|
|
||||||
});
|
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||||
if (response.status === 200) {
|
|
||||||
// response = await response.json();
|
|
||||||
|
|
||||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
const key = crypto.randomBytes(32);
|
||||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
|
||||||
localStorage.setItem('iv', iv);
|
// create encrypted private key by encrypting the private
|
||||||
localStorage.setItem('tag', tag);
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: privateKey,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString('hex'),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await completeAccountInformationSignup({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier,
|
||||||
|
token: verificationToken,
|
||||||
|
organizationName: `${firstName}'s organization`
|
||||||
|
});
|
||||||
|
|
||||||
|
// if everything works, go the main dashboard page.
|
||||||
|
if (response.status === 200) {
|
||||||
|
// response = await response.json();
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
await attemptLogin(email, password, () => {}, router, true, false);
|
await attemptLogin(email, password, () => {}, router, true, false);
|
||||||
incrementStep();
|
incrementStep();
|
||||||
} catch (error) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserP
|
|||||||
import getUser from '@app/pages/api/user/getUser';
|
import getUser from '@app/pages/api/user/getUser';
|
||||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
|
|
||||||
import { encryptAssymmetric } from './cryptography/crypto';
|
import { deriveArgonKey, encryptAssymmetric } from './cryptography/crypto';
|
||||||
import encryptSecrets from './secrets/encryptSecrets';
|
import encryptSecrets from './secrets/encryptSecrets';
|
||||||
import Telemetry from './telemetry/Telemetry';
|
import Telemetry from './telemetry/Telemetry';
|
||||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||||
@@ -59,29 +59,81 @@ const attemptLogin = async (
|
|||||||
const clientProof = client.getProof(); // called M1
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
|
const { // mfaEnabled
|
||||||
|
encryptionVersion,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
token,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = await login2(
|
||||||
email,
|
email,
|
||||||
clientProof
|
clientProof
|
||||||
);
|
);
|
||||||
|
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
const privateKey = Aes256Gcm.decrypt({
|
let privateKey;
|
||||||
ciphertext: encryptedPrivateKey,
|
if (encryptionVersion === 1) {
|
||||||
iv,
|
privateKey = Aes256Gcm.decrypt({
|
||||||
tag,
|
ciphertext: encryptedPrivateKey,
|
||||||
secret: password
|
iv,
|
||||||
.slice(0, 32)
|
tag,
|
||||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
secret: password
|
||||||
});
|
.slice(0, 32)
|
||||||
|
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||||
|
});
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
saveTokenToLocalStorage({
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
|
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error('Failed to derive key');
|
||||||
|
|
||||||
|
const key = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag,
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
// decrypt back the private key
|
||||||
|
privateKey = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
secret: Buffer.from(key, 'hex')
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privateKey) throw new Error('Failed to decrypt private key');
|
||||||
|
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ const BLOCK_SIZE_BYTES = 16; // 128 bit
|
|||||||
|
|
||||||
interface EncryptProps {
|
interface EncryptProps {
|
||||||
text: string;
|
text: string;
|
||||||
secret: string;
|
secret: string | Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DecryptProps {
|
interface DecryptProps {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
secret: string;
|
secret: string | Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EncryptOutputProps {
|
interface EncryptOutputProps {
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
/* eslint-disable new-cap */
|
/* eslint-disable new-cap */
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import jsrp from 'jsrp';
|
import jsrp from 'jsrp';
|
||||||
|
|
||||||
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
|
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
|
||||||
import SRP1 from '@app/pages/api/auth/SRP1';
|
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||||
|
|
||||||
|
import { saveTokenToLocalStorage } from '../saveTokenToLocalStorage';
|
||||||
import Aes256Gcm from './aes-256-gcm';
|
import Aes256Gcm from './aes-256-gcm';
|
||||||
|
import { deriveArgonKey } from './crypto';
|
||||||
|
|
||||||
const clientOldPassword = new jsrp.client();
|
const clientOldPassword = new jsrp.client();
|
||||||
const clientNewPassword = new jsrp.client();
|
const clientNewPassword = new jsrp.client();
|
||||||
|
|
||||||
|
// TODO: modify this function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||||
* @param {*} email
|
* @param {*} email
|
||||||
@@ -63,43 +69,75 @@ const changePassword = async (
|
|||||||
},
|
},
|
||||||
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., é, å, ø)
|
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
const derivedKey = await deriveArgonKey({
|
||||||
text: localStorage.getItem('PRIVATE_KEY') as string,
|
password: newPassword,
|
||||||
secret: newPassword
|
salt: result.salt,
|
||||||
.slice(0, 32)
|
mem: 65536,
|
||||||
.padStart(
|
time: 3,
|
||||||
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
|
parallelism: 1,
|
||||||
'0'
|
hashLen: 32
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ciphertext) {
|
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
|
||||||
localStorage.setItem('iv', iv);
|
|
||||||
localStorage.setItem('tag', tag);
|
|
||||||
|
|
||||||
let res;
|
const key = crypto.randomBytes(32);
|
||||||
try {
|
|
||||||
res = await changePassword2({
|
// create encrypted private key by encrypting the private
|
||||||
encryptedPrivateKey: ciphertext,
|
// key with the symmetric key [key]
|
||||||
iv,
|
const {
|
||||||
tag,
|
ciphertext: encryptedPrivateKey,
|
||||||
salt: result.salt,
|
iv: encryptedPrivateKeyIV,
|
||||||
verifier: result.verifier,
|
tag: encryptedPrivateKeyTag
|
||||||
clientProof
|
} = Aes256Gcm.encrypt({
|
||||||
});
|
text: localStorage.getItem('PRIVATE_KEY') as string,
|
||||||
if (res && res.status === 400) {
|
secret: key
|
||||||
setCurrentPasswordError(true);
|
});
|
||||||
} else if (res && res.status === 200) {
|
|
||||||
setPasswordChanged(true);
|
// create the protected key by encrypting the symmetric key
|
||||||
setCurrentPassword('');
|
// [key] with the derived key
|
||||||
setNewPassword('');
|
const {
|
||||||
}
|
ciphertext: protectedKey,
|
||||||
} catch (error) {
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString('hex'),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await changePassword2({
|
||||||
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
saveTokenToLocalStorage({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 400) {
|
||||||
setCurrentPasswordError(true);
|
setCurrentPasswordError(true);
|
||||||
console.log(error);
|
} else if (res && res.status === 200) {
|
||||||
|
setPasswordChanged(true);
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setCurrentPasswordError(true);
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import argon2 from 'argon2-browser';
|
||||||
|
|
||||||
import aes from './aes-256-gcm';
|
import aes from './aes-256-gcm';
|
||||||
|
|
||||||
const nacl = require('tweetnacl');
|
const nacl = require('tweetnacl');
|
||||||
@@ -9,6 +11,50 @@ type EncryptAsymmetricProps = {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.password - password to derive key from
|
||||||
|
* @param {String} obj.salt - salt to derive key from
|
||||||
|
* @param {Number} obj.mem - used memory, in KiB
|
||||||
|
* @param {Number} obj.time - number of iterations
|
||||||
|
* @param {Number} obj.parallelism - desired parallelism
|
||||||
|
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const deriveArgonKey = async ({
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
mem,
|
||||||
|
time,
|
||||||
|
parallelism,
|
||||||
|
hashLen
|
||||||
|
}: {
|
||||||
|
password: string;
|
||||||
|
salt: string;
|
||||||
|
mem: number;
|
||||||
|
time: number;
|
||||||
|
parallelism: number;
|
||||||
|
hashLen: number;
|
||||||
|
}) => {
|
||||||
|
let derivedKey;
|
||||||
|
try {
|
||||||
|
derivedKey = await argon2.hash({
|
||||||
|
pass: password,
|
||||||
|
salt,
|
||||||
|
type: argon2.ArgonType.Argon2id,
|
||||||
|
mem,
|
||||||
|
time,
|
||||||
|
parallelism,
|
||||||
|
hashLen
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return derivedKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
* [publicKey] likely belongs to the recipient.
|
* [publicKey] likely belongs to the recipient.
|
||||||
@@ -138,4 +184,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
|||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
|
export {
|
||||||
|
decryptAssymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
deriveArgonKey,
|
||||||
|
encryptAssymmetric,
|
||||||
|
encryptSymmetric
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
publicKey: string;
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
publicKey?: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
privateKey: string;
|
privateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveTokenToLocalStorage = ({
|
export const saveTokenToLocalStorage = ({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
@@ -14,11 +20,38 @@ export const saveTokenToLocalStorage = ({
|
|||||||
privateKey,
|
privateKey,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("publicKey", publicKey);
|
localStorage.removeItem("protectedKey");
|
||||||
|
localStorage.removeItem("protectedKeyIV");
|
||||||
|
localStorage.removeItem("protectedKeyTag");
|
||||||
|
localStorage.removeItem("publicKey");
|
||||||
|
localStorage.removeItem("encryptedPrivateKey");
|
||||||
|
localStorage.removeItem("iv");
|
||||||
|
localStorage.removeItem("tag");
|
||||||
|
localStorage.removeItem("PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (protectedKey) {
|
||||||
|
localStorage.setItem("protectedKey", protectedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protectedKeyIV) {
|
||||||
|
localStorage.setItem("protectedKeyIV", protectedKeyIV);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protectedKeyTag) {
|
||||||
|
localStorage.setItem("protectedKeyTag", protectedKeyTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKey) {
|
||||||
|
localStorage.setItem("publicKey", publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey) {
|
||||||
|
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
||||||
localStorage.setItem("iv", iv);
|
localStorage.setItem("iv", iv);
|
||||||
localStorage.setItem("tag", tag);
|
localStorage.setItem("tag", tag);
|
||||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
clientProof: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
encryptedPrivateKeyIV: string;
|
||||||
tag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
clientProof: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +17,17 @@ interface Props {
|
|||||||
* @param {*} clientPublicKey
|
* @param {*} clientPublicKey
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
|
const changePassword2 = ({
|
||||||
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
}: Props) =>
|
||||||
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -22,9 +35,12 @@ const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientP
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
clientProof,
|
clientProof,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ interface Props {
|
|||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
ciphertext: string;
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
token: string;
|
token: string;
|
||||||
@@ -19,6 +22,9 @@ interface Props {
|
|||||||
* @param {string} obj.email - email of the user completing signup
|
* @param {string} obj.email - email of the user completing signup
|
||||||
* @param {string} obj.firstName - first name 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.lastName - last name of the user completing sign up
|
||||||
|
* @param {string} obj.protectedKey - protected key in encryption version 2
|
||||||
|
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
|
||||||
|
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
|
||||||
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
|
* @param {string} obj.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.publicKey - public key of the user completing signup
|
||||||
* @param {string} obj.ciphertext
|
* @param {string} obj.ciphertext
|
||||||
@@ -33,15 +39,18 @@ const completeAccountInformationSignup = ({
|
|||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName,
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
ciphertext,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
token
|
token,
|
||||||
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
|
organizationName
|
||||||
|
}: Props) => fetch('/api/v2/signup/complete-account/signup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -51,13 +60,16 @@ const completeAccountInformationSignup = ({
|
|||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey,
|
||||||
organizationName,
|
encryptedPrivateKeyIV,
|
||||||
iv,
|
encryptedPrivateKeyTag,
|
||||||
tag,
|
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier,
|
||||||
|
organizationName
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ interface Props {
|
|||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
ciphertext: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
encryptedPrivateKeyIV: string;
|
||||||
tag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
token: string;
|
token: string;
|
||||||
@@ -31,27 +34,33 @@ const completeAccountInformationSignupInvite = ({
|
|||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
ciphertext,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
token
|
token
|
||||||
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
|
}: Props) => fetch('/api/v2/signup/complete-account/invite', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${ token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface Login1 {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const login1 = async (email: string, clientPublicKey: string) => {
|
const login1 = async (email: string, clientPublicKey: string) => {
|
||||||
const response = await fetch("/api/v1/auth/login1", {
|
const response = await fetch("/api/v2/auth/login1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
interface Login2Response {
|
interface Login2Response {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: number;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
publicKey: string;
|
|
||||||
tag: string;
|
tag: string;
|
||||||
token: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +18,7 @@ interface Login2Response {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const login2 = async (email: string, clientProof: string) => {
|
const login2 = async (email: string, clientProof: string) => {
|
||||||
const response = await fetch('/api/v1/auth/login2', {
|
const response = await fetch('/api/v2/auth/login2', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ const logout = async () =>
|
|||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
SecurityClient.setToken('');
|
SecurityClient.setToken('');
|
||||||
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
||||||
localStorage.setItem('publicKey', '');
|
localStorage.removeItem('protectedKey');
|
||||||
localStorage.setItem('encryptedPrivateKey', '');
|
localStorage.removeItem('protectedKeyIV');
|
||||||
localStorage.setItem('iv', '');
|
localStorage.removeItem('protectedKeyTag');
|
||||||
localStorage.setItem('tag', '');
|
localStorage.removeItem('publicKey');
|
||||||
localStorage.setItem('PRIVATE_KEY', '');
|
localStorage.removeItem('encryptedPrivateKey');
|
||||||
|
localStorage.removeItem('iv');
|
||||||
|
localStorage.removeItem('tag');
|
||||||
|
localStorage.removeItem('PRIVATE_KEY');
|
||||||
|
|
||||||
console.log('User logged out', res);
|
console.log('User logged out', res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
verificationToken: string;
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
iv: string;
|
encryptedPrivateKeyIV: string;
|
||||||
tag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
|
verificationToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,22 +22,28 @@ interface Props {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const resetPasswordOnAccountRecovery = ({
|
const resetPasswordOnAccountRecovery = ({
|
||||||
verificationToken,
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier,
|
||||||
|
verificationToken,
|
||||||
}: Props) => fetch('/api/v1/password/password-reset', {
|
}: Props) => fetch('/api/v1/password/password-reset', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${ verificationToken}`
|
Authorization: `Bearer ${verificationToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
encryptedPrivateKeyIV,
|
||||||
tag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -12,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
|||||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
|
||||||
|
import { deriveArgonKey } from '../components/utilities/cryptography/crypto';
|
||||||
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
|
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
|
||||||
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
|
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
|
||||||
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
|
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
|
||||||
@@ -39,6 +42,7 @@ export default function PasswordReset() {
|
|||||||
const getEncryptedKeyHandler = async () => {
|
const getEncryptedKeyHandler = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||||
|
|
||||||
setPrivateKey(
|
setPrivateKey(
|
||||||
Aes256Gcm.decrypt({
|
Aes256Gcm.decrypt({
|
||||||
ciphertext: result.encryptedPrivateKey,
|
ciphertext: result.encryptedPrivateKey,
|
||||||
@@ -64,13 +68,12 @@ export default function PasswordReset() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
// Generate a random pair of a public and a private key
|
// const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
// text: privateKey,
|
||||||
text: privateKey,
|
// secret: newPassword
|
||||||
secret: newPassword
|
// .slice(0, 32)
|
||||||
.slice(0, 32)
|
// .padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
|
||||||
.padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
|
// }) as { ciphertext: string; iv: string; tag: string };
|
||||||
}) as { ciphertext: string; iv: string; tag: string };
|
|
||||||
|
|
||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
@@ -79,13 +82,51 @@ export default function PasswordReset() {
|
|||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
const response = await resetPasswordOnAccountRecovery({
|
const derivedKey = await deriveArgonKey({
|
||||||
verificationToken,
|
password: newPassword,
|
||||||
encryptedPrivateKey: ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier
|
mem: 65536,
|
||||||
|
time: 3,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLen: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
// create encrypted private key by encrypting the private
|
||||||
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: privateKey,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString('hex'),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await resetPasswordOnAccountRecovery({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier,
|
||||||
|
verificationToken
|
||||||
});
|
});
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -17,6 +19,7 @@ import InputField from '@app/components/basic/InputField';
|
|||||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||||
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
||||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||||
|
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
|
||||||
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
||||||
|
|
||||||
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
||||||
@@ -75,17 +78,17 @@ export default function SignupInvite() {
|
|||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
const secretKeyUint8Array = pair.secretKey;
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
const publicKeyUint8Array = pair.publicKey;
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||||
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||||
|
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
// const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||||
text: PRIVATE_KEY,
|
// text: PRIVATE_KEY,
|
||||||
secret: password
|
// secret: password
|
||||||
.slice(0, 32)
|
// .slice(0, 32)
|
||||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
// .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||||
});
|
// });
|
||||||
|
|
||||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
localStorage.setItem('PRIVATE_KEY', privateKey);
|
||||||
|
|
||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
@@ -94,35 +97,73 @@ export default function SignupInvite() {
|
|||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
client.createVerifier(async (err, result) => {
|
client.createVerifier(async (err, result) => {
|
||||||
let response = await completeAccountInformationSignupInvite({
|
try {
|
||||||
email,
|
const derivedKey = await deriveArgonKey({
|
||||||
firstName,
|
password,
|
||||||
lastName,
|
salt: result.salt,
|
||||||
publicKey: PUBLIC_KEY,
|
mem: 65536,
|
||||||
ciphertext,
|
time: 3,
|
||||||
iv,
|
parallelism: 1,
|
||||||
tag,
|
hashLen: 32
|
||||||
salt: result.salt,
|
});
|
||||||
verifier: result.verifier,
|
|
||||||
token: verificationToken
|
|
||||||
});
|
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||||
if (!errorCheck && response.status === 200) {
|
|
||||||
response = await response.json();
|
|
||||||
|
|
||||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
const key = crypto.randomBytes(32);
|
||||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
|
||||||
localStorage.setItem('iv', iv);
|
// create encrypted private key by encrypting the private
|
||||||
localStorage.setItem('tag', tag);
|
// key with the symmetric key [key]
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: privateKey,
|
||||||
|
secret: key
|
||||||
|
});
|
||||||
|
|
||||||
|
// create the protected key by encrypting the symmetric key
|
||||||
|
// [key] with the derived key
|
||||||
|
const {
|
||||||
|
ciphertext: protectedKey,
|
||||||
|
iv: protectedKeyIV,
|
||||||
|
tag: protectedKeyTag
|
||||||
|
} = Aes256Gcm.encrypt({
|
||||||
|
text: key.toString('hex'),
|
||||||
|
secret: Buffer.from(derivedKey.hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = await completeAccountInformationSignupInvite({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier,
|
||||||
|
token: verificationToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// if everything works, go the main dashboard page.
|
||||||
|
if (!errorCheck && response.status === 200) {
|
||||||
|
response = await response.json();
|
||||||
|
|
||||||
|
localStorage.setItem('publicKey', publicKey);
|
||||||
|
localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
|
||||||
|
localStorage.setItem('iv', encryptedPrivateKeyIV);
|
||||||
|
localStorage.setItem('tag', encryptedPrivateKeyTag);
|
||||||
|
|
||||||
try {
|
|
||||||
await attemptLogin(email, password, setErrorLogin, router, false, false);
|
await attemptLogin(email, password, setErrorLogin, router, false, false);
|
||||||
setStep(3);
|
setStep(3);
|
||||||
} catch (error) {
|
|
||||||
setIsLoading(false);
|
|
||||||
console.log('Error', error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsLoading(false);
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user