support seal key handling in webapp

This commit is contained in:
Roland Osborne 2022-12-06 22:52:53 -08:00
parent 538094bf7f
commit 009f5c3a96
13 changed files with 8778 additions and 7316 deletions

View File

@ -3699,9 +3699,12 @@ components:
- privateKeyEncrypted
- publicKey
properties:
salt:
passwordSalt:
type: string
format: base64 encoded data
format: hex encoded data
privateKeyIv:
type: string
format: hex encoded data
privateKeyEncrypted:
type: string
format: base64 encoded data

View File

@ -23,7 +23,8 @@ func GetAccountStatus(w http.ResponseWriter, r *http.Request) {
// construct response
seal := &Seal{}
seal.Salt = account.AccountDetail.SealSalt
seal.PasswordSalt = account.AccountDetail.SealSalt
seal.PrivateKeyIV = account.AccountDetail.SealIV
seal.PrivateKeyEncrypted = account.AccountDetail.SealPrivate
seal.PublicKey = account.AccountDetail.SealPublic
status := &AccountStatus{}

View File

@ -22,7 +22,8 @@ func SetAccountSeal(w http.ResponseWriter, r *http.Request) {
}
// update record
account.AccountDetail.SealSalt = seal.Salt
account.AccountDetail.SealSalt = seal.PasswordSalt
account.AccountDetail.SealIV = seal.PrivateKeyIV
account.AccountDetail.SealPrivate = seal.PrivateKeyEncrypted
account.AccountDetail.SealPublic = seal.PublicKey

View File

@ -406,9 +406,11 @@ type Revision struct {
//Seal key for channel sealing
type Seal struct {
Salt string `json:"salt"`
PasswordSalt string `json:"passwordSalt"`
PrivateKeyEncrypted string `json:"privateKeyEncrypted,omitempty"`
PrivateKeyIV string `json:"privateKeyIv,omitempty"`
PrivateKeyEncrypted string `json:"privateKeyEncrypted,omitempty"`
PublicKey string `json:"publicKey,omitempty"`
}

View File

@ -96,6 +96,7 @@ type AccountDetail struct {
Location string
Image string
SealSalt string
SealIV string
SealPrivate string
SealPublic string
}

View File

@ -6,9 +6,11 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"antd": "^4.19.1",
"antd": "^5.0.4",
"axios": "^0.27.2",
"base-64": "^1.0.0",
"crypto-js": "^4.1.1",
"jsencrypt": "^2.3.1",
"react": "^17.0.2",
"react-color": "^2.19.3",
"react-dom": "^17.0.2",
@ -17,7 +19,6 @@
"react-resize-detector": "^7.0.0",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"react-virtualized": "^9.22.3",
"styled-components": "^5.3.3",
"web-vitals": "^2.1.0"
},

View File

@ -1,5 +1,5 @@
import 'antd/dist/antd.min.css';
import 'antd/dist/reset.css';
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import { AppContextProvider } from 'context/AppContext';
@ -29,8 +29,8 @@ function App() {
<GroupContextProvider>
<ArticleContextProvider>
<ProfileContextProvider>
<AccountContextProvider>
<StoreContextProvider>
<StoreContextProvider>
<AccountContextProvider>
<ViewportContextProvider>
<AppContextProvider>
<AppWrapper>
@ -51,8 +51,8 @@ function App() {
</AppWrapper>
</AppContextProvider>
</ViewportContextProvider>
</StoreContextProvider>
</AccountContextProvider>
</AccountContextProvider>
</StoreContextProvider>
</ProfileContextProvider>
</ArticleContextProvider>
</GroupContextProvider>

View File

@ -0,0 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function setAccountSeal(token, seal) {
let res = await fetchWithTimeout('/account/seal?agent=' + token, { method: 'PUT', body: JSON.stringify(seal) })
checkResponse(res);
}

View File

@ -1,17 +1,23 @@
import { useState, useRef } from 'react';
import { useContext, useState, useRef } from 'react';
import { setAccountSearchable } from 'api/setAccountSearchable';
import { setAccountSeal } from 'api/setAccountSeal';
import { getAccountStatus } from 'api/getAccountStatus';
import { setAccountLogin } from 'api/setAccountLogin';
import { StoreContext } from './StoreContext';
export function useAccountContext() {
const [state, setState] = useState({
init: false,
status: null,
seal: null,
sealPrivate: null,
});
const access = useRef(null);
const revision = useRef(null);
const next = useRef(null);
const storeContext = useContext(StoreContext);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }))
}
@ -20,7 +26,30 @@ export function useAccountContext() {
if (next.current == null) {
if (revision.current !== rev) {
let status = await getAccountStatus(access.current);
updateState({ init: true, status });
let seal = status.seal?.publicKey ? status.seal : null;
let sealPrivate = null;
const pubKey = await storeContext.actions.getValue("seal:public");
const privKey = await storeContext.actions.getValue("seal:private");
if (status.seal?.publicKey == null) {
if (pubKey != null) {
await storeContext.actions.setValue("seal:public", null);
}
if (privKey != null) {
await storeContext.actions.setValue("seal:private", null);
}
}
else {
if (pubKey !== status.seal?.publicKey) {
if (privKey != null) {
await storeContext.actions.setValue("seal:private", null);
}
await storeContext.actions.setValue("seal:public", status.seal?.publicKey);
}
if (privKey != null) {
sealPrivate = privKey;
}
}
updateState({ init: true, status, seal, sealPrivate });
revision.current = rev;
}
if (next.current != null) {
@ -48,6 +77,18 @@ export function useAccountContext() {
setSearchable: async (flag) => {
await setAccountSearchable(access.current, flag);
},
setSeal: async (seal, sealPrivate) => {
await storeContext.actions.setValue("seal:private", null);
await storeContext.actions.setValue("seal:public", seal.publicKey);
await storeContext.actions.setValue("seal:private", sealPrivate);
await setAccountSeal(access.current, seal);
updateState({ seal, sealPrivate });
},
unlockSeal: async (sealPrivate) => {
console.log("UNLOCKING: ", sealPrivate);
await storeContext.actions.setValue("seal:private", sealPrivate);
updateState({ sealPrivate });
},
setLogin: async (username, password) => {
await setAccountLogin(access.current, username, password);
},

View File

@ -1,16 +1,30 @@
import { AccountAccessWrapper, EditFooter } from './AccountAccess.styled';
import { AccountAccessWrapper, SealModal, EditFooter } from './AccountAccess.styled';
import { useAccountAccess } from './useAccountAccess.hook';
import { AccountLogin } from './accountLogin/AccountLogin';
import { Button, Modal, Checkbox } from 'antd';
import { LockOutlined } from '@ant-design/icons';
import { Button, Modal, Switch, Input } from 'antd';
import { SettingOutlined, LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
export function AccountAccess() {
const { state, actions } = useAccountAccess();
const saveSearchable = async (e) => {
const saveSeal = async () => {
try {
await actions.setSearchable(e.target.checked);
await actions.saveSeal();
actions.clearEditSeal();
}
catch (err) {
console.log(err);
Modal.error({
title: 'Failed to Set Sealing Key',
comment: 'Please try again.',
});
}
}
const saveSearchable = async (enable) => {
try {
await actions.setSearchable(enable);
}
catch (err) {
console.log(err);
@ -43,13 +57,71 @@ export function AccountAccess() {
</EditFooter>
);
const editSealFooter = (
<EditFooter>
<div class="select"></div>
<Button key="back" onClick={actions.clearEditSeal}>Cancel</Button>
{ state.editSealMode == null && state.seal && !state.sealPrivate && (
<Button key="save" type="primary" onClick={() => saveSeal()} disabled={!actions.canSaveSeal()} loading={state.busy}>Unlock</Button>
)}
{ !(state.editSealMode == null && state.seal && !state.sealPrivate) && (
<Button key="save" type="primary" onClick={() => saveSeal()} disabled={!actions.canSaveSeal()} loading={state.busy}>Save</Button>
)}
</EditFooter>
);
return (
<AccountAccessWrapper>
<Checkbox checked={state.searchable} onChange={(e) => saveSearchable(e)}>Visible in Registry</Checkbox>
<div class="switch">
<Switch size="small" checked={state.searchable} onChange={enable => saveSearchable(enable)} />
<div class="switchLabel">Visible in Registry &nbsp;&nbsp;</div>
</div>
<div class="link" onClick={actions.setEditSeal}>
<SettingOutlined />
<div class="label">Sealed Topics</div>
</div>
<div class="link" onClick={actions.setEditLogin}>
<LockOutlined />
<div class="label">Change Login</div>
</div>
<Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal}>
<SealModal>
<div class="switch">
<Switch size="small" checked={state.editSealEnabled} onChange={enable => actions.enableSeal(enable)} />
<div class="switchLabel">Enable Sealed Topics</div>
</div>
{ (state.editSealMode === 'updating' || state.editSealMode === 'sealing') && (
<div class="sealPassword">
<Input.Password placeholder="New Password" spellCheck="false" onChange={(e) => actions.setSealPassword(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</div>
)}
{ (state.editSealMode === 'updating' || state.editSealMode === 'sealing') && (
<div class="sealPassword">
<Input.Password placeholder="Confirm Password" spellCheck="false" onChange={(e) => actions.setSealConfirm(e.target.value)}
autocomplete="new-password" prefix={<LockOutlined />} />
</div>
)}
{ state.editSealMode === 'unsealing' && (
<div class="sealPassword">
<Input placeholder="Type 'delete' to remove key" spellCheck="false" onChange={(e) => actions.setUnseal(e.target.value)}
prefix={<ExclamationCircleOutlined />} />
</div>
)}
{ state.editSealMode == null && state.editSealEnabled && state.sealPrivate && (
<div class="sealPassword" onClick={() => actions.updateSeal()}>
<Input.Password defaultValue="xxxxxxxxxx" disabled={true} prefix={<LockOutlined />} />
<div class="editPassword" />
</div>
)}
{ state.editSealMode == null && state.seal && !state.sealPrivate && (
<div class="sealPassword">
<Input placeholder="Password" spellCheck="false" onChange={(e) => actions.setUnlock(e.target.value)}
prefix={<LockOutlined />} />
</div>
)}
</SealModal>
</Modal>
<Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter}
onCancel={actions.clearEditLogin}>
<AccountLogin state={state} actions={actions} />

View File

@ -8,6 +8,27 @@ export const AccountAccessWrapper = styled.div`
justify-content: center;
padding-bottom: 8px;
.switch {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 8px;
.switchEnabled {
color: ${Colors.primary};
cursor: pointer;
}
.switchDisabled {
color: ${Colors.grey};
}
.switchLabel {
padding-left: 8px;
padding-right: 8px;
}
}
.link {
display: flex;
flex-direction: row;
@ -15,6 +36,7 @@ export const AccountAccessWrapper = styled.div`
cursor: pointer;
color: ${Colors.primary};
padding-top: 8px;
padding-bottom: 8px;
.label {
padding-left: 8px;
@ -22,6 +44,42 @@ export const AccountAccessWrapper = styled.div`
}
`;
export const SealModal = styled.div`
display: flex;
flex-direction: column;
padding-bottom: 8px;
.switch {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 8px;
align-items: center;
justify-content: center;
.switchLabel {
color: ${Colors.text};
padding-left: 8px;
padding-right: 8px;
}
}
.sealPassword {
padding-top: 4px;
padding-bottom: 4px;
position: relative;
.editPassword {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
}
`
export const EditFooter = styled.div`
width: 100%;
display: flex;

View File

@ -2,20 +2,31 @@ import { useRef, useState, useEffect, useContext } from 'react';
import { AccountContext } from 'context/AccountContext';
import { ProfileContext } from 'context/ProfileContext';
import { getUsername } from 'api/getUsername';
import CryptoJS from 'crypto-js';
import { JSEncrypt } from 'jsencrypt'
export function useAccountAccess() {
const [state, setState] = useState({
editLogin: false,
editSeal: false,
handle: null,
editHandle: null,
editStatus: null,
editMessage: null,
editPassword: null,
editConfirm: null,
EditConfirm: null,
busy: false,
searchable: null,
checked: true,
editSealEnabled: false,
editSealMode: null,
sealPassword: null,
sealConfirm: null,
unseal: null,
unlock: null,
seal: null,
sealPrivate: null,
});
const profile = useContext(ProfileContext);
@ -35,11 +46,241 @@ export function useAccountAccess() {
useEffect(() => {
if (account?.state?.status) {
updateState({ searchable: account.state.status.searchable });
const { seal, sealPrivate, status } = account.state;
updateState({ searchable: status.searchabled, seal, sealPrivate });
}
}, [account]);
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 () => {
console.log("UNLOCKING");
console.log(state.seal.passwordSalt);
console.log(state.seal.privateKeyIv);
console.log(state.unlock);
// generate key to encrypt private key
const salt = CryptoJS.enc.Hex.parse(state.seal.passwordSalt);
const aes = CryptoJS.PBKDF2(state.unlock, 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 });
// store keuy
await account.actions.unlockSeal(dec.toString(CryptoJS.enc.Utf8))
};
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
const crypto = new JSEncrypt({ default_key_size: 2048 });
const key = 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 });
// update account
const seal = {
passwordSalt: salt.toString(),
privateKeyIv: iv.toString(),
privateKeyEncrypted: enc.ciphertext.toString(CryptoJS.enc.Base64),
publicKey: convertPem(crypto.getPublicKey()),
}
await account.actions.setSeal(seal, privateKey);
};
const sealRemove = async () => {
await account.actions.setSeal({});
};
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.privateKey, 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.setSeal(seal, state.privateKey);
};
const test = async () => {
console.log("TESTING");
var salt = CryptoJS.lib.WordArray.random(128 / 8);
var key256Bits = CryptoJS.PBKDF2("Secret Passphrase", salt, {
keySize: 256 / 32,
iterations: 1024,
});
console.log(key256Bits);
const crypto = new JSEncrypt({ default_key_size: 2048 });
console.log(crypto);
const key = crypto.getKey();
console.log(key);
console.log(crypto.getPrivateKey());
const encrypted = crypto.encrypt("TEST MESSAGE");
console.log(encrypted);
const decrypted = crypto.decrypt(encrypted);
console.log(decrypted);
const recrypt = crypto.encrypt("TEST MESSAGE");
console.log(recrypt);
const output = crypto.decrypt(recrypt);
console.log(output);
var aes = CryptoJS.lib.WordArray.random(256 / 8);
var iv = CryptoJS.lib.WordArray.random(128 / 8);
var enc = CryptoJS.AES.encrypt("Message", key, { iv: iv });
console.log(aes);
console.log(key256Bits);
console.log(enc);
var cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: enc.ciphertext,
iv: iv
});
var dec = CryptoJS.AES.decrypt(cipherParams, key, { iv: iv });
console.log(dec);
console.log(dec.toString(CryptoJS.enc.Utf8));
};
const actions = {
setEditSeal: () => {
updateState({ editSeal: true, editSealMode: null, unlock: null, editSealEnabled: state.seal });
},
clearEditSeal: () => {
updateState({ editSeal: false });
},
setSealPassword: (sealPassword) => {
updateState({ sealPassword });
},
setSealConfirm: (sealConfirm) => {
updateState({ sealConfirm });
},
setUnseal: (unseal) => {
updateState({ unseal });
},
setUnlock: (unlock) => {
updateState({ unlock });
},
updateSeal: () => {
updateState({ editSealMode: 'updating', sealConfirm: null, sealPassword: null });
},
enableSeal: (enable) => {
if (enable && state.seal) {
updateState({ editSealEnabled: true, editSealMode: null });
}
else if (enable) {
updateState({ editSealEnabled: true, editSealMode: 'sealing', sealConfirm: null, sealPassword: null });
}
else if (!enable && state.seal) {
updateState({ editSealEnabled: false, editSealMode: 'unsealing', unseal: null });
}
else {
updateState({ editSealEnabled: false, editSealMode: null });
}
},
canSaveSeal: () => {
if (state.editSealMode === 'unsealing' && state.unseal === 'delete') {
return true;
}
if (state.editSealMode === 'sealing' && state.sealPassword && state.sealPassword === state.sealConfirm) {
return true;
}
if (state.editSealMode === 'updating' && state.sealPassword && state.sealPassword === state.sealConfirm) {
return true;
}
if (state.editSealMode == null && state.seal && !state.sealPrivate) {
return true;
}
return false;
},
saveSeal: async () => {
if (state.busy) {
throw new Error("operation in progress");
}
updateState({ busy: true });
try {
if (state.editSealMode === 'sealing') {
await sealEnable();
}
else if (state.editSealMode === 'unsealing') {
await sealRemove();
}
else if (state.editSealMode === 'updating') {
await sealUpdate();
}
else {
await sealUnlock();
}
updateState({ busy: false });
}
catch (err) {
updateState({ busy: false });
console.log(err);
throw new Error("failed to save seal");
}
},
setEditLogin: () => {
updateState({ editLogin: true });
},

File diff suppressed because it is too large Load Diff