Finish preliminary backwards-compatible transition from user encryption scheme v1 to v2 with argon2 and protected key

This commit is contained in:
Tuan Dang
2023-01-30 19:38:13 +07:00
parent 5cadb9e2f9
commit cf5603c8e3
33 changed files with 1058 additions and 478 deletions
+2
View File
@@ -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
}, },
+2 -204
View File
@@ -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
});
};
+10 -6
View File
@@ -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,
+2
View File
@@ -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
});
};
+25 -9
View File
@@ -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
}, },
+23 -3
View File
@@ -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
}, },
+26 -20
View File
@@ -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 -36
View File
@@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { 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;
+2
View File
@@ -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,
+49
View File
@@ -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;
+32 -4
View File
@@ -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`.
+5 -1
View File
@@ -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.
+6
View File
@@ -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
View File
@@ -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,
+33
View File
@@ -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",
+3
View File
@@ -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",
+76 -34
View File
@@ -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(
+22 -6
View File
@@ -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
}) })
+1 -1
View File
@@ -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",
+8 -3
View File
@@ -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'
+9 -5
View File
@@ -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
}) })
+54 -13
View File
@@ -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.
+73 -32
View File
@@ -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);
} }
}); });
} }