Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes, custom CA, client certificate chain support #908

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
80 changes: 45 additions & 35 deletions src/app/libs/libManageSieve/SieveClient.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ class SieveNodeClient extends SieveAbstractClient {

this.socket = net.connect(this.port, this.host);

// Do TLS handshake before installing connection state event handlers

if (this.security === TLS_SECURITY_IMPLICIT) {
// In case of implicit tls we directly upgrade the socket...
// ... so no need to set a data listener here ...
// Any exception including socket error and TLS handshake error will spew
// out of the returned Promise. Let it bubble upwards.
await this.startTLS(options);
}
else {
// Not a TLS session. Install the event directly to the socket
this.socket.on('data', async (data) => {
this.onData(data);
});
}

// Now install the handlers

this.socket.on('error', async(error) => {
this.getLogger().logState(`SieveClient: OnError (Connection ${this.host}:${this.port})`);
// Node guarantees that close is called after error.
Expand All @@ -95,17 +113,6 @@ class SieveNodeClient extends SieveAbstractClient {
await this.disconnect(new Error("Server closed connection unexpectedly"));
});

// In case of implicit tls we directly upgrade the socket...
// ... so no need to set a data listener here ...
if (this.security === TLS_SECURITY_IMPLICIT) {
await this.startTLS(options);
return this;
}

this.socket.on('data', async (data) => {
this.onData(data);
});

return this;
}

Expand All @@ -131,12 +138,15 @@ class SieveNodeClient extends SieveAbstractClient {

await super.startTLS();

return await new Promise((resolve) => {
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 @@ -180,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 @@ -216,18 +230,14 @@ class SieveNodeClient extends SieveAbstractClient {
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,

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

} catch (ex) {
// We cannot exit here with a reject or exception. Because the server
// side will hang up the connection thus triggers a disconnect event.
// As this disconnect will cause an exception we would race against it.
//
// To avoid this race we call disconnect gracefully here, which will
// mark the connection as dead, thus the disconnect handler will not fire,
// but the disconnect call will still throw an error for us.
reject(ex);
this.disconnect(ex);
return;
}
});

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
Loading