Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e1a5cad
issue 2590 km integration
Feb 26, 2020
b0e639c
prepare test email
Feb 26, 2020
19bb85f
prepare for tests
Feb 26, 2020
5461558
fix code style
Feb 26, 2020
2eb60ab
code style
Feb 27, 2020
47b701b
added test for get.key@key-manager-autogen (will fail)
Feb 27, 2020
02611db
basic implementation / not done
Feb 27, 2020
6ad601d
expect pubkey be submitted to km
Feb 27, 2020
af8ed0b
hide pass phrase settings
Feb 27, 2020
50dc4a4
throw if idToken missing only on initial setup
Feb 27, 2020
29326c4
add idToken to url params
Feb 27, 2020
d48fc80
fix tests
Feb 27, 2020
75ba5cf
fix conflicting html attributes
Feb 27, 2020
1806b92
fix checking security frame + fix test
Feb 27, 2020
274f930
create SetupPageRecipe.autoKeygen
Feb 27, 2020
e2341a4
added test for put.key
Feb 27, 2020
602d761
honor getEnforcedKeygenAlgo
Feb 27, 2020
a256636
fix code style
Feb 27, 2020
24e1cef
hide idToken in err msgs
Feb 27, 2020
3e8177b
don't show mobile prompt
Feb 27, 2020
4492762
fix code style
Feb 27, 2020
d592714
hide add_key
Feb 27, 2020
a2f11cd
ajaxErrorDetails
Feb 27, 2020
275f5a2
test get.error and put.error
Feb 27, 2020
c31239e
test friendly err when KM down
Feb 27, 2020
7bb573c
code style
Feb 27, 2020
d673a2a
only submit main email to attester
Feb 27, 2020
b5fce9a
dry
Feb 27, 2020
cdd7b07
throw on key parse err
Feb 27, 2020
fb7d159
throw if cannot decrypt generated key
Feb 27, 2020
3bcdde0
safe retry for storing key
Feb 27, 2020
7de3bb1
fixed test
Feb 28, 2020
77bc0d6
throw on unsupported functionality
Feb 28, 2020
fccd33c
change km api
Feb 28, 2020
33c766d
fix test after km api change
Feb 28, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,12 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
} else if (ApiErr.isReqTooLarge(e)) {
await Ui.modal.error(`Could not send: message or attachments too large.`);
} else if (ApiErr.isBadReq(e)) {
const errMsg = e.parseErrResMsg('google');
if (errMsg === e.STD_ERR_MSGS.GOOGLE_INVALID_TO_HEADER || errMsg === e.STD_ERR_MSGS.GOOGLE_RECIPIENT_ADDRESS_REQUIRED) {
if (e.parsedErrMsg === e.STD_ERR_MSGS.GOOGLE_INVALID_TO_HEADER || e.parsedErrMsg === e.STD_ERR_MSGS.GOOGLE_RECIPIENT_ADDRESS_REQUIRED) {
await Ui.modal.error('Error from google: Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.');
} else {
if (await Ui.modal.confirm(`Google returned an error when sending message. Please help us improve FlowCrypt by reporting the error to us.`)) {
const page = '/chrome/settings/modules/help.htm';
const pageUrlParams = { bugReport: BrowserExtension.prepareBugReport(`composer: send: bad request (errMsg: ${errMsg})`, {}, e) };
const pageUrlParams = { bugReport: BrowserExtension.prepareBugReport(`composer: send: bad request (errMsg: ${e.parsedErrMsg})`, {}, e) };
BrowserMsg.send.bg.settings({ acctEmail: this.view.acctEmail, page, pageUrlParams });
}
}
Expand Down
4 changes: 2 additions & 2 deletions extension/chrome/settings/index.htm
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ <h1 class="text-center">FlowCrypt Settings</h1>
<div class="col-sm-12" style="padding-left: 0;">
<img src="/img/svgs/key-icon.svg" class="key-icon" alt="key icon">
<span class="my-keys-copy">My Keys</span>
<span class="text-right change-passphrase">
<a href="#" class="show_settings_page add_key" page="/chrome/settings/modules/add_key.htm">Add Key</a>
<span class="text-right keys-link-top-right-container">
<a href="#" class="show_settings_page add_key" data-test="action-open-add-key-page" page="/chrome/settings/modules/add_key.htm">Add Key</a>
</span>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion extension/chrome/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ View.run(class SettingsView extends View {
if (this.rules && !this.rules.canSubmitPubToAttester()) {
$('.public_profile_indicator_container').hide(); // contact page is useless if user cannot submit to attester
}
if (this.rules && this.rules.getPrivateKeyManagerUrl()) {
$(".add_key").hide(); // users which a key manager should not be adding keys manually
}
$.get('/changelog.txt', data => ($('#status-row #status_v') as any as JQS).featherlight(String(data).replace(/\n/g, '<br>')), 'html');
await this.initialize();
await Assert.abortAndRenderErrOnUnprotectedKey(this.acctEmail, this.tabId);
Expand Down Expand Up @@ -236,10 +239,11 @@ View.run(class SettingsView extends View {
$('.auth_denied_warning').removeClass('hidden');
}
const globalStorage = await GlobalStore.get(['install_mobile_app_notification_dismissed']);
if (!globalStorage.install_mobile_app_notification_dismissed && rules.canBackupKeys() && rules.canCreateKeys()) {
if (!globalStorage.install_mobile_app_notification_dismissed && rules.canBackupKeys() && rules.canCreateKeys() && !rules.getPrivateKeyManagerUrl()) {
// only show this notification if user is allowed to:
// - backup keys: when not allowed, company typically has other forms of backup
// - create keys: when not allowed, key must have been imported from some other system that already takes care of backups
// and doesn't use custom key manager, because backups are then taken care of
$('.install_app_notification').removeClass('hidden');
}
$('.dismiss_install_app_notification').click(this.setHandler(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class BackupAutomaticModule extends ViewModule<BackupView> {
try {
await this.setupCreateSimpleAutomaticInboxBackup();
} catch (e) {
return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToBackUpKey, this.setupCreateSimpleAutomaticInboxBackup);
return await Settings.promptToRetry(e, Lang.setup.failedToBackUpKey, this.setupCreateSimpleAutomaticInboxBackup);
}
}

Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/modules/debug_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ View.run(class DebugApiView extends View {
} else if (this.which === 'local_store') {
const storage = await AcctStore.get(this.acctEmail, [
'notification_setup_needed_dismissed', 'email_provider', 'google_token_scopes', 'hide_message_password', 'sendAs', 'outgoing_language',
'full_name', 'cryptup_enabled', 'setup_done', 'is_newly_created_key',
'full_name', 'cryptup_enabled', 'setup_done',
'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'openid',
'rules', 'subscription', 'use_rich_text',
]);
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/modules/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ View.run(class ExperimentalView extends View {
'acctEmail: ' + this.acctEmail,
];
const globalStorage = await GlobalStore.get(['version']);
const acctStorage = await AcctStore.get(this.acctEmail, ['is_newly_created_key', 'setup_date', 'full_name']);
const acctStorage = await AcctStore.get(this.acctEmail, ['setup_date', 'full_name']);
text.push('global_storage: ' + JSON.stringify(globalStorage));
text.push('account_storage: ' + JSON.stringify(acctStorage));
text.push('');
Expand Down
4 changes: 2 additions & 2 deletions extension/chrome/settings/modules/security.htm
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<div class="line title_container">
<h1>Security</h1>
</div>
<div class="line title_container">
<div class="line title_container hide_if_pass_phrase_not_user_configurable">
<h3>Private key pass phrase</h3>
</div>
<div class="settings-form-group">
<div class="settings-form-group hide_if_pass_phrase_not_user_configurable">
<div class="line change_passhrase_container">
<a href="#" class="action_change_passphrase" data-test="action-change-passphrase-begin">Change pass phrase</a>
</div>
Expand Down
6 changes: 6 additions & 0 deletions extension/chrome/settings/modules/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { initPassphraseToggle } from '../../../js/common/ui/passphrase-ui.js';
import { AcctStore } from '../../../js/common/platform/store/acct-store.js';
import { KeyStore } from '../../../js/common/platform/store/key-store.js';
import { PassphraseStore } from '../../../js/common/platform/store/passphrase-store.js';
import { Rules } from '../../../js/common/rules.js';

View.run(class SecurityView extends View {

private readonly acctEmail: string;
private readonly parentTabId: string;
private primaryKi: KeyInfo | undefined;
private authInfo: FcUuidAuth | undefined;
private rules!: Rules;

constructor() {
super();
Expand All @@ -37,10 +39,14 @@ View.run(class SecurityView extends View {
Assert.abortAndRenderErrorIfKeyinfoEmpty(this.primaryKi);
this.authInfo = await AcctStore.authInfo(this.acctEmail);
const storage = await AcctStore.get(this.acctEmail, ['hide_message_password', 'outgoing_language']);
this.rules = await Rules.newInstance(this.acctEmail);
$('#hide_message_password').prop('checked', storage.hide_message_password === true);
$('.password_message_language').val(storage.outgoing_language || 'EN');
await this.renderPassPhraseOptionsIfStoredPermanently();
await this.loadAndRenderPwdEncryptedMsgSettings();
if (this.rules.mustAutogenPassPhraseQuietly()) {
$('.hide_if_pass_phrase_not_user_configurable').hide();
}
}

public setHandlers = () => {
Expand Down
27 changes: 19 additions & 8 deletions extension/chrome/settings/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,38 @@ import { Scopes, AcctStoreDict, AcctStore } from '../../js/common/platform/store
import { KeyStore } from '../../js/common/platform/store/key-store.js';
import { PassphraseStore } from '../../js/common/platform/store/passphrase-store.js';
import { ContactStore } from '../../js/common/platform/store/contact-store.js';
import { KeyManager } from '../../js/common/api/key-manager.js';
import { SetupKeyManagerAutogenModule } from './setup/setup-key-manager-autogen.js';

export interface SetupOptions {
passphrase: string;
passphrase_save: boolean;
submit_main: boolean;
submit_all: boolean;
recovered?: boolean;
is_newly_created_key: boolean;
}

export class SetupView extends View {

public readonly acctEmail: string;
public readonly parentTabId: string | undefined;
public readonly action: 'add_key' | 'finalize' | undefined;
public readonly idToken: string | undefined; // only needed for initial setup, not for add_key or 'finalize'

public readonly keyImportUi = new KeyImportUi({ checkEncryption: true });
public readonly gmail: Gmail;
public readonly setupRecoverKey: SetupRecoverKeyModule;
public readonly setupCreateKey: SetupCreateKeyModule;
public readonly setupImportKey: SetupImportKeyModule;
public readonly setupRender: SetupRenderModule;
public readonly setupKeyManagerAutogen: SetupKeyManagerAutogenModule;

public tabId!: string;
public scopes!: Scopes;
public storage!: AcctStoreDict;
public rules!: Rules;
public keyserver!: Keyserver;
public keyManager: KeyManager | undefined; // not set if no url in org rules

public acctEmailAttesterLongid: string | undefined;
public fetchedKeyBackups: KeyInfo[] = [];
Expand All @@ -66,12 +70,16 @@ export class SetupView extends View {

constructor() {
super();
const uncheckedUrlParams = Url.parse(['acctEmail', 'action', 'parentTabId']);
const uncheckedUrlParams = Url.parse(['acctEmail', 'action', 'idToken', 'parentTabId']);
this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
this.idToken = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'idToken');
this.action = Assert.urlParamRequire.oneof(uncheckedUrlParams, 'action', ['add_key', 'finalize', undefined]) as 'add_key' | 'finalize' | undefined;
if (this.action === 'add_key') {
this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
}
if (this.action !== 'add_key' && this.action !== 'finalize') {
Assert.urlParamRequire.string(uncheckedUrlParams, 'idToken'); // will render error if missing
}
if (this.acctEmail) {
BrowserMsg.send.bg.updateUninstallUrl();
} else {
Expand All @@ -87,6 +95,7 @@ export class SetupView extends View {
this.setupCreateKey = new SetupCreateKeyModule(this);
this.setupImportKey = new SetupImportKeyModule(this);
this.setupRender = new SetupRenderModule(this);
this.setupKeyManagerAutogen = new SetupKeyManagerAutogenModule(this);
}

public render = async () => {
Expand All @@ -97,6 +106,9 @@ export class SetupView extends View {
this.storage.email_provider = this.storage.email_provider || 'gmail';
this.rules = await Rules.newInstance(this.acctEmail);
this.keyserver = new Keyserver(this.rules);
if (this.rules.getPrivateKeyManagerUrl() && this.idToken) {
this.keyManager = new KeyManager(this.rules.getPrivateKeyManagerUrl()!, this.idToken);
}
if (!this.rules.canCreateKeys()) {
const forbidden = `${Lang.setup.creatingKeysNotAllowedPleaseImport} <a href="${Xss.escape(window.location.href)}">Back</a>`;
Xss.sanitizeRender('#step_2a_manual_create, #step_2_easy_generating', `<div class="aligncenter"><div class="line">${forbidden}</div></div>`);
Expand All @@ -109,6 +121,9 @@ export class SetupView extends View {
$('#step_2a_manual_create .input_passphrase_save').prop('checked', true);
$('#step_2b_manual_enter .input_passphrase_save').prop('checked', true);
}
if (this.rules.getEnforcedKeygenAlgo()) {
$('.key_type').val(this.rules.getEnforcedKeygenAlgo()!).prop('disabled', true);
}
this.tabId = await BrowserMsg.requiredTabId();
await this.setupRender.renderInitial();
}
Expand Down Expand Up @@ -164,11 +179,7 @@ export class SetupView extends View {
}

public preFinalizeSetup = async (options: SetupOptions): Promise<void> => {
await AcctStore.set(this.acctEmail, {
tmp_submit_main: options.submit_main,
tmp_submit_all: options.submit_all,
is_newly_created_key: options.is_newly_created_key,
});
await AcctStore.set(this.acctEmail, { tmp_submit_main: options.submit_main, tmp_submit_all: options.submit_all });
}

public finalizeSetup = async ({ submit_main, submit_all }: { submit_main: boolean, submit_all: boolean }): Promise<void> => {
Expand All @@ -177,7 +188,7 @@ export class SetupView extends View {
try {
await this.submitPublicKeyIfNeeded(primaryKi.public, { submit_main, submit_all });
} catch (e) {
return await Settings.promptToRetry('REQUIRED', e, Lang.setup.failedToSubmitToAttester, () => this.finalizeSetup({ submit_main, submit_all }));
return await Settings.promptToRetry(e, Lang.setup.failedToSubmitToAttester, () => this.finalizeSetup({ submit_main, submit_all }));
}
await AcctStore.set(this.acctEmail, { setup_date: Date.now(), setup_done: true, cryptup_enabled: true });
await AcctStore.remove(this.acctEmail, ['tmp_submit_main', 'tmp_submit_all']);
Expand Down
5 changes: 1 addition & 4 deletions extension/chrome/settings/setup/setup-create-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
'use strict';

import { SetupOptions, SetupView } from '../setup.js';

import { Catch } from '../../../js/common/platform/catch.js';
import { Lang } from '../../../js/common/lang.js';
import { PgpKey, KeyAlgo } from '../../../js/common/core/pgp-key.js';
Expand Down Expand Up @@ -33,9 +32,8 @@ export class SetupCreateKeyModule {
submit_main: this.view.shouldSubmitPubkey('#step_2a_manual_create .input_submit_key'),
submit_all: this.view.shouldSubmitPubkey('#step_2a_manual_create .input_submit_all'),
recovered: false,
is_newly_created_key: true,
};
const keyAlgo = $('#step_2a_manual_create .key_type').val() as KeyAlgo;
const keyAlgo = this.view.rules.getEnforcedKeygenAlgo() || $('#step_2a_manual_create .key_type').val() as KeyAlgo;
const action = $('#step_2a_manual_create .input_backup_inbox').prop('checked') ? 'setup_automatic' : 'setup_manual';
await this.createSaveKeyPair(options, keyAlgo);
await this.view.preFinalizeSetup(options);
Expand Down Expand Up @@ -67,7 +65,6 @@ export class SetupCreateKeyModule {
const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']);
try {
const key = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keyAlgo, options.passphrase); // todo - add all addresses?
options.is_newly_created_key = true;
const prv = await PgpKey.read(key.private);
await this.view.saveKeys([prv], options);
} catch (e) {
Expand Down
1 change: 0 additions & 1 deletion extension/chrome/settings/setup/setup-import-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export class SetupImportKeyModule {
submit_main: this.view.shouldSubmitPubkey('#step_2b_manual_enter .input_submit_key'),
submit_all: this.view.shouldSubmitPubkey('#step_2b_manual_enter .input_submit_all'),
passphrase_save: Boolean($('#step_2b_manual_enter .input_passphrase_save').prop('checked')),
is_newly_created_key: false,
recovered: false,
};
try {
Expand Down
77 changes: 77 additions & 0 deletions extension/chrome/settings/setup/setup-key-manager-autogen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */

'use strict';

import { SetupOptions, SetupView } from '../setup.js';

import { PgpKey } from '../../../js/common/core/pgp-key.js';
import { Ui } from '../../../js/common/browser/ui.js';
import { Url } from '../../../js/common/core/common.js';
import { AcctStore } from '../../../js/common/platform/store/acct-store.js';
import { Buf } from '../../../js/common/core/buf.js';
import { PgpPwd } from '../../../js/common/core/pgp-password.js';
import { ApiErr } from '../../../js/common/api/error/api-error.js';
import { Api } from '../../../js/common/api/api.js';
import { Settings } from '../../../js/common/settings.js';

export class SetupKeyManagerAutogenModule {

constructor(private view: SetupView) {
}

public getKeyFromKeyManagerOrAutogenAndStoreItThenRenderSetupDone = async () => {
if (!this.view.rules.mustAutogenPassPhraseQuietly()) {
const notSupportedErr = 'Combination of org rules not yet supported: PRV_AUTOIMPORT_OR_AUTOGEN cannot yet be used without PASS_PHRASE_QUIET_AUTOGEN.';
await Ui.modal.error(`${notSupportedErr}\n\nPlease write human@flowcrypt.com to add support.`);
window.location.href = Url.create('index.htm', { acctEmail: this.view.acctEmail });
return;
}
const keygenAlgo = this.view.rules.getEnforcedKeygenAlgo();
if (!keygenAlgo) {
const notSupportedErr = 'Combination of org rules not yet supported: PRV_AUTOIMPORT_OR_AUTOGEN cannot yet be used without enforce_keygen_algo.';
await Ui.modal.error(`${notSupportedErr}\n\nPlease write human@flowcrypt.com to add support.`);
window.location.href = Url.create('index.htm', { acctEmail: this.view.acctEmail });
return;
}
const passphrase = PgpPwd.random(); // mustAutogenPassPhraseQuietly
const opts: SetupOptions = { passphrase_save: true, submit_main: true, submit_all: false, passphrase };
try {
const { privateKeys } = await this.view.keyManager!.getPrivateKeys();
if (privateKeys.length) { // keys already exist on keyserver, auto-import
const { keys } = await PgpKey.readMany(Buf.fromUtfStr(privateKeys.map(pk => pk.decryptedPrivateKey).join('\n')));
if (!keys.length) {
throw new Error(`Could not parse any valid keys from Key Manager response for user ${this.view.acctEmail}`);
}
for (const prv of keys) {
if (!prv.isPrivate()) {
throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} is not a private key`);
}
if (!prv.isFullyDecrypted()) {
throw new Error(`Key ${await PgpKey.longid(prv)} for user ${this.view.acctEmail} from FlowCrypt Email Key Manager is not fully decrypted`);
}
await PgpKey.encrypt(prv, passphrase);
}
await this.view.saveKeys(keys, opts);
} else { // generate keys and store them on key manager
const { full_name } = await AcctStore.get(this.view.acctEmail, ['full_name']);
const generated = await PgpKey.create([{ name: full_name || '', email: this.view.acctEmail }], keygenAlgo, passphrase);
const decryptablePrv = await PgpKey.read(generated.private);
const generatedKeyLongid = await PgpKey.longid(decryptablePrv);
if (! await PgpKey.decrypt(decryptablePrv, passphrase)) {
throw new Error('Unexpectedly cannot decrypt newly generated key');
}
const storePrvOnKm = () => this.view.keyManager!.storePrivateKey(decryptablePrv.armor(), decryptablePrv.toPublic().armor(), generatedKeyLongid!);
await Settings.retryUntilSuccessful(storePrvOnKm, 'Failed to store newly generated key on FlowCrypt Email Key Manager');
await this.view.saveKeys([await PgpKey.read(generated.private)], opts); // store encrypted key + pass phrase locally
}
await this.view.finalizeSetup(opts);
await this.view.setupRender.renderSetupDone();
} catch (e) {
if (ApiErr.isNetErr(e) && await Api.isInternetAccessible()) { // frendly message when key manager is down, helpful during initial infrastructure setup
e.message = `FlowCrypt Email Key Manager at ${this.view.rules.getPrivateKeyManagerUrl()} is down, please inform your network admin.`;
}
throw e;
}
}

}
1 change: 0 additions & 1 deletion extension/chrome/settings/setup/setup-recover-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export class SetupRecoverKeyModule {
passphrase,
passphrase_save: true, // todo - reevaluate saving passphrase when recovering
recovered: true,
is_newly_created_key: false,
};
await this.view.saveKeys(newlyMatchingKeys, options);
const { setup_done } = await AcctStore.get(this.view.acctEmail, ['setup_done']);
Expand Down
Loading