Skip to content

Commit

Permalink
Improvements and fixes: thsmi#803 thsmi#892
Browse files Browse the repository at this point in the history
- Add facilities for using custom CA, client certificate and private key
- Use SNI if using custom client certificate and private key
- Fix broken `ignoreCertErrors()`
- Change behaviour of "Forget Password" button ...
  - The button becomes disabled after a click event rather than
    disappearing
  - The button clears both auth password and TLS private key passphrase
- Pressing 'Ctrl + Shift + I' or 'F12' combo launches dev tool
  • Loading branch information
dxdxdt committed Oct 17, 2023
1 parent b61bc16 commit 063b277
Show file tree
Hide file tree
Showing 13 changed files with 535 additions and 31 deletions.
9 changes: 8 additions & 1 deletion src/app/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ import { SieveI18n } from "./libs/managesieve.ui/utils/SieveI18n.mjs";
return {
"general": {
security: await account.getSecurity().getTLS(),
sasl: await account.getSecurity().getMechanism()
sasl: await account.getSecurity().getMechanism(),
tlsfiles: await account.getSecurity().getTLSFiles()
},
"authentication": {
username: await (await account.getAuthentication()).getUsername(),
Expand All @@ -203,6 +204,7 @@ import { SieveI18n } from "./libs/managesieve.ui/utils/SieveI18n.mjs";

const account = await accounts.getAccountById(msg.payload.account);
await (await account.getAuthentication()).forget();
await (await account.getSecurity()).clearStoredTLSPassphrase();
},

"account-settings-set-credentials": async function (msg) {
Expand All @@ -213,6 +215,7 @@ import { SieveI18n } from "./libs/managesieve.ui/utils/SieveI18n.mjs";

await account.getSecurity().setTLS(msg.payload.general.security);
await account.getSecurity().setMechanism(msg.payload.general.sasl);
await account.getSecurity().setTLSFiles(msg.payload.general.tlsfiles);

await account.getAuthentication().setUsername(msg.payload.authentication.username);

Expand Down Expand Up @@ -629,6 +632,10 @@ import { SieveI18n } from "./libs/managesieve.ui/utils/SieveI18n.mjs";

"decrypt-string" : async(msg) => {
return await ipcRenderer.invoke("decrypt-string", msg.payload);
},

"ipcrenderer-open-dialog" : async(msg) => {
return await ipcRenderer.invoke("open-dialog", msg.payload.options);
}
};

Expand Down
40 changes: 24 additions & 16 deletions src/app/libs/libManageSieve/SieveClient.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,13 @@ class SieveNodeClient extends SieveAbstractClient {

return await new Promise((resolve, reject) => {
// Upgrade the current socket.
// this.tlsSocket = tls.TLSSocket(socket, options).connect();
this.tlsSocket = tls.connect({
socket: this.socket,
rejectUnauthorized: false
rejectUnauthorized: false,
// Do SNI if using client cert because users who use it will probably
// want it ;)
servername: options.tlsContext ? this.host : undefined,
secureContext: options.tlsContext
});

this.tlsSocket.on('secureConnect', () => {
Expand Down Expand Up @@ -187,25 +190,29 @@ class SieveNodeClient extends SieveAbstractClient {
//
// It will be non null e.g. for self signed certificates.
const error = this.tlsSocket.ssl.verifyError();
let code = null;

if ((error !== null ) && (options.ignoreCertErrors.includes(error.code))) {
if (error !== null ) {
code = error.code;

if (this.isPinnedCert(cert, options.fingerprints)) {
this.secured = true;
resolve();
return;
}
if (options.ignoreCertErrors.includes(error.code)) {
if (this.isPinnedCert(cert, options.fingerprints)) {
this.secured = true;
resolve();
return;
}

throw new SieveCertValidationException({
host: this.host,
port: this.port,
throw new SieveCertValidationException({
host: this.host,
port: this.port,

fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,

code: error.code,
message: error.message
});
code: code,
message: error.message
});
}
}

if (this.tlsSocket.authorizationError === "ERR_TLS_CERT_ALTNAME_INVALID") {
Expand All @@ -223,6 +230,7 @@ class SieveNodeClient extends SieveAbstractClient {
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,

code: code,
message: `Error upgrading (${this.tlsSocket.authorizationError})`
});

Expand Down
74 changes: 74 additions & 0 deletions src/app/libs/libManageSieve/SieveSessions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* Thomas Schmid <schmid-thomas@gmx.net>
*/

const fs = require('fs');
const tls = require('tls');
import { SieveSession } from "./SieveSession.mjs";

/**
Expand Down Expand Up @@ -89,6 +91,77 @@ class SieveNodeSessions {
return await (await account.getAuthorization()).getAuthorization();
}

/**
* Called before STARTTLS is initiated.
*
* @param {SieveAccount}account the SieveAccount instance
* @returns {tls.SecurityContext}
* the SecurityContext instance set up for the particular tls connection
*/
async onStartTLS(account) {
const options = {};
let loaded = false;
const sec = await account.getSecurity();
const tlsfiles = await sec.getTLSFiles();
let r;
let tlsCtx = undefined;

if (tlsfiles.cachain) {
options.ca = await fs.promises.readFile(tlsfiles.cachain);
loaded = true;
}
if (tlsfiles.cert) {
options.cert = await fs.promises.readFile(tlsfiles.cert);
loaded = true;
}
if (tlsfiles.key) {
options.key = await fs.promises.readFile(tlsfiles.key);
loaded = true;
}

if (!loaded) {
// No option set. The user doesn't want this.
return undefined;
}

options.passphrase = await sec.getStoredTLSPassphrase();

do {
try {
tlsCtx = tls.createSecureContext(options);
break;
}
catch (ex) {
// Assume that the error is caused by wrong passphrase.
// Nodejs does not gift-wrap errors from the underlying crypto
// lib(openssl). Making guesses on what the error is based on the
// error messages from the crypto library could be a bad idea.

// Even if createSecureContext() fails because of some other reason,
// the user may notice it from the error message that would say
// something other than "BAD_PASS" on the next iteration.
r = await sec.promptPassphrase(tlsfiles.key, ex.toString());

if (r) {
// New passphrase to try on next iteration
options.passphrase = r.passphrase;
continue;
}
else {
// The user closed the dialog. Give up.
throw ex;
}
}
} while (!tlsCtx);

if (r && r.remember) {
// This means a new passphrase has been tried successfully and the
// user intends to save it.
await sec.setStoredTLSPassphrase(r.passphrase);
}

return tlsCtx;
}


/**
Expand Down Expand Up @@ -123,6 +196,7 @@ class SieveNodeSessions {

session.on("authenticate", async (hasPassword) => { return await this.onAuthenticate(account, hasPassword); });
session.on("authorize", async () => { return await this.onAuthorize(account); });
session.on("starttls", async () => { return await this.onStartTLS(account); });

this.sessions.set(id, session);
}
Expand Down
11 changes: 10 additions & 1 deletion src/app/libs/managesieve.ui/accounts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
SieveDeleteAccountDialog,
SieveErrorDialog,
SievePasswordDialog,
SieveAuthorizationDialog
SieveAuthorizationDialog,
SieveTLSPassphraseDialog
} from "./dialogs/SieveDialogUI.mjs";

/**
Expand Down Expand Up @@ -176,8 +177,16 @@ async function main() {
SieveIpcClient.setRequestHandler("accounts", "account-disconnected",
async (msg) => { return await accounts.render(msg.payload); });

SieveIpcClient.setRequestHandler("accounts", "tls-show-passphrase",
async (msg) => {
return await (new SieveTLSPassphraseDialog(
msg.payload.filepath,
msg.payload.options)).show();
});

accounts.render();
(new SieveUpdaterUI()).check();

}

if (document.readyState !== 'loading')
Expand Down
99 changes: 99 additions & 0 deletions src/app/libs/managesieve.ui/settings/logic/SieveSecurity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Thomas Schmid <schmid-thomas@gmx.net>
*/

import { SieveIpcClient } from "../../utils/SieveIpcClient.mjs";
import {
SieveAbstractSecurity,
SECURITY_NONE,
Expand All @@ -18,6 +19,10 @@ import {

const PREF_MECHANISM = "security.mechanism";
const PREF_TLS = "security.tls";
const PREF_TLSFILES_CA = "security.tlsfiles.ca";
const PREF_TLSFILES_CERT = "security.tlsfiles.cert";
const PREF_TLSFILES_KEY = "security.tlsfiles.key";
const PREF_TLSFILES_PASSPHRASE = "security.tlsfiles.passphrase";

/**
* Manages the account's security related settings
Expand Down Expand Up @@ -71,6 +76,100 @@ class SieveSecurity extends SieveAbstractSecurity {
return this;
}

/**
* Gets the TLS files map object.
*
* @returns {object}
* the TLS files map object
*/
async getTLSFiles () {
const cfg = this.account.getConfig();

return await {
cachain: await cfg.getString(PREF_TLSFILES_CA, ""),
cert: await cfg.getString(PREF_TLSFILES_CERT, ""),
key: await cfg.getString(PREF_TLSFILES_KEY, "")
};
}

/**
* Sets the TLS files map object.
*
* @param {object} map
* the map object
* @returns {SieveSecurity}
* a self reference
*/
async setTLSFiles (map) {
const cfg = this.account.getConfig();

await cfg.setString(PREF_TLSFILES_CA, map.cachain);
await cfg.setString(PREF_TLSFILES_CERT, map.cert);
await cfg.setString(PREF_TLSFILES_KEY, map.key);

return this;
}

/**
* @inheritdoc
*/
async getStoredTLSPassphrase() {
const cfg = this.account.getConfig();
const enp = await cfg.getValue(PREF_TLSFILES_PASSPHRASE);

if (!enp) {
return null;
}

return await SieveIpcClient.sendMessage(
"core", "decrypt-string", enp, window);
}

/**
* Encrypts and stores the passphrase for the private key.
*
* @param {string} passphrase the passphrase string
* @returns {SieveSecurity} a self reference
*/
async setStoredTLSPassphrase(passphrase) {
const cfg = this.account.getConfig();
const enp = await SieveIpcClient.sendMessage(
"core", "encrypt-string", passphrase, window);

await cfg.setValue(PREF_TLSFILES_PASSPHRASE, enp);

return this;
}

/**
* Clears the stored TLS passphrase
*
* @returns {SieveSecurity} a self reference
*/
async clearStoredTLSPassphrase() {
await this.account.getConfig().removeKey(PREF_TLSFILES_PASSPHRASE);
return this;
}

/**
* Prompt a new passphrase for the private key.
*
* @param {string} filepath the path to the private key
* @param {string} error the message describing the error, usually from openssl
* @returns {{
* passphrase: {string},
* remember: {boolean}
* }} the object containing prompt result
*/
async promptPassphrase(filepath, error) {
return await SieveIpcClient.sendMessage(
"accounts",
"tls-show-passphrase",
{
filepath: filepath,
options: { remember: true, error: error }
});
}
}

export { SieveSecurity };
Loading

0 comments on commit 063b277

Please sign in to comment.