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
+22 -6
View File
@@ -1,12 +1,15 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
clientProof: string;
}
/**
@@ -14,7 +17,17 @@ interface Props {
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
const changePassword2 = ({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: Props) =>
SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST',
headers: {
@@ -22,9 +35,12 @@ const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientP
},
body: JSON.stringify({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
@@ -2,11 +2,14 @@ interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
organizationName: string;
iv: string;
tag: string;
salt: string;
verifier: string;
token: string;
@@ -19,6 +22,9 @@ interface Props {
* @param {string} obj.email - email of the user completing signup
* @param {string} obj.firstName - first name of the user completing signup
* @param {string} obj.lastName - last name of the user completing sign up
* @param {string} obj.protectedKey - protected key in encryption version 2
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
* @param {string} obj.publicKey - public key of the user completing signup
* @param {string} obj.ciphertext
@@ -33,15 +39,18 @@ const completeAccountInformationSignup = ({
email,
firstName,
lastName,
organizationName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
token,
organizationName
}: Props) => fetch('/api/v2/signup/complete-account/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -51,13 +60,16 @@ const completeAccountInformationSignup = ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey: ciphertext,
organizationName,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
verifier,
organizationName
})
});
@@ -2,10 +2,13 @@ interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
iv: string;
tag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
@@ -31,27 +34,33 @@ const completeAccountInformationSignupInvite = ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
}: Props) => fetch('/api/v2/signup/complete-account/invite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token}`
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
+1 -1
View File
@@ -10,7 +10,7 @@ interface Login1 {
* @returns
*/
const login1 = async (email: string, clientPublicKey: string) => {
const response = await fetch("/api/v1/auth/login1", {
const response = await fetch("/api/v2/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
+8 -3
View File
@@ -1,9 +1,14 @@
interface Login2Response {
mfaEnabled: boolean;
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
publicKey: string;
tag: string;
token: string;
}
/**
@@ -13,7 +18,7 @@ interface Login2Response {
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const response = await fetch('/api/v1/auth/login2', {
const response = await fetch('/api/v2/auth/login2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+9 -5
View File
@@ -15,11 +15,15 @@ const logout = async () =>
if (res?.status === 200) {
SecurityClient.setToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem('publicKey', '');
localStorage.setItem('encryptedPrivateKey', '');
localStorage.setItem('iv', '');
localStorage.setItem('tag', '');
localStorage.setItem('PRIVATE_KEY', '');
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');
console.log('User logged out', res);
return res;
}
@@ -1,10 +1,13 @@
interface Props {
verificationToken: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
verificationToken: string;
}
/**
@@ -19,22 +22,28 @@ interface Props {
* @returns
*/
const resetPasswordOnAccountRecovery = ({
verificationToken,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
verifier,
verificationToken,
}: Props) => fetch('/api/v1/password/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ verificationToken}`
Authorization: `Bearer ${verificationToken}`
},
body: JSON.stringify({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
+54 -13
View File
@@ -1,3 +1,5 @@
import crypto from 'crypto';
import { useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
@@ -12,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { deriveArgonKey } from '../components/utilities/cryptography/crypto';
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
@@ -39,6 +42,7 @@ export default function PasswordReset() {
const getEncryptedKeyHandler = async () => {
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
@@ -64,13 +68,12 @@ export default function PasswordReset() {
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: privateKey,
secret: newPassword
.slice(0, 32)
.padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
// const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
// text: privateKey,
// secret: newPassword
// .slice(0, 32)
// .padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
// }) as { ciphertext: string; iv: string; tag: string };
client.init(
{
@@ -79,13 +82,51 @@ export default function PasswordReset() {
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
const response = await resetPasswordOnAccountRecovery({
verificationToken,
encryptedPrivateKey: ciphertext,
iv,
tag,
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
verifier: result.verifier
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const response = await resetPasswordOnAccountRecovery({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken
});
// if everything works, go the main dashboard page.
+73 -32
View File
@@ -1,5 +1,7 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from 'crypto';
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
@@ -17,6 +19,7 @@ import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
@@ -75,17 +78,17 @@ export default function SignupInvite() {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
// const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
// text: PRIVATE_KEY,
// secret: password
// .slice(0, 32)
// .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
// });
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
localStorage.setItem('PRIVATE_KEY', privateKey);
client.init(
{
@@ -94,35 +97,73 @@ export default function SignupInvite() {
},
async () => {
client.createVerifier(async (err, result) => {
let response = await completeAccountInformationSignupInvite({
email,
firstName,
lastName,
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken
});
try {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
// if everything works, go the main dashboard page.
if (!errorCheck && response.status === 200) {
response = await response.json();
if (!derivedKey) throw new Error('Failed to derive key from password');
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
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);
setStep(3);
} catch (error) {
setIsLoading(false);
console.log('Error', error);
}
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}