move delete account logic to it's own dialog

pull/1833/head
audric 4 years ago
parent e43e9df8e0
commit 9991dc2364

@ -131,7 +131,7 @@
"deleteForEveryone": "Delete for Everyone",
"deleteConversationConfirmation": "Permanently delete the messages in this conversation?",
"clearAllData": "Clear All Data",
"deleteAccountWarning": "This will permanently delete your messages, sessions, and contacts.",
"deleteAccountWarning": "This will permanently delete your messages and contacts.",
"deleteContactConfirmation": "Are you sure you want to delete this conversation?",
"quoteThumbnailAlt": "Thumbnail of image from quoted message",
"imageAttachmentAlt": "Image attached to message",
@ -419,5 +419,11 @@
"latestUnreadIsAbove": "First unread message is above",
"sendRecoveryPhraseTitle": "Sending Recovery Phrase",
"sendRecoveryPhraseMessage": "You are attempting to send your recovery phrase which can be used to access your account. Are you sure you want to send this message?",
"notificationSubtitle": "Notifications - $setting$"
"notificationSubtitle": "Notifications - $setting$",
"dialogClearAllDataDeletionFailedTitle": "Data not deleted",
"dialogClearAllDataDeletionFailedDesc": "Data not deleted with an unknown error",
"dialogClearAllDataDeletionFailedMultiple": "Data not deleted by all those Service Nodes: $snodes$",
"dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?",
"deviceOnly": "Device Only",
"entireAccount": "Entire Account"
}

@ -13,7 +13,7 @@ const functions = {
generateEphemeralKeyPair,
decryptAttachmentBuffer,
encryptAttachmentBuffer,
bytesFromString
bytesFromString,
};
onmessage = async e => {

@ -0,0 +1,202 @@
import React, { useCallback, useState } from 'react';
import { ed25519Str } from '../../session/onions/onionPath';
import {
forceNetworkDeletion,
forceSyncConfigurationNowIfNeeded,
} from '../../session/utils/syncUtils';
import { updateConfirmModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text';
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
import { SessionHtmlRenderer } from '../session/SessionHTMLRenderer';
import { SessionSpinner } from '../session/SessionSpinner';
import { SessionWrapperModal } from '../session/SessionWrapperModal';
const deleteDbLocally = async () => {
window?.log?.info('configuration message sent successfully. Deleting everything');
await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
// 'unlink' => toast will be shown on app restart
window.localStorage.setItem('restart-reason', 'delete-account');
};
async function sendConfigMessageAndDeleteEverything() {
try {
// DELETE LOCAL DATA ONLY, NOTHING ON NETWORK
window?.log?.info('DeleteAccount => Sending a last SyncConfiguration');
// be sure to wait for the message being effectively sent. Otherwise we won't be able to encrypt it for our devices !
await forceSyncConfigurationNowIfNeeded(true);
window?.log?.info('Last configuration message sent!');
await deleteDbLocally();
window.restart();
} catch (error) {
// if an error happened, it's not related to the delete everything on network logic as this is handled above.
// this could be a last sync configuration message not being sent.
// in all case, we delete everything, and restart
window?.log?.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
try {
await deleteDbLocally();
} catch (e) {
window?.log?.error(e);
} finally {
window.restart();
}
}
}
async function deleteEverythingAndNetworkData() {
try {
// DELETE EVERYTHING ON NETWORK, AND THEN STUFF LOCALLY STORED
// a bit of duplicate code below, but it's easier to follow every case like that (helped with returns)
// send deletion message to the network
const potentiallyMaliciousSnodes = await forceNetworkDeletion();
if (potentiallyMaliciousSnodes === null) {
window?.log?.warn('DeleteAccount => forceNetworkDeletion failed');
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('dialogClearAllDataDeletionFailedTitle'),
message: window.i18n('dialogClearAllDataDeletionFailedDesc'),
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await deleteDbLocally();
window.restart();
},
})
);
return;
}
if (potentiallyMaliciousSnodes.length > 0) {
const snodeStr = potentiallyMaliciousSnodes.map(ed25519Str);
window?.log?.warn(
'DeleteAccount => forceNetworkDeletion Got some potentially malicious snodes',
snodeStr
);
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('dialogClearAllDataDeletionFailedTitle'),
message: window.i18n('dialogClearAllDataDeletionFailedMultiple', snodeStr),
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await deleteDbLocally();
window.restart();
},
})
);
return;
}
// We removed everything on the network successfully (no malicious node!). Now delete the stuff we got locally
// without sending a last configuration message (otherwise this one will still be on the network)
await deleteDbLocally();
window.restart();
} catch (error) {
// if an error happened, it's not related to the delete everything on network logic as this is handled above.
// this could be a last sync configuration message not being sent.
// in all case, we delete everything, and restart
window?.log?.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
try {
await deleteDbLocally();
} catch (e) {
window?.log?.error(e);
}
window.restart();
}
}
export const DeleteAccountModal = () => {
const [isLoading, setIsLoading] = useState(false);
const onDeleteEverythingLocallyOnly = async () => {
setIsLoading(true);
try {
window.log.warn('Deleting everything excluding network data');
await sendConfigMessageAndDeleteEverything();
} catch (e) {
window.log.warn(e);
} finally {
setIsLoading(false);
}
dispatch(updateConfirmModal(null));
};
const onDeleteEverythingAndNetworkData = async () => {
setIsLoading(true);
try {
window.log.warn('Deleting everything including network data');
await deleteEverythingAndNetworkData();
} catch (e) {
window.log.warn(e);
} finally {
setIsLoading(false);
}
dispatch(updateConfirmModal(null));
};
/**
* Performs specified on close action then removes the modal.
*/
const onClickCancelHandler = useCallback(() => {
window.inboxStore?.dispatch(updateDeleteAccountModal(null));
}, []);
return (
<SessionWrapperModal
title={window.i18n('clearAllData')}
onClose={onClickCancelHandler}
showExitIcon={true}
>
<SpacerLG />
<div className="session-modal__centered">
<SessionHtmlRenderer
tag="span"
className="session-confirm-main-message"
html={window.i18n('deleteAccountWarning')}
/>
<SessionHtmlRenderer
tag="span"
className="session-confirm-main-message"
html={window.i18n('dialogClearAllDataDeletionQuestion')}
/>
<SpacerLG />
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('entireAccount')}
buttonColor={SessionButtonColor.Danger}
onClick={onDeleteEverythingAndNetworkData}
disabled={isLoading}
/>
<SessionButton
text={window.i18n('deviceOnly')}
buttonColor={SessionButtonColor.Danger}
onClick={onDeleteEverythingLocallyOnly}
disabled={isLoading}
/>
</div>
<SessionSpinner loading={isLoading} />
</div>
</SessionWrapperModal>
);
};
function dispatch(arg0: {
payload: import('../../state/ducks/modalDialog').ConfirmModalState;
type: string;
}) {
throw new Error('Function not implemented.');
}

@ -7,14 +7,15 @@ import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { SessionSettingCategory } from './settings/SessionSettings';
import { DefaultTheme } from 'styled-components';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { deleteAccount } from '../../util/accountManager';
import { useDispatch, useSelector } from 'react-redux';
import { showSettingsSection } from '../../state/ducks/section';
import { getFocusedSettingsSection } from '../../state/selectors/section';
import { getTheme } from '../../state/selectors/theme';
import { SessionConfirm } from './SessionConfirm';
import { SessionSeedModal } from './SessionSeedModal';
import { recoveryPhraseModal, updateConfirmModal } from '../../state/ducks/modalDialog';
import {
recoveryPhraseModal,
updateConfirmModal,
updateDeleteAccountModal,
} from '../../state/ducks/modalDialog';
type Props = {
settingsCategory: SessionSettingCategory;
@ -100,33 +101,12 @@ const LeftPaneSettingsCategories = () => {
);
};
const onDeleteAccount = () => {
const title = window.i18n('clearAllData');
const message = window.i18n('deleteAccountWarning');
const onClickClose = () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
};
window.inboxStore?.dispatch(
updateConfirmModal({
title,
message,
okTheme: SessionButtonColor.Danger,
onClickOk: deleteAccount,
onClickClose,
})
);
};
const onShowRecoveryPhrase = () => {
window.inboxStore?.dispatch(recoveryPhraseModal({}));
};
const LeftPaneBottomButtons = () => {
const dangerButtonText = window.i18n('clearAllData');
const showRecoveryPhrase = window.i18n('showRecoveryPhrase');
const dispatch = useDispatch();
return (
<div className="left-pane-setting-bottom-buttons" key={1}>
<SessionButton
@ -134,7 +114,7 @@ const LeftPaneBottomButtons = () => {
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.Danger}
onClick={() => {
onDeleteAccount();
dispatch(updateDeleteAccountModal({}));
}}
/>
@ -143,7 +123,7 @@ const LeftPaneBottomButtons = () => {
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
onClick={() => {
onShowRecoveryPhrase();
dispatch(recoveryPhraseModal({}));
}}
/>
</div>
@ -151,7 +131,6 @@ const LeftPaneBottomButtons = () => {
};
export const LeftPaneSettingSection = () => {
const theme = useSelector(getTheme);
return (
<div className="left-pane-setting-section">
<LeftPaneSectionHeader label={window.i18n('settingsHeader')} />

@ -5,6 +5,7 @@ import {
getAdminLeaveClosedGroupDialog,
getChangeNickNameDialog,
getConfirmModal,
getDeleteAccountModalState,
getEditProfileDialog,
getInviteContactModal,
getOnionPathDialog,
@ -21,6 +22,7 @@ import { AddModeratorsDialog } from '../conversation/ModeratorsAddDialog';
import { RemoveModeratorsDialog } from '../conversation/ModeratorsRemoveDialog';
import { UpdateGroupMembersDialog } from '../conversation/UpdateGroupMembersDialog';
import { UpdateGroupNameDialog } from '../conversation/UpdateGroupNameDialog';
import { DeleteAccountModal } from '../dialog/DeleteAccountModal';
import { EditProfileDialog } from '../EditProfileDialog';
import { OnionPathModal } from '../OnionStatusPathDialog';
import { UserDetailsDialog } from '../UserDetailsDialog';
@ -43,6 +45,7 @@ export const ModalContainer = () => {
const recoveryPhraseModalState = useSelector(getRecoveryPhraseDialog);
const adminLeaveClosedGroupModalState = useSelector(getAdminLeaveClosedGroupDialog);
const sessionPasswordModalState = useSelector(getSessionPasswordDialog);
const deleteAccountModalState = useSelector(getDeleteAccountModalState);
return (
<>
@ -63,6 +66,7 @@ export const ModalContainer = () => {
<AdminLeaveClosedGroupDialog {...adminLeaveClosedGroupModalState} />
)}
{sessionPasswordModalState && <SessionPasswordModal {...sessionPasswordModalState} />}
{deleteAccountModalState && <DeleteAccountModal {...deleteAccountModalState} />}
</>
);
};

@ -41,14 +41,14 @@ class SessionSeedModalInner extends React.Component<{}, State> {
public componentDidMount() {
setTimeout(() => ($('#seed-input-password') as any).focus(), 100);
void this.checkHasPassword();
void this.getRecoveryPhrase();
}
public render() {
const i18n = window.i18n;
void this.checkHasPassword();
void this.getRecoveryPhrase();
const { hasPassword, passwordValid } = this.state;
const loading = this.state.loadingPassword || this.state.loadingSeed;
const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));

@ -60,13 +60,12 @@ class SessionRecordingInner extends React.Component<Props, State> {
};
}
public componentWillMount() {
public componentDidMount() {
// This turns on the microphone on the system. Later we need to turn it off.
void this.initiateRecordingStream();
}
public componentDidMount() {
void this.initiateRecordingStream();
// Callback to parent on load complete
if (this.props.onLoadVoiceNoteView) {
this.props.onLoadVoiceNoteView();
}

@ -2,6 +2,9 @@ import React from 'react';
import { icons, SessionIconSize, SessionIconType } from '../icon';
import styled, { css, DefaultTheme, keyframes, useTheme } from 'styled-components';
import _ from 'lodash';
import { useSelector } from 'react-redux';
import { getTheme } from '../../../state/selectors/theme';
import { lightTheme } from '../../../state/ducks/SessionTheme';
export type SessionIconProps = {
iconType: SessionIconType;
@ -179,7 +182,7 @@ export const SessionIcon = (props: SessionIconProps) => {
iconSize = iconSize || SessionIconSize.Medium;
iconRotation = iconRotation || 0;
const themeToUse = theme || useTheme();
const themeToUse = theme || useTheme() || lightTheme;
const iconDimensions = getIconDimensionFromIconSize(iconSize);
const iconDef = icons[iconType];

@ -159,8 +159,8 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str
}
}
export class RegistrationTabs extends React.Component<any, State> {
constructor() {
super({});
constructor(props: any) {
super(props);
this.state = {
selectedTab: TabType.SignUp,
generatedRecoveryPhrase: '',

@ -23,6 +23,7 @@ import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
import { sessionPassword } from '../../../state/ducks/modalDialog';
import { PasswordAction } from '../SessionPasswordModal';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { withTheme } from 'styled-components';
export enum SessionSettingCategory {
Appearance = 'appearance',
@ -96,12 +97,10 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
window.addEventListener('keyup', this.onKeyUp);
}
public async componentWillMount() {
const mediaSetting = await window.getSettingValue('media-permissions');
public componentDidMount() {
const mediaSetting = window.getSettingValue('media-permissions');
this.setState({ mediaSetting });
}
public componentDidMount() {
setTimeout(() => ($('#password-lock-input') as any).focus(), 100);
}

@ -42,34 +42,29 @@ export type OpenGroupV2InfoJoinable = OpenGroupV2Info & {
};
export const TextToBase64 = async (text: string) => {
const arrayBuffer = (await window.callWorker(
'bytesFromString',
text
))
const arrayBuffer = await window.callWorker('bytesFromString', text);
const base64 = ( await window.callWorker(
'arrayBufferToStringBase64',
arrayBuffer
))
const base64 = await window.callWorker('arrayBufferToStringBase64', arrayBuffer);
return base64;
}
};
export const textToArrayBuffer = async (text: string) => {
return (await window.callWorker(
'bytesFromString',
text
))
}
export const verifyED25519Signature = async (pubkey: string, base64EncodedData: string, base64EncondedSignature: string): Promise<Boolean> => {
return (await window.callWorker(
return await window.callWorker('bytesFromString', text);
};
export const verifyED25519Signature = async (
pubkey: string,
base64EncodedData: string,
base64EncondedSignature: string
): Promise<Boolean> => {
return await window.callWorker(
'verifySignature',
pubkey,
base64EncodedData,
base64EncondedSignature
));
}
);
};
export const parseMessages = async (
rawMessages: Array<Record<string, any>>

@ -17,6 +17,7 @@ import { ConversationModel } from '../../models/conversation';
import { DURATION, SWARM_POLLING_TIMEOUT } from '../constants';
import { getConversationController } from '../conversations';
import { perfEnd, perfStart } from '../utils/Performance';
import { ed25519Str } from '../onions/onionPath';
type PubkeyToHash = { [key: string]: string };
@ -203,7 +204,9 @@ export class SwarmPolling {
if (isGroup) {
window?.log?.info(
`Polled for group(${pubkey}): group.pubkey, got ${messages.length} messages back.`
`Polled for group(${ed25519Str(pubkey.key)}): group.pubkey, got ${
messages.length
} messages back.`
);
// update the last fetched timestamp
this.groupPolling = this.groupPolling.map(group => {

@ -138,7 +138,7 @@ const getNetworkTime = async (snode: Snode): Promise<string | number> => {
// tslint:disable-next-line: max-func-body-length
export const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const sodium = await getSodium();
const userX25519PublicKey = UserUtils.getOurPubKeyFromCache();
const userX25519PublicKey = UserUtils.getOurPubKeyStrFromCache();
const userED25519KeyPair = await UserUtils.getUserED25519KeyPair();
@ -150,7 +150,7 @@ export const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
try {
const maliciousSnodes = await pRetry(async () => {
const userSwarm = await getSwarmFor(userX25519PublicKey.key);
const userSwarm = await getSwarmFor(userX25519PublicKey);
const snodeToMakeRequestTo: Snode | undefined = _.sample(userSwarm);
const edKeyPrivBytes = fromHexToArray(edKeyPriv);
@ -169,7 +169,7 @@ export const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const signatureBase64 = fromUInt8ArrayToBase64(signature);
const deleteMessageParams = {
pubkey: userX25519PublicKey.key,
pubkey: userX25519PublicKey,
pubkey_ed25519: userED25519KeyPair.pubKey.toUpperCase(),
timestamp,
signature: signatureBase64,
@ -179,7 +179,7 @@ export const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
'delete_all',
deleteMessageParams,
snodeToMakeRequestTo,
userX25519PublicKey.key
userX25519PublicKey
);
if (!ret) {
@ -239,7 +239,7 @@ export const forceNetworkDeletion = async (): Promise<Array<string> | null> => {
const hashes = snodeJson.deleted as Array<string>;
const signatureSnode = snodeJson.signature as string;
// The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
const dataToVerify = `${userX25519PublicKey.key}${timestamp}${hashes.join('')}`;
const dataToVerify = `${userX25519PublicKey}${timestamp}${hashes.join('')}`;
const dataToVerifyUtf8 = StringUtils.encode(dataToVerify, 'utf8');
const isValid = sodium.crypto_sign_verify_detached(
fromBase64ToArray(signatureSnode),

@ -13,6 +13,7 @@ export type AdminLeaveClosedGroupModalState = InviteContactModalState;
export type EditProfileModalState = {} | null;
export type OnionPathModalState = EditProfileModalState;
export type RecoveryPhraseModalState = EditProfileModalState;
export type DeleteAccountModalState = EditProfileModalState;
export type SessionPasswordModalState = { passwordAction: PasswordAction; onOk: () => void } | null;
@ -36,6 +37,7 @@ export type ModalState = {
recoveryPhraseModal: RecoveryPhraseModalState;
adminLeaveClosedGroup: AdminLeaveClosedGroupModalState;
sessionPasswordModal: SessionPasswordModalState;
deleteAccountModal: DeleteAccountModalState;
};
export const initialModalState: ModalState = {
@ -52,6 +54,7 @@ export const initialModalState: ModalState = {
recoveryPhraseModal: null,
adminLeaveClosedGroup: null,
sessionPasswordModal: null,
deleteAccountModal: null,
};
const ModalSlice = createSlice({
@ -97,6 +100,9 @@ const ModalSlice = createSlice({
sessionPassword(state, action: PayloadAction<SessionPasswordModalState>) {
return { ...state, sessionPasswordModal: action.payload };
},
updateDeleteAccountModal(state, action: PayloadAction<DeleteAccountModalState>) {
return { ...state, deleteAccountModal: action.payload };
},
},
});
@ -115,5 +121,6 @@ export const {
recoveryPhraseModal,
adminLeaveClosedGroup,
sessionPassword,
updateDeleteAccountModal,
} = actions;
export const modalReducer = reducer;

@ -6,6 +6,7 @@ import {
AdminLeaveClosedGroupModalState,
ChangeNickNameModalState,
ConfirmModalState,
DeleteAccountModalState,
EditProfileModalState,
InviteContactModalState,
ModalState,
@ -86,3 +87,8 @@ export const getSessionPasswordDialog = createSelector(
getModal,
(state: ModalState): SessionPasswordModalState => state.sessionPasswordModal
);
export const getDeleteAccountModalState = createSelector(
getModal,
(state: ModalState): DeleteAccountModalState => state.deleteAccountModal
);

@ -13,6 +13,9 @@ import { mn_decode, mn_encode } from '../session/crypto/mnemonic';
import { ConversationTypeEnum } from '../models/conversation';
import _ from 'underscore';
import { persistStore } from 'redux-persist';
import { ed25519Str } from '../session/onions/onionPath';
import { SessionButtonColor } from '../components/session/SessionButton';
import { updateConfirmModal } from '../state/ducks/modalDialog';
/**
* Might throw
@ -129,51 +132,6 @@ export async function generateMnemonic() {
return mn_encode(hex);
}
async function bouncyDeleteAccount(reason?: string) {
const deleteEverything = async () => {
window?.log?.info('configuration message sent successfully. Deleting everything');
await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
// 'unlink' => toast will be shown on app restart
window.localStorage.setItem('restart-reason', reason || '');
};
try {
window?.log?.info('DeleteAccount => Sending a last SyncConfiguration');
// send deletion message to the network
const ret = await forceNetworkDeletion();
debugger;
// be sure to wait for the message being effectively sent. Otherwise we won't be able to encrypt it for our devices !
await forceSyncConfigurationNowIfNeeded(true);
window?.log?.info('Last configuration message sent!');
// return;
await deleteEverything();
} catch (error) {
window?.log?.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
debugger;
return;
try {
await deleteEverything();
} catch (e) {
window?.log?.error(e);
}
}
window.restart();
}
export async function deleteAccount(reason?: string) {
return bouncyDeleteAccount(reason);
}
async function createAccount(identityKeyPair: any) {
const sodium = await getSodium();
let password = fromArrayBufferToBase64(sodium.randombytes_buf(16));

Loading…
Cancel
Save