Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
02137ed
include multiple keys from a single backup
Sep 13, 2021
6ba3e16
removed extra spaces
martgil Sep 14, 2021
0ebb20d
Add ability to select and deselect key to backup based on fingerprint
martgil Sep 14, 2021
86b463a
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 14, 2021
6165968
Fix css error
martgil Sep 14, 2021
d2c5079
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 15, 2021
3025215
remove ugly spagetti code; add xss escaped and tags
martgil Sep 15, 2021
e36c4ed
apply proper data type to PrvIdentity object
martgil Sep 15, 2021
dabf24d
added failsafe getFirstRequired
martgil Sep 15, 2021
5894dbc
added // xss-escaped tag
martgil Sep 15, 2021
7f3c062
fix dom ui not to look "stucked"
martgil Sep 15, 2021
929425b
fix backup not submitting on selection
martgil Sep 16, 2021
f07b78c
Merge branch 'issue-2482-backup-should-include-multiple-keys' of http…
martgil Sep 20, 2021
932f61d
remove debug code
martgil Sep 22, 2021
f2e40b3
fully functional backups for single and multiple keys
martgil Sep 22, 2021
ec7fcc0
corrected type String to string
martgil Sep 22, 2021
60254cb
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 22, 2021
2a281ae
Merge branch 'master' of https://github.com/FlowCrypt/flowcrypt-browser
martgil Sep 23, 2021
d4fc038
Merge branch 'issue-2482-backup-should-include-multiple-keys' of http…
martgil Sep 23, 2021
1d453eb
move "selectable email listing" to manual backup constructor.
martgil Sep 23, 2021
662ec29
fix floating promise error
martgil Sep 23, 2021
31634fb
remove await from the constructor
martgil Sep 23, 2021
dbec498
Added a stub unit test for backup
rrrooommmaaa Sep 26, 2021
a6eac74
improve ui for backup selection of multiple keys
martgil Sep 27, 2021
cc4b3e7
fix floating promises
martgil Sep 27, 2021
b691ded
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 27, 2021
c17789c
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 28, 2021
2ca1b9c
remove unecessary await
martgil Sep 28, 2021
ef5012a
functional backup mechanism (may needs syntax improvement)
martgil Sep 28, 2021
05514fb
add supporting tests to 'manual backup several keys'
martgil Sep 28, 2021
7409627
fixes tslint error
martgil Sep 28, 2021
d5aa28e
fixes selector error in test
martgil Sep 28, 2021
663bd12
added helper for adding and removing key to backup
martgil Sep 28, 2021
429773d
added another addKeyToBackup on preparePrvKeysBackupSelection
martgil Sep 28, 2021
b92927d
use fat arrow method
martgil Sep 28, 2021
9fa503d
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Sep 29, 2021
9b1aa9f
allow async view.setHandlers
rrrooommmaaa Sep 30, 2021
e9d2221
added minor requested change
martgil Oct 1, 2021
8e443b2
use first email when selecting a a key
martgil Oct 4, 2021
0e4a81f
move the backup view rendering to backup.ts instead from backup-manua…
martgil Oct 4, 2021
39200d1
added typed KeyIdentity in key.ts
martgil Oct 4, 2021
0fb59f8
fix unawaited promise
martgil Oct 4, 2021
c7e48f9
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
martgil Oct 6, 2021
e0d5dd9
fixes error prompt appearing when user chooses not to have any backup
martgil Oct 6, 2021
f4bda8d
added backup selection function to setup_manual
martgil Oct 6, 2021
d3ddb84
refactoring to use KeyIdentity
rrrooommmaaa Oct 7, 2021
761bd59
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
rrrooommmaaa Oct 8, 2021
725890e
some renamings and refactorings
rrrooommmaaa Oct 8, 2021
04a6bb9
passphrase check fixes
rrrooommmaaa Oct 8, 2021
300d37a
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
rrrooommmaaa Oct 9, 2021
7b0b4c6
passphrase checks
rrrooommmaaa Oct 9, 2021
8c9aa63
fixed setup_manual mode
rrrooommmaaa Oct 9, 2021
2ea3a43
fix tslint
rrrooommmaaa Oct 9, 2021
8655b8f
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
rrrooommmaaa Oct 11, 2021
854c749
some fixes and test
rrrooommmaaa Oct 11, 2021
5d91513
Added inbox backup tests
rrrooommmaaa Oct 12, 2021
118f5c4
testing backing up several keys to inbox
rrrooommmaaa Oct 13, 2021
911cb96
simplification, avoid warnings in loop
rrrooommmaaa Oct 13, 2021
3de18e9
more checks
rrrooommmaaa Oct 13, 2021
630278b
more passphrase tests
rrrooommmaaa Oct 15, 2021
99b549c
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
rrrooommmaaa Oct 15, 2021
c268bd4
test fix
rrrooommmaaa Oct 15, 2021
e2cc9a7
test fix
rrrooommmaaa Oct 15, 2021
d2ffc7e
more scenarios tested
rrrooommmaaa Oct 17, 2021
0e777b8
Merge remote-tracking branch 'origin/master' into issue-2482-backup-s…
rrrooommmaaa Oct 17, 2021
edfda59
fix tests
rrrooommmaaa Oct 17, 2021
f865df2
simplification
rrrooommmaaa Oct 18, 2021
b143450
Improved change_passphrase scenario
rrrooommmaaa Oct 18, 2021
60746b0
niceties
rrrooommmaaa Oct 18, 2021
4ca44cd
correctly render backup done for both automatic and manual setups
rrrooommmaaa Oct 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extension/chrome/elements/passphrase.htm
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<h1>Enter your pass phrase to read encrypted email</h1>
</div>
<div class="separator"></div>
<div class="line which_key" style="display: none;"></div>
<div class="line which_key" data-test="which-key" style="display: none;"></div>
<div class="line">
<input type="text" placeholder="Enter Pass Phrase" id="passphrase" data-test="input-pass-phrase">
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class BackupAutomaticModule extends ViewModule<BackupView> {
}
try {
await this.view.manualModule.doBackupOnEmailProvider(primaryKi.private);
await this.view.renderBackupDone();
await this.view.renderBackupDone(1);
} catch (e) {
if (ApiErr.isAuthErr(e)) {
await Ui.modal.info("Authorization Error. FlowCrypt needs to reconnect your Gmail account");
Expand Down
150 changes: 92 additions & 58 deletions extension/chrome/settings/modules/backup-manual-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import { BackupView } from './backup.js';
import { Attachment } from '../../../js/common/core/attachment.js';
import { SendableMsg } from '../../../js/common/api/email-provider/sendable-msg.js';
import { GMAIL_RECOVERY_EMAIL_SUBJECTS } from '../../../js/common/core/const.js';
import { KeyInfo, KeyUtil } from '../../../js/common/core/crypto/key.js';
import { KeyIdentity, KeyInfo, KeyUtil, TypedKeyInfo } from '../../../js/common/core/crypto/key.js';
import { Ui } from '../../../js/common/browser/ui.js';
import { ApiErr } from '../../../js/common/api/shared/api-error.js';
import { BrowserMsg, Bm } from '../../../js/common/browser/browser-msg.js';
import { Catch } from '../../../js/common/platform/catch.js';
import { Browser } from '../../../js/common/browser/browser.js';
import { Url, PromiseCancellation } from '../../../js/common/core/common.js';
import { PromiseCancellation, Value } from '../../../js/common/core/common.js';
import { Settings } from '../../../js/common/settings.js';
import { Buf } from '../../../js/common/core/buf.js';
import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js';
import { KeyStore } from '../../../js/common/platform/store/key-store.js';

const differentPassphrasesError = `Your keys are protected with different pass phrases.\n\nBacking them up together isn't supported yet.`;
export class BackupManualActionModule extends ViewModule<BackupView> {

private ppChangedPromiseCancellation: PromiseCancellation = { cancel: false };
private readonly proceedBtn = $('#module_manual .action_manual_backup');

Expand Down Expand Up @@ -55,15 +55,36 @@ export class BackupManualActionModule extends ViewModule<BackupView> {

private actionManualBackupHandler = async () => {
const selected = $('input[type=radio][name=input_backup_choice]:checked').val();
const primaryKi = await KeyStore.getFirstRequired(this.view.acctEmail);
if (! await this.isPrivateKeyEncrypted(primaryKi)) {
await Ui.modal.error('Sorry, cannot back up private key because it\'s not protected with a pass phrase.');
if (this.view.prvKeysToManuallyBackup.length <= 0) {
await Ui.modal.error('No keys are selected to back up! Please select a key to continue.');
return;
}
const allKis = await KeyStore.getTypedKeyInfos(this.view.acctEmail);
const kinfos = KeyUtil.filterKeys(allKis, this.view.prvKeysToManuallyBackup);
if (kinfos.length <= 0) {
await Ui.modal.error('Sorry, could not extract these keys from storage. Please restart your browser and try again.');
return;
}
if (selected === 'inbox') {
await this.backupOnEmailProviderAndUpdateUi(primaryKi);
} else if (selected === 'file') {
await this.backupAsFile(primaryKi);
// todo: this check can also be moved to encryptForBackup method when we solve the same passphrase issue (#4060)
for (const ki of kinfos) {
if (! await this.isPrivateKeyEncrypted(ki)) {
await Ui.modal.error('Sorry, cannot back up private key because it\'s not protected with a pass phrase.');
return;
}
}
if (selected === 'inbox' || selected === 'file') {
// in setup_manual we don't have passphrase-related message handlers, so limit the checks
const encrypted = await this.encryptForBackup(kinfos, { strength: selected === 'inbox' && this.view.action !== 'setup_manual' }, allKis[0]);
if (encrypted) {
if (selected === 'inbox') {
if (!await this.backupOnEmailProviderAndUpdateUi(encrypted)) {
return; // some error occured, message displayed, can retry, no reload needed
}
} else {
await this.backupAsFile(encrypted);
}
await this.view.renderBackupDone(kinfos.length);
}
} else if (selected === 'print') {
await this.backupByBrint();
} else {
Expand All @@ -75,81 +96,94 @@ export class BackupManualActionModule extends ViewModule<BackupView> {
return new Attachment({ name: `flowcrypt-backup-${this.view.acctEmail.replace(/[^A-Za-z0-9]+/g, '')}.asc`, type: 'application/pgp-keys', data: Buf.fromUtfStr(armoredKey) });
}

private backupOnEmailProviderAndUpdateUi = async (primaryKi: KeyInfo) => {
const pp = await PassphraseStore.get(this.view.acctEmail, primaryKi.fingerprints[0]);
if (!this.view.parentTabId) {
await Ui.modal.error(`Missing parentTabId. Please restart your browser and try again.`);
return;
private encryptForBackup = async (kinfos: TypedKeyInfo[], checks: { strength: boolean }, primaryKeyIdentity: KeyIdentity): Promise<string | undefined> => {
const kisWithPp = await Promise.all(kinfos.map(async (ki) => {
const passphrase = await PassphraseStore.getByKeyIdentity(this.view.acctEmail, ki);
// test that the key can actually be decrypted with the passphrase provided
const mismatch = passphrase && !await KeyUtil.decrypt(await KeyUtil.parse(ki.private), passphrase);
return { ...ki, mismatch, passphrase: mismatch ? undefined : passphrase };
}));
const distinctPassphrases = Value.arr.unique(kisWithPp.filter(ki => ki.passphrase).map(ki => ki.passphrase!));
if (distinctPassphrases.length > 1) {
await Ui.modal.error(differentPassphrasesError);
return undefined;
}
if (!pp) {
BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'backup', longids: [primaryKi.longid] });
if (! await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, [primaryKi.longid], 1000, this.ppChangedPromiseCancellation)) {
return;
if (checks.strength && distinctPassphrases[0] && !(Settings.evalPasswordStrength(distinctPassphrases[0]).word.pass)) {
await Ui.modal.warning('Please change your pass phrase first.\n\nIt\'s too weak for this backup method.');
// Actually, until #956 is resolved, we can only modify the pass phrase of the first key
if (this.view.parentTabId && KeyUtil.identityEquals(kisWithPp[0], primaryKeyIdentity) && kisWithPp[0].passphrase === distinctPassphrases[0]) {
Settings.redirectSubPage(this.view.acctEmail, this.view.parentTabId, '/chrome/settings/modules/change_passphrase.htm');
}
await this.backupOnEmailProviderAndUpdateUi(primaryKi);
return;
return undefined;
}
if (!this.isPassPhraseStrongEnough(primaryKi, pp)) {
await Ui.modal.warning('Your key is not protected with strong pass phrase.\n\nYou should change your pass phrase.');
window.location.href = Url.create('/chrome/settings/modules/change_passphrase.htm', { acctEmail: this.view.acctEmail, parentTabId: this.view.parentTabId });
return;
if (distinctPassphrases.length === 1) {
// trying to apply the known pass phrase
for (const ki of kisWithPp.filter(ki => !ki.passphrase)) {
if (await KeyUtil.decrypt(await KeyUtil.parse(ki.private), distinctPassphrases[0])) {
ki.passphrase = distinctPassphrases[0];
}
}
}
const kisMissingPp = kisWithPp.filter(ki => !ki.passphrase);
if (kisMissingPp.length) {
if (distinctPassphrases.length >= 1) {
await Ui.modal.error(differentPassphrasesError);
return undefined;
}
// todo: reset invalid pass phrases (mismatch === true)?
const longids = kisMissingPp.map(ki => ki.longid);
if (this.view.parentTabId) {
BrowserMsg.send.passphraseDialog(this.view.parentTabId, { type: 'backup', longids });
if (! await PassphraseStore.waitUntilPassphraseChanged(this.view.acctEmail, longids, 1000, this.ppChangedPromiseCancellation)) {
return undefined;
}
} else {
await Ui.modal.error(`Sorry, can't back up private key because its pass phrase can't be extracted. Please restart your browser and try again.`);
return undefined;
}
// re-start the function recursively with newly discovered pass phrases
// todo: #4059 however, this code is never actually executed, because our backup frame gets wiped out by the passphrase frame
return await this.encryptForBackup(kinfos, checks, primaryKeyIdentity);
}
return kinfos.map(ki => ki.private).join('\n'); // todo: remove extra \n ?
}

private backupOnEmailProviderAndUpdateUi = async (data: string): Promise<boolean> => {
const origBtnText = this.proceedBtn.text();
Xss.sanitizeRender(this.proceedBtn, Ui.spinner('white'));
try {
await this.doBackupOnEmailProvider(primaryKi.private);
await this.doBackupOnEmailProvider(data);
return true;
} catch (e) {
if (ApiErr.isNetErr(e)) {
return await Ui.modal.warning('Need internet connection to finish. Please click the button again to retry.');
await Ui.modal.warning('Need internet connection to finish. Please click the button again to retry.');
} else if (ApiErr.isAuthErr(e)) {
BrowserMsg.send.notificationShowAuthPopupNeeded(this.view.parentTabId, { acctEmail: this.view.acctEmail });
return await Ui.modal.warning('Account needs to be re-connected first. Please try later.');
if (this.view.parentTabId) {
BrowserMsg.send.notificationShowAuthPopupNeeded(this.view.parentTabId, { acctEmail: this.view.acctEmail });
}
await Ui.modal.warning('Account needs to be re-connected first. Please try later.');
} else {
Catch.reportErr(e);
return await Ui.modal.error(`Error happened: ${String(e)}`);
await Ui.modal.error(`Error happened: ${String(e)}`);
}
return false;
} finally {
this.proceedBtn.text(origBtnText);
}
await this.view.renderBackupDone();
}

private backupAsFile = async (primaryKi: KeyInfo) => { // todo - add a non-encrypted download option
const attachment = this.asBackupFile(primaryKi.private);
private backupAsFile = async (data: string) => { // todo - add a non-encrypted download option
const attachment = this.asBackupFile(data);
Browser.saveToDownloads(attachment);
await Ui.modal.info('Downloading private key backup file..');
await this.view.renderBackupDone();
}

private backupByBrint = async () => { // todo - implement + add a non-encrypted print option
throw new Error('not implemented');
}

private backupRefused = async () => {
await this.view.renderBackupDone(false);
}

private isPassPhraseStrongEnough = async (ki: KeyInfo, passphrase: string) => {
const prv = await KeyUtil.parse(ki.private);
if (!prv.fullyEncrypted) {
return false;
}
if (!passphrase) {
const pp = prompt('Please enter your pass phrase:');
if (!pp) {
return false;
}
if (await KeyUtil.decrypt(prv, pp) !== true) {
await Ui.modal.warning('Pass phrase did not match, please try again.');
return false;
}
passphrase = pp;
}
if (Settings.evalPasswordStrength(passphrase).word.pass === true) {
return true;
}
await Ui.modal.warning('Please change your pass phrase first.\n\nIt\'s too weak for this backup method.');
return false;
await this.view.renderBackupDone(0);
}

private isPrivateKeyEncrypted = async (ki: KeyInfo) => {
Expand Down
11 changes: 7 additions & 4 deletions extension/chrome/settings/modules/backup-status-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@ export class BackupStatusModule extends ViewModule<BackupView> {
}
}

private renderGoManualButton = (htmlEscapedText: string) => {
Xss.sanitizeRender('#module_status .container', `<button class="button long green action_go_manual" data-test="action-go-manual">${htmlEscapedText}</button>`);
}

private renderBackupSummaryAndActionButtons = (backups: Backups) => {
if (!backups.longids.backups.length) {
$('.status_summary').text('No backups found on this account. If you lose your device, or it stops working, you will not be able to read your encrypted email.');
Xss.sanitizeRender('#module_status .container', '<button class="button long green action_go_manual">BACK UP MY KEY</button>');
this.renderGoManualButton('BACK UP MY KEYS');
} else if (backups.longids.importedNotBackedUp.length) {
$('.status_summary').text('Some of your keys have not been backed up.');
// todo - this would not yet work because currently only backing up first key
// Xss.sanitizeRender('#module_status .container', '<button class="button long green action_go_manual">BACK UP MY KEY</button>');
this.renderGoManualButton('BACK UP MY KEYS');
} else if (backups.longids.backupsNotImported.length) {
$('.status_summary').text('Some of your backups have not been loaded. This may cause incoming encrypted email to not be readable.');
Xss.sanitizeRender('#module_status .container', '<button class="button long green action_go_add_key">IMPORT MISSING BACKUPS</button>');
} else {
$('.status_summary').text('Your account keys are backed up and loaded correctly.');
Xss.sanitizeRender('#module_status .container', '<button class="button long green action_go_manual">SEE BACKUP OPTIONS</button>');
this.renderGoManualButton('SEE BACKUP OPTIONS');
}
}

Expand Down
5 changes: 5 additions & 0 deletions extension/chrome/settings/modules/backup.htm
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@

<div id="module_manual">
<div class="line">Your key is stored in the browser. Backups are useful if you ever lose your device.</div>
<div id="key_backup_selection_container" class="display_none">
<div class="line">Kindly select or deselect the keys you want to backup.</div>
<div class="key_backup_selection line"></div>
</div>
<div class="line">
<div class="details mt-40">
<div>
Expand Down Expand Up @@ -107,6 +111,7 @@
<script src="/lib/sweetalert2.js"></script>
<script src="/lib/zxcvbn.js"></script>
<script src="/lib/openpgp.js"></script>
<script src="/lib/forge.js"></script>
<script src="/lib/emailjs/punycode.js"></script>
<script src="/lib/emailjs/emailjs-stringencoding.js"></script>
<script src="/lib/emailjs/emailjs-mime-codec.js"></script>
Expand Down
Loading