Skip to content

Commit

Permalink
update @ProtonMail web clients: bump tor/onion address v2 => v3
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed Jun 19, 2021
1 parent 8e410d9 commit 8c14460
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 38 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Some Linux package types are available for installing from the repositories (`Pa
- :mag_right: **Full-text search**. Including email **body content** scanning capability. Enabled with [v2.2.0](https://github.com/vladimiry/ElectronMail/releases/tag/v2.2.0) release. See the respective [issue](https://github.com/vladimiry/ElectronMail/issues/92) for details.
- :mag_right: **JavaScript-based/unlimited messages filtering**. Enabled since [v4.11.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.11.0) release. See the respective [#257](https://github.com/vladimiry/ElectronMail/issues/257) for details. Requires [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature to be enabled.
- :package: **Offline access to the email messages** (attachments content not stored locally, but emails body content). The [local store](https://github.com/vladimiry/ElectronMail/wiki/FAQ) feature enables storing your messages in the encrypted `database.bin` file (see [FAQ](https://github.com/vladimiry/ElectronMail/wiki/FAQ) for file purpose details). So the app allows you to view your messages offline, running full-text search against them, exporting them to EML/JSON files. etc. Enabled since [v2.0.0](https://github.com/vladimiry/ElectronMail/releases/tag/v2.0.0) release.
- :mailbox: **Multi accounts** support including supporting individual [API entry points](https://github.com/vladimiry/ElectronMail/issues/29). For example, you can force the specific email account added in the app connect to the email provider via the [Tor](https://www.torproject.org/) only by selecting the `https://protonirockerxow.onion/` API entry point in the dropdown list and configuring a proxy as described in [this](https://github.com/vladimiry/ElectronMail/issues/113#issuecomment-529130116) message.
- :mailbox: **Multi accounts** support including supporting individual [API entry points](https://github.com/vladimiry/ElectronMail/issues/29). For example, you can force the specific email account added in the app connect to the email provider via the [Tor](https://www.torproject.org/) only by selecting the `Tor version 3 address` API entry point in the dropdown list and configuring a proxy as described in [this](https://github.com/vladimiry/ElectronMail/issues/113#issuecomment-529130116) message.
- :unlock: **Automatic login into the app** with a remembered the system keychain remembered master ([keep me signed in](images/keep-me-signed-in.png) feature). Integration with as a system keychain is done with the [keytar](https://github.com/atom/node-keytar) module. By the way, on Linux [KeePassXC](https://github.com/keepassxreboot/keepassxc) implements the [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) interface and so it can be acting as a system keychain (for details, see the "automatic login into the app"-related point in the [FAQ](https://github.com/vladimiry/ElectronMail/wiki/FAQ)).
- :unlock: **Automatic login into the email accounts**, including filling [2FA tokens](https://github.com/vladimiry/ElectronMail/issues/10). Two auto-login delay scenarios supported in order to make it harder to correlate the identities, see the respective [issue](https://github.com/vladimiry/ElectronMail/issues/121).
- :unlock: **Persistent email account sessions**. The feature introduced since [v4.2.0](https://github.com/vladimiry/ElectronMail/releases/tag/v4.2.0) version with the `experimental` label, [#227](https://github.com/vladimiry/ElectronMail/issues/227). The feature enables the scenario when you to enter the account credentials on the login form only once, manually or automatically by the app, and then you never see the login form anymore for this email account even if you restart the app (unless you explicitly dropped the session in the admin area or it got dropped by the service due to the inactivity/expiration). If this feature is enabled for the account, manual credentials filling is the preferred option as a more secure option since you don't save the account credentials anywhere (`credentials` are encrypted though even if saved, see `settings.bin` file description in the [FAQ](https://github.com/vladimiry/ElectronMail/wiki/FAQ)).
Expand Down
6 changes: 3 additions & 3 deletions scripts/prepare-webclient/protonmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const folderAsDomainEntries: Array<FolderAsDomainEntry<{
configApiParam:
| "electron-mail:app.protonmail.ch"
| "electron-mail:mail.protonmail.com"
| "electron-mail:protonirockerxow.onion";
| "electron-mail:protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion";
}>> = [
{
folderNameAsDomain: "app.protonmail.ch",
Expand All @@ -26,9 +26,9 @@ const folderAsDomainEntries: Array<FolderAsDomainEntry<{
},
},
{
folderNameAsDomain: "protonirockerxow.onion",
folderNameAsDomain: "protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion",
options: {
configApiParam: "electron-mail:protonirockerxow.onion",
configApiParam: "electron-mail:protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion",
},
},
];
Expand Down
2 changes: 1 addition & 1 deletion src/electron-main/session-storage/const.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import {SessionStorageModel} from "src/electron-main/session-storage/model";

export const SESSION_STORAGE_VERSION: Required<SessionStorageModel>["version"] = 1;
export const SESSION_STORAGE_VERSION: Required<SessionStorageModel>["version"] = 2;
66 changes: 46 additions & 20 deletions src/electron-main/session-storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {EncryptionAdapter, KeyBasedPreset} from "fs-json-store-encryption-adapte

import {AccountConfig, AccountPersistentSession} from "src/shared/model/account";
import {ApiEndpointOriginFieldContainer, LoginFieldContainer} from "src/shared/model/container";
import {ONE_KB_BYTES} from "src/shared/constants";
import {ONE_KB_BYTES, PROTON_API_ENTRY_TOR_V2_VALUE, PROTON_API_ENTRY_TOR_V3_VALUE} from "src/shared/constants";
import {SESSION_STORAGE_VERSION} from "src/electron-main/session-storage/const";
import {SessionStorageModel} from "src/electron-main/session-storage/model";
import {curryFunctionMembers, verifyUrlOriginValue} from "src/shared/util";
Expand Down Expand Up @@ -80,8 +80,23 @@ export class SessionStorage {
this.logger.info(nameof(SessionStorage.prototype.load)); // eslint-disable-line @typescript-eslint/unbound-method
const store = await this.resolveStore();
this.entity = await store.read() ?? SessionStorage.emptyEntity();
// TODO upgrade the structure once on app start
if (this.entity.version !== 1) {
{
const check1 = this.afterLoadEntityUpgrade(); // TODO upgrade the structure once on app start
const check2 = this.removeNonExistingLogins(actualLogins);
if (check1 || check2) {
await this.save();
}
}
}

private afterLoadEntityUpgrade(): boolean {
const version = typeof this.entity.version !== "number" || isNaN(this.entity.version)
? 0
: this.entity.version;
let shouldSave = false;

if (version < 1) {
shouldSave = true;
this.logger.verbose(
// eslint-disable-next-line @typescript-eslint/unbound-method
`${nameof(SessionStorage.prototype.load)} upgrading session storage structure (version: ${String(this.entity.version)})`,
Expand All @@ -90,9 +105,35 @@ export class SessionStorage {
...SessionStorage.emptyEntity(),
instance: this.entity as unknown as SessionStorageModel["instance"],
};
await this.save();
}
await this.removeNonExistingLogins(actualLogins);
if (version < 2) {
shouldSave = true;
const sessionBundleTorKeys = {v2: PROTON_API_ENTRY_TOR_V2_VALUE, v3: PROTON_API_ENTRY_TOR_V3_VALUE} as const;
Object.entries(this.entity.instance).forEach(([/* login */, sessionBundle]) => {
if (!sessionBundle) {
return;
}
const v2TorSession: AccountPersistentSession | undefined = (sessionBundle)[sessionBundleTorKeys.v2];
if (v2TorSession) {
sessionBundle[sessionBundleTorKeys.v3] = v2TorSession;
delete sessionBundle[sessionBundleTorKeys.v2];
}
});
}

return shouldSave;
}

private removeNonExistingLogins(
actualLogins: ReadonlyArray<AccountConfig["login"]>,
): boolean {
// eslint-disable-next-line @typescript-eslint/unbound-method
this.logger.info(nameof(SessionStorage.prototype.removeNonExistingLogins));
const loginsToRemove = Object.keys(this.entity.instance).filter((savedLogin) => !actualLogins.includes(savedLogin));
for (const login of loginsToRemove) {
delete this.entity.instance[login];
}
return Boolean(loginsToRemove.length);
}

private async save(): Promise<void> {
Expand All @@ -109,21 +150,6 @@ export class SessionStorage {
});
}

private async removeNonExistingLogins(
actualLogins: ReadonlyArray<AccountConfig["login"]>,
): Promise<number> {
// eslint-disable-next-line @typescript-eslint/unbound-method
this.logger.info(nameof(SessionStorage.prototype.removeNonExistingLogins));
const loginsToRemove = Object.keys(this.entity.instance).filter((savedLogin) => !actualLogins.includes(savedLogin));
for (const login of loginsToRemove) {
delete this.entity.instance[login];
}
if (loginsToRemove.length) {
await this.save();
}
return loginsToRemove.length;
}

private async resolveStore(): Promise<FsJsonStore.Store<typeof SessionStorage.prototype.entity>> {
const key = Buffer.from(await this.options.encryption.keyResolver(), BASE64_ENCODING);
const preset = await this.options.encryption.presetResolver();
Expand Down
19 changes: 13 additions & 6 deletions src/electron-main/storage-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
LAYOUT_MODES,
PACKAGE_VERSION,
PROTON_API_ENTRY_PRIMARY_VALUE,
PROTON_API_ENTRY_TOR_V2_VALUE,
PROTON_API_ENTRY_TOR_V3_VALUE,
PROTON_API_ENTRY_URLS,
PROTON_API_ENTRY_VALUE_PREFIX,
ZOOM_FACTORS,
Expand Down Expand Up @@ -523,16 +525,11 @@ export const upgradeSettings: (settings: Settings, ctx: Context) => boolean = ((
}
})();

settings.accounts.forEach((account, index) => {
settings.accounts.forEach((account) => {
delete (account as (typeof account & { type?: "protonmail" | "tutanota" })).type;

if (account.entryUrl.startsWith(PROTON_API_ENTRY_VALUE_PREFIX)) {
account.entryUrl = account.entryUrl.substr(PROTON_API_ENTRY_VALUE_PREFIX.length);
}

if (!PROTON_API_ENTRY_URLS.includes(account.entryUrl)) {
throw new Error(`Invalid entry url value: "${account.entryUrl}" (account index: ${index})`);
}
});
},
"4.12.2": (settings): void => {
Expand All @@ -543,6 +540,16 @@ export const upgradeSettings: (settings: Settings, ctx: Context) => boolean = ((
settings.dataSaltBase64 = `any non empty value ${randomString(50)}`;
}
},
"4.12.3": (settings): void => {
settings.accounts.forEach((account, index) => {
if (account.entryUrl === PROTON_API_ENTRY_TOR_V2_VALUE) {
account.entryUrl = PROTON_API_ENTRY_TOR_V3_VALUE;
}
if (!PROTON_API_ENTRY_URLS.includes(account.entryUrl)) {
throw new Error(`Invalid entry url value: "${account.entryUrl}" (account index: ${index})`);
}
});
},
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/electron-main/web-request/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export const patchResponseHeaders: (
},
);

// this patching is needed for CORS request to work with https://protonirockerxow.onion/ entry point via Tor proxy
// TODO apply "access-control-allow-credentials" headers patch for "https://protonirockerxow.onion/*" requests only
// this patching is needed for CORS request to work with Tor entry point via Tor proxy
// TODO apply "access-control-allow-credentials" headers patch for Tor requests only
// see "details.url" value
patchResponseHeader(
responseHeaders,
Expand All @@ -107,8 +107,8 @@ export const patchResponseHeaders: (
{extend: false},
);

// this patching is needed for CORS request to work with https://protonirockerxow.onion/ entry point via Tor proxy
// TODO apply "access-control-allow-headers" headers patch for "https://protonirockerxow.onion/*" requests only
// this patching is needed for CORS request to work with Tor entry point via Tor proxy
// TODO apply "access-control-allow-headers" headers patch for Tor requests only
// see "details.url" value
patchResponseHeader(
responseHeaders,
Expand Down
9 changes: 7 additions & 2 deletions src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export const PROTON_API_ENTRY_VALUE_PREFIX = "local:::";

export const PROTON_API_ENTRY_PRIMARY_VALUE = "https://mail.protonmail.com";

// @deprecated Tor v2 address will be retired by October 15, 2021
export const PROTON_API_ENTRY_TOR_V2_VALUE = "https://protonirockerxow.onion";

export const PROTON_API_ENTRY_TOR_V3_VALUE = "https://protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion";

function getBuiltInWebClientTitle(): string {
return PROVIDER_REPO_MAP["proton-mail"].commit.substr(0, 7);
}
Expand All @@ -77,8 +82,8 @@ export const PROTON_API_ENTRY_RECORDS: DeepReadonly<EntryUrlItem[]> = [
title: `https://app.protonmail.ch (${getBuiltInWebClientTitle()})`,
},
{
value: "https://protonirockerxow.onion",
title: `https://protonirockerxow.onion (${getBuiltInWebClientTitle()})`,
value: PROTON_API_ENTRY_TOR_V3_VALUE,
title: `Tor version 3 address (${getBuiltInWebClientTitle()})`,
},
];

Expand Down
3 changes: 2 additions & 1 deletion src/shared/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {PasswordBasedPreset} from "fs-json-store-encryption-adapter";
import type {RateLimiterMemory} from "rate-limiter-flexible";
import {URL} from "@cliqz/url-parser";
import {pick} from "remeda";

import {
ACCOUNT_EXTERNAL_CONTENT_PROXY_URL_REPLACE_PATTERN,
DEFAULT_API_CALL_TIMEOUT,
Expand Down Expand Up @@ -674,7 +675,7 @@ export const depersonalizeLoggedUrlsInString: (value: unknown) => string = (() =
// at the moment only proton urls get depersonalized, ie urls that start from the following urls
// https://app.protonmail.ch/
// https://mail.protonmail.com/
// https://protonirockerxow.onion/
// https://protonmailrmez3lotccipshtkleegetolb73fuirgj7r4o4vfu7ozyd.onion/
const protonUrlRe = new RegExp(PROTON_API_ENTRY_URLS.map((value) => `${value}/\\S*`).join("|"), "gi");
const result: typeof depersonalizeLoggedUrlsInString = (value) => {
if (typeof value !== "string") {
Expand Down

0 comments on commit 8c14460

Please sign in to comment.