more webapp profile refactor

This commit is contained in:
Roland Osborne 2023-01-23 15:09:09 -08:00
parent 91c5a88096
commit 4cba051a5d
8 changed files with 175 additions and 174 deletions

View File

@ -79,3 +79,107 @@ export function decryptTopicSubject(subject, contentKey) {
return JSON.parse(dec.toString(CryptoJS.enc.Utf8));
}
function convertPem(pem) {
var lines = pem.split('\n');
var encoded = '';
for(var i = 0;i < lines.length;i++){
if (lines[i].trim().length > 0 &&
lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 &&
lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) {
encoded += lines[i].trim();
}
}
return encoded
};
export async function generateSeal(password) {
// generate key to encrypt private key
const salt = CryptoJS.lib.WordArray.random(128 / 8);
const aes = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// generate rsa key for sealing channel, delay for activity indicators
await new Promise(r => setTimeout(r, 1000));
const crypto = new JSEncrypt({ default_key_size: 2048 });
crypto.getKey();
// encrypt private key
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const privateKey = convertPem(crypto.getPrivateKey());
const enc = CryptoJS.AES.encrypt(privateKey, aes, { iv: iv });
const publicKey = convertPem(crypto.getPublicKey());
// update account
const seal = {
passwordSalt: salt.toString(),
privateKeyIv: iv.toString(),
privateKeyEncrypted: enc.ciphertext.toString(CryptoJS.enc.Base64),
publicKey: publicKey,
}
const sealKey = {
public: publicKey,
private: privateKey,
}
return { seal, sealKey };
}
export function unlockSeal(seal, password) {
// generate key to encrypt private key
const salt = CryptoJS.enc.Hex.parse(seal.passwordSalt);
const aes = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// decrypt private key
const iv = CryptoJS.enc.Hex.parse(seal.privateKeyIv);
const enc = CryptoJS.enc.Base64.parse(seal.privateKeyEncrypted)
let cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: enc,
iv: iv
});
const dec = CryptoJS.AES.decrypt(cipherParams, aes, { iv: iv });
const privateKey = dec.toString(CryptoJS.enc.Utf8)
// store ke
const sealKey = {
public: seal.publicKey,
private: privateKey,
}
return sealKey;
}
export function updateSeal(seal, sealKey, password) {
// generate key to encrypt private key
const salt = CryptoJS.lib.WordArray.random(128 / 8);
const aes = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// encrypt private key
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const enc = CryptoJS.AES.encrypt(sealKey.private, aes, { iv: iv });
// update account
const updated = {
passwordSalt: salt.toString(),
privateKeyIv: iv.toString(),
privateKeyEncrypted: enc.ciphertext.toString(CryptoJS.enc.Base64),
publicKey: seal.publicKey,
}
return { seal: updated, sealKey };
}

View File

@ -28,7 +28,7 @@ export function useProfileContext() {
const token = access.current;
const revision = curRevision.current;
const identity = await getProfile(access.current);
const imageUrl = identity.image ? getProfileImageUrl(token, revision) : null;
const imageUrl = identity.image ? getProfileImageUrl(token, identity.revision) : null;
setRevision.current = revision;
updateState({ offsync: false, identity, imageUrl });
}

View File

@ -140,7 +140,7 @@ export function Profile({ closeProfile }) {
</div>
</div>
</div>
{ state.init && state.display !== 'xlarge' && (
{ state.display !== 'xlarge' && (
<div className="account">
<div className="section">Account Settings</div>
<div className="controls">

View File

@ -173,8 +173,6 @@ export const ProfileWrapper = styled.div`
flex-direction: column;
align-items: center;
justify-content: center;
border-top: 1px solid ${Colors.divider};
border-radius: 4px;
padding: 8px;
width: 75%;
}

View File

@ -1,11 +1,11 @@
import { AccountAccessWrapper, SealModal, EditFooter } from './AccountAccess.styled';
import { useAccountAccess } from './useAccountAccess.hook';
import { AccountLogin } from './accountLogin/AccountLogin';
import { Button, Modal, Switch, Input } from 'antd';
import { SettingOutlined, LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Button, Modal, Switch, Form, Input } from 'antd';
import { SettingOutlined, UserOutlined, LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
export function AccountAccess() {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useAccountAccess();
const saveSeal = async () => {
@ -15,7 +15,7 @@ export function AccountAccess() {
}
catch (err) {
console.log(err);
Modal.error({
modal.error({
title: 'Failed to Set Sealing Key',
comment: 'Please try again.',
});
@ -28,7 +28,7 @@ export function AccountAccess() {
}
catch (err) {
console.log(err);
Modal.error({
modal.error({
title: 'Update Registry Failed',
content: 'Please try again.',
});
@ -42,7 +42,7 @@ export function AccountAccess() {
}
catch (err) {
console.log(err);
Modal.error({
modal.error({
title: 'Failed to Save',
comment: 'Please try again.',
});
@ -51,7 +51,7 @@ export function AccountAccess() {
const editLoginFooter = (
<EditFooter>
<div class="select"></div>
<div className="select"></div>
<Button key="back" onClick={actions.clearEditLogin}>Cancel</Button>
<Button key="save" type="primary" onClick={saveLogin} disabled={!actions.canSaveLogin()} loading={state.busy}>Save</Button>
</EditFooter>
@ -59,7 +59,7 @@ export function AccountAccess() {
const editSealFooter = (
<EditFooter>
<div class="select"></div>
<div className="select"></div>
<Button key="back" onClick={actions.clearEditSeal}>Cancel</Button>
{ state.sealMode === 'enabled' && (
<Button key="save" type="primary" onClick={saveSeal} loading={state.busy}>Forget</Button>
@ -75,50 +75,51 @@ export function AccountAccess() {
return (
<AccountAccessWrapper>
<div class="switch">
{ modalContext }
<div className="switch">
<Switch size="small" checked={state.searchable} onChange={enable => saveSearchable(enable)} />
<div class="switchLabel">Visible in Registry &nbsp;&nbsp;</div>
<div className="switchLabel">Visible in Registry &nbsp;&nbsp;</div>
</div>
<div class="link" onClick={actions.setEditSeal}>
<div className="link" onClick={actions.setEditSeal}>
<SettingOutlined />
<div class="label">Sealed Topics</div>
<div className="label">Sealed Topics</div>
</div>
<div class="link" onClick={actions.setEditLogin}>
<div className="link" onClick={actions.setEditLogin}>
<LockOutlined />
<div class="label">Change Login</div>
<div className="label">Change Login</div>
</div>
<Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal}>
<SealModal>
<div class="switch">
<div className="switch">
<Switch size="small" checked={state.sealEnabled} onChange={enable => actions.enableSeal(enable)} />
<div class="switchLabel">Enable Sealed Topics</div>
<div className="switchLabel">Enable Sealed Topics</div>
</div>
{ (state.sealMode === 'updating' || state.sealMode === 'enabling') && (
<div class="sealPassword">
<div className="sealPassword">
<Input.Password placeholder="New Password" spellCheck="false" onChange={(e) => actions.setSealPassword(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</div>
)}
{ (state.sealMode === 'updating' || state.sealMode === 'enabling') && (
<div class="sealPassword">
<div className="sealPassword">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setSealConfirm(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</div>
)}
{ state.sealMode === 'disabling' && (
<div class="sealPassword">
<div className="sealPassword">
<Input placeholder="Type 'delete' to remove key" spellCheck="false" onChange={(e) => actions.setSealDelete(e.target.value)}
prefix={<ExclamationCircleOutlined />} />
</div>
)}
{ state.sealMode === 'enabled' && (
<div class="sealPassword" onClick={() => actions.updateSeal()}>
<div className="sealPassword" onClick={() => actions.updateSeal()}>
<Input.Password defaultValue="xxxxxxxxxx" disabled={true} prefix={<LockOutlined />} />
<div class="editPassword" />
<div className="editPassword" />
</div>
)}
{ state.sealMode === 'unlocking' && (
<div class="sealPassword">
<div className="sealPassword">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setSealUnlock(e.target.value)}
prefix={<LockOutlined />} />
</div>
@ -127,7 +128,22 @@ export function AccountAccess() {
</Modal>
<Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter}
onCancel={actions.clearEditLogin}>
<AccountLogin state={state} actions={actions} />
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)}
defaultValue={state.editHandle} autocomplete="username" autocapitalize="none" prefix={<UserOutlined />} />
</Form.Item>
<Form.Item name="password">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setEditPassword(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
<Form.Item name="confirm">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setEditConfirm(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
</Form>
</Modal>
</AccountAccessWrapper>
);

View File

@ -1,24 +0,0 @@
import { Form, Input } from 'antd';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
export function AccountLogin({ state, actions }) {
return (
<Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)}
defaultValue={state.editHandle} autocomplete="username" autocapitalize="none" prefix={<UserOutlined />} />
</Form.Item>
<Form.Item name="password">
<Input.Password placeholder="Password" spellCheck="false" onChange={(e) => actions.setEditPassword(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
<Form.Item name="confirm">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setEditConfirm(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</Form.Item>
</Form>
);
}

View File

@ -1,6 +1,7 @@
import { useRef, useState, useEffect, useContext } from 'react';
import { AccountContext } from 'context/AccountContext';
import { ProfileContext } from 'context/ProfileContext';
import { generateSeal, unlockSeal, updateSeal } from 'context/sealUtil';
import { getUsername } from 'api/getUsername';
import CryptoJS from 'crypto-js';
import { JSEncrypt } from 'jsencrypt'
@ -40,63 +41,18 @@ export function useAccountAccess() {
}
useEffect(() => {
if (profile.state.identity?.guid) {
const { handle } = profile.state.identity;
updateState({ handle, editHandle: handle });
}
}, [profile]);
const { handle } = profile.state.identity;
updateState({ handle, editHandle: handle });
}, [profile.state]);
useEffect(() => {
if (account?.state?.status) {
const { seal, sealKey, status } = account.state;
updateState({ searchable: status.searchable, seal, sealKey });
}
}, [account]);
const { seal, sealKey, status } = account.state;
updateState({ searchable: status.searchable, seal, sealKey });
}, [account.state]);
const convertPem = (pem) => {
var lines = pem.split('\n');
var encoded = '';
for(var i = 0;i < lines.length;i++){
if (lines[i].trim().length > 0 &&
lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 &&
lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) {
encoded += lines[i].trim();
}
}
return encoded
};
const sealUnlock = async () => {
// generate key to encrypt private key
const salt = CryptoJS.enc.Hex.parse(state.seal.passwordSalt);
const aes = CryptoJS.PBKDF2(state.sealUnlock, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// decrypt private key
const iv = CryptoJS.enc.Hex.parse(state.seal.privateKeyIv);
const enc = CryptoJS.enc.Base64.parse(state.seal.privateKeyEncrypted)
let cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: enc,
iv: iv
});
const dec = CryptoJS.AES.decrypt(cipherParams, aes, { iv: iv });
const privateKey = dec.toString(CryptoJS.enc.Utf8)
// store ke
const sealKey = {
public: state.seal.publicKey,
private: privateKey,
}
await account.actions.unlockSeal(sealKey);
const unlocked = unlockSeal(state.seal, state.sealUnlock);
await account.actions.unlockSeal(unlocked);
};
const sealForget = async () => {
@ -104,37 +60,8 @@ export function useAccountAccess() {
};
const sealEnable = async () => {
// generate key to encrypt private key
const salt = CryptoJS.lib.WordArray.random(128 / 8);
const aes = CryptoJS.PBKDF2(state.sealPassword, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// generate rsa key for sealing channel, delay for activity indicator
await new Promise(r => setTimeout(r, 1000));
const crypto = new JSEncrypt({ default_key_size: 2048 });
crypto.getKey();
// encrypt private key
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const privateKey = convertPem(crypto.getPrivateKey());
const enc = CryptoJS.AES.encrypt(privateKey, aes, { iv: iv });
const publicKey = convertPem(crypto.getPublicKey());
// update account
const seal = {
passwordSalt: salt.toString(),
privateKeyIv: iv.toString(),
privateKeyEncrypted: enc.ciphertext.toString(CryptoJS.enc.Base64),
publicKey: publicKey,
}
const sealKey = {
public: publicKey,
private: privateKey,
}
await account.actions.setSeal(seal, sealKey);
const generated = await generateSeal(state.sealPassword);
await account.actions.setSeal(generated.seal, generated.sealKey);
};
const sealRemove = async () => {
@ -142,26 +69,8 @@ export function useAccountAccess() {
};
const sealUpdate = async () => {
// generate key to encrypt private key
const salt = CryptoJS.lib.WordArray.random(128 / 8);
const aes = CryptoJS.PBKDF2(state.sealPassword, salt, {
keySize: 256 / 32,
iterations: 1024,
});
// encrypt private key
const iv = CryptoJS.lib.WordArray.random(128 / 8);
const enc = CryptoJS.AES.encrypt(state.sealKey.private, aes, { iv: iv });
// update account
const seal = {
passwordSalt: salt.toString(),
privateKeyIv: iv.toString(),
privateKeyEncrypted: enc.ciphertext.toString(CryptoJS.enc.Base64),
publicKey: state.seal.publicKey,
}
await account.actions.updateSeal(seal);
const updated = updateSeal(state.seal, state.sealKey, state.sealPassword);
await account.actions.updateSeal(state.seal);
};
const isEnabled = () => {
@ -296,18 +205,20 @@ export function useAccountAccess() {
if (editHandle.toLowerCase() === state.handle.toLowerCase()) {
updateState({ checked: true, editStatus: 'success', editMessage: '' });
}
try {
let valid = await getUsername(editHandle);
if (valid) {
else {
try {
let valid = await getUsername(editHandle);
if (valid) {
updateState({ checked: true, editStatus: 'success', editMessage: '' });
}
else {
updateState({ checked: true, editStatus: 'error', editMessage: 'Username is not available' });
}
}
catch(err) {
console.log(err);
updateState({ checked: true, editStatus: 'success', editMessage: '' });
}
else {
updateState({ checked: true, editStatus: 'error', editMessage: 'Username is not available' });
}
}
catch(err) {
console.log(err);
updateState({ checked: true, editStatus: 'success', editMessage: '' });
}
}, 500);
},

View File

@ -18,10 +18,8 @@ export function useProfile() {
editProfileImage: false,
editProfileDetails: false,
clip: { w: 0, h: 0, x: 0, y: 0 },
crop: { x: 0, y: 0},
zoom: 1,
busy: false,
});
@ -35,13 +33,11 @@ export function useProfile() {
}
useEffect(() => {
if (profile.state.identity.guid) {
const { node, name, handle, location, description, image, imageUrl } = profile.state.identity;
let url = !image ? null : profile.state.imageUrl;
let editImage = !image ? avatar : url;
updateState({ name, location, description, node, handle, url,
editName: name, editLocation: location, editDescription: description, editHandle: handle, editImage });
}
const { node, name, handle, location, description, image, imageUrl } = profile.state.identity;
let url = !image ? null : profile.state.imageUrl;
let editImage = !image ? avatar : url;
updateState({ name, location, description, node, handle, url,
editName: name, editLocation: location, editDescription: description, editHandle: handle, editImage });
}, [profile.state]);
useEffect(() => {